/** * 插件公共 UI 组件库(@aso/plugin-sdk/ui)——宿主视觉规范的代码化。 * * 仅限 renderer 组件槽位使用(组件渲染在宿主 DOM 里,直接复用宿主样式表); * iframe 插件页是物理隔离文档,请参考 DESIGN.md §4 的 HTML 模式自行实现。 * * 样式来源只有两类: * - 宿主已生成的 Tailwind 类(DESIGN.md §2 类名词汇,全部经过存在性核验) * - 语义 token 的内联变量(var(--accent) 等,用于宿主类词汇未覆盖的场合) * 因此深浅主题自动跟随宿主,禁止出现硬编码色值。 * * 等待/失败/空态铁律(docs/plugins/08-performance.md §4):一切异步区块必须呈现 * LoadingState / ErrorState / EmptyState 三态之一,禁止裸文本或无反馈。 */ import { useState } from 'react' import type { ButtonHTMLAttributes, ComponentProps, InputHTMLAttributes, ReactElement, ReactNode, SelectHTMLAttributes, TextareaHTMLAttributes } from 'react' /** 拼接类名(过滤 falsy;轻量版 cn,不引依赖) */ export function cn(...classes: Array): string { return classes.filter(Boolean).join(' ') } // ---- 等待态 ---- export interface SpinnerProps { /** 直径 px,默认 14 */ size?: number className?: string } /** 旋转指示器:强调色弧 + 弱色圆环 */ export function Spinner({ size = 14, className }: SpinnerProps): ReactElement { return ( ) } export interface LoadingStateProps { /** 默认「加载中…」 */ label?: ReactNode className?: string } /** 区块级等待态:任何异步内容就绪前的统一占位 */ export function LoadingState({ label = '加载中…', className }: LoadingStateProps): ReactElement { return (
{label}
) } // ---- 失败态 ---- export interface ErrorStateProps { /** 默认「出错了」 */ title?: ReactNode /** 错误详情(错误消息、排查建议),保留换行 */ description?: ReactNode /** 提供则显示重试按钮 */ onRetry?: () => void retryLabel?: string className?: string } /** 区块级失败态:错误必须可见、可重试,禁止静默吞掉 */ export function ErrorState({ title = '出错了', description, onRetry, retryLabel = '重试', className }: ErrorStateProps): ReactElement { return (
{title}
{description !== undefined && description !== null && description !== '' && (
{description}
)} {onRetry && ( )}
) } // ---- 空态 ---- export interface EmptyStateProps { title: ReactNode /** 怎么产生的提示(如何让它不再为空) */ description?: ReactNode /** 可选动作(如「新建」「去设置」) */ action?: ReactNode className?: string } /** 区块级空态(DESIGN.md §4 标准模式的组件化) */ export function EmptyState({ title, description, action, className }: EmptyStateProps): ReactElement { return (
{title}
{description !== undefined && description !== null && description !== '' && (
{description}
)} {action &&
{action}
}
) } // ---- 按钮 ---- export interface ButtonProps extends ButtonHTMLAttributes { /** primary=强调实心 / ghost=描边(默认)/ danger=危险描边 */ variant?: 'primary' | 'ghost' | 'danger' /** sm=辅助按钮 11px / md=标准 11.5px(默认) */ size?: 'sm' | 'md' /** true 时禁用并显示旋转指示器(异步提交防重复点击) */ loading?: boolean } export function Button({ variant = 'ghost', size = 'md', loading = false, disabled, className, children, ...rest }: ButtonProps): ReactElement { const variantCls = variant === 'primary' ? 'bg-accent text-white hover:opacity-90' : variant === 'danger' ? 'border border-line-strong text-red hover:bg-red/10' : 'border border-line-strong text-ink hover:bg-hover' const sizeCls = size === 'sm' ? 'px-2 py-1 text-[11px]' : 'px-3 py-1.5 text-[11.5px]' return ( ) } // ---- 输入控件 ---- /** 单行输入(设置行/表单通用;与宿主设置页同款) */ export function Input({ className, ...rest }: InputHTMLAttributes): ReactElement { return ( ) } export function Textarea({ className, ...rest }: TextareaHTMLAttributes): ReactElement { return (