style(web): UI kit 组件与画布/后台面板 Prettier 格式化与 migrations.go gofmt 清理

This commit is contained in:
maomaochong-ai
2026-09-06 20:28:52 +08:00
parent 7fef6a297b
commit 3da49bd0da
25 changed files with 123 additions and 322 deletions

View File

@@ -336,10 +336,26 @@ export function AiArtCritiqueModal({ node, upstreamNodes, open, onClose, onUpdat
<aside className="min-h-0 flex-1 overflow-y-auto" data-canvas-wheel-scroll>
<div className="flex min-h-full flex-col gap-4 p-4">
{running || visibleState.status === "running" ? <ArtCritiqueProgress stage={progressStage} theme={theme} /> : null}
{draftReportVisible && visibleState.status === "running" && visibleState.report ? <Callout tone="info" title="报告初稿已生成">问题和建议已先展示,正在精确定位并复核矩形标注。</Callout> : null}
{!enabled && !visibleState.report ? <Callout tone="warning" title="插件尚未启用">请先到插件管理中开启 AI 审美批改,再运行分析。</Callout> : null}
{visibleState.status === "stale" ? <Callout tone="warning" title="输入图片已经变化">当前报告针对旧图片生成,请重新批改。</Callout> : null}
{localError || visibleState.errorMessage ? <Callout tone="error" title="批改失败">{localError || visibleState.errorMessage}</Callout> : null}
{draftReportVisible && visibleState.status === "running" && visibleState.report ? (
<Callout tone="info" title="报告初稿已生成">
问题和建议已先展示,正在精确定位并复核矩形标注。
</Callout>
) : null}
{!enabled && !visibleState.report ? (
<Callout tone="warning" title="插件尚未启用">
请先到插件管理中开启 AI 审美批改,再运行分析。
</Callout>
) : null}
{visibleState.status === "stale" ? (
<Callout tone="warning" title="输入图片已经变化">
当前报告针对旧图片生成,请重新批改。
</Callout>
) : null}
{localError || visibleState.errorMessage ? (
<Callout tone="error" title="批改失败">
{localError || visibleState.errorMessage}
</Callout>
) : null}
{view === "detail" && detailIssue ? (
<IssueDetailView

View File

@@ -180,11 +180,7 @@ export function CanvasHistoryDrawer({ open, onClose }: { open: boolean; onClose:
{item.title}
</button>
)}
{isDeleted ? (
<StatusBadge tone="error" size="sm" label="已删除" className="m-0" />
) : (
<StatusBadge tone="success" size="sm" label="活跃中" className="m-0" />
)}
{isDeleted ? <StatusBadge tone="error" size="sm" label="已删除" className="m-0" /> : <StatusBadge tone="success" size="sm" label="活跃中" className="m-0" />}
</div>
<div className="space-y-1 text-xs text-stone-600 dark:text-stone-300">

View File

@@ -544,7 +544,11 @@ export function CanvasPromptOptimizerDrawer({ open, children, prompt, generation
bubbleItems.push({
key: "error",
role: "system",
content: <Callout className="canvas-prompt-optimizer-alert" tone="error" title="优化失败">{error}</Callout>,
content: (
<Callout className="canvas-prompt-optimizer-alert" tone="error" title="优化失败">
{error}
</Callout>
),
});
}

View File

@@ -1,6 +1,5 @@
import { Tooltip } from "@/components/ui/base/tooltip";
import { releaseDirectorFocusAfterPointer } from "@/lib/canvas/director/director-shortcuts";
import { DIRECTOR_VIEW_MODES, type DirectorViewMode } from "@/lib/canvas/director/director-view-modes";

View File

@@ -4,12 +4,12 @@
## 分层
| 层 | 目录 | 内容 |
| --- | --- | --- |
| base | `ui/base/` | 原语 + 基础件:`buttons/`、`badges/`、`kbd/`、`segmented-control/` 等 |
| product | `ui/product/` | 跨页面模式件:`empty-state/`、`callout/`、`loading-state/`、`section-card/`、`property-row/` |
| fx | `ui/fx/` | 特效件(原 `aceternity/*` 归位至此命名空间,引用随用随迁,不重写) |
| application | `ui/application/` | 产品块(P3 起):agent-chat、notification-center、app-shell 等 |
| 层 | 目录 | 内容 |
| ----------- | ----------------- | -------------------------------------------------------------------------------------------- |
| base | `ui/base/` | 原语 + 基础件:`buttons/`、`badges/`、`kbd/`、`segmented-control/` 等 |
| product | `ui/product/` | 跨页面模式件:`empty-state/`、`callout/`、`loading-state/`、`section-card/`、`property-row/` |
| fx | `ui/fx/` | 特效件(原 `aceternity/*` 归位至此命名空间,引用随用随迁,不重写) |
| application | `ui/application/` | 产品块(P3 起):agent-chat、notification-center、app-shell 等 |
> 视觉数值唯一权威源:`composio-brand-package`(仓库根同级兄弟目录),消费规则见实施计划 §2.4。

View File

@@ -78,17 +78,9 @@ export type StatusBadgeProps = HTMLAttributes<HTMLSpanElement> &
export function StatusBadge({ tone, variant = "outline", size, label, className, children, ...props }: StatusBadgeProps) {
const resolvedTone = tone ?? "neutral";
return (
<span
data-slot="status-badge"
className={cn(statusBadgeVariants({ variant, size }), variant === "filled" && filledTintClass[resolvedTone], className)}
{...props}
>
{variant === "outline" ? (
<span aria-hidden className={cn(statusBadgeTone({ tone }), "size-1.5", tone === "loading" && "motion-safe:animate-pulse")} />
) : null}
{label != null ? (
<span className={cn("text-muted-foreground", variant === "filled" && filledInkClass[resolvedTone])}>{label}</span>
) : null}
<span data-slot="status-badge" className={cn(statusBadgeVariants({ variant, size }), variant === "filled" && filledTintClass[resolvedTone], className)} {...props}>
{variant === "outline" ? <span aria-hidden className={cn(statusBadgeTone({ tone }), "size-1.5", tone === "loading" && "motion-safe:animate-pulse")} /> : null}
{label != null ? <span className={cn("text-muted-foreground", variant === "filled" && filledInkClass[resolvedTone])}>{label}</span> : null}
{children}
</span>
);

View File

@@ -19,25 +19,20 @@ import { cn } from "@/lib/utils";
* 明暗:全部颜色走 token;active 按压底用 bg-surface-active,明暗自动适配。
*/
export const iconButtonVariants = cva(
[
"inline-flex shrink-0 items-center justify-center rounded-md",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"disabled:pointer-events-none disabled:opacity-45",
"motion-safe:active:scale-[0.96]",
],
["inline-flex shrink-0 items-center justify-center rounded-md", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-45", "motion-safe:active:scale-[0.96]"],
{
variants: {
variant: {
// 纯透明 + hover 浮底(最常用:面板/工具条)
ghost: "text-muted-foreground hover:bg-surface-hover hover:text-foreground active:bg-surface-active",
// 纯透明 + hover 浮底(最常用:面板/工具条)
ghost: "text-muted-foreground hover:bg-surface-hover hover:text-foreground active:bg-surface-active",
// 常驻浅底
default: "bg-surface-secondary text-muted-foreground hover:bg-surface-hover hover:text-foreground active:bg-surface-active",
default: "bg-surface-secondary text-muted-foreground hover:bg-surface-hover hover:text-foreground active:bg-surface-active",
// 描边
outline: "border border-border bg-transparent text-muted-foreground hover:bg-surface-hover hover:text-foreground",
outline: "border border-border bg-transparent text-muted-foreground hover:bg-surface-hover hover:text-foreground",
// 危险(文本色常驻红)
danger: "text-status-error hover:bg-surface-hover active:bg-surface-active",
// 实心(中性黑底白图标,主操作;active 反转成白底黑字 + 内描边,供选中态使用)
solid: "bg-foreground text-background hover:opacity-85 active:opacity-75 data-[active=true]:bg-background data-[active=true]:text-foreground data-[active=true]:ring-1 data-[active=true]:ring-inset data-[active=true]:ring-border",
danger: "text-status-error hover:bg-surface-hover active:bg-surface-active",
// 实心(中性黑底白图标,主操作;active 反转成白底黑字 + 内描边,供选中态使用)
solid: "bg-foreground text-background hover:opacity-85 active:opacity-75 data-[active=true]:bg-background data-[active=true]:text-foreground data-[active=true]:ring-1 data-[active=true]:ring-inset data-[active=true]:ring-border",
},
size: {
xs: "size-6 [&_svg]:size-3.5",
@@ -65,15 +60,7 @@ export type IconButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
export function IconButton({ variant, size, icon: Icon, loading = false, active = false, className, type = "button", disabled, ...props }: IconButtonProps) {
return (
<button
data-slot="icon-button"
type={type}
disabled={disabled || loading}
aria-busy={loading || undefined}
data-active={active || undefined}
className={cn(iconButtonVariants({ variant, size }), className)}
{...props}
>
<button data-slot="icon-button" type={type} disabled={disabled || loading} aria-busy={loading || undefined} data-active={active || undefined} className={cn(iconButtonVariants({ variant, size }), className)} {...props}>
{loading ? <Loader2 aria-hidden className="animate-spin" /> : <Icon aria-hidden />}
</button>
);

View File

@@ -21,12 +21,7 @@ import { cn } from "@/lib/utils";
* 明暗:全部颜色走 token;active 用按压底语义 token,明暗自动适配。
*/
export const toolButtonVariants = cva(
[
"inline-flex select-none items-center justify-center gap-1.5 rounded-md",
"font-medium transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"disabled:pointer-events-none disabled:opacity-45",
],
["inline-flex select-none items-center justify-center gap-1.5 rounded-md", "font-medium transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-45"],
{
variants: {
variant: {
@@ -79,20 +74,7 @@ export type ToolButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
label?: ReactNode;
};
export function ToolButton({
variant,
size,
tone,
icon,
active = false,
expands = false,
loading = false,
label,
className,
type = "button",
disabled,
...props
}: ToolButtonProps) {
export function ToolButton({ variant, size, tone, icon, active = false, expands = false, loading = false, label, className, type = "button", disabled, ...props }: ToolButtonProps) {
const pressed = !expands && active;
const expanded = expands && active;
return (

View File

@@ -30,14 +30,7 @@ export interface CheckboxGroupProps<V extends string = string> {
className?: string;
}
export function CheckboxGroup<V extends string = string>({
value = [],
onChange,
options,
disabled = false,
ariaLabel,
className,
}: CheckboxGroupProps<V>) {
export function CheckboxGroup<V extends string = string>({ value = [], onChange, options, disabled = false, ariaLabel, className }: CheckboxGroupProps<V>) {
const toggle = (optionValue: V, checked: boolean) => {
if (disabled) return;
const next = checked ? [...value, optionValue] : value.filter((item) => item !== optionValue);
@@ -47,12 +40,7 @@ export function CheckboxGroup<V extends string = string>({
return (
<div data-slot="checkbox-group" role="group" aria-label={ariaLabel} className={className}>
{options.map((option) => (
<Checkbox
key={option.value}
checked={value.includes(option.value)}
disabled={disabled || option.disabled}
onChange={(event) => toggle(option.value, event.target.checked)}
>
<Checkbox key={option.value} checked={value.includes(option.value)} disabled={disabled || option.disabled} onChange={(event) => toggle(option.value, event.target.checked)}>
{option.label}
</Checkbox>
))}

View File

@@ -35,16 +35,7 @@ const BOX_SIZE: Record<"sm" | "md", string> = {
md: "size-4 rounded",
};
export function Checkbox({
size = "md",
indeterminate = false,
children,
bare = false,
className,
disabled,
checked,
...props
}: CheckboxProps) {
export function Checkbox({ size = "md", indeterminate = false, children, bare = false, className, disabled, checked, ...props }: CheckboxProps) {
const inputRef = useRef<HTMLInputElement | null>(null);
useLayoutEffect(() => {
@@ -55,14 +46,7 @@ export function Checkbox({
const inner = (
<>
<input
ref={inputRef}
type="checkbox"
disabled={disabled}
checked={checked}
className="peer sr-only"
{...props}
/>
<input ref={inputRef} type="checkbox" disabled={disabled} checked={checked} className="peer sr-only" {...props} />
<span
aria-hidden="true"
data-indeterminate={indeterminate || undefined}
@@ -77,17 +61,11 @@ export function Checkbox({
)}
>
{/* checked 勾(indeterminate 时隐藏,改显短横线) */}
<svg
viewBox="0 0 12 12"
className={cn("size-3 stroke-current stroke-[2.2]", indeterminate ? "hidden" : "block")}
>
<svg viewBox="0 0 12 12" className={cn("size-3 stroke-current stroke-[2.2]", indeterminate ? "hidden" : "block")}>
<path d="M2.5 6.5 L5 9 L9.5 3.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
{/* indeterminate 短横线 */}
<svg
viewBox="0 0 12 12"
className={cn("size-3 stroke-current stroke-[2]", indeterminate ? "block" : "hidden")}
>
<svg viewBox="0 0 12 12" className={cn("size-3 stroke-current stroke-[2]", indeterminate ? "block" : "hidden")}>
<path d="M2.8 6 H9.2" fill="none" strokeLinecap="round" />
</svg>
</span>
@@ -95,32 +73,13 @@ export function Checkbox({
);
if (bare) {
return (
<span
className={cn(
"group/checkbox inline-flex items-center",
disabled && "cursor-not-allowed",
className,
)}
>
{inner}
</span>
);
return <span className={cn("group/checkbox inline-flex items-center", disabled && "cursor-not-allowed", className)}>{inner}</span>;
}
return (
<label
data-slot="checkbox"
className={cn(
"group/checkbox inline-flex cursor-pointer select-none items-center gap-2 align-middle",
disabled && "cursor-not-allowed",
className,
)}
>
<label data-slot="checkbox" className={cn("group/checkbox inline-flex cursor-pointer select-none items-center gap-2 align-middle", disabled && "cursor-not-allowed", className)}>
{inner}
{children !== undefined && (
<span className={cn("text-sm leading-none text-foreground", disabled && "opacity-60")}>{children}</span>
)}
{children !== undefined && <span className={cn("text-sm leading-none text-foreground", disabled && "opacity-60")}>{children}</span>}
</label>
);
}

View File

@@ -18,12 +18,7 @@ export function Kbd({ className, children, ...props }: KbdProps) {
return (
<kbd
data-slot="kbd"
className={cn(
"inline-flex h-5 shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-sm",
"border border-[color:var(--workspace-border)] bg-background/60 px-1.5",
"font-mono text-tiny font-medium text-foreground/60",
className,
)}
className={cn("inline-flex h-5 shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-sm", "border border-[color:var(--workspace-border)] bg-background/60 px-1.5", "font-mono text-tiny font-medium text-foreground/60", className)}
{...props}
>
{children}

View File

@@ -53,16 +53,7 @@ const SIZE_CLASS: Record<SegmentedSize, { track: string; thumb: string; item: st
},
};
export function SegmentedControl<V extends string = string>({
value,
onChange,
options,
size = "md",
block = false,
disabled = false,
ariaLabel,
className = "",
}: SegmentedControlProps<V>) {
export function SegmentedControl<V extends string = string>({ value, onChange, options, size = "md", block = false, disabled = false, ariaLabel, className = "" }: SegmentedControlProps<V>) {
const trackRef = useRef<HTMLDivElement | null>(null);
const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);
const [thumb, setThumb] = useState<{ left: number; width: number } | null>(null);
@@ -70,8 +61,7 @@ export function SegmentedControl<V extends string = string>({
const selectedIndex = options.findIndex((option) => option.value === value);
/** 无选中项时兜底首个可用项,保证 roving tabindex 下键盘始终可达 */
const focusableIndex =
selectedIndex >= 0 ? selectedIndex : options.findIndex((option) => !option.disabled);
const focusableIndex = selectedIndex >= 0 ? selectedIndex : options.findIndex((option) => !option.disabled);
useLayoutEffect(() => {
const track = trackRef.current;
@@ -123,29 +113,14 @@ export function SegmentedControl<V extends string = string>({
};
const itemBase =
"relative z-10 inline-flex select-none items-center justify-center gap-1.5 whitespace-nowrap font-medium outline-none transition-colors motion-reduce:transition-none " +
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ";
"relative z-10 inline-flex select-none items-center justify-center gap-1.5 whitespace-nowrap font-medium outline-none transition-colors motion-reduce:transition-none " + "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ";
return (
<div
ref={trackRef}
role="radiogroup"
aria-label={ariaLabel}
className={[
"relative inline-flex bg-surface-tertiary",
SIZE_CLASS[size].track,
block ? "flex w-full" : "",
disabled ? "opacity-60" : "",
className,
].join(" ")}
>
<div ref={trackRef} role="radiogroup" aria-label={ariaLabel} className={["relative inline-flex bg-surface-tertiary", SIZE_CLASS[size].track, block ? "flex w-full" : "", disabled ? "opacity-60" : "", className].join(" ")}>
{thumb !== null && !disabled && (
<span
aria-hidden="true"
className={[
"pointer-events-none absolute inset-y-0.5 z-0 bg-surface-strong transition-[left,width] duration-200 ease-out motion-reduce:transition-none",
SIZE_CLASS[size].thumb,
].join(" ")}
className={["pointer-events-none absolute inset-y-0.5 z-0 bg-surface-strong transition-[left,width] duration-200 ease-out motion-reduce:transition-none", SIZE_CLASS[size].thumb].join(" ")}
style={{ left: thumb.left, width: thumb.width }}
/>
)}
@@ -160,7 +135,7 @@ export function SegmentedControl<V extends string = string>({
type="button"
role="radio"
aria-checked={selected}
aria-label={option.label ? undefined : option.title ?? option.value}
aria-label={option.label ? undefined : (option.title ?? option.value)}
title={option.title}
disabled={disabled || option.disabled}
tabIndex={index === focusableIndex ? 0 : -1}
@@ -174,11 +149,7 @@ export function SegmentedControl<V extends string = string>({
SIZE_CLASS[size].item,
SIZE_CLASS[size].thumb,
block ? "flex-1" : "",
option.disabled
? "cursor-not-allowed text-muted-foreground/40"
: selected
? "cursor-default text-foreground"
: "text-muted-foreground hover:bg-surface-hover hover:text-foreground",
option.disabled ? "cursor-not-allowed text-muted-foreground/40" : selected ? "cursor-default text-foreground" : "text-muted-foreground hover:bg-surface-hover hover:text-foreground",
].join(" ")}
>
{option.icon}

View File

@@ -1,11 +1,5 @@
import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
import {
Button as AriaButton,
ListBox as AriaListBox,
ListBoxItem as AriaListBoxItem,
Popover as AriaPopover,
Select as AriaSelect,
} from "react-aria-components";
import { Button as AriaButton, ListBox as AriaListBox, ListBoxItem as AriaListBoxItem, Popover as AriaPopover, Select as AriaSelect } from "react-aria-components";
import { Check, ChevronDown, X } from "lucide-react";
import { cn } from "@/lib/utils";
@@ -52,17 +46,7 @@ const TRIGGER_CLASS: Record<SelectSize, string> = {
md: "h-8 text-sm",
};
export function Select<V extends string = string>({
value,
onChange,
options = [],
placeholder,
disabled = false,
allowClear = false,
size = "md",
ariaLabel,
className,
}: SelectProps<V>) {
export function Select<V extends string = string>({ value, onChange, options = [], placeholder, disabled = false, allowClear = false, size = "md", ariaLabel, className }: SelectProps<V>) {
const triggerRef = useRef<HTMLButtonElement | null>(null);
const popoverRef = useRef<HTMLDivElement | null>(null);
const [open, setOpen] = useState(false);
@@ -122,9 +106,7 @@ export function Select<V extends string = string>({
TRIGGER_CLASS[size],
)}
>
<span className={cn("min-w-0 flex-1 truncate text-left", displayLabel === undefined && "text-muted-foreground")}>
{displayLabel === undefined ? placeholder : displayLabel}
</span>
<span className={cn("min-w-0 flex-1 truncate text-left", displayLabel === undefined && "text-muted-foreground")}>{displayLabel === undefined ? placeholder : displayLabel}</span>
{allowClear && value !== undefined && !disabled && (
<span
role="button"
@@ -140,25 +122,9 @@ export function Select<V extends string = string>({
<X className="size-3.5" />
</span>
)}
<ChevronDown
aria-hidden="true"
className={cn(
"size-4 shrink-0 text-muted-foreground transition-transform duration-200 motion-reduce:transition-none",
open && "rotate-180",
)}
/>
<ChevronDown aria-hidden="true" className={cn("size-4 shrink-0 text-muted-foreground transition-transform duration-200 motion-reduce:transition-none", open && "rotate-180")} />
</AriaButton>
<AriaPopover
ref={popoverRef}
isNonModal
placement="bottom"
offset={4}
style={popoverWidth ? { width: popoverWidth } : undefined}
className={cn(
"z-50 rounded-lg border border-border bg-surface-strong p-1 shadow-lg",
"ra-pop-in",
)}
>
<AriaPopover ref={popoverRef} isNonModal placement="bottom" offset={4} style={popoverWidth ? { width: popoverWidth } : undefined} className={cn("z-50 rounded-lg border border-border bg-surface-strong p-1 shadow-lg", "ra-pop-in")}>
<AriaListBox aria-label={ariaLabel ?? placeholder ?? "选项"} className="max-h-60 overflow-auto outline-none">
{options.map((option) => (
<AriaListBoxItem

View File

@@ -36,10 +36,7 @@ export interface SwitchProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement
}
/** 几何档:轨道尺寸与 thumb 行程(thumb 用 left-0.5 起始 + translate-x 档位) */
const SWITCH_SIZE: Record<
"sm" | "md",
{ root: string; thumbPlain: string; thumbText: string; hasTextRoot: string }
> = {
const SWITCH_SIZE: Record<"sm" | "md", { root: string; thumbPlain: string; thumbText: string; hasTextRoot: string }> = {
sm: {
root: "h-5 w-8 rounded-full",
thumbPlain: "size-3.5 data-[state=on]:translate-x-3.5",
@@ -54,19 +51,7 @@ const SWITCH_SIZE: Record<
},
};
export function Switch({
size = "md",
checked: checkedProp,
defaultChecked = false,
loading = false,
checkedChildren,
unCheckedChildren,
onChange,
disabled,
className,
"aria-label": ariaLabel,
...props
}: SwitchProps) {
export function Switch({ size = "md", checked: checkedProp, defaultChecked = false, loading = false, checkedChildren, unCheckedChildren, onChange, disabled, className, "aria-label": ariaLabel, ...props }: SwitchProps) {
const [innerChecked, setInnerChecked] = useState(defaultChecked);
const controlled = checkedProp !== undefined;
const checked = controlled ? checkedProp : innerChecked;
@@ -113,42 +98,24 @@ export function Switch({
{unCheckedChildren !== undefined ? (
<span
aria-hidden="true"
className={cn(
"absolute top-1/2 right-1.5 -translate-y-1/2 text-xs leading-none font-medium transition-opacity motion-reduce:transition-none",
checked ? "text-foreground/55 opacity-0" : "text-foreground/55 opacity-100",
)}
className={cn("absolute top-1/2 right-1.5 -translate-y-1/2 text-xs leading-none font-medium transition-opacity motion-reduce:transition-none", checked ? "text-foreground/55 opacity-0" : "text-foreground/55 opacity-100")}
>
{unCheckedChildren}
</span>
) : null}
{/* 开态文案(左侧,反色) */}
{checkedChildren !== undefined ? (
<span
aria-hidden="true"
className={cn(
"absolute top-1/2 left-1.5 -translate-y-1/2 text-xs leading-none font-medium transition-opacity motion-reduce:transition-none",
checked ? "text-background opacity-100" : "text-background opacity-0",
)}
>
<span aria-hidden="true" className={cn("absolute top-1/2 left-1.5 -translate-y-1/2 text-xs leading-none font-medium transition-opacity motion-reduce:transition-none", checked ? "text-background opacity-100" : "text-background opacity-0")}>
{checkedChildren}
</span>
) : null}
{/* thumb / loading 转圈 */}
{loading ? (
<span
aria-hidden="true"
className={cn(
"absolute inset-0 m-auto size-3.5 animate-spin rounded-full border-[1.5px] border-current border-t-transparent",
checked ? "text-background" : "text-foreground/60",
)}
/>
<span aria-hidden="true" className={cn("absolute inset-0 m-auto size-3.5 animate-spin rounded-full border-[1.5px] border-current border-t-transparent", checked ? "text-background" : "text-foreground/60")} />
) : (
<span
aria-hidden="true"
className={cn(
"sc-switch-thumb absolute left-0.5 top-1/2 -translate-y-1/2 rounded-full bg-surface-strong border border-border/80 transition-transform duration-200 ease-out motion-reduce:transition-none",
thumbClass,
)}
className={cn("sc-switch-thumb absolute left-0.5 top-1/2 -translate-y-1/2 rounded-full bg-surface-strong border border-border/80 transition-transform duration-200 ease-out motion-reduce:transition-none", thumbClass)}
/>
)}
</button>

View File

@@ -43,15 +43,7 @@ export function Tooltip({ title, placement = "top", delay = 350, className, chil
return (
<TooltipTrigger delay={delay}>
<span className="inline-flex">{children}</span>
<RACTooltip
placement={PLACEMENT_MAP[placement]}
offset={6}
className={cn(
"z-50 max-w-64 rounded-md border border-border bg-surface-strong px-2 py-1 text-xs leading-relaxed text-foreground shadow-md",
"ra-pop-in",
className,
)}
>
<RACTooltip placement={PLACEMENT_MAP[placement]} offset={6} className={cn("z-50 max-w-64 rounded-md border border-border bg-surface-strong px-2 py-1 text-xs leading-relaxed text-foreground shadow-md", "ra-pop-in", className)}>
{title}
</RACTooltip>
</TooltipTrigger>

View File

@@ -42,33 +42,15 @@ export function Callout({ tone = "default", title, icon, action, onClose, closeL
const resolved = tone === "default" ? "info" : tone;
const Icon = icon ?? calloutIconByTone[resolved];
return (
<div
data-slot="callout"
data-tone={resolved}
className={cn(
"flex items-start gap-2.5 rounded-lg border border-border bg-surface-secondary px-3.5 py-2.5",
className,
)}
{...props}
>
<div data-slot="callout" data-tone={resolved} className={cn("flex items-start gap-2.5 rounded-lg border border-border bg-surface-secondary px-3.5 py-2.5", className)} {...props}>
<Icon
aria-hidden
strokeWidth={1.75}
className={cn(
"mt-px size-4 shrink-0",
resolved === "success" && "text-status-success",
resolved === "warning" && "text-status-warning",
resolved === "error" && "text-status-error",
resolved === "info" && "text-muted-foreground",
)}
className={cn("mt-px size-4 shrink-0", resolved === "success" && "text-status-success", resolved === "warning" && "text-status-warning", resolved === "error" && "text-status-error", resolved === "info" && "text-muted-foreground")}
/>
<div className="min-w-0 flex-1">
{title != null ? <p className="text-caption font-medium text-foreground">{title}</p> : null}
{children != null ? (
<div className={cn("text-caption leading-relaxed text-muted-foreground", title != null && "mt-0.5")}>
{children}
</div>
) : null}
{children != null ? <div className={cn("text-caption leading-relaxed text-muted-foreground", title != null && "mt-0.5")}>{children}</div> : null}
</div>
{action != null || onClose != null ? (
<div className="flex shrink-0 items-start gap-1">

View File

@@ -38,15 +38,7 @@ export type EmptyStateProps = HTMLAttributes<HTMLDivElement> &
action?: ReactNode;
};
export function EmptyState({
size,
icon: Icon = Inbox,
title,
description,
action,
className,
...props
}: EmptyStateProps) {
export function EmptyState({ size, icon: Icon = Inbox, title, description, action, className, ...props }: EmptyStateProps) {
return (
<div data-slot="empty-state" className={cn(emptyStateVariants({ size }), className)} {...props}>
<div className="flex size-11 shrink-0 items-center justify-center rounded-full bg-secondary/60">

View File

@@ -82,13 +82,7 @@ export function WorkflowFieldMappingEditor({ fields, onChange, disabled = false
<Button type="text" size="small" danger icon={<PowerOff className="size-3.5" />} disabled={disabled || enabledControllableCount === 0} onClick={() => updateAllFields(false)}>
关闭全部
</Button>
<ToolButton
size="sm"
icon={<ListFilter />}
active={showOnlyEnabled}
label={showOnlyEnabled ? "显示全部" : "仅显示已开启"}
onClick={() => setShowOnlyEnabled((current) => !current)}
/>
<ToolButton size="sm" icon={<ListFilter />} active={showOnlyEnabled} label={showOnlyEnabled ? "显示全部" : "仅显示已开启"} onClick={() => setShowOnlyEnabled((current) => !current)} />
</div>
</div>
<Collapse

View File

@@ -140,16 +140,7 @@ export function AdminExportButton({
}
export function AdminTableEmpty({ filtered = false, title, description, action }: { filtered?: boolean; title?: string; description?: string; action?: ReactNode }) {
return (
<EmptyState
size="compact"
icon={SearchX}
title={title || (filtered ? "没有符合筛选条件的数据" : "暂无数据")}
description={description}
action={action}
className="min-h-40 w-full justify-center"
/>
);
return <EmptyState size="compact" icon={SearchX} title={title || (filtered ? "没有符合筛选条件的数据" : "暂无数据")} description={description} action={action} className="min-h-40 w-full justify-center" />;
}
export function AdminFilterChip({ label, onRemove }: { label: ReactNode; onRemove: () => void }) {

View File

@@ -312,7 +312,11 @@ export function CapabilityScopeEditor({ capability, sourceSpecs, value, onChange
</CapabilityBlock>
) : null}
{!sourceInputs.length && !sourceOptions.length && !source.operations?.length ? <Callout tone="info" title="该类型暂无额外能力参数">线路仍可按优先级和权重参与路由。</Callout> : null}
{!sourceInputs.length && !sourceOptions.length && !source.operations?.length ? (
<Callout tone="info" title="该类型暂无额外能力参数">
线路仍可按优先级和权重参与路由。
</Callout>
) : null}
</div>
);
}

View File

@@ -499,7 +499,9 @@ export default function AdminPaymentsPage() {
}
scroll
>
<Callout className="my-4" tone="info" title="平台不提供支付退款">管理端仅提供查单、关单和对账。关单前始终先向渠道查单;对账发现已支付未入账订单时会幂等补发积分。</Callout>
<Callout className="my-4" tone="info" title="平台不提供支付退款">
管理端仅提供查单、关单和对账。关单前始终先向渠道查单;对账发现已支付未入账订单时会幂等补发积分。
</Callout>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
@@ -612,7 +614,11 @@ export default function AdminPaymentsPage() {
>
{providerDrawer ? (
<Form form={providerForm} layout="vertical" requiredMark="optional">
<Callout className="mb-4" tone={providerDrawer.pluginEnabled ? "info" : "warning"} title={providerDrawer.pluginEnabled ? "密钥会加密保存,历史订单固定使用创建时的配置版本。" : "该宿主插件当前已在插件管理中停用;保存配置后仍需开放插件才能接受新订单。"} />
<Callout
className="mb-4"
tone={providerDrawer.pluginEnabled ? "info" : "warning"}
title={providerDrawer.pluginEnabled ? "密钥会加密保存,历史订单固定使用创建时的配置版本。" : "该宿主插件当前已在插件管理中停用;保存配置后仍需开放插件才能接受新订单。"}
/>
<Form.Item name="enabled" label="渠道配置启用" valuePropName="checked">
<Switch />
</Form.Item>
@@ -700,7 +706,11 @@ export default function AdminPaymentsPage() {
setDetailItems([]);
}}
>
{detailRun?.error ? <Callout className="mb-4" tone="error" title="对账执行失败">{detailRun.error}</Callout> : null}
{detailRun?.error ? (
<Callout className="mb-4" tone="error" title="对账执行失败">
{detailRun.error}
</Callout>
) : null}
<AdminDataTable
toolbar={
<Select

View File

@@ -239,8 +239,8 @@ export function ComfyUIBridgeSettingsPane() {
<h4 className="text-sm font-semibold">安装 Bridge</h4>
<p className="mt-1 text-xs text-foreground/60">在能访问 ComfyUI 的机器上运行;Windows 使用独立程序,Linux 使用对应架构的原生程序,不需要安装 Node.js、npm 或项目源码。</p>
</div>
<SegmentedControl
size="sm"
<SegmentedControl
size="sm"
value={bridgePlatform}
options={[
{ label: "Windows", value: "windows" },
@@ -252,8 +252,8 @@ size="sm"
{bridgePlatform === "linux" ? (
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-foreground/65">
<span>Linux 架构:</span>
<SegmentedControl
size="sm"
<SegmentedControl
size="sm"
value={linuxArchitecture}
options={[
{ label: "x64", value: "amd64" },
@@ -372,7 +372,7 @@ size="sm"
<Input value={config.workflowId} placeholder="workflow.json" onChange={(event) => update({ workflowId: event.target.value })} onBlur={(event) => selectWorkflow(event.target.value.trim())} />
</Form.Item>
<div className="workflow-workspace-tabs lg:col-span-12">
<SegmentedControl
<SegmentedControl
block
value={workspaceMode}
options={[

View File

@@ -99,9 +99,18 @@ export default function DiagnosticsPanel({ taskId, projectId }: DiagnosticsPanel
<span className="rounded-full bg-foreground/[.045] px-2.5 py-1 text-[var(--fs-tiny)] font-medium text-foreground/48">最长 24 小时</span>
</div>
<div className="mt-4 grid gap-4 md:grid-cols-[minmax(0,230px)_minmax(0,1fr)] md:items-end">
<label className="block">
<label className="block">
<span className="mb-2 block text-xs font-semibold text-foreground/65">时间范围</span>
<Select ariaLabel="时间范围" className="w-full" value={range} options={rangeOptions} onChange={(value) => { const next = rangeOptions.find((option) => option.value === value)?.value; if (next) setRange(next); }} />
<Select
ariaLabel="时间范围"
className="w-full"
value={range}
options={rangeOptions}
onChange={(value) => {
const next = rangeOptions.find((option) => option.value === value)?.value;
if (next) setRange(next);
}}
/>
</label>
<div className="rounded-lg border border-border/55 bg-foreground/[.025] px-3.5 py-3" aria-live="polite">
<div className="flex items-center gap-2 text-sm font-medium text-foreground/78">

View File

@@ -194,7 +194,12 @@ export function LocalCliSettings() {
<h2 id="dreamina-cli-title" className="text-base font-semibold">
Dreamina CLI
</h2>
<StatusBadge variant="filled" className="m-0" tone={presentation.dreamina.tone === "processing" ? "loading" : presentation.dreamina.tone === "default" ? "neutral" : presentation.dreamina.tone} label={presentation.dreamina.label} />
<StatusBadge
variant="filled"
className="m-0"
tone={presentation.dreamina.tone === "processing" ? "loading" : presentation.dreamina.tone === "default" ? "neutral" : presentation.dreamina.tone}
label={presentation.dreamina.label}
/>
{status?.version ? <Tag className="m-0">v{status.version}</Tag> : null}
{presentation.dreamina.creditLabel ? (
<Tag color="blue" className="m-0">

View File

@@ -261,7 +261,7 @@ export function RunningHubSettingsPane() {
/>
</Form.Item>
<Form.Item label="类型" className="mb-0 lg:col-span-3">
<SegmentedControl
<SegmentedControl
block
value={kind}
options={[
@@ -296,7 +296,7 @@ export function RunningHubSettingsPane() {
</div>
</Form.Item>
<div className="workflow-workspace-tabs lg:col-span-12">
<SegmentedControl
<SegmentedControl
block
value={workspaceMode}
options={[