style(web): UI kit 组件与画布/后台面板 Prettier 格式化与 migrations.go gofmt 清理
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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。
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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={[
|
||||
|
||||
Reference in New Issue
Block a user