feat: 共享 UI 组件库
- Button, Badge, Card, Table, Modal, Input, Select, Toast, Pagination - 统一样式规范,供 assets/issue 及后续站点使用
This commit is contained in:
commit
83b26021b2
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
'use client'
|
||||||
|
type Variant = 'default' | 'success' | 'warning' | 'danger' | 'info'
|
||||||
|
const v: Record<Variant, string> = {
|
||||||
|
default: 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300',
|
||||||
|
success: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400',
|
||||||
|
warning: 'bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400',
|
||||||
|
danger: 'bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-400',
|
||||||
|
info: 'bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400',
|
||||||
|
}
|
||||||
|
export default function Badge({ variant = 'default', children }: { variant?: Variant; children: React.ReactNode }) {
|
||||||
|
return <span className={`px-2.5 py-0.5 rounded-full text-xs font-medium ${v[variant]}`}>{children}</span>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
'use client'
|
||||||
|
import { ButtonHTMLAttributes, forwardRef } from 'react'
|
||||||
|
type Variant = 'primary' | 'secondary' | 'danger' | 'ghost'
|
||||||
|
type Size = 'sm' | 'md' | 'lg'
|
||||||
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { variant?: Variant; size?: Size; loading?: boolean }
|
||||||
|
const v: Record<Variant, string> = {
|
||||||
|
primary: 'bg-blue-600 hover:bg-blue-700 text-white shadow-sm disabled:opacity-50 disabled:cursor-not-allowed',
|
||||||
|
secondary: 'bg-slate-100 hover:bg-slate-200 text-slate-700 dark:bg-slate-800 dark:hover:bg-slate-700 dark:text-slate-300',
|
||||||
|
danger: 'bg-red-600 hover:bg-red-700 text-white disabled:opacity-50 disabled:cursor-not-allowed',
|
||||||
|
ghost: 'text-slate-600 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800',
|
||||||
|
}
|
||||||
|
const s: Record<Size, string> = { sm: 'px-3 py-1.5 text-xs', md: 'px-4 py-2 text-sm', lg: 'px-6 py-2.5 text-base' }
|
||||||
|
const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ variant = 'primary', size = 'md', loading, children, className = '', disabled, ...props }, ref) => (
|
||||||
|
<button ref={ref} disabled={disabled || loading} className={`inline-flex items-center justify-center gap-2 font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500/50 disabled:opacity-50 disabled:cursor-not-allowed ${v[variant]} ${s[size]} ${className}`} {...props}>
|
||||||
|
{loading && <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /></svg>}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
Button.displayName = 'Button'
|
||||||
|
export default Button
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
'use client'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
export default function Card({ children, className = '' }: { children: ReactNode; className?: string }) {
|
||||||
|
return <div className={`bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm ${className}`}>{children}</div>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
'use client'
|
||||||
|
import { InputHTMLAttributes } from 'react'
|
||||||
|
interface InputProps extends InputHTMLAttributes<HTMLInputElement> { label?: string; error?: string }
|
||||||
|
export function Input({ label, error, className = '', ...props }: InputProps) {
|
||||||
|
return (<div className="space-y-1">{label && <label className="block text-sm font-medium text-slate-700 dark:text-slate-300">{label}</label>}<input className={`w-full px-3 py-2 rounded-lg border bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 border-slate-300 dark:border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition-colors text-sm ${className}`} {...props} />{error && <p className="text-sm text-red-500">{error}</p>}</div>)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
'use client'
|
||||||
|
import { ReactNode, useEffect } from 'react'
|
||||||
|
interface ModalProps { open: boolean; onClose: () => void; title?: string; children: ReactNode; maxWidth?: string }
|
||||||
|
export default function Modal({ open, onClose, title, children, maxWidth = 'max-w-lg' }: ModalProps) {
|
||||||
|
useEffect(() => { if (open) document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = '' } }, [open])
|
||||||
|
if (!open) return null
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
||||||
|
<div className={`relative bg-white dark:bg-slate-900 rounded-xl shadow-xl border border-slate-200 dark:border-slate-800 w-full ${maxWidth} p-6`}>
|
||||||
|
{title && <h2 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">{title}</h2>}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
'use client'
|
||||||
|
interface PaginationProps { page: number; totalPages: number; onPageChange: (page: number) => void }
|
||||||
|
export function Pagination({ page, totalPages, onPageChange }: PaginationProps) {
|
||||||
|
if (totalPages <= 1) return null
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
<span>第 {page} / {totalPages} 页</span>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button onClick={() => onPageChange(page - 1)} disabled={page <= 1} className="px-3 py-1 rounded border border-slate-200 dark:border-slate-700 disabled:opacity-50">上一页</button>
|
||||||
|
<button onClick={() => onPageChange(page + 1)} disabled={page >= totalPages} className="px-3 py-1 rounded border border-slate-200 dark:border-slate-700 disabled:opacity-50">下一页</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
'use client'
|
||||||
|
import { SelectHTMLAttributes } from 'react'
|
||||||
|
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> { label?: string; options: { value: string; label: string }[] }
|
||||||
|
export function Select({ label, options, className = '', ...props }: SelectProps) {
|
||||||
|
return (<div className="space-y-1">{label && <label className="block text-sm font-medium text-slate-700 dark:text-slate-300">{label}</label>}<select className={`w-full px-3 py-2 rounded-lg border bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 border-slate-300 dark:border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition-colors text-sm ${className}`} {...props}>{options.map((o) => (<option key={o.value} value={o.value}>{o.label}</option>))}</select></div>)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
'use client'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
interface TableHeadersProps { headers: string[]; children: ReactNode }
|
||||||
|
export function Table({ headers, children }: TableHeadersProps) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-800">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-slate-50 dark:bg-slate-800">
|
||||||
|
<tr>{headers.map((h) => (<th key={h} className="px-4 py-3 text-left font-medium text-slate-500 dark:text-slate-400">{h}</th>))}</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-200 dark:divide-slate-700">{children}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
'use client'
|
||||||
|
interface ToastProps { message: string; type?: 'success' | 'error' | 'info'; onClose: () => void }
|
||||||
|
export function Toast({ message, type = 'info', onClose }: ToastProps) {
|
||||||
|
const c = { success: 'bg-emerald-50 text-emerald-800 border-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20', error: 'bg-red-50 text-red-800 border-red-200 dark:bg-red-500/10 dark:text-red-400 dark:border-red-500/20', info: 'bg-blue-50 text-blue-800 border-blue-200 dark:bg-blue-500/10 dark:text-blue-400 dark:border-blue-500/20' }
|
||||||
|
return (<div className={`fixed bottom-6 right-6 z-50 px-4 py-3 rounded-lg border shadow-lg text-sm font-medium ${c[type]}`}><div className="flex items-center gap-3"><span>{message}</span><button onClick={onClose} className="text-current opacity-60 hover:opacity-100">×</button></div></div>)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export { default as Button } from './Button'
|
||||||
|
export { default as Badge } from './Badge'
|
||||||
|
export { default as Card } from './Card'
|
||||||
|
export { Table } from './Table'
|
||||||
|
export { default as Modal } from './Modal'
|
||||||
|
export { Input } from './Input'
|
||||||
|
export { Select } from './Select'
|
||||||
|
export { Toast } from './Toast'
|
||||||
|
export { Pagination } from './Pagination'
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "@tlyq/shared-ui",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TLYQ 统一 UI 组件库",
|
||||||
|
"main": "index.tsx",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue