feat(canvas): 设计token系统 - 建立三层架构骨架 (#101)

* feat(canvas): 设计token系统 - 建立三层架构骨架 #94 #96

建立 Primitive→Semantic→Component 三层设计 token 体系,覆盖 9 个维度:
色彩/间距/字号/圆角/阴影/Elevation/动效/描边/不透明度。

- Primitive 层:纯值无语义,含 neutral 色板+节点类型色板+状态色、
  4px 栅格间距、10档字号、10档圆角、阴影+glow、12档 z-index、
  动效(duration+easing+delay+motion-scale)、描边、不透明度
- Semantic 层:引用 Primitive,随主题切换,含通用语义色、
  Canvas 语义(--cn-*)、Aceternity spatial 语义、节点类型语义、状态语义
- Component 层:引用 Semantic,含 Dock/Node/Modal/Panel/Prompt面板/
  进度条/缩放控制专属 token
- @theme inline 映射:节点类型色、状态色、Canvas 语义色、字号映射到 Tailwind utility
- prefers-reduced-motion 三级降级(L0正常/L1简化/L2禁用)
- AGENTS.md 新增 Design Token 使用规范,禁止新增裸 px 值
- 附带修复 canvas-drawing-excalidraw-editor.tsx TS7006 类型注解
- 附带修复 canvas-drawing-editor-modal.tsx TS2339 engine 字段缺失

Wayfinder 决策来源:#95 #96 #97 #98 #99

* fix(canvas): 设计token系统 - 修复CSS变量循环引用和命名兼容性 #94 #96

修复两个问题:

1. CSS变量循环引用:Primitive层 --color-node-*/--color-status-* 与
   @theme inline 中的 --color-node-*/--color-status-* 同名,经 Semantic
   层中转后形成循环(A→B→A)。改用 --palette- 前缀隔离 Primitive 色板,
   打断循环链路。

2. 变量名小数点兼容性:--space-0.5/1.5/2.5/3.5 中的小数点被部分 CSS
   linter 误判为类选择器。改为 --space-half/-1-half/-2-half/-3-half,
   保持语义清晰的同时提升工具链兼容性。

* refactor(canvas): 画布核心组件 - 迁移硬编码值到设计token #94

批次1:5个画布核心组件的裸 text-[Npx]/rounded-[Npx]/z-[N] 替换为
对应设计token,统一视觉语言:

- canvas-node.tsx:text-[10/11/9/17px]→fs-tiny/label/micro/heading-lg,
  rounded-[18px]→node-radius
- canvas-node-hover-toolbar.tsx:text-[11/17/10/9px]→fs-*,
  rounded-[14/9px]→dock-radius/dock-item-radius,z-[70]→z-panel-floating
- canvas-toolbar.tsx:text-[9/10/11px]→fs-*,
  rounded-[17/16/11/10px]→panel-radius/dock-item-radius-*,
  z-[30/40]→z-toolbar/z-toolbar-floating
- canvas-assistant-panel.tsx:text-[8/9/10/11px]→fs-*
- canvas-script-node.tsx:text-[9/10/11px]→fs-*

视觉零回归(token值与原硬编码值一致),代码可维护性提升。

* refactor(canvas): 画布辅助组件 - 迁移硬编码值到设计token #94

批次2:6个画布辅助组件的裸 text-[Npx]/rounded-[Npx]/z-[N] 替换为
对应设计token:

- canvas-style-picker-modal.tsx:text-[10/11px]→fs-tiny/label
- canvas-file-drop-overlay.tsx:z-[110]→z-modal-overlay,
  rounded-[28/20px]→r-5xl/r-3xl
- canvas-character-reference-node.tsx:text-[10px]→fs-tiny
- canvas-drawing-editor-modal.tsx:text-[11px]→fs-label
- canvas-video-prompt-tools.tsx:z-[1500]→z-toast,
  rounded-[12/10px]→r-lg/dock-item-radius
- asset-picker-modal.tsx:text-[10px]→fs-tiny

视觉零回归(token值与原硬编码值一致)。

* refactor(*): 全项目硬编码值 - 批量迁移到设计token #94

批次3:全项目 .tsx 文件中 372 处裸 text-[Npx]/rounded-[Npx] 批量
替换为对应设计token,剩余 25 处无直接 token 对应的低频值保留。

替换覆盖(按频率):
- text-[10px]→fs-tiny (132处)
- text-[11px]→fs-label (100处)
- text-[9px]→fs-micro (45处)
- text-[12px]→fs-caption, text-[13/14px]→fs-body, text-[15px]→fs-body-lg
- text-[18px]→fs-heading-lg, text-[22px]→fs-title, text-[8px]→fs-micro
- rounded-[9/10px]→dock-item-radius, rounded-[12px]→r-lg
- rounded-[14px]→dock-radius, rounded-[16px]→r-2xl
- rounded-[17px]→panel-radius, rounded-[8px]→r-md

涉及 components/ 和 pages/ 下所有 .tsx 文件。
视觉零回归(token值与原硬编码值一致)。

* feat(canvas): 节点视觉层次规范 - 色带+状态徽章+四态递进 #97 #94

基于 Wayfinder 地图 #97 决议执行节点视觉重设计:

1. 节点类型色带(决策1)
   - 新增 nodeTypeColorVar() 函数,10种节点类型对应色带色
   - 节点左侧3px色条,引用 --cn-type-* token
   - 远距离最强信号,用户扫视画布即可识别节点类型

2. 节点状态徽章(决策2)
   - 新增 NodeStatusBadge 组件,loading/success/error 三态
   - 左上角圆角徽章,backdrop-blur 玻璃质感
   - success 态 2s 后自动淡出(canvas-status-success-fade 动画)

3. 四态递进视觉权重(决策3)
   - idle:无额外阴影
   - hover:轻微抬升阴影(16px 48px)
   - selected:2px 边框环(accent.primary 40% 透明度)
   - selected-primary:4px 软色环 + resize handle
   - active:6px 外发光环 + inner glow(最强强调)
   - 120-150ms ease-out 过渡

4. 阴影迁移
   - boxShadow 从硬编码改为分层语义化表达
   - 边框色 hover 态使用 color-mix 动态加深

Closes #97

---------

Co-authored-by: sikongyue <3155157671@qq.com>
This commit is contained in:
爱笑的毛毛虫
2026-08-03 21:38:38 +08:00
committed by GitHub
parent d83ec033bb
commit b4d2b34fae
79 changed files with 844 additions and 400 deletions

View File

@@ -53,7 +53,7 @@ function PickerCard({ asset, onClick }: { asset: InsertableAsset; onClick: () =>
<div className="p-2.5">
<div className="flex items-center justify-between gap-2">
<span className="line-clamp-1 text-xs font-medium text-stone-800 dark:text-stone-200">{title}</span>
<Tag className="m-0 shrink-0 text-[10px]">{kind === "image" ? "图片" : kind === "video" ? "视频" : kind === "audio" ? "音频" : "文本"}</Tag>
<Tag className="m-0 shrink-0 text-[var(--fs-tiny)]">{kind === "image" ? "图片" : kind === "video" ? "视频" : kind === "audio" ? "音频" : "文本"}</Tag>
</div>
</div>
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-stone-950/0 text-sm font-medium text-white opacity-0 transition group-hover:bg-stone-950/55 group-hover:opacity-100">插入</div>

View File

@@ -44,7 +44,7 @@ export function CanvasActiveTaskPanel({ tasks }: { tasks: GenerationTask[] }) {
<LayoutGroup id="canvas-active-tasks">
<motion.section
layout
className="pointer-events-auto overflow-hidden rounded-[17px] border backdrop-blur-2xl"
className="pointer-events-auto overflow-hidden rounded-[var(--panel-radius)] border backdrop-blur-2xl"
style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 24px 72px ${theme.spatial.shadow}` }}
aria-label="当前画布生成任务"
>
@@ -57,12 +57,12 @@ export function CanvasActiveTaskPanel({ tasks }: { tasks: GenerationTask[] }) {
aria-controls="canvas-active-task-list"
>
<span className="flex min-w-0 items-center gap-2">
<span className="grid size-8 shrink-0 place-items-center rounded-[10px]" style={{ background: theme.accent.primarySoft, color: theme.accent.primary }}>
<span className="grid size-8 shrink-0 place-items-center rounded-[var(--dock-item-radius)]" style={{ background: theme.accent.primarySoft, color: theme.accent.primary }}>
<ListTodo className="size-4" />
</span>
<span className="min-w-0">
<span className="block text-sm font-semibold leading-5">生成任务</span>
<span className="block truncate text-[11px]" style={{ color: theme.node.muted }}>当前画布 · {tasks.length} 个进行中</span>
<span className="block truncate text-[var(--fs-label)]" style={{ color: theme.node.muted }}>当前画布 · {tasks.length} 个进行中</span>
</span>
</span>
<span className="flex shrink-0 items-center gap-2" style={{ color: theme.accent.primary }}>
@@ -127,9 +127,9 @@ function ActiveTaskCard({ task, now, theme, expanded, onToggle, reducedMotion }:
<span className="min-w-0 flex-1">
<span className="flex items-center justify-between gap-2">
<span className="truncate text-xs font-semibold" title={formatTaskKind(task)}>{formatTaskKind(task)}</span>
<span className="shrink-0 rounded-full border px-1.5 py-0.5 text-[10px] font-medium" style={{ borderColor: `${statusTone}44`, color: statusTone }}>{statusLabel[task.status]}</span>
<span className="shrink-0 rounded-full border px-1.5 py-0.5 text-[var(--fs-tiny)] font-medium" style={{ borderColor: `${statusTone}44`, color: statusTone }}>{statusLabel[task.status]}</span>
</span>
<span className="mt-1 block truncate text-[11px]" style={{ color: theme.node.muted }} title={task.stage || statusLabel[task.status]}>{task.stage || statusLabel[task.status]}</span>
<span className="mt-1 block truncate text-[var(--fs-label)]" style={{ color: theme.node.muted }} title={task.stage || statusLabel[task.status]}>{task.stage || statusLabel[task.status]}</span>
</span>
{expanded ? <ChevronUp className="mt-0.5 size-3.5 shrink-0" style={{ color: theme.node.muted }} /> : <ChevronDown className="mt-0.5 size-3.5 shrink-0" style={{ color: theme.node.muted }} />}
</div>
@@ -138,7 +138,7 @@ function ActiveTaskCard({ task, now, theme, expanded, onToggle, reducedMotion }:
<motion.div className="h-full rounded-full" animate={{ width: `${progress ?? 8}%` }} transition={reducedMotion ? { duration: 0 } : { duration: 0.3 }} style={{ background: statusTone }} />
</div>
<div className="mt-3 grid grid-cols-2 gap-2 text-[10px]" style={{ color: theme.node.muted }}>
<div className="mt-3 grid grid-cols-2 gap-2 text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>
<span className="inline-flex min-w-0 items-center gap-1 truncate" title={durationLabel}><Clock3 className="size-3 shrink-0" />{durationLabel}</span>
<span className="inline-flex min-w-0 items-center justify-end gap-1 truncate" title={billingLabel}><Coins className="size-3 shrink-0" />{billingLabel}</span>
</div>
@@ -151,7 +151,7 @@ function ActiveTaskCard({ task, now, theme, expanded, onToggle, reducedMotion }:
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={transition}
className="border-t px-3 pb-3 pt-2 text-[11px]"
className="border-t px-3 pb-3 pt-2 text-[var(--fs-label)]"
style={{ borderColor: theme.toolbar.border, color: theme.node.muted }}
>
<div className="flex items-center justify-between gap-2">

View File

@@ -49,7 +49,7 @@ export function AgentChatMessage({ item, theme, user, onRejectTool, onApproveToo
<div className={`min-w-0 max-w-[86%] text-sm leading-6 ${isUser ? "rounded-md px-3 py-2.5 text-right" : "text-left"}`} style={{ color, ...(isUser ? { background: theme.accent.primarySoft } : {}) }}>
<div className="whitespace-pre-wrap break-words text-left">{item.text}</div>
{item.attachments?.length ? <AgentMessageAttachments attachments={item.attachments} /> : null}
{item.meta ? <div className="mt-1 text-[11px] opacity-45">{item.meta}</div> : null}
{item.meta ? <div className="mt-1 text-[var(--fs-label)] opacity-45">{item.meta}</div> : null}
</div>
{isUser ? <AgentUserAvatar user={user} theme={theme} /> : null}
</div>
@@ -69,7 +69,7 @@ export function AgentPendingToolCard({ summary, detail, theme, onReject, onAppro
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2 text-sm font-semibold leading-5">
<span>确认工具调用</span>
<span className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium" style={{ color: "#d97706", background: "rgba(217,119,6,.1)" }}>等待确认</span>
<span className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[var(--fs-label)] font-medium" style={{ color: "#d97706", background: "rgba(217,119,6,.1)" }}>等待确认</span>
</div>
<div className="mt-2 text-sm leading-6" style={{ color: theme.node.text }}>{summary}</div>
</div>
@@ -103,7 +103,7 @@ export function AgentPendingToolCard({ summary, detail, theme, onReject, onAppro
}
function ImpactMetric({ label, value, attention = false, theme }: { label: string; value: number; attention?: boolean; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
return <div className="px-1 py-1"><div className="text-[10px]" style={{ color: theme.node.muted }}>{label}</div><div className="mt-0.5 text-sm font-semibold tabular-nums" style={{ color: attention ? "#d97706" : theme.node.text }}>{value}</div></div>;
return <div className="px-1 py-1"><div className="text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>{label}</div><div className="mt-0.5 text-sm font-semibold tabular-nums" style={{ color: attention ? "#d97706" : theme.node.text }}>{value}</div></div>;
}
function agentImpactFromDetail(detail: unknown) {
@@ -132,7 +132,7 @@ export function AgentToolCard({ title, text, detail, theme }: { title: string; t
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2 text-sm font-semibold leading-5">
<span className="min-w-0 truncate">{title}</span>
<span className="inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium" style={{ color: state.color, background: state.softBg }}>
<span className="inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-[var(--fs-label)] font-medium" style={{ color: state.color, background: state.softBg }}>
{state.label}
</span>
{detail ? <span className="ml-auto text-xs font-normal" style={{ color: theme.node.muted }}>详情</span> : null}
@@ -272,7 +272,7 @@ export function AgentPanelTabs<T extends string>({ value, items, theme, right, o
function AgentDetailBlock({ detail, theme }: { detail: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
return (
<pre className="thin-scrollbar mt-3 max-h-64 overflow-auto rounded-md p-3 text-[11px] leading-4" style={{ background: theme.toolbar.panel, color: theme.node.muted }}>
<pre className="thin-scrollbar mt-3 max-h-64 overflow-auto rounded-md p-3 text-[var(--fs-label)] leading-4" style={{ background: theme.toolbar.panel, color: theme.node.muted }}>
{JSON.stringify(detail, null, 2)}
</pre>
);

View File

@@ -38,7 +38,7 @@ export function AgentPanelChrome({
</span>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold leading-5">Agent</div>
<div className="truncate text-[11px] leading-4" style={{ color: theme.node.muted }}>画布协作</div>
<div className="truncate text-[var(--fs-label)] leading-4" style={{ color: theme.node.muted }}>画布协作</div>
</div>
<AgentModeSwitch value={mode} theme={theme} onChange={onModeChange} />
<Tooltip title="收起 Agent">
@@ -46,7 +46,7 @@ export function AgentPanelChrome({
</Tooltip>
</div>
<div className="mt-2 flex min-h-8 flex-wrap items-center gap-x-2.5 gap-y-1 px-0.5 text-[11px]" style={{ color: theme.node.muted }}>
<div className="mt-2 flex min-h-8 flex-wrap items-center gap-x-2.5 gap-y-1 px-0.5 text-[var(--fs-label)]" style={{ color: theme.node.muted }}>
<span className="font-medium" style={{ color: theme.node.text }}>{context.nodeCount} 个节点</span>
{context.selectedCount ? <span className="inline-flex items-center gap-1"><Focus className="size-3" />选中 {context.selectedCount}</span> : <span>未选择节点</span>}
{context.chapterLabel ? <span className="inline-flex min-w-0 items-center gap-1"><BookOpenText className="size-3 shrink-0" /><span className="max-w-32 truncate">{context.chapterLabel}{context.shotLabel ? ` · ${context.shotLabel}` : ""}</span></span> : null}
@@ -67,7 +67,7 @@ export function AgentPanelChrome({
function AgentModeSwitch({ value, theme, onChange }: { value: CanvasAgentMode; theme: CanvasTheme; onChange: (value: CanvasAgentMode) => void }) {
return (
<div className="inline-flex h-8 shrink-0 items-center rounded-md p-0.5 text-[11px]" style={{ background: theme.spatial.surface }} role="group" aria-label="Agent 运行位置">
<div className="inline-flex h-8 shrink-0 items-center rounded-md p-0.5 text-[var(--fs-label)]" style={{ background: theme.spatial.surface }} role="group" aria-label="Agent 运行位置">
{(["online", "local"] as const).map((item) => {
const active = value === item;
const Icon = item === "online" ? Globe2 : Laptop;
@@ -95,7 +95,7 @@ export function AgentChatEmptyState({ theme, nodeCount, onSelect }: { theme: Can
<div className="mx-auto w-full max-w-[380px]">
<div className="flex items-center gap-2">
<span className="grid size-7 place-items-center rounded-md" style={{ background: theme.accent.primarySoft, color: theme.accent.primary }}><Bot className="size-3.5" /></span>
<span className="text-[11px] font-medium" style={{ color: theme.node.muted }}>{nodeCount} 个节点已就绪</span>
<span className="text-[var(--fs-label)] font-medium" style={{ color: theme.node.muted }}>{nodeCount} 个节点已就绪</span>
</div>
<h2 className="mt-3 text-[19px] font-semibold leading-6" style={{ color: theme.node.text }}>从当前画布开始</h2>
<div className="mt-4 grid grid-cols-1 gap-1">

View File

@@ -143,12 +143,12 @@ export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true,
<div className="flex items-center justify-between gap-2 px-1 pb-2.5 pt-1.5">
<div className="flex min-w-0 items-center gap-2">
<span className="grid size-8 shrink-0 place-items-center rounded-[10px] border" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.accent.primary }}>
<span className="grid size-8 shrink-0 place-items-center rounded-[var(--dock-item-radius)] border" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.accent.primary }}>
<FolderOpen className="size-3.5" />
</span>
<span className="min-w-0">
<span className="block text-xs font-semibold">素材空间</span>
<span className="mt-0.5 block truncate text-[9px]" style={{ color: theme.node.muted }}>拖入画布,或定位已经使用的图片</span>
<span className="mt-0.5 block truncate text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>拖入画布,或定位已经使用的图片</span>
</span>
</div>
<motion.button type="button" whileHover={motionEnabled ? { rotate: -5, scale: 1.05 } : undefined} whileTap={motionEnabled ? { scale: 0.92 } : undefined} className="grid size-7 shrink-0 place-items-center rounded-full border outline-none focus-visible:ring-2" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.node.muted }} onClick={() => setOpen(false)} aria-label="收起素材空间">
@@ -156,14 +156,14 @@ export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true,
</motion.button>
</div>
<div className={cn("relative grid gap-1 rounded-[12px] border p-0.5", showLibrary ? "grid-cols-2" : "grid-cols-1")} style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
<div className={cn("relative grid gap-1 rounded-[var(--r-lg)] border p-0.5", showLibrary ? "grid-cols-2" : "grid-cols-1")} style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
{showLibrary ? <TrayTabButton active={tab === "library"} label={`素材库 ${assetImages.length}`} theme={theme} onClick={() => setTab("library")} /> : null}
<TrayTabButton active={tab === "canvas"} label={`当前画布 ${canvasImages.length}`} theme={theme} onClick={() => setTab("canvas")} />
</div>
<label className="mt-2 flex h-8 items-center gap-1.5 rounded-[11px] border px-2.5 focus-within:ring-2" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
<Search className="size-3.5 shrink-0" style={{ color: theme.node.muted }} />
<input type="search" value={keyword} onChange={(event) => setKeyword(event.target.value)} placeholder="搜索图片素材..." className="min-w-0 flex-1 bg-transparent text-[10px] outline-none placeholder:opacity-55" aria-label="搜索图片素材" />
<input type="search" value={keyword} onChange={(event) => setKeyword(event.target.value)} placeholder="搜索图片素材..." className="min-w-0 flex-1 bg-transparent text-[var(--fs-tiny)] outline-none placeholder:opacity-55" aria-label="搜索图片素材" />
{keyword ? <button type="button" className="grid size-6 shrink-0 place-items-center rounded-full opacity-55 hover:opacity-100" onClick={() => setKeyword("")} aria-label="清空搜索"><X className="size-3" /></button> : null}
</label>
@@ -189,7 +189,7 @@ export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true,
)}
</div>
<div className="flex items-center justify-between px-1 pt-2.5 text-[10px]" style={{ color: theme.node.muted }}>
<div className="flex items-center justify-between px-1 pt-2.5 text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>
<span>{showLibrary && tab === "library" ? "点击插入 · 拖拽定位" : "点击回到节点"}</span>
<span className="rounded-full border px-2 py-0.5 tabular-nums" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>{activeItems.length} 项</span>
</div>
@@ -206,8 +206,8 @@ type CanvasTheme = (typeof canvasThemes)[keyof typeof canvasThemes];
function TrayTabButton({ active, label, theme, onClick }: { active: boolean; label: string; theme: CanvasTheme; onClick: () => void }) {
return (
<button type="button" className={cn("relative z-10 h-7 rounded-[9px] px-2 text-[10px] font-semibold outline-none transition-colors focus-visible:ring-2", active ? "" : "opacity-55 hover:opacity-90")} style={{ color: active ? theme.node.text : theme.node.muted }} onClick={onClick}>
{active ? <motion.span layoutId="canvas-asset-tray-active-tab" className="absolute inset-0 -z-10 rounded-[9px] border" style={{ background: theme.node.panel, borderColor: theme.toolbar.border, boxShadow: `0 6px 16px ${theme.spatial.shadow}` }} transition={aceternityMotion.spring.dock} /> : null}
<button type="button" className={cn("relative z-10 h-7 rounded-[var(--dock-item-radius)] px-2 text-[var(--fs-tiny)] font-semibold outline-none transition-colors focus-visible:ring-2", active ? "" : "opacity-55 hover:opacity-90")} style={{ color: active ? theme.node.text : theme.node.muted }} onClick={onClick}>
{active ? <motion.span layoutId="canvas-asset-tray-active-tab" className="absolute inset-0 -z-10 rounded-[var(--dock-item-radius)] border" style={{ background: theme.node.panel, borderColor: theme.toolbar.border, boxShadow: `0 6px 16px ${theme.spatial.shadow}` }} transition={aceternityMotion.spring.dock} /> : null}
{label}
</button>
);
@@ -223,19 +223,19 @@ function AssetTrayRow({ title, imageUrl, storageKey, icon, active = false, dragg
whileHover={motionEnabled ? { x: 4, scale: 1.008 } : undefined}
whileTap={motionEnabled ? { scale: 0.985 } : undefined}
transition={aceternityMotion.spring.dock}
className="group grid h-12 w-full grid-cols-[36px_minmax(0,1fr)_auto_24px] items-center gap-1.5 rounded-[12px] border px-1.5 text-left outline-none focus-visible:ring-2"
className="group grid h-12 w-full grid-cols-[36px_minmax(0,1fr)_auto_24px] items-center gap-1.5 rounded-[var(--r-lg)] border px-1.5 text-left outline-none focus-visible:ring-2"
style={{ background: active ? theme.accent.primarySoft : theme.spatial.surface, borderColor: active ? theme.spatial.glowStrong : theme.toolbar.border, color: active ? theme.accent.primary : theme.node.text }}
onClick={onClick}
onDragStartCapture={onDragStart}
>
<span className="grid size-9 shrink-0 place-items-center overflow-hidden rounded-[9px] border" style={{ background: theme.node.fill, borderColor: theme.toolbar.border }}>
<span className="grid size-9 shrink-0 place-items-center overflow-hidden rounded-[var(--dock-item-radius)] border" style={{ background: theme.node.fill, borderColor: theme.toolbar.border }}>
{imageUrl ? <img src={imageUrl} alt="" width={36} height={36} className="size-full object-cover" draggable={false} /> : <ImageIcon className="size-3.5 opacity-55" />}
</span>
<span className="min-w-0">
<span className="block truncate text-[10px] font-semibold">{title}</span>
<span className="mt-0.5 block text-[8px] opacity-45">{active ? "当前已选择" : draggable ? "拖入画布或点击插入" : "点击定位到画布"}</span>
<span className="block truncate text-[var(--fs-tiny)] font-semibold">{title}</span>
<span className="mt-0.5 block text-[var(--fs-micro)] opacity-45">{active ? "当前已选择" : draggable ? "拖入画布或点击插入" : "点击定位到画布"}</span>
</span>
<span className={cn("rounded-full px-1.5 py-0.5 text-[9px] font-semibold", location === "oss" ? "bg-emerald-500/12 text-emerald-700 dark:text-emerald-300" : location === "local" ? "bg-amber-500/12 text-amber-700 dark:text-amber-300" : "bg-black/5 text-stone-400 dark:bg-white/8 dark:text-stone-500")} title={resourceStorageTitle(storageKey)}>
<span className={cn("rounded-full px-1.5 py-0.5 text-[var(--fs-micro)] font-semibold", location === "oss" ? "bg-emerald-500/12 text-emerald-700 dark:text-emerald-300" : location === "local" ? "bg-amber-500/12 text-amber-700 dark:text-amber-300" : "bg-black/5 text-stone-400 dark:bg-white/8 dark:text-stone-500")} title={resourceStorageTitle(storageKey)}>
{resourceStorageLabel(storageKey)}
</span>
<span className="grid size-6 place-items-center rounded-full border opacity-45 transition-opacity group-hover:opacity-90" style={{ background: theme.node.panel, borderColor: theme.toolbar.border }}>{icon}</span>
@@ -245,7 +245,7 @@ function AssetTrayRow({ title, imageUrl, storageKey, icon, active = false, dragg
function TrayEmpty({ text, theme }: { text: string; theme: CanvasTheme }) {
return (
<div className="grid h-full min-h-[200px] place-items-center rounded-[14px] border border-dashed text-center" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.node.muted }}>
<div className="grid h-full min-h-[200px] place-items-center rounded-[var(--dock-radius)] border border-dashed text-center" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.node.muted }}>
<span><Images className="mx-auto size-5 opacity-35" /><span className="mt-2 block text-xs opacity-55">{text}</span></span>
</div>
);

View File

@@ -795,7 +795,7 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, project
left={
<>
<AgentTextModelPicker config={effectiveConfig} value={effectiveConfig.textModel} onChange={(model) => updateConfig("textModel", model)} />
{cinematicEntryActive ? <span className="ml-2 inline-flex h-6 items-center rounded-md px-2 text-[10px] font-medium" style={{ background: theme.spatial.surface, color: theme.node.muted }}>影视项目</span> : null}
{cinematicEntryActive ? <span className="ml-2 inline-flex h-6 items-center rounded-md px-2 text-[var(--fs-tiny)] font-medium" style={{ background: theme.spatial.surface, color: theme.node.muted }}>影视项目</span> : null}
</>
}
/>
@@ -952,13 +952,13 @@ function AssistantHistory({
<div className="flex items-center gap-2">
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-1.5">
{session.id === activeSession?.id ? <span className="shrink-0 text-[10px] font-medium" style={{ color: theme.node.text }}>当前</span> : null}
{session.id === activeSession?.id ? <span className="shrink-0 text-[var(--fs-tiny)] font-medium" style={{ color: theme.node.text }}>当前</span> : null}
<div className="truncate text-sm font-medium leading-5">{session.title}</div>
</div>
<div className="truncate text-[11px] leading-4 opacity-65">{sessionPreview(session)}</div>
<div className="truncate text-[var(--fs-label)] leading-4 opacity-65">{sessionPreview(session)}</div>
</div>
<div className="flex shrink-0 items-center gap-1">
<span className="text-[10px] opacity-55">{formatSessionTime(session.updatedAt || session.createdAt)}</span>
<span className="text-[var(--fs-tiny)] opacity-55">{formatSessionTime(session.updatedAt || session.createdAt)}</span>
<Button size="small" className="!h-6 !px-2" onClick={() => onOpen(session.id)}>
进入
</Button>
@@ -1057,7 +1057,7 @@ function AssistantReferenceChip({ item, label, onRemove }: { item: CanvasAssista
{item.dataUrl ? (
<span className="relative block size-8 shrink-0">
<img src={item.dataUrl} alt="" className="size-8 rounded-lg object-cover" />
{label ? <span className="absolute left-0.5 top-0.5 rounded bg-black/60 px-1 py-0.5 text-[8px] font-medium leading-none text-white">{label}</span> : null}
{label ? <span className="absolute left-0.5 top-0.5 rounded bg-black/60 px-1 py-0.5 text-[var(--fs-micro)] font-medium leading-none text-white">{label}</span> : null}
</span>
) : (
<span className="grid size-8 place-items-center rounded-md text-sm font-medium" style={{ background: theme.spatial.surface }}>

View File

@@ -27,7 +27,7 @@ export function CanvasCharacterReferenceModal({ node, open, onClose }: { node: C
<div className="grid h-[min(720px,calc(100dvh-48px))] min-h-0 grid-rows-[minmax(240px,42vh)_minmax(0,1fr)] overflow-hidden bg-background text-foreground md:grid-cols-[minmax(0,1.55fr)_minmax(360px,.85fr)] md:grid-rows-1">
<section className="relative min-h-0 overflow-hidden border-b border-border bg-foreground/[.035] md:border-b-0 md:border-r" aria-label="角色三视图">
<div className="absolute inset-x-0 top-0 z-10 flex h-14 items-center justify-between px-5 pr-14">
<span className="inline-flex h-7 items-center gap-1.5 rounded-md border border-border/80 bg-background/80 px-2.5 text-[11px] font-medium shadow-sm backdrop-blur-xl">
<span className="inline-flex h-7 items-center gap-1.5 rounded-md border border-border/80 bg-background/80 px-2.5 text-[var(--fs-label)] font-medium shadow-sm backdrop-blur-xl">
<ImageIcon className="size-3.5" />
人物三视图
</span>
@@ -60,7 +60,7 @@ export function CanvasCharacterReferenceModal({ node, open, onClose }: { node: C
</span>
<div className="min-w-0">
<h2 className="truncate text-xl font-semibold leading-7">{name}</h2>
<p className="mt-1 text-[11px] text-foreground/45">项目角色引用 · 当前版本</p>
<p className="mt-1 text-[var(--fs-label)] text-foreground/45">项目角色引用 · 当前版本</p>
</div>
</div>
<div className="mt-4 flex flex-wrap gap-1.5">
@@ -109,7 +109,7 @@ function DetailSection({ icon, title, children }: { icon: ReactNode; title: stri
function DetailRow({ label, value, emphasis = false }: { label: string; value?: string; emphasis?: boolean }) {
return (
<div className="grid grid-cols-[76px_minmax(0,1fr)] gap-3 py-3 text-[12px] leading-5">
<div className="grid grid-cols-[76px_minmax(0,1fr)] gap-3 py-3 text-[var(--fs-caption)] leading-5">
<dt className="text-foreground/42">{label}</dt>
<dd className={value ? (emphasis ? "font-medium text-emerald-600 dark:text-emerald-300" : "text-foreground/78") : "text-foreground/28"}>{value || "未设置"}</dd>
</div>
@@ -118,7 +118,7 @@ function DetailRow({ label, value, emphasis = false }: { label: string; value?:
function StatusDot({ icon, ready, readyLabel, emptyLabel }: { icon: ReactNode; ready: boolean; readyLabel: string; emptyLabel: string }) {
return (
<span className={`inline-flex h-7 items-center gap-1.5 rounded-md border px-2 text-[10px] font-medium ${ready ? "border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300" : "border-border bg-background/65 text-foreground/42"}`}>
<span className={`inline-flex h-7 items-center gap-1.5 rounded-md border px-2 text-[var(--fs-tiny)] font-medium ${ready ? "border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300" : "border-border bg-background/65 text-foreground/42"}`}>
<span className="[&_svg]:size-3">{icon}</span>
{ready ? readyLabel : emptyLabel}
</span>

View File

@@ -9,7 +9,7 @@ export function CanvasCharacterReferenceNodeContent({ node }: { node: CanvasNode
return <div className="flex h-full w-full flex-col overflow-hidden bg-background">
<div className="relative min-h-0 flex-1 overflow-hidden bg-foreground/[.045]">
{node.metadata?.characterCoverUrl ? <img src={node.metadata.characterCoverUrl} alt={node.metadata?.characterName || node.title} className="h-full w-full object-contain p-2" draggable={false} /> : <div className="grid h-full place-items-center text-foreground/22"><UserRound className="size-12" /></div>}
<span className="absolute left-3 top-3 rounded bg-black/60 px-2 py-1 text-[10px] font-medium text-white">角色引用</span>
<span className="absolute left-3 top-3 rounded bg-black/60 px-2 py-1 text-[var(--fs-tiny)] font-medium text-white">角色引用</span>
</div>
<div className="shrink-0 border-t border-border/70 px-3 py-2.5">
<div className="truncate text-sm font-semibold">{node.metadata?.characterName || node.title}</div>
@@ -22,5 +22,5 @@ export function CanvasCharacterReferenceNodeContent({ node }: { node: CanvasNode
}
function State({ icon, ready, label }: { icon: ReactNode; ready: boolean; label: string }) {
return <span className={`inline-flex min-w-0 items-center gap-1 rounded px-1.5 py-1 text-[10px] ${ready ? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300" : "bg-foreground/[.055] text-foreground/45"}`}>{icon}<span className="truncate">{label}</span></span>;
return <span className={`inline-flex min-w-0 items-center gap-1 rounded px-1.5 py-1 text-[var(--fs-tiny)] ${ready ? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300" : "bg-foreground/[.055] text-foreground/45"}`}>{icon}<span className="truncate">{label}</span></span>;
}

View File

@@ -168,7 +168,7 @@ export function CanvasConfigComposer({ value, inputs, skillReferences = [], gene
<div className="mb-2 flex items-center justify-between gap-2">
<div className="flex min-w-0 items-baseline gap-2">
<div className="shrink-0 text-xs font-semibold">{simpleMode ? "快速生成" : "组装提示词"}</div>
<div className="truncate text-[11px] opacity-55">{simpleMode ? "已连接素材会自动带入" : "@ 引用已连接素材或已激活技能,发送前自动组装"}</div>
<div className="truncate text-[var(--fs-label)] opacity-55">{simpleMode ? "已连接素材会自动带入" : "@ 引用已连接素材或已激活技能,发送前自动组装"}</div>
</div>
<div className="flex shrink-0 items-center gap-1">
{simpleMode ? null : <CanvasPresetPicker mode={generationMode || "image"} skillReferences={skillReferences} open={presetOpen} onOpenChange={setPresetOpen} onSelect={insertPreset} />}

View File

@@ -57,7 +57,7 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
<div className="flex h-full w-full cursor-move flex-col px-3 pb-3 pt-7 text-sm" style={{ color: theme.node.text }} onWheel={(event) => event.stopPropagation()}>
<div className="mb-2 flex items-center justify-between gap-3">
<div className="shrink-0 text-sm font-semibold">{simpleMode ? "快速生成" : "生成配置"}</div>
{simpleMode ? <span className="rounded-md px-2 py-1 text-[10px]" style={{ background: theme.node.fill, color: theme.node.muted }}>自动配置</span> : <div className="cursor-default" onMouseDown={(event) => event.stopPropagation()}>
{simpleMode ? <span className="rounded-md px-2 py-1 text-[var(--fs-tiny)]" style={{ background: theme.node.fill, color: theme.node.muted }}>自动配置</span> : <div className="cursor-default" onMouseDown={(event) => event.stopPropagation()}>
<Segmented
size="small"
className="canvas-config-mode !rounded-md !p-0.5"
@@ -110,7 +110,7 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
<InputChip label="参考图" value={`${inputSummary.imageCount} 张`} style={chipStyle} />
<InputChip label="参考视频" value={`${inputSummary.videoCount} 个`} style={chipStyle} />
<InputChip label="参考音频" value={`${inputSummary.audioCount} 个`} style={chipStyle} />
<button type="button" className="inline-flex h-7 cursor-pointer items-center gap-1 rounded-md border px-2 text-[11px]" style={chipStyle} onMouseDown={(event) => event.stopPropagation()} onClick={onComposerToggle}>
<button type="button" className="inline-flex h-7 cursor-pointer items-center gap-1 rounded-md border px-2 text-[var(--fs-label)]" style={chipStyle} onMouseDown={(event) => event.stopPropagation()} onClick={onComposerToggle}>
{simpleMode ? <MessageSquare className="size-3.5" /> : <Settings2 className="size-3.5" />}
{simpleMode ? "编辑生成内容" : "组装提示词"}
</button>
@@ -137,7 +137,7 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
) : null}
{simpleMode ? (
<div className="mb-2 rounded-lg px-2 py-2 text-[11px]" style={{ background: theme.node.fill, color: theme.node.muted }}>将使用当前默认模型与生成参数</div>
<div className="mb-2 rounded-lg px-2 py-2 text-[var(--fs-label)]" style={{ background: theme.node.fill, color: theme.node.muted }}>将使用当前默认模型与生成参数</div>
) : (
<div className={`mb-2 grid min-w-0 cursor-default items-center gap-2 ${mode === "image" || mode === "video" || mode === "audio" ? "grid-cols-[minmax(0,1fr)_148px]" : "grid-cols-1"}`} onMouseDown={(event) => event.stopPropagation()}>
<ModelPicker className="canvas-compact-control h-10" config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} capability={mode} onMissingConfig={() => navigateToSettings({ continueCreation: true })} fullWidth />
@@ -197,7 +197,7 @@ function defaultVideoOperation(inputSummary: CanvasConfigNodePanelProps["inputSu
function InputChip({ label, value, style }: { label: string; value: string; style: CSSProperties }) {
return (
<div className="inline-flex h-7 items-center gap-1 rounded-md border px-2 text-[11px]" style={style}>
<div className="inline-flex h-7 items-center gap-1 rounded-md border px-2 text-[var(--fs-label)]" style={style}>
<span>{label}</span>
<span className="font-medium">{value}</span>
</div>

View File

@@ -274,7 +274,7 @@ function AddNodeContextMenu({ parentPosition, workspaceMode, isProjectLinked, re
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, x: left > parentPosition.left ? -4 : 4, scale: 0.98 }}
transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }}
className="aceternity-floating-panel fixed z-[181] w-[260px] origin-top overflow-hidden rounded-[14px] border p-2 backdrop-blur-2xl"
className="aceternity-floating-panel fixed z-[181] w-[260px] origin-top overflow-hidden rounded-[var(--dock-radius)] border p-2 backdrop-blur-2xl"
style={{ left, top: parentPosition.top, background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 30px 90px ${theme.spatial.shadow}` }}
onContextMenu={(event) => event.preventDefault()}
onPointerDown={(event) => event.stopPropagation()}
@@ -296,13 +296,13 @@ function MenuHeader({ title, description, onBack }: { title: string; description
return (
<div className="mb-0.5 flex items-start gap-1 px-1.5 py-1.5">
{onBack ? <button type="button" onClick={onBack} className="mt-0.5 grid size-6 shrink-0 place-items-center rounded-md outline-none hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/8" aria-label="返回媒体操作"><ArrowLeft className="size-3.5" /></button> : null}
<span className="min-w-0"><span className="block truncate text-xs font-semibold">{title}</span>{description && description !== title ? <span className="mt-0.5 block truncate text-[9px]" style={{ color: theme.node.muted }}>{description}</span> : null}</span>
<span className="min-w-0"><span className="block truncate text-xs font-semibold">{title}</span>{description && description !== title ? <span className="mt-0.5 block truncate text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>{description}</span> : null}</span>
</div>
);
}
function MenuSection({ label }: { label: string }) {
return <div className="px-2 pb-1 pt-1.5 text-[9px] font-medium opacity-45">{label}</div>;
return <div className="px-2 pb-1 pt-1.5 text-[var(--fs-micro)] font-medium opacity-45">{label}</div>;
}
function MenuButton({ icon, label, detail, shortcut, badge, chevron = false, active = false, disabled = false, danger = false, onClick }: { icon: ReactNode; label: string; detail?: string; shortcut?: string; badge?: string; chevron?: boolean; active?: boolean; disabled?: boolean; danger?: boolean; onClick?: () => void }) {
@@ -317,8 +317,8 @@ function MenuButton({ icon, label, detail, shortcut, badge, chevron = false, act
onClick={onClick}
>
<span className="canvas-menu-item-icon grid size-7 shrink-0 place-items-center rounded-md border opacity-75 group-hover:opacity-100 [&_svg]:size-3.5" style={{ background: danger ? `${theme.accent.danger}12` : theme.spatial.surface, borderColor: danger ? `${theme.accent.danger}33` : theme.toolbar.border, color: danger ? theme.accent.danger : theme.node.text }}>{icon}</span>
<span className="min-w-0 flex-1"><span className="flex items-center gap-1 text-xs font-medium"><span className="truncate">{label}</span>{badge ? <span className="rounded-full border px-1 py-0.5 text-[7px] font-bold" style={{ background: theme.toolbar.activeBg, borderColor: theme.toolbar.border, color: theme.node.muted }}>{badge}</span> : null}</span>{detail ? <span className="mt-0.5 block truncate text-[9px]" style={{ color: theme.node.muted }}>{detail}</span> : null}</span>
{shortcut ? <span className="shrink-0 text-[9px] opacity-38">{shortcut}</span> : null}
<span className="min-w-0 flex-1"><span className="flex items-center gap-1 text-xs font-medium"><span className="truncate">{label}</span>{badge ? <span className="rounded-full border px-1 py-0.5 text-[7px] font-bold" style={{ background: theme.toolbar.activeBg, borderColor: theme.toolbar.border, color: theme.node.muted }}>{badge}</span> : null}</span>{detail ? <span className="mt-0.5 block truncate text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>{detail}</span> : null}</span>
{shortcut ? <span className="shrink-0 text-[var(--fs-micro)] opacity-38">{shortcut}</span> : null}
{chevron ? <ChevronRight className="size-3 shrink-0 opacity-45 transition-transform group-hover:translate-x-0.5" /> : null}
</button>
);

View File

@@ -28,7 +28,7 @@ export function CanvasCreateCommandGrid({ commands, variant = "node" }: { comman
transition={aceternityMotion.spring.dock}
className={cn(
"group relative min-w-0 border border-black/10 bg-white/70 text-center outline-none transition-colors hover:border-black/20 hover:bg-black/5 focus-visible:ring-2 dark:border-white/10 dark:bg-white/[.04] dark:hover:border-white/20 dark:hover:bg-white/8",
variant === "node" ? "flex h-12 flex-col items-center justify-center gap-1 rounded-[10px]" : "flex h-9 items-center justify-center gap-1.5 rounded-[9px] px-2",
variant === "node" ? "flex h-12 flex-col items-center justify-center gap-1 rounded-[var(--dock-item-radius)]" : "flex h-9 items-center justify-center gap-1.5 rounded-[var(--dock-item-radius)] px-2",
)}
style={{ color: theme.node.text, "--tw-ring-color": theme.node.muted } as CSSProperties}
title={command.label}
@@ -36,7 +36,7 @@ export function CanvasCreateCommandGrid({ commands, variant = "node" }: { comman
onClick={command.onClick}
>
<span className="grid size-5 shrink-0 place-items-center opacity-65 transition-opacity group-hover:opacity-100 [&_svg]:size-3.5">{command.icon}</span>
<span className="max-w-full truncate text-[9px] font-semibold leading-none">{command.label}</span>
<span className="max-w-full truncate text-[var(--fs-micro)] font-semibold leading-none">{command.label}</span>
{command.badge ? <span className="absolute right-1 top-1 rounded-full border px-1 py-0.5 text-[6px] font-bold leading-none" style={{ background: theme.toolbar.activeBg, borderColor: theme.toolbar.border, color: theme.node.muted }}>{command.badge}</span> : null}
</motion.button>
))}

View File

@@ -83,7 +83,7 @@ export function CanvasDrawingEditorModal({ open, projectId, node, onClose, onSav
<Modal open={open} onCancel={() => void handleClose()} footer={null} closable={false} destroyOnHidden width="100vw" centered styles={{ body: { padding: 0 }, container: { padding: 0, overflow: "hidden" } }} className="canvas-drawing-editor-modal">
<div className="flex h-[min(92dvh,980px)] flex-col">
<div className="flex h-12 shrink-0 items-center justify-between border-b px-4" style={{ background: "var(--background)", borderColor: "var(--border)" }}>
<div className="flex min-w-0 items-center gap-2"><Maximize2 className="size-4 opacity-55" /><span className="truncate text-sm font-semibold">{node?.title || "绘图"}</span><span className="text-[11px] opacity-45">{drawingEngineLabel(engine)} · {ready ? "已加载" : "正在加载"}</span></div>
<div className="flex min-w-0 items-center gap-2"><Maximize2 className="size-4 opacity-55" /><span className="truncate text-sm font-semibold">{node?.title || "绘图"}</span><span className="text-[var(--fs-label)] opacity-45">{drawingEngineLabel(engine)} · {ready ? "已加载" : "正在加载"}</span></div>
<div className="flex items-center gap-2">
<button type="button" className="inline-flex h-8 items-center gap-1.5 rounded-md border px-3 text-xs font-medium transition hover:bg-black/5 disabled:opacity-45 dark:hover:bg-white/10" disabled={!ready || saving || unavailable} onClick={() => void handleSave()}><Check className="size-3.5" />{saving ? "保存中" : "保存绘图"}</button>
<button type="button" className="grid size-8 place-items-center rounded-md border transition hover:bg-black/5 dark:hover:bg-white/10" aria-label="关闭绘图编辑器" onClick={() => void handleClose()}><X className="size-4" /></button>

View File

@@ -207,7 +207,7 @@ function FaceSelectionOverlay({ node, viewport, containerRef, imageWidth, imageH
<div
ref={overlayRef}
data-canvas-no-zoom
className={`absolute z-[105] overflow-hidden rounded-[16px] ${status === "manual" ? "cursor-crosshair touch-none" : "pointer-events-none"}`}
className={`absolute z-[105] overflow-hidden rounded-[var(--r-2xl)] ${status === "manual" ? "cursor-crosshair touch-none" : "pointer-events-none"}`}
style={{ left: 0, top: 0, width: node.width * viewport.k, height: node.height * viewport.k }}
onPointerDown={status === "manual" ? (event) => {
event.preventDefault();
@@ -263,7 +263,7 @@ function FaceSelectionOverlay({ node, viewport, containerRef, imageWidth, imageH
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => { event.stopPropagation(); onFaceSelect(face); }}
>
{selected ? <span className="absolute -top-2.5 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full px-1.5 py-0.5 text-[8px] font-semibold text-white" style={{ background: active ? theme.accent.primary : "rgba(20,20,22,.82)" }}>{selected.name}</span> : null}
{selected ? <span className="absolute -top-2.5 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full px-1.5 py-0.5 text-[var(--fs-micro)] font-semibold text-white" style={{ background: active ? theme.accent.primary : "rgba(20,20,22,.82)" }}>{selected.name}</span> : null}
</motion.button>
);
})}
@@ -295,8 +295,8 @@ function SelectionToolbar({ node, viewport, containerRef, status, faceCount, err
<button type="button" aria-label="关闭情绪调节" className="grid size-8 shrink-0 place-items-center rounded-full hover:bg-black/5 dark:hover:bg-white/10" onClick={onClose}><X className="size-4" /></button>
<span className="mx-2 h-5 w-px" style={{ background: theme.toolbar.border }} />
<span className="grid size-8 shrink-0 place-items-center rounded-full" style={{ background: theme.toolbar.itemHover }}>{status === "detecting" ? <LoaderCircle className="size-4 animate-spin" /> : <ScanFace className="size-4" />}</span>
<span className="min-w-0 flex-1 truncate px-2 text-[11px] font-medium leading-none">{label}</span>
{status !== "detecting" ? <button type="button" className="flex h-8 shrink-0 items-center gap-1.5 rounded-[9px] px-2 text-[11px] font-medium leading-none transition hover:bg-black/5 dark:hover:bg-white/10" onClick={onManualSelect}><SquareDashedMousePointer className="size-3.5" />手动框选</button> : null}
<span className="min-w-0 flex-1 truncate px-2 text-[var(--fs-label)] font-medium leading-none">{label}</span>
{status !== "detecting" ? <button type="button" className="flex h-8 shrink-0 items-center gap-1.5 rounded-[var(--dock-item-radius)] px-2 text-[var(--fs-label)] font-medium leading-none transition hover:bg-black/5 dark:hover:bg-white/10" onClick={onManualSelect}><SquareDashedMousePointer className="size-3.5" />手动框选</button> : null}
</div>
</SpotlightSurface>
);

View File

@@ -15,15 +15,15 @@ export function CanvasFileDropOverlay({ active, theme }: { active: boolean; them
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: aceternityMotion.duration.state }}
className="pointer-events-none absolute inset-3 z-[110] grid place-items-center overflow-hidden rounded-[28px] border backdrop-blur-2xl"
className="pointer-events-none absolute inset-3 z-[var(--z-modal-overlay)] grid place-items-center overflow-hidden rounded-[var(--r-5xl)] border backdrop-blur-2xl"
style={{ background: theme.spatial.dropzone, borderColor: theme.spatial.glowStrong, color: theme.node.text, boxShadow: `inset 0 0 0 1px ${theme.spatial.glow}, 0 30px 100px rgba(0,0,0,.22)` }}
>
<div className="aceternity-drop-grid absolute inset-0 opacity-70" style={{ color: theme.spatial.glowStrong }} />
<motion.div initial={{ y: 20, scale: 0.94 }} animate={{ y: 0, scale: 1 }} exit={{ y: 12, scale: 0.96 }} transition={aceternityMotion.spring.panel} className="relative flex w-[min(480px,calc(100vw-48px))] flex-col items-center px-8 py-10 text-center">
<div className="relative mb-8 h-32 w-44">
<motion.div animate={{ x: -22, y: 10, rotate: -8 }} className="absolute inset-0 rounded-[20px] border backdrop-blur-xl" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }} />
<motion.div animate={{ x: 22, y: 10, rotate: 8 }} className="absolute inset-0 rounded-[20px] border backdrop-blur-xl" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }} />
<motion.div animate={{ y: -10, scale: 1.04 }} transition={aceternityMotion.spring.panel} className="absolute inset-0 grid place-items-center rounded-[20px] border" style={{ background: theme.node.panel, borderColor: theme.spatial.glowStrong, boxShadow: `0 24px 60px ${theme.spatial.shadow}` }}>
<motion.div animate={{ x: -22, y: 10, rotate: -8 }} className="absolute inset-0 rounded-[var(--r-3xl)] border backdrop-blur-xl" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }} />
<motion.div animate={{ x: 22, y: 10, rotate: 8 }} className="absolute inset-0 rounded-[var(--r-3xl)] border backdrop-blur-xl" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }} />
<motion.div animate={{ y: -10, scale: 1.04 }} transition={aceternityMotion.spring.panel} className="absolute inset-0 grid place-items-center rounded-[var(--r-3xl)] border" style={{ background: theme.node.panel, borderColor: theme.spatial.glowStrong, boxShadow: `0 24px 60px ${theme.spatial.shadow}` }}>
<UploadCloud className="size-10" style={{ color: theme.accent.primary }} />
</motion.div>
</div>

View File

@@ -158,7 +158,7 @@ export const CanvasFrameNode = React.memo(function CanvasFrameNode({
>
<CometCard
containerClassName="h-full w-full"
className="canvas-frame-shell overflow-hidden rounded-[14px] border"
className="canvas-frame-shell overflow-hidden rounded-[var(--dock-radius)] border"
rotateDepth={2.4}
translateDepth={2}
disabled={Boolean(dragOffset) || !collapsed || editing || scale < 0.32}
@@ -216,7 +216,7 @@ export const CanvasFrameNode = React.memo(function CanvasFrameNode({
{data.title}
</button>
)}
<span className="ml-auto shrink-0 pr-1 text-[11px] tabular-nums" style={{ color: theme.node.muted }}>
<span className="ml-auto shrink-0 pr-1 text-[var(--fs-label)] tabular-nums" style={{ color: theme.node.muted }}>
{childNodes.length}
</span>
</div>
@@ -272,7 +272,7 @@ function FramePreview({ nodes, frame, theme }: { nodes: CanvasNodeData[]; frame:
</div>
))
) : (
<div className="grid h-full place-items-center text-[11px]" style={{ color: theme.node.faint }}>空背板</div>
<div className="grid h-full place-items-center text-[var(--fs-label)]" style={{ color: theme.node.faint }}>空背板</div>
)}
</div>
);

View File

@@ -57,7 +57,7 @@ export function ImageToolSettingsModal({ open, tools, selectedIds, showLabels, o
>
<div className="flex h-11 items-center justify-between gap-3 border-b px-4" style={{ borderColor: theme.toolbar.border }}>
<span className="flex min-w-0 items-center gap-2">
<span className="grid size-7 shrink-0 place-items-center rounded-[8px]" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}><Type className="size-3.5" /></span>
<span className="grid size-7 shrink-0 place-items-center rounded-[var(--r-md)]" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}><Type className="size-3.5" /></span>
<span className="flex h-5 items-center text-xs font-medium leading-none">显示功能名</span>
</span>
<Switch size="small" checked={showLabels} onChange={onShowLabelsChange} aria-label="显示节点 Dock 功能名" />
@@ -69,13 +69,13 @@ export function ImageToolSettingsModal({ open, tools, selectedIds, showLabels, o
</div>
</div>
<div className="px-4 py-3">
<div className="mb-2 flex h-5 items-center justify-between"><span className="text-xs font-semibold">快捷工具</span><Tag className="m-0 leading-5 text-[10px]" style={{ background: theme.accent.primarySoft, borderColor: theme.spatial.glowStrong, color: theme.accent.primary }}>{selectedTools.length}/{maxSelected}</Tag></div>
<div className="mb-2 flex h-5 items-center justify-between"><span className="text-xs font-semibold">快捷工具</span><Tag className="m-0 leading-5 text-[var(--fs-tiny)]" style={{ background: theme.accent.primarySoft, borderColor: theme.spatial.glowStrong, color: theme.accent.primary }}>{selectedTools.length}/{maxSelected}</Tag></div>
<Checkbox.Group value={selectedIds} className="grid w-full grid-cols-2 gap-1 sm:grid-cols-4" onChange={(values) => updateSelectedTools(values as ImageQuickToolId[])}>
{tools.map((tool) => (
<label key={tool.id} className="flex h-8 min-w-0 cursor-pointer items-center gap-1 rounded-[7px] border px-1.5 transition-colors" style={{ background: selected.has(tool.id) ? theme.accent.primarySoft : "transparent", borderColor: selected.has(tool.id) ? theme.accent.primary : theme.toolbar.border, color: selected.has(tool.id) ? theme.accent.primary : theme.node.text }}>
<Checkbox className="canvas-image-tool-checkbox shrink-0" style={{ "--tool-accent": theme.accent.primary } as CSSProperties} value={tool.id} disabled={!selected.has(tool.id) && selectedTools.length >= maxSelected} />
<span className="grid size-5 shrink-0 place-items-center rounded-[5px] [&_svg]:size-3" style={{ background: selected.has(tool.id) ? theme.accent.primary : theme.toolbar.itemHover, color: selected.has(tool.id) ? "#ffffff" : theme.node.muted }}>{tool.icon}</span>
<span className="min-w-0 truncate text-[10px] font-medium leading-none">{tool.label}</span>
<span className="min-w-0 truncate text-[var(--fs-tiny)] font-medium leading-none">{tool.label}</span>
</label>
))}
</Checkbox.Group>

View File

@@ -562,7 +562,7 @@ export const CanvasLocalAgentPanel = memo(function CanvasLocalAgentPanel({ snaps
onSubmit={sendPrompt}
onAddFiles={addAttachments}
onRemoveAttachment={removeAttachment}
left={attachments.length ? <span className="text-[11px]" style={{ color: theme.node.muted }}>{formatBytes(attachmentPayloadBytes(attachments))} / 30MB</span> : null}
left={attachments.length ? <span className="text-[var(--fs-label)]" style={{ color: theme.node.muted }}>{formatBytes(attachmentPayloadBytes(attachments))} / 30MB</span> : null}
/>
</>
)}
@@ -662,7 +662,7 @@ function AgentConnectView({ theme, url, token, enabled, connected, activity, con
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-2">
<span className="shrink-0 text-sm font-medium leading-5">网页连接</span>
<span className="inline-flex min-w-0 items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] leading-4" style={{ background: theme.node.fill, color: statusColor }}>
<span className="inline-flex min-w-0 items-center gap-1.5 rounded-full px-2 py-0.5 text-[var(--fs-label)] leading-4" style={{ background: theme.node.fill, color: statusColor }}>
<span className="size-1.5 shrink-0 rounded-full" style={{ background: statusColor }} />
<span className="truncate">{statusText}</span>
</span>
@@ -734,13 +734,13 @@ function AgentHistoryView({ theme, threads, activeThreadId, workspacePath, loadi
<div className="flex items-center gap-2">
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-1.5">
{active ? <span className="shrink-0 text-[10px] font-medium" style={{ color: theme.node.text }}>当前</span> : null}
{active ? <span className="shrink-0 text-[var(--fs-tiny)] font-medium" style={{ color: theme.node.text }}>当前</span> : null}
<div className="truncate text-sm font-medium leading-5">{thread.name || thread.preview || "未命名对话"}</div>
</div>
<div className="truncate text-[11px] leading-4 opacity-65">{thread.preview || thread.id}</div>
<div className="truncate text-[var(--fs-label)] leading-4 opacity-65">{thread.preview || thread.id}</div>
</div>
<div className="flex shrink-0 items-center gap-1">
<span className="text-[10px] opacity-55">{formatThreadTime(thread.updatedAt || thread.createdAt)}</span>
<span className="text-[var(--fs-tiny)] opacity-55">{formatThreadTime(thread.updatedAt || thread.createdAt)}</span>
<Button size="small" className="!h-6 !px-2" disabled={loading} onClick={() => onResumeThread(thread.id)}>
进入
</Button>

View File

@@ -54,13 +54,13 @@ export function CanvasNodeAnglePanel({ dataUrl, onClose, onConfirm }: { dataUrl:
initial={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 8, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={reducedMotion ? { duration: 0 } : aceternityMotion.spring.panel}
className="aceternity-floating-panel w-[580px] max-w-full overflow-hidden rounded-[16px] border backdrop-blur-2xl"
className="aceternity-floating-panel w-[580px] max-w-full overflow-hidden rounded-[var(--r-2xl)] border backdrop-blur-2xl"
style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 28px 80px ${theme.spatial.shadow}` }}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
>
<div className="flex h-11 items-center gap-2 border-b px-2.5" style={{ borderColor: theme.toolbar.border }}>
<span className="grid size-7 shrink-0 place-items-center rounded-[8px]" style={{ background: theme.toolbar.itemHover }}><Camera className="size-3.5" /></span>
<span className="grid size-7 shrink-0 place-items-center rounded-[var(--r-md)]" style={{ background: theme.toolbar.itemHover }}><Camera className="size-3.5" /></span>
<span className="text-xs font-semibold">多角度编辑器</span>
<span className="min-w-0 flex-1" />
<Tooltip title="关闭"><button type="button" aria-label="关闭多角度编辑器" className="grid size-7 place-items-center rounded-full transition hover:bg-black/5 dark:hover:bg-white/10" onClick={onClose}><X className="size-3.5" /></button></Tooltip>
@@ -70,40 +70,40 @@ export function CanvasNodeAnglePanel({ dataUrl, onClose, onConfirm }: { dataUrl:
{presets.map((preset) => <AnglePresetButton key={preset.label} active={activePreset?.label === preset.label} label={preset.label} theme={theme} onClick={() => setParams((current) => ({ ...current, horizontalAngle: preset.horizontalAngle, pitchAngle: preset.pitchAngle }))} />)}
</div>
<div className="grid h-[208px] grid-cols-[188px_minmax(0,1fr)] gap-2.5 p-2.5">
<div className="relative grid size-[188px] cursor-grab place-items-center overflow-hidden rounded-[12px] border active:cursor-grabbing" style={{ borderColor: theme.toolbar.border, background: theme.spatial.surface }} onPointerDown={startCameraDrag} onPointerMove={moveCamera} onPointerUp={(event) => { dragRef.current = null; event.currentTarget.releasePointerCapture(event.pointerId); }} onPointerCancel={() => { dragRef.current = null; }}>
<div className="relative grid size-[188px] cursor-grab place-items-center overflow-hidden rounded-[var(--r-lg)] border active:cursor-grabbing" style={{ borderColor: theme.toolbar.border, background: theme.spatial.surface }} onPointerDown={startCameraDrag} onPointerMove={moveCamera} onPointerUp={(event) => { dragRef.current = null; event.currentTarget.releasePointerCapture(event.pointerId); }} onPointerCancel={() => { dragRef.current = null; }}>
<GlobeGrid color={theme.node.muted} />
<img src={dataUrl} alt="角度参考" className="relative z-10 size-[72px] rounded-[8px] object-cover shadow-lg" draggable={false} style={{ transform: previewTransform(params) }} />
<img src={dataUrl} alt="角度参考" className="relative z-10 size-[72px] rounded-[var(--r-md)] object-cover shadow-lg" draggable={false} style={{ transform: previewTransform(params) }} />
<div className="pointer-events-none absolute z-20 grid size-7 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border shadow" style={{ left: `${marker.x}%`, top: `${marker.y}%`, background: theme.toolbar.panel, borderColor: theme.node.activeStroke, color: theme.node.activeStroke }}><Camera className="size-3.5" /></div>
<span className="pointer-events-none absolute bottom-2 text-[9px]" style={{ color: theme.node.muted }}>拖动调整摄影机</span>
<span className="pointer-events-none absolute bottom-2 text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>拖动调整摄影机</span>
</div>
<div className="flex min-w-0 flex-col justify-center gap-2 rounded-[12px] border px-2.5 py-2" style={{ background: theme.toolbar.itemHover, borderColor: theme.toolbar.border }}>
<div className="flex min-w-0 flex-col justify-center gap-2 rounded-[var(--r-lg)] border px-2.5 py-2" style={{ background: theme.toolbar.itemHover, borderColor: theme.toolbar.border }}>
<AngleSlider label="水平环绕" value={params.horizontalAngle} min={-180} max={180} suffix="°" onChange={(value) => update("horizontalAngle", value)} />
<AngleSlider label="垂直俯仰" value={params.pitchAngle} min={-75} max={75} suffix="°" onChange={(value) => update("pitchAngle", value)} />
<AngleSlider label="景别缩放" value={params.cameraDistance} min={1} max={10} step={0.1} suffix={distanceLabel(params.cameraDistance)} onChange={(value) => update("cameraDistance", value)} />
<div className="grid h-8 grid-cols-[62px_minmax(0,1fr)] items-center gap-2">
<span className="text-[10px] font-medium" style={{ color: theme.node.muted }}>镜头</span>
<span className="text-[var(--fs-tiny)] font-medium" style={{ color: theme.node.muted }}>镜头</span>
<Segmented block size="small" value={params.wideAngle ? "wide" : "standard"} options={[{ label: "标准", value: "standard" }, { label: "广角", value: "wide" }]} onChange={(value) => update("wideAngle", value === "wide")} />
</div>
</div>
</div>
<div className="flex h-11 items-center gap-2 border-t px-3" style={{ borderColor: theme.toolbar.border }}>
<span className="text-[10px]" style={{ color: theme.node.muted }}>当前视角</span>
<span className="text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>当前视角</span>
<span className="text-xs font-semibold">{activePreset?.label || "自定义"}</span>
<span className="text-[10px]" style={{ color: theme.node.muted }}>{params.horizontalAngle}° / {params.pitchAngle}° · {params.cameraDistance.toFixed(1)} {distanceLabel(params.cameraDistance)}</span>
<span className="text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>{params.horizontalAngle}° / {params.pitchAngle}° · {params.cameraDistance.toFixed(1)} {distanceLabel(params.cameraDistance)}</span>
<span className="flex-1" />
<button type="button" className="flex h-8 items-center gap-1.5 rounded-[9px] px-2 text-[11px] font-medium transition hover:bg-black/5 dark:hover:bg-white/10" onClick={() => setParams(defaultParams)}><RotateCcw className="size-3.5" />重置</button>
<motion.button type="button" whileHover={reducedMotion ? undefined : { y: -1 }} whileTap={reducedMotion ? undefined : { scale: 0.97 }} className="flex h-8 items-center gap-1.5 rounded-[9px] px-3 text-[11px] font-semibold" style={{ background: theme.node.activeStroke, color: theme.node.panel }} onClick={() => onConfirm(params)}><Send className="size-3.5" />生成新角度</motion.button>
<button type="button" className="flex h-8 items-center gap-1.5 rounded-[var(--dock-item-radius)] px-2 text-[var(--fs-label)] font-medium transition hover:bg-black/5 dark:hover:bg-white/10" onClick={() => setParams(defaultParams)}><RotateCcw className="size-3.5" />重置</button>
<motion.button type="button" whileHover={reducedMotion ? undefined : { y: -1 }} whileTap={reducedMotion ? undefined : { scale: 0.97 }} className="flex h-8 items-center gap-1.5 rounded-[var(--dock-item-radius)] px-3 text-[var(--fs-label)] font-semibold" style={{ background: theme.node.activeStroke, color: theme.node.panel }} onClick={() => onConfirm(params)}><Send className="size-3.5" />生成新角度</motion.button>
</div>
</SpotlightSurface>
);
}
function AngleSlider({ label, value, min, max, step = 1, suffix, onChange }: { label: string; value: number; min: number; max: number; step?: number; suffix: string; onChange: (value: number) => void }) {
return <div className="grid h-8 grid-cols-[62px_minmax(0,1fr)_60px] items-center gap-2"><span className="text-[10px] font-medium opacity-60">{label}</span><Slider min={min} max={max} step={step} value={value} onChange={onChange} /><span className="text-right text-[10px] font-semibold">{Number.isInteger(value) ? value : value.toFixed(1)}{suffix.startsWith("°") ? suffix : ""}{!suffix.startsWith("°") ? ` ${suffix}` : ""}</span></div>;
return <div className="grid h-8 grid-cols-[62px_minmax(0,1fr)_60px] items-center gap-2"><span className="text-[var(--fs-tiny)] font-medium opacity-60">{label}</span><Slider min={min} max={max} step={step} value={value} onChange={onChange} /><span className="text-right text-[var(--fs-tiny)] font-semibold">{Number.isInteger(value) ? value : value.toFixed(1)}{suffix.startsWith("°") ? suffix : ""}{!suffix.startsWith("°") ? ` ${suffix}` : ""}</span></div>;
}
function AnglePresetButton({ active, label, theme, onClick }: { active: boolean; label: string; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onClick?: () => void }) {
return <button type="button" aria-pressed={active} className="h-7 shrink-0 rounded-[8px] border px-2.5 text-[10px] font-medium transition hover:bg-black/5 dark:hover:bg-white/10" style={{ background: active ? theme.toolbar.activeBg : theme.spatial.surface, borderColor: active ? theme.spatial.glowStrong : theme.toolbar.border }} onClick={onClick}>{label}</button>;
return <button type="button" aria-pressed={active} className="h-7 shrink-0 rounded-[var(--r-md)] border px-2.5 text-[var(--fs-tiny)] font-medium transition hover:bg-black/5 dark:hover:bg-white/10" style={{ background: active ? theme.toolbar.activeBg : theme.spatial.surface, borderColor: active ? theme.spatial.glowStrong : theme.toolbar.border }} onClick={onClick}>{label}</button>;
}
function GlobeGrid({ color }: { color: string }) {

View File

@@ -64,7 +64,7 @@ export function CanvasNodeEmotionPanel({ dataUrl, imageWidth, imageHeight, chara
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 5, scale: 0.98 }}
transition={reducedMotion ? { duration: 0 } : aceternityMotion.spring.panel}
className="aceternity-floating-panel w-[580px] max-w-full overflow-hidden rounded-[16px] border backdrop-blur-2xl"
className="aceternity-floating-panel w-[580px] max-w-full overflow-hidden rounded-[var(--r-2xl)] border backdrop-blur-2xl"
style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 28px 80px ${theme.spatial.shadow}` }}
>
<div className="flex h-11 items-center gap-1.5 border-b px-2.5" style={{ borderColor: theme.toolbar.border }}>
@@ -75,16 +75,16 @@ export function CanvasNodeEmotionPanel({ dataUrl, imageWidth, imageHeight, chara
type="button"
layout
whileTap={reducedMotion ? undefined : { scale: 0.96 }}
className="relative flex h-8 shrink-0 items-center gap-1.5 rounded-[9px] border px-1.5 pr-2 text-[11px] font-medium outline-none"
className="relative flex h-8 shrink-0 items-center gap-1.5 rounded-[var(--dock-item-radius)] border px-1.5 pr-2 text-[var(--fs-label)] font-medium outline-none"
style={{ background: activeCharacterId === character.id ? theme.toolbar.activeBg : theme.spatial.surface, borderColor: activeCharacterId === character.id ? theme.spatial.glowStrong : theme.toolbar.border }}
onClick={() => onSelectCharacter(character.id)}
>
{activeCharacterId === character.id ? <motion.span layoutId="emotion-active-character" className="absolute inset-0 -z-10 rounded-[8px]" style={{ boxShadow: `inset 0 0 0 1px ${theme.accent.primarySoft}` }} transition={aceternityMotion.spring.dock} /> : null}
{activeCharacterId === character.id ? <motion.span layoutId="emotion-active-character" className="absolute inset-0 -z-10 rounded-[var(--r-md)]" style={{ boxShadow: `inset 0 0 0 1px ${theme.accent.primarySoft}` }} transition={aceternityMotion.spring.dock} /> : null}
<FaceThumbnail dataUrl={dataUrl} imageWidth={imageWidth} imageHeight={imageHeight} box={character.faceBox} />
<span>{character.name}</span>
</motion.button>
))}
<button type="button" className="flex h-8 shrink-0 items-center gap-1.5 rounded-[9px] border px-2 text-[11px] font-medium opacity-70 transition hover:opacity-100" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }} onClick={onManualSelect}>
<button type="button" className="flex h-8 shrink-0 items-center gap-1.5 rounded-[var(--dock-item-radius)] border px-2 text-[var(--fs-label)] font-medium opacity-70 transition hover:opacity-100" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }} onClick={onManualSelect}>
<ScanFace className="size-3.5" />手动框选
</button>
</div>
@@ -97,17 +97,17 @@ export function CanvasNodeEmotionPanel({ dataUrl, imageWidth, imageHeight, chara
</div>
<div className="flex min-h-11 items-center gap-2 border-t px-3" style={{ borderColor: theme.toolbar.border }}>
<span className="text-[10px]" style={{ color: theme.node.muted }}>情绪定位</span>
<span className="text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>情绪定位</span>
<AnimatePresence mode="wait" initial={false}>
<motion.span key={preset.id} initial={reducedMotion ? false : { opacity: 0, y: 4, filter: "blur(4px)" }} animate={{ opacity: 1, y: 0, filter: "blur(0px)" }} exit={reducedMotion ? undefined : { opacity: 0, y: -3, filter: "blur(3px)" }} transition={{ duration: reducedMotion ? 0 : 0.18 }} className="text-xs font-semibold">{preset.label}</motion.span>
</AnimatePresence>
{error ? <span className="min-w-0 flex-1 truncate text-right text-[10px]" style={{ color: theme.accent.danger }} title={error}>{error}</span> : <span className="flex-1" />}
{error ? <span className="min-w-0 flex-1 truncate text-right text-[var(--fs-tiny)]" style={{ color: theme.accent.danger }} title={error}>{error}</span> : <span className="flex-1" />}
<motion.button
type="button"
disabled={generating}
whileHover={reducedMotion || generating ? undefined : { y: -1 }}
whileTap={reducedMotion || generating ? undefined : { scale: 0.97 }}
className="flex h-8 shrink-0 items-center gap-1.5 rounded-[9px] px-3 text-[11px] font-semibold disabled:cursor-wait disabled:opacity-55"
className="flex h-8 shrink-0 items-center gap-1.5 rounded-[var(--dock-item-radius)] px-3 text-[var(--fs-label)] font-semibold disabled:cursor-wait disabled:opacity-55"
style={{ background: theme.node.activeStroke, color: theme.node.panel }}
onClick={onConfirm}
>
@@ -144,11 +144,11 @@ function EmotionPad({ preset, onChange }: { preset: CanvasEmotionPreset; onChang
const selectedColumn = 2 - preset.intimacy;
const selectedRow = 2 - preset.arousal;
return (
<div className="relative rounded-[12px] border px-[25px] pb-[22px] pt-[24px]" style={{ background: theme.toolbar.itemHover, borderColor: theme.toolbar.border }}>
<span className="pointer-events-none absolute inset-x-0 top-1.5 text-center text-[9px]" style={{ color: theme.node.muted }}>激动</span>
<span className="pointer-events-none absolute inset-x-0 bottom-1.5 text-center text-[9px]" style={{ color: theme.node.muted }}>平静</span>
<span className="pointer-events-none absolute left-1 top-1/2 -translate-y-1/2 text-[9px] [writing-mode:vertical-rl]" style={{ color: theme.node.muted }}>亲近</span>
<span className="pointer-events-none absolute right-1 top-1/2 -translate-y-1/2 text-[9px] [writing-mode:vertical-rl]" style={{ color: theme.node.muted }}>疏离</span>
<div className="relative rounded-[var(--r-lg)] border px-[25px] pb-[22px] pt-[24px]" style={{ background: theme.toolbar.itemHover, borderColor: theme.toolbar.border }}>
<span className="pointer-events-none absolute inset-x-0 top-1.5 text-center text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>激动</span>
<span className="pointer-events-none absolute inset-x-0 bottom-1.5 text-center text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>平静</span>
<span className="pointer-events-none absolute left-1 top-1/2 -translate-y-1/2 text-[var(--fs-micro)] [writing-mode:vertical-rl]" style={{ color: theme.node.muted }}>亲近</span>
<span className="pointer-events-none absolute right-1 top-1/2 -translate-y-1/2 text-[var(--fs-micro)] [writing-mode:vertical-rl]" style={{ color: theme.node.muted }}>疏离</span>
<div
role="slider"
aria-label="情绪强度"
@@ -192,7 +192,7 @@ function EmotionPad({ preset, onChange }: { preset: CanvasEmotionPreset; onChang
function EmotionHeadPreview({ preset }: { preset: CanvasEmotionPreset }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
return (
<div className="relative overflow-hidden rounded-[12px] border" style={{ background: "#26272a", borderColor: theme.toolbar.border }}>
<div className="relative overflow-hidden rounded-[var(--r-lg)] border" style={{ background: "#26272a", borderColor: theme.toolbar.border }}>
<Canvas frameloop="demand" dpr={[1, 1.5]} camera={{ fov: 38, near: 0.1, far: 20, position: [0, 0, 4.15] }} gl={{ antialias: true, alpha: false, powerPreference: "high-performance" }}>
<color attach="background" args={["#26272a"]} />
<ambientLight intensity={0.82} />
@@ -202,7 +202,7 @@ function EmotionHeadPreview({ preset }: { preset: CanvasEmotionPreset }) {
</Canvas>
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-10 bg-gradient-to-t from-black/55 to-transparent" />
<AnimatePresence mode="wait" initial={false}>
<motion.span key={preset.id} initial={{ opacity: 0, filter: "blur(6px)" }} animate={{ opacity: 1, filter: "blur(0px)" }} exit={{ opacity: 0, filter: "blur(5px)" }} transition={{ duration: 0.2 }} className="pointer-events-none absolute bottom-2 left-2.5 text-[10px] font-medium text-white/72">实时预览 · {preset.label}</motion.span>
<motion.span key={preset.id} initial={{ opacity: 0, filter: "blur(6px)" }} animate={{ opacity: 1, filter: "blur(0px)" }} exit={{ opacity: 0, filter: "blur(5px)" }} transition={{ duration: 0.2 }} className="pointer-events-none absolute bottom-2 left-2.5 text-[var(--fs-tiny)] font-medium text-white/72">实时预览 · {preset.label}</motion.span>
</AnimatePresence>
</div>
);

View File

@@ -314,7 +314,7 @@ export function CanvasNodeHoverToolbar({
<>
<div
ref={toolbarRef}
className="canvas-node-toolbar absolute z-[70] flex -translate-x-1/2 -translate-y-full items-end justify-center overflow-visible"
className="canvas-node-toolbar absolute z-[var(--z-panel-floating)] flex -translate-x-1/2 -translate-y-full items-end justify-center overflow-visible"
style={{ left: anchor.left, top: anchor.top, width: "max-content", maxWidth: `min(calc(100% - 20px), ${showDockLabels ? 840 : 560}px)`, color: theme.node.text }}
onMouseEnter={() => onKeep(node.id)}
onMouseLeave={() => {
@@ -323,7 +323,7 @@ export function CanvasNodeHoverToolbar({
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
>
<div className={`aceternity-floating-dock thin-scrollbar relative flex max-w-full overflow-x-auto rounded-[14px] border backdrop-blur-2xl ${showDockLabels ? "h-11 items-center px-2 py-1" : "h-10 items-end gap-1 px-1.5 pb-1"}`} style={showDockLabels ? labeledDockStyle : dockShellStyle}>
<div className={`aceternity-floating-dock thin-scrollbar relative flex max-w-full overflow-x-auto rounded-[var(--dock-radius)] border backdrop-blur-2xl ${showDockLabels ? "h-11 items-center px-2 py-1" : "h-10 items-end gap-1 px-1.5 pb-1"}`} style={showDockLabels ? labeledDockStyle : dockShellStyle}>
{dockItems.length ? <FloatingDock embedded items={dockItems} size="compact" showLabels={showDockLabels} ariaLabel="节点快捷工具" className={`pointer-events-auto shrink-0 ${showDockLabels ? "" : "max-w-[min(calc(100vw-20px),400px)]"}`} style={embeddedDockStyle} /> : null}
{hasImage && !simpleMode ? (
<Dropdown
@@ -341,13 +341,13 @@ export function CanvasNodeHoverToolbar({
>
<button
type="button"
className={`aceternity-dock-command pointer-events-auto shrink-0 outline-none focus-visible:ring-2 ${showDockLabels ? "is-labeled inline-flex h-8 items-center justify-center gap-1.5 rounded-[9px] px-2.5" : "grid size-8 place-items-center rounded-full"}`}
className={`aceternity-dock-command pointer-events-auto shrink-0 outline-none focus-visible:ring-2 ${showDockLabels ? "is-labeled inline-flex h-8 items-center justify-center gap-1.5 rounded-[var(--dock-item-radius)] px-2.5" : "grid size-8 place-items-center rounded-full"}`}
style={{ color: theme.node.text, "--tw-ring-color": theme.accent.primary } as CSSProperties}
aria-label="更多图片工具"
title="更多图片工具"
>
<Ellipsis className="size-3.5" />
{showDockLabels ? <span className="inline-flex h-4 items-center text-[11px] font-medium leading-none">更多</span> : null}
{showDockLabels ? <span className="inline-flex h-4 items-center text-[var(--fs-label)] font-medium leading-none">更多</span> : null}
</button>
</Dropdown>
) : null}
@@ -433,7 +433,7 @@ export function CanvasNodeInfoModal({ node, open, onClose, onMetadataChange, rea
const title = (
<div className="flex items-center justify-between gap-4 pr-10">
<div className="min-w-0">
<div className="text-[17px] font-semibold tracking-[-0.02em]">节点信息</div>
<div className="text-[var(--fs-heading-lg)] font-semibold tracking-[-0.02em]">节点信息</div>
{node ? <div className="mt-0.5 truncate text-xs opacity-45">{node.id}</div> : null}
</div>
<Segmented
@@ -481,7 +481,7 @@ export function CanvasNodeInfoModal({ node, open, onClose, onMetadataChange, rea
return <button key={option.value} type="button" disabled={readOnly} onClick={() => saveAssetCategory(option.value)} className="h-7 rounded-md border px-2 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-60" style={{ borderColor: active ? theme.accent.primary : theme.toolbar.border, background: active ? theme.accent.primarySoft : theme.toolbar.panel, color: active ? theme.accent.primary : theme.node.muted }}>{option.label}</button>;
})}
</div>
<div className="mt-2 text-[11px] leading-5 opacity-45">生成后会按此分类进入项目资产;角色、场景和画风工作流会自动预填。</div>
<div className="mt-2 text-[var(--fs-label)] leading-5 opacity-45">生成后会按此分类进入项目资产;角色、场景和画风工作流会自动预填。</div>
</div>
) : null}
{node.metadata?.prompt ? (

View File

@@ -148,7 +148,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
<span className="grid size-3.5 shrink-0 place-items-center" style={{ color: monochromeAccent }}>
<GenerationModeIcon mode={mode} />
</span>
<span className="truncate text-[10px] font-medium">{modeDisplayName(mode)}创作</span>
<span className="truncate text-[var(--fs-tiny)] font-medium">{modeDisplayName(mode)}创作</span>
</div>
)}
{!simpleMode ? (
@@ -182,12 +182,12 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
const renderComposerControls = (expanded: boolean) => simpleMode ? (
<div className="flex min-w-0 items-center justify-between gap-2 p-1" style={{ background: controlsSurface }}>
<span className="min-w-0 truncate px-2 text-[10px]" style={{ color: theme.node.muted }}>
<span className="min-w-0 truncate px-2 text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>
{activeReferenceCount ? `已连接 ${activeReferenceCount} 个素材` : "将使用默认模型与参数"}
</span>
<Button
type="text"
className="!inline-flex !h-8 shrink-0 !items-center !gap-1 !rounded-full !px-2.5 !text-[10px] !font-medium"
className="!inline-flex !h-8 shrink-0 !items-center !gap-1 !rounded-full !px-2.5 !text-[var(--fs-tiny)] !font-medium"
danger={isRunning}
disabled={isSubmitDisabled}
style={{ background: isSubmitDisabled ? theme.toolbar.itemHover : isRunning ? theme.accent.danger : monochromeAccent, color: isSubmitDisabled ? theme.node.faint : isRunning ? "#ffffff" : theme.canvas.background, boxShadow: isSubmitDisabled ? "none" : `0 8px 20px ${theme.spatial.shadow}, inset 0 1px 0 rgba(255,255,255,.18)` }}
@@ -201,22 +201,22 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
) : (
<div className="flex min-w-0 items-center justify-between gap-0.5 p-1" style={{ background: controlsSurface }}>
<div className={`${expanded ? "max-w-[320px]" : mode === "image" || mode === "video" ? "max-w-[240px]" : "max-w-[174px]"} min-w-[104px] flex-1`}>
<ModelPicker className="!h-7 !w-full !min-w-0 !text-[10px] !font-normal [&_img]:!size-3 [&_.lucide]:!size-3" fullWidth config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} capability={mode} onMissingConfig={() => navigateToSettings({ continueCreation: true })} showSelectedPrice={false} />
<ModelPicker className="!h-7 !w-full !min-w-0 !text-[var(--fs-tiny)] !font-normal [&_img]:!size-3 [&_.lucide]:!size-3" fullWidth config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} capability={mode} onMissingConfig={() => navigateToSettings({ continueCreation: true })} showSelectedPrice={false} />
</div>
<div className="ml-auto flex min-w-0 shrink-0 items-center gap-1">
{mode === "image" ? (
<CanvasImageSettingsPopover
config={config}
placement={expanded ? "topRight" : "topLeft"}
buttonClassName="!h-7 !w-[146px] !justify-start !rounded-full !border-0 !px-2.5 !text-[10px] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3"
buttonClassName="!h-7 !w-[146px] !justify-start !rounded-full !border-0 !px-2.5 !text-[var(--fs-tiny)] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3"
onConfigChange={(key, value) => onConfigChange(node.id, key === "count" ? { count: Number(value) || 1 } : { [key]: value })}
onMissingConfig={() => navigateToSettings({ continueCreation: true })}
onOpenChange={expanded ? undefined : onImageSettingsOpenChange}
/>
) : mode === "video" ? (
<CanvasVideoSettingsPopover config={config} buttonClassName="!h-7 !w-[144px] !justify-start !rounded-full !border-0 !px-2.5 !text-[10px] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3" onConfigChange={(key, value) => onConfigChange(node.id, videoConfigPatch(key, value))} />
<CanvasVideoSettingsPopover config={config} buttonClassName="!h-7 !w-[144px] !justify-start !rounded-full !border-0 !px-2.5 !text-[var(--fs-tiny)] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3" onConfigChange={(key, value) => onConfigChange(node.id, videoConfigPatch(key, value))} />
) : mode === "audio" ? (
<CanvasAudioSettingsPopover config={config} buttonClassName="!h-7 !w-[146px] !justify-start !rounded-full !border-0 !px-2.5 !text-[10px] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3" onConfigChange={(key, value) => onConfigChange(node.id, audioConfigPatch(key, value))} />
<CanvasAudioSettingsPopover config={config} buttonClassName="!h-7 !w-[146px] !justify-start !rounded-full !border-0 !px-2.5 !text-[var(--fs-tiny)] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3" onConfigChange={(key, value) => onConfigChange(node.id, audioConfigPatch(key, value))} />
) : null}
<GenerationCostBadge credits={credits} theme={theme} />
<Button
@@ -254,7 +254,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
references={mentionReferences}
onChange={updatePrompt}
containerClassName="min-h-0 flex-1"
className="thin-scrollbar h-full w-full resize-none overflow-y-auto border-none bg-transparent px-2.5 py-2 text-[13px] leading-5 !outline-none !ring-0 !shadow-none focus:!outline-none focus:!ring-0 focus:!shadow-none placeholder:text-current placeholder:opacity-35"
className="thin-scrollbar h-full w-full resize-none overflow-y-auto border-none bg-transparent px-2.5 py-2 text-[var(--fs-body)] leading-5 !outline-none !ring-0 !shadow-none focus:!outline-none focus:!ring-0 focus:!shadow-none placeholder:text-current placeholder:opacity-35"
style={{ color: theme.node.text, outline: "none", boxShadow: "none" }}
placeholder={promptPlaceholder(mode, hasImageContent, hasTextContent)}
onContentSizeChange={updatePromptContentHeight}
@@ -292,7 +292,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
references={mentionReferences}
onChange={updatePrompt}
containerClassName="min-h-0 flex-1"
className="thin-scrollbar h-full w-full resize-none overflow-y-auto border-none bg-transparent px-3 py-2.5 text-[15px] leading-6 !outline-none !ring-0 !shadow-none focus:!outline-none focus:!ring-0 focus:!shadow-none placeholder:text-current placeholder:opacity-35"
className="thin-scrollbar h-full w-full resize-none overflow-y-auto border-none bg-transparent px-3 py-2.5 text-[var(--fs-body-lg)] leading-6 !outline-none !ring-0 !shadow-none focus:!outline-none focus:!ring-0 focus:!shadow-none placeholder:text-current placeholder:opacity-35"
style={{ color: theme.node.text, outline: "none", boxShadow: "none" }}
placeholder={promptPlaceholder(mode, hasImageContent, hasTextContent)}
aria-label={`${modeDisplayName(mode)}提示词`}
@@ -313,7 +313,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
function ComposerPill({ theme, borderColor, icon, label }: { theme: CanvasTheme; borderColor: string; icon: ReactNode; label: string }) {
return (
<span
className="inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-1.5 text-[9px] font-medium transition hover:brightness-125"
className="inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-1.5 text-[var(--fs-micro)] font-medium transition hover:brightness-125"
style={{ background: theme.canvas.background, borderColor, color: theme.node.activeStroke }}
>
{icon}
@@ -355,7 +355,7 @@ function ConnectedReferenceShelf({ references, theme, onInsert }: { references:
<span className="block size-full overflow-hidden rounded-md">
<ReferenceThumbnail reference={reference} />
</span>
<span className="absolute left-0.5 top-0.5 grid size-3.5 place-items-center rounded-full bg-black/65 text-[8px] font-semibold text-white backdrop-blur-sm">{index + 1}</span>
<span className="absolute left-0.5 top-0.5 grid size-3.5 place-items-center rounded-full bg-black/65 text-[var(--fs-micro)] font-semibold text-white backdrop-blur-sm">{index + 1}</span>
<span className="absolute bottom-0.5 right-0.5 grid size-3.5 place-items-center rounded-full bg-black/65 text-white backdrop-blur-sm"><AtSign className="size-2" /></span>
</button>
))}
@@ -379,8 +379,8 @@ function ReferenceThumbnail({ reference }: { reference: CanvasResourceReference
function GenerationCostBadge({ credits, theme }: { credits: number | null; theme: CanvasTheme }) {
if (credits === null) return null;
return (
<span className="canvas-generation-cost-badge inline-flex h-7 shrink-0 items-center gap-1 rounded-full px-2 text-[9px] font-semibold tabular-nums" style={{ background: theme.accent.primarySoft, color: theme.accent.primary }} title={`预计消耗 ${credits.toLocaleString()} 积分`} aria-label={`预计消耗 ${credits.toLocaleString()} 积分`}>
<CreditSymbol className="text-[11px]" />
<span className="canvas-generation-cost-badge inline-flex h-7 shrink-0 items-center gap-1 rounded-full px-2 text-[var(--fs-micro)] font-semibold tabular-nums" style={{ background: theme.accent.primarySoft, color: theme.accent.primary }} title={`预计消耗 ${credits.toLocaleString()} 积分`} aria-label={`预计消耗 ${credits.toLocaleString()} 积分`}>
<CreditSymbol className="text-[var(--fs-label)]" />
{credits.toLocaleString()}
</span>
);

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { ReactNode } from "react";
import { BookOpenCheck, ChevronRight, Clapperboard, Clock3, FileText, Image as ImageIcon, LoaderCircle, Lock, Maximize2, Music2, Pencil, Play, Plus, RefreshCw, Replace, Settings2, Square, Star, Type, Video } from "lucide-react";
import { AlertCircle, BookOpenCheck, CheckCircle2, ChevronRight, Clapperboard, Clock3, FileText, Image as ImageIcon, LoaderCircle, Lock, Maximize2, Music2, Pencil, Play, Plus, RefreshCw, Replace, Settings2, Square, Star, Type, Video } from "lucide-react";
import { canvasThemes } from "@/lib/canvas-theme";
import { CometCard } from "@/components/ui/aceternity/comet-card";
@@ -320,7 +320,7 @@ export const CanvasNode = React.memo(function CanvasNode({
/>
<CometCard
containerClassName="overflow-visible"
className={`canvas-node-shell relative h-full w-full overflow-visible rounded-[18px] ${flushMediaContent ? "border-0" : "border"} ${isGeneratingNode ? "canvas-node-shell-generating" : ""}`}
className={`canvas-node-shell relative h-full w-full overflow-visible rounded-[var(--node-radius)] ${flushMediaContent ? "border-0" : "border"} ${isGeneratingNode ? "canvas-node-shell-generating" : ""}`}
rotateDepth={cometDepth}
translateDepth={cometTranslate}
disabled={cometDisabled}
@@ -328,8 +328,17 @@ export const CanvasNode = React.memo(function CanvasNode({
data-state={data.metadata?.status || (isActive ? "active" : isRelated ? "related" : "idle")}
style={{
background: hasImageContent || hasVideoContent ? "transparent" : theme.node.fill,
borderColor: flushMediaContent ? undefined : isActive ? theme.accent.primary : isRelated ? theme.accent.primary : theme.node.stroke,
boxShadow: isActive ? `0 0 0 1px ${theme.accent.primary}66, 0 28px 80px ${theme.spatial.shadow}` : isRelated && !isBatchChild ? `0 0 0 1px ${theme.accent.primary}35, 0 22px 60px ${theme.spatial.shadow}` : undefined,
borderColor: flushMediaContent ? undefined : isActive ? theme.accent.primary : isRelated ? theme.accent.primary : hovered ? "color-mix(in oklch, var(--workspace-border) 80%, var(--workspace-fg) 12%)" : theme.node.stroke,
boxShadow: isFocusRelated
? `0 0 0 1px ${theme.accent.primary}66, 0 0 0 6px ${theme.accent.primary}1a, 0 28px 80px ${theme.spatial.shadow}` // active:最强强调(外发光环6px + inner glow)
: isConnectionTarget
? `0 0 0 1px ${theme.accent.primary}66, 0 0 0 4px ${theme.accent.primary}1a, 0 24px 72px ${theme.spatial.shadow}` // selected-primary:4px软色环 + resize handle
: isSelected || (isRelated && !isBatchChild)
? `0 0 0 2px ${theme.accent.primary}40, 0 22px 60px ${theme.spatial.shadow}` // selected:2px边框环
: hovered
? `0 16px 48px ${theme.spatial.shadow}` // hover:轻微抬升阴影
: undefined, // idle:无额外阴影
transition: "border-color 120ms ease-out, box-shadow 150ms ease-out",
}}
onMouseDown={(event) => onMouseDown(event, data.id)}
onDoubleClick={(event) => {
@@ -376,6 +385,16 @@ export const CanvasNode = React.memo(function CanvasNode({
} as React.CSSProperties
}
>
{/* 节点类型色带(对应 #97 决策1:左侧3px色条,远距离最强信号)*/}
<div
className="pointer-events-none absolute left-0 top-0 z-10 h-full"
style={{ width: "var(--node-type-strip-width)", background: nodeTypeColorVar(data.type, data.metadata?.workflowKind === "character"), borderRadius: "var(--node-radius) 0 0 var(--node-radius)" }}
aria-hidden
/>
{/* 节点状态徽章(对应 #97 决策2:左上角 loading/success/error,近距离确认信号)*/}
{data.metadata?.status && data.metadata.status !== "idle" && data.type !== CanvasNodeType.Frame ? (
<NodeStatusBadge status={data.metadata.status} />
) : null}
<NodeContent
node={data}
theme={theme}
@@ -449,7 +468,7 @@ export const CanvasNode = React.memo(function CanvasNode({
{data.metadata?.versionLabel ? (
<button
type="button"
className="absolute left-3 top-3 z-40 inline-flex h-7 items-center gap-1 rounded-md border px-2 text-[10px] font-semibold backdrop-blur transition hover:brightness-110"
className="absolute left-3 top-3 z-40 inline-flex h-7 items-center gap-1 rounded-md border px-2 text-[var(--fs-tiny)] font-semibold backdrop-blur transition hover:brightness-110"
style={{ background: theme.toolbar.panel, borderColor: data.metadata.versionPrimary ? theme.node.activeStroke : theme.toolbar.border, color: data.metadata.versionPrimary ? theme.node.activeStroke : theme.node.text }}
title="查看版本对比"
onMouseDown={(event) => event.stopPropagation()}
@@ -560,7 +579,7 @@ function DrawingContent({ node, theme, drawingProjectId }: NodeContentRendererPr
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex items-end justify-between gap-3 px-4 pb-3 pt-12" style={{ background: `linear-gradient(to top, ${theme.node.fill}, ${theme.node.fill}e6 55%, transparent)` }}>
<div className="min-w-0">
<div className="truncate text-xs font-semibold" title={node.title || "绘图"}>{node.title || "绘图"}</div>
<div className="mt-0.5 text-[10px]" style={{ color: theme.node.muted }}>{shapeCount} 个图形 · {pageCount} 个页面</div>
<div className="mt-0.5 text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>{shapeCount} 个图形 · {pageCount} 个页面</div>
</div>
<Pencil className="size-3.5 shrink-0" style={{ color: theme.accent.primary }} />
</div>
@@ -576,10 +595,10 @@ function LoadingContent({ node, theme, onCancelTask, onOpenTaskDetails }: Pick<N
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-2.5 px-5 text-center" style={{ color: theme.node.activeStroke }}>
<div className="size-10 animate-spin rounded-full border-2" style={{ borderColor: theme.node.stroke, borderTopColor: theme.node.activeStroke }} />
<span className="text-[10px] font-semibold">{node.metadata?.taskStage || (taskId ? "任务处理中" : "正在创建任务")}</span>
<span className="text-[var(--fs-tiny)] font-semibold">{node.metadata?.taskStage || (taskId ? "任务处理中" : "正在创建任务")}</span>
{taskId ? (
<div className="flex w-full max-w-[210px] flex-col items-center gap-1.5">
<div className="max-w-full truncate text-[11px] font-medium" style={{ color: theme.node.text }}>
<div className="max-w-full truncate text-[var(--fs-label)] font-medium" style={{ color: theme.node.text }}>
{statusLabel}
{progress !== null ? ` · ${progress}%` : ""}
</div>
@@ -588,12 +607,12 @@ function LoadingContent({ node, theme, onCancelTask, onOpenTaskDetails }: Pick<N
<div className="h-full rounded-full transition-[width]" style={{ width: `${progress}%`, background: theme.node.activeStroke }} />
</div>
) : null}
<div className="max-w-full truncate text-[10px] tabular-nums" style={{ color: theme.node.muted }}>
<div className="max-w-full truncate text-[var(--fs-tiny)] tabular-nums" style={{ color: theme.node.muted }}>
<Clock3 className="mr-1 inline size-3" />{elapsed} · {shortTaskId(taskId)}
</div>
<div className="mt-0.5 flex items-center gap-1.5">
<button type="button" className="inline-flex h-7 items-center gap-1 rounded-md px-2 text-[10px] font-medium transition hover:brightness-110" style={{ background: theme.toolbar.itemHover, color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onOpenTaskDetails?.(node); }}><FileText className="size-3" />详情</button>
<button type="button" className="inline-flex h-7 items-center gap-1 rounded-md px-2 text-[10px] font-medium transition hover:brightness-110" style={{ background: `${theme.accent.danger}16`, color: theme.accent.danger }} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onCancelTask?.(node); }}><Square className="size-2.5 fill-current" />取消</button>
<button type="button" className="inline-flex h-7 items-center gap-1 rounded-md px-2 text-[var(--fs-tiny)] font-medium transition hover:brightness-110" style={{ background: theme.toolbar.itemHover, color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onOpenTaskDetails?.(node); }}><FileText className="size-3" />详情</button>
<button type="button" className="inline-flex h-7 items-center gap-1 rounded-md px-2 text-[var(--fs-tiny)] font-medium transition hover:brightness-110" style={{ background: `${theme.accent.danger}16`, color: theme.accent.danger }} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onCancelTask?.(node); }}><Square className="size-2.5 fill-current" />取消</button>
</div>
</div>
) : null}
@@ -635,7 +654,7 @@ function ErrorContent({ node, theme, onRetry }: Pick<NodeContentRendererProps, "
<div className="flex max-w-[260px] flex-col items-center gap-3 px-5 text-center">
<div className="text-xs leading-5" style={{ color: theme.accent.danger }}>{generationErrorMessage(node.metadata?.errorDetails)}</div>
{moderationFailure ? (
<div className="rounded-md border px-3 py-2 text-[11px] leading-4" style={{ background: theme.node.fill, borderColor: theme.toolbar.border, color: theme.node.muted }}>
<div className="rounded-md border px-3 py-2 text-[var(--fs-label)] leading-4" style={{ background: theme.node.fill, borderColor: theme.toolbar.border, color: theme.node.muted }}>
修改节点提示词后,可重新点击生成。
</div>
) : (
@@ -724,7 +743,7 @@ function SkillContent({ node, theme }: NodeContentRendererProps) {
</span>
<div className="min-w-0">
<div className="truncate text-sm font-semibold" title={skill?.name || node.title || "技能"}>{skill?.name || node.title || "技能"}</div>
<div className="mt-0.5 flex items-center gap-1.5 text-[11px]" style={{ color: theme.node.muted }}>
<div className="mt-0.5 flex items-center gap-1.5 text-[var(--fs-label)]" style={{ color: theme.node.muted }}>
<span>{skillCategoryLabel(skill?.category)}</span>
<span>·</span>
<span>{skillOutputModeLabel(skill?.outputMode)}</span>
@@ -749,10 +768,10 @@ function SkillContent({ node, theme }: NodeContentRendererProps) {
<div className="mt-3 flex flex-wrap gap-1.5">
{tags.length ? tags.map((tag) => (
<span key={tag} className="rounded-md border px-1.5 py-0.5 text-[10px]" style={{ borderColor: theme.node.stroke, color: theme.node.muted }}>
<span key={tag} className="rounded-md border px-1.5 py-0.5 text-[var(--fs-tiny)]" style={{ borderColor: theme.node.stroke, color: theme.node.muted }}>
{tag}
</span>
)) : <span className="text-[11px]" style={{ color: theme.node.muted }}>连接到图片、视频、音频或文本节点后生效</span>}
)) : <span className="text-[var(--fs-label)]" style={{ color: theme.node.muted }}>连接到图片、视频、音频或文本节点后生效</span>}
</div>
</div>
);
@@ -775,7 +794,7 @@ function skillOutputModeLabel(mode?: string) {
function ResourceLabelBadge({ reference, theme }: { reference: CanvasResourceReference; theme: CanvasTheme }) {
return (
<span className="pointer-events-none min-w-0 max-w-28 truncate rounded-md px-1.5 py-1 text-[10px] font-medium leading-none text-white shadow-sm" style={{ background: reference.active ? theme.accent.primary : "rgba(0,0,0,.35)", opacity: reference.active ? 1 : 0.75 }} title={reference.title || reference.label}>
<span className="pointer-events-none min-w-0 max-w-28 truncate rounded-md px-1.5 py-1 text-[var(--fs-tiny)] font-medium leading-none text-white shadow-sm" style={{ background: reference.active ? theme.accent.primary : "rgba(0,0,0,.35)", opacity: reference.active ? 1 : 0.75 }} title={reference.title || reference.label}>
{reference.label}
</span>
);
@@ -785,7 +804,7 @@ function ResourceStorageBadge({ storageKey, active, theme }: { storageKey?: stri
const location = resourceStorageLocation(storageKey);
const background = active ? (location === "local" ? "rgba(245,158,11,.9)" : theme.accent.primary) : "rgba(0,0,0,.35)";
return (
<span className="pointer-events-auto shrink-0 rounded-md px-1.5 py-1 text-[10px] font-medium leading-none text-white shadow-sm" style={{ background, opacity: active ? 1 : 0.75 }} title={resourceStorageTitle(storageKey)}>
<span className="pointer-events-auto shrink-0 rounded-md px-1.5 py-1 text-[var(--fs-tiny)] font-medium leading-none text-white shadow-sm" style={{ background, opacity: active ? 1 : 0.75 }} title={resourceStorageTitle(storageKey)}>
{resourceStorageLabel(storageKey)}
</span>
);
@@ -797,7 +816,7 @@ function NodeLockBadge({ theme }: { theme: CanvasTheme }) {
function BatchToggleBadge({ count, expanded, theme, onToggle }: { count: number; expanded: boolean; theme: CanvasTheme; onToggle: () => void }) {
return (
<button type="button" className="canvas-node-tool-button inline-flex h-7 shrink-0 items-center gap-1 rounded-md border px-2 text-[10px] font-semibold backdrop-blur-md" style={{ background: `${theme.toolbar.panel}d9`, borderColor: `${theme.toolbar.border}cc`, color: theme.node.text }} aria-label={expanded ? "图片组已展开" : "图片组已收起"} onClick={(event) => { event.stopPropagation(); onToggle(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
<button type="button" className="canvas-node-tool-button inline-flex h-7 shrink-0 items-center gap-1 rounded-md border px-2 text-[var(--fs-tiny)] font-semibold backdrop-blur-md" style={{ background: `${theme.toolbar.panel}d9`, borderColor: `${theme.toolbar.border}cc`, color: theme.node.text }} aria-label={expanded ? "图片组已展开" : "图片组已收起"} onClick={(event) => { event.stopPropagation(); onToggle(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
<span className="leading-none" style={{ color: theme.accent.primary }}>{count}</span>
<ChevronRight className={`size-3 opacity-55 transition-transform ${expanded ? "rotate-90" : ""}`} />
</button>
@@ -806,7 +825,7 @@ function BatchToggleBadge({ count, expanded, theme, onToggle }: { count: number;
function BatchPrimaryBadge({ visible, selected, theme, onSelect }: { visible: boolean; selected: boolean; theme: CanvasTheme; onSelect: () => void }) {
return (
<button type="button" className={`canvas-node-tool-button inline-flex h-7 shrink-0 items-center gap-1 rounded-md border px-2 text-[10px] font-medium backdrop-blur-md transition-opacity ${visible ? "opacity-100" : "pointer-events-none opacity-0"}`} style={{ background: theme.toolbar.panel, borderColor: selected ? theme.accent.primary : theme.toolbar.border, color: selected ? theme.accent.primary : theme.node.text }} aria-label={selected ? "当前主图" : "设置为主图"} aria-pressed={selected} onClick={(event) => { event.stopPropagation(); onSelect(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
<button type="button" className={`canvas-node-tool-button inline-flex h-7 shrink-0 items-center gap-1 rounded-md border px-2 text-[var(--fs-tiny)] font-medium backdrop-blur-md transition-opacity ${visible ? "opacity-100" : "pointer-events-none opacity-0"}`} style={{ background: theme.toolbar.panel, borderColor: selected ? theme.accent.primary : theme.toolbar.border, color: selected ? theme.accent.primary : theme.node.text }} aria-label={selected ? "当前主图" : "设置为主图"} aria-pressed={selected} onClick={(event) => { event.stopPropagation(); onSelect(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
<Star className={`size-3 ${selected ? "fill-current" : ""}`} style={{ color: theme.accent.primary }} />
{selected ? "当前主图" : "主图"}
</button>
@@ -819,7 +838,7 @@ function AssetTagBadges({ tags, theme }: { tags: string[]; theme: (typeof canvas
{tags.map((tag, index) => (
<span
key={`${tag}-${index}`}
className="max-w-full truncate rounded-md border px-1.5 py-1 text-[10px] font-medium leading-none backdrop-blur-sm"
className="max-w-full truncate rounded-md border px-1.5 py-1 text-[var(--fs-tiny)] font-medium leading-none backdrop-blur-sm"
style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}
>
{tag.trim()}
@@ -871,9 +890,9 @@ function EmptyImageContent({ node, theme, isBatchRoot, batchCount, batchExpanded
{isCharacterReference ? (
<div className="max-w-[80%] text-center">
<div className="truncate text-xs font-medium" title={node.metadata?.characterName || node.title} style={{ color: theme.node.muted }}>{node.metadata?.characterName || node.title}</div>
<div className="mt-1 text-[10px] tracking-[0.12em] opacity-50">多视角参考 · 待生成</div>
<div className="mt-1 text-[var(--fs-tiny)] tracking-[0.12em] opacity-50">多视角参考 · 待生成</div>
</div>
) : <span className="text-[10px] tracking-[0.18em] opacity-50">空图片节点</span>}
) : <span className="text-[var(--fs-tiny)] tracking-[0.18em] opacity-50">空图片节点</span>}
</div>
);
if (isBatchRoot)
@@ -898,7 +917,7 @@ function VideoNodeContent({ node, theme, reduceMediaEffects }: NodeContentRender
if (!url) {
return <DeferredMediaLoad icon={loading ? <LoaderCircle className="size-5 animate-spin" /> : <Play className="size-5 fill-current" />} label={loading ? "正在缓存视频" : "加载并缓存视频"} disabled={loading} onClick={() => { playWhenReadyRef.current = true; void load(); }} />;
}
return <VideoPlayer src={url} mimeType={node.metadata?.mimeType} title={node.title || "视频"} preload={reduceMediaEffects ? "none" : "metadata"} autoPlay={playWhenReadyRef.current} onCanPlay={() => { playWhenReadyRef.current = false; }} brandColor={theme.accent.primary} className="h-full w-full rounded-[18px] bg-black" dataCanvasNoZoom compactControls />;
return <VideoPlayer src={url} mimeType={node.metadata?.mimeType} title={node.title || "视频"} preload={reduceMediaEffects ? "none" : "metadata"} autoPlay={playWhenReadyRef.current} onCanPlay={() => { playWhenReadyRef.current = false; }} brandColor={theme.accent.primary} className="h-full w-full rounded-[var(--node-radius)] bg-black" dataCanvasNoZoom compactControls />;
}
function AudioNodeContent({ node, theme }: NodeContentRendererProps) {
@@ -956,7 +975,7 @@ function ImageContent({
return (
<BatchFrame batchCount={isBatchRoot ? batchCount : 0} batchExpanded={batchExpanded} batchOpening={batchOpening} batchRecovering={batchRecovering} theme={theme} onToggleBatch={onToggleBatch}>
<div ref={imageContainerRef} className="h-full w-full overflow-hidden rounded-[18px]">
<div ref={imageContainerRef} className="h-full w-full overflow-hidden rounded-[var(--node-radius)]">
{url ? (
<img
src={url}
@@ -975,7 +994,7 @@ function ImageContent({
function DeferredMediaLoad({ icon, label, disabled, onClick }: { icon: ReactNode; label: string; disabled: boolean; onClick: () => void }) {
return (
<button type="button" data-canvas-no-zoom className="flex size-full flex-col items-center justify-center gap-2 rounded-[16px] bg-black text-white/75 transition hover:text-white disabled:cursor-wait" disabled={disabled} onClick={(event) => { event.stopPropagation(); onClick(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
<button type="button" data-canvas-no-zoom className="flex size-full flex-col items-center justify-center gap-2 rounded-[var(--r-2xl)] bg-black text-white/75 transition hover:text-white disabled:cursor-wait" disabled={disabled} onClick={(event) => { event.stopPropagation(); onClick(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
<span className="grid size-10 place-items-center rounded-full bg-white/10">{icon}</span>
<span className="text-xs font-medium">{label}</span>
</button>
@@ -1063,7 +1082,7 @@ function ImageInfoBar({ node }: { node: CanvasNodeData }) {
const height = Math.round(node.metadata?.naturalHeight || node.height);
const size = formatBytes(node.metadata?.bytes || 0);
return (
<span className="ml-auto max-w-full shrink-0 truncate rounded-md bg-black/55 px-2 py-1 text-[11px] font-medium leading-none text-white backdrop-blur-sm">
<span className="ml-auto max-w-full shrink-0 truncate rounded-md bg-black/55 px-2 py-1 text-[var(--fs-label)] font-medium leading-none text-white backdrop-blur-sm">
{width} x {height}
{size ? ` · ${size}` : ""}
</span>
@@ -1185,6 +1204,59 @@ function nodeTypeIcon(type: CanvasNodeType) {
return Type;
}
// 节点类型色带色(对应 #97 决策1 色带方案,引用 --cn-type-* token)
function nodeTypeColorVar(type: CanvasNodeType, isCharacter?: boolean): string {
if (isCharacter) return "var(--cn-type-character)";
if (type === CanvasNodeType.Image) return "var(--cn-type-image)";
if (type === CanvasNodeType.Video) return "var(--cn-type-video)";
if (type === CanvasNodeType.Audio) return "var(--cn-type-audio)";
if (type === CanvasNodeType.Drawing) return "var(--cn-type-drawing)";
if (type === CanvasNodeType.Script) return "var(--cn-type-script)";
if (type === CanvasNodeType.Config) return "var(--cn-type-config)";
if (type === CanvasNodeType.Skill) return "var(--cn-type-skill)";
if (type === CanvasNodeType.Frame) return "var(--cn-type-frame)";
return "var(--cn-type-text)";
}
// 节点状态徽章(对应 #97 决策2:左上角状态指示,loading/success/error)
function NodeStatusBadge({ status }: { status: "loading" | "success" | "error" }) {
if (status === "loading") {
return (
<div
className="pointer-events-none absolute left-2 top-2 z-20 flex items-center gap-1 rounded-full px-2 py-0.5 backdrop-blur-sm"
style={{ background: "color-mix(in oklch, var(--status-loading) 20%, transparent)", color: "var(--status-loading)" }}
aria-label="生成中"
>
<span className="size-1.5 animate-pulse rounded-full" style={{ background: "var(--status-loading)" }} />
<span className="text-[var(--fs-micro)] font-medium leading-none">生成中</span>
</div>
);
}
if (status === "error") {
return (
<div
className="pointer-events-none absolute left-2 top-2 z-20 flex items-center gap-1 rounded-full px-2 py-0.5 backdrop-blur-sm"
style={{ background: "color-mix(in oklch, var(--status-error) 20%, transparent)", color: "var(--status-error)" }}
aria-label="生成失败"
>
<AlertCircle className="size-3" strokeWidth={2} />
<span className="text-[var(--fs-micro)] font-medium leading-none">失败</span>
</div>
);
}
// success:短暂闪现后淡出(由 CSS animation 控制,2s 后消失)
return (
<div
className="pointer-events-none absolute left-2 top-2 z-20 flex items-center gap-1 rounded-full px-2 py-0.5 backdrop-blur-sm"
style={{ background: "color-mix(in oklch, var(--status-success) 20%, transparent)", color: "var(--status-success)", animation: "canvas-status-success-fade 2s ease-out forwards" }}
aria-label="生成完成"
>
<CheckCircle2 className="size-3" strokeWidth={2} />
<span className="text-[var(--fs-micro)] font-medium leading-none">完成</span>
</div>
);
}
function ConnectionSideRail({ side, scale, visible, theme, onPointerDown }: { side: "left" | "right"; scale: number; visible: boolean; theme: CanvasTheme; onPointerDown: (event: React.PointerEvent, anchorRatio: number) => void }) {
const handleRef = useRef<HTMLSpanElement>(null);
const anchorRatioRef = useRef(0.5);

View File

@@ -33,7 +33,7 @@ export function CanvasPortraitTexturePopover({ value, placement = "topLeft", onC
<div className="space-y-1">
{PORTRAIT_TEXTURE_GROUPS.map((group) => (
<div key={group.key} className="grid grid-cols-[60px_minmax(0,1fr)] items-center gap-2 py-1">
<span className="text-[11px]" style={{ color: theme.node.muted }}>{group.label}</span>
<span className="text-[var(--fs-label)]" style={{ color: theme.node.muted }}>{group.label}</span>
<div className="grid min-w-0 grid-cols-3 gap-1" role="radiogroup" aria-label={group.label}>
{group.options.map((option) => {
const selected = settings[group.key] === option.value;
@@ -43,7 +43,7 @@ export function CanvasPortraitTexturePopover({ value, placement = "topLeft", onC
type="button"
role="radio"
aria-checked={selected}
className="h-7 min-w-0 rounded-md border px-1 text-[10px] transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 motion-reduce:transition-none"
className="h-7 min-w-0 rounded-md border px-1 text-[var(--fs-tiny)] transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 motion-reduce:transition-none"
style={{
background: selected ? theme.accent.primarySoft : theme.toolbar.itemHover,
borderColor: selected ? theme.accent.primary : "transparent",
@@ -78,7 +78,7 @@ export function CanvasPortraitTexturePopover({ value, placement = "topLeft", onC
aria-label="打开人物质感调节面板"
>
<SlidersHorizontal className="size-3 shrink-0" />
<span className="truncate text-[10px] font-medium">人物质感调节</span>
<span className="truncate text-[var(--fs-tiny)] font-medium">人物质感调节</span>
</button>
</Popover>
);

View File

@@ -104,9 +104,9 @@ export function CanvasPresetPicker({ mode, skillReferences = [], open, onOpenCha
<span className="min-w-0 flex-1">
<span className="flex items-center gap-1.5 text-xs font-semibold" style={{ color: theme.node.text }}>
<span className="truncate">{preset.name}</span>
<span className="shrink-0 text-[9px] font-medium" style={{ color: theme.accent.primary }}>{preset.source === "skill" ? "技能" : "预设"}</span>
<span className="shrink-0 text-[var(--fs-micro)] font-medium" style={{ color: theme.accent.primary }}>{preset.source === "skill" ? "技能" : "预设"}</span>
</span>
<span className="mt-0.5 block truncate text-[10px] leading-4" style={{ color: theme.node.muted }}>{preset.description}</span>
<span className="mt-0.5 block truncate text-[var(--fs-tiny)] leading-4" style={{ color: theme.node.muted }}>{preset.description}</span>
</span>
</button>
)) : <div className="py-8 text-center text-xs" style={{ color: theme.node.muted }}>没有匹配的预设</div>}
@@ -118,7 +118,7 @@ export function CanvasPresetPicker({ mode, skillReferences = [], open, onOpenCha
<Popover open={actualOpen} onOpenChange={setOpen} trigger="click" placement="topLeft" arrow={false} content={content} classNames={{ root: "canvas-preset-picker-popover", container: "canvas-composer-popover-surface", content: "canvas-composer-popover-content" }}>
<button type="button" className={`canvas-preset-picker-trigger inline-flex shrink-0 items-center justify-center gap-1 rounded-lg transition hover:brightness-110 focus-visible:outline-none ${compact ? "size-6" : dense ? "h-6 px-1.5" : "h-7 px-2"}`} style={{ background: theme.accent.primarySoft, color: theme.accent.primary }} title="打开提示词预设" aria-label="打开提示词预设" aria-expanded={actualOpen}>
<WandSparkles className={dense ? "size-3" : "size-3.5"} />
{compact ? null : <span className="text-[10px] font-semibold">预设</span>}
{compact ? null : <span className="text-[var(--fs-tiny)] font-semibold">预设</span>}
</button>
</Popover>
);

View File

@@ -44,12 +44,12 @@ export function CanvasProjectAssetModal({ open, detail, initialCategory = "all",
return <Modal open={open} title={null} footer={null} destroyOnHidden onCancel={onClose} width="min(920px, calc(100vw - 24px))" styles={{ container: { padding: 0, overflow: "hidden" }, body: { padding: 0 } }}>
<div className="flex h-[min(620px,calc(100vh-80px))] min-h-[440px] flex-col overflow-hidden">
<header className="flex h-12 shrink-0 items-center justify-between border-b border-border py-0 pl-4 pr-12"><h2 className="text-sm font-semibold">引用项目角色与资产</h2><span className="text-[11px] text-foreground/42">已选 {selectedIds.size} 项</span></header>
<header className="flex h-12 shrink-0 items-center justify-between border-b border-border py-0 pl-4 pr-12"><h2 className="text-sm font-semibold">引用项目角色与资产</h2><span className="text-[var(--fs-label)] text-foreground/42">已选 {selectedIds.size} 项</span></header>
<div className="grid min-h-0 flex-1 grid-rows-[auto_minmax(0,1fr)] sm:grid-cols-[150px_minmax(0,1fr)] sm:grid-rows-1">
<nav className="thin-scrollbar flex min-w-0 gap-1 overflow-x-auto border-b border-border p-2 sm:block sm:overflow-y-auto sm:border-b-0 sm:border-r" aria-label="项目资产分类">{categories.map((item) => <button key={item} type="button" onClick={() => setCategory(item)} className={`flex h-11 min-w-[104px] shrink-0 items-center justify-between rounded-md px-2 text-xs sm:w-full sm:min-w-0 ${category === item ? "bg-foreground/[.08] font-medium" : "text-foreground/55 hover:bg-foreground/[.04]"}`}><span>{categoryLabels[item] || "其他"}</span><span className="min-w-5 rounded bg-foreground/[.05] px-1 text-center text-[10px] tabular-nums">{item === "all" ? items.length : items.filter((asset) => asset.category === item).length}</span></button>)}</nav>
<nav className="thin-scrollbar flex min-w-0 gap-1 overflow-x-auto border-b border-border p-2 sm:block sm:overflow-y-auto sm:border-b-0 sm:border-r" aria-label="项目资产分类">{categories.map((item) => <button key={item} type="button" onClick={() => setCategory(item)} className={`flex h-11 min-w-[104px] shrink-0 items-center justify-between rounded-md px-2 text-xs sm:w-full sm:min-w-0 ${category === item ? "bg-foreground/[.08] font-medium" : "text-foreground/55 hover:bg-foreground/[.04]"}`}><span>{categoryLabels[item] || "其他"}</span><span className="min-w-5 rounded bg-foreground/[.05] px-1 text-center text-[var(--fs-tiny)] tabular-nums">{item === "all" ? items.length : items.filter((asset) => asset.category === item).length}</span></button>)}</nav>
<div className="thin-scrollbar min-h-0 overflow-y-auto p-3">{visible.length ? <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4">{visible.map((item) => <ProjectAssetCard key={item.id} item={item} selected={selectedIds.has(item.id)} onToggle={() => toggle(item.id)} />)}</div> : <WorkspaceState icon="assets" compact className="h-full" title="此分类没有可引用资产" description="先在项目角色与资产中完成角色确认或素材关联。" />}</div>
</div>
<footer className="flex h-12 shrink-0 items-center justify-between border-t border-border px-3"><span className="text-[10px] text-foreground/42">角色引用会在生成时解析当前角色版本</span><div className="flex gap-2"><Button size="small" onClick={onClose}>取消</Button><Button size="small" type="primary" disabled={!selectedIds.size} loading={inserting} onClick={() => void insert()}>引入 {selectedIds.size || ""} 项</Button></div></footer>
<footer className="flex h-12 shrink-0 items-center justify-between border-t border-border px-3"><span className="text-[var(--fs-tiny)] text-foreground/42">角色引用会在生成时解析当前角色版本</span><div className="flex gap-2"><Button size="small" onClick={onClose}>取消</Button><Button size="small" type="primary" disabled={!selectedIds.size} loading={inserting} onClick={() => void insert()}>引入 {selectedIds.size || ""} 项</Button></div></footer>
</div>
</Modal>;
}
@@ -62,7 +62,7 @@ function ProjectAssetCard({ item, selected, onToggle }: { item: ProjectPickerIte
const Icon = character ? UserRound : media?.kind === "video" ? Video : media?.kind === "audio" ? Music2 : media?.kind === "text" ? FileText : ImageIcon;
const label = character ? "角色卡" : media?.kind === "video" ? "视频" : media?.kind === "audio" ? "音频" : media?.kind === "text" ? "文本" : "图片";
const title = character?.title || media?.title || "未命名资产";
return <button type="button" onClick={onToggle} className={`relative min-w-0 overflow-hidden rounded-md border text-left transition-colors ${selected ? "border-[var(--workspace-accent)] bg-[var(--workspace-accent-soft)]" : "border-border/80 hover:border-foreground/30"}`}><div className="relative aspect-[4/3] overflow-hidden bg-foreground/[.04]">{cover ? <img src={cover} alt={title} loading="lazy" decoding="async" className="h-full w-full object-contain p-1" /> : <AssetMediaPreview asset={media} alt={title} className="h-full w-full bg-black object-cover" fallback={<div className="grid h-full place-items-center text-foreground/25"><Icon className="size-7" /></div>} />}<span className={`absolute right-1.5 top-1.5 grid size-5 place-items-center rounded border ${selected ? "border-[var(--workspace-accent)] bg-[var(--workspace-accent)] text-white" : "border-white/60 bg-black/25 text-transparent backdrop-blur"}`}><Check className="size-3" /></span><span className="absolute bottom-1.5 left-1.5 rounded bg-black/55 px-1.5 py-0.5 text-[9px] text-white">{label}</span></div><div className="px-2 py-1.5"><div className="truncate text-[11px] font-medium">{title}</div>{character ? <div className="mt-0.5 truncate text-[9px] text-foreground/42">{character.character?.visualStatus === "ready" ? "形象就绪" : "形象待完善"} · {character.character?.voiceStatus === "ready" ? "声音已绑定" : "声音未绑定"}</div> : null}</div></button>;
return <button type="button" onClick={onToggle} className={`relative min-w-0 overflow-hidden rounded-md border text-left transition-colors ${selected ? "border-[var(--workspace-accent)] bg-[var(--workspace-accent-soft)]" : "border-border/80 hover:border-foreground/30"}`}><div className="relative aspect-[4/3] overflow-hidden bg-foreground/[.04]">{cover ? <img src={cover} alt={title} loading="lazy" decoding="async" className="h-full w-full object-contain p-1" /> : <AssetMediaPreview asset={media} alt={title} className="h-full w-full bg-black object-cover" fallback={<div className="grid h-full place-items-center text-foreground/25"><Icon className="size-7" /></div>} />}<span className={`absolute right-1.5 top-1.5 grid size-5 place-items-center rounded border ${selected ? "border-[var(--workspace-accent)] bg-[var(--workspace-accent)] text-white" : "border-white/60 bg-black/25 text-transparent backdrop-blur"}`}><Check className="size-3" /></span><span className="absolute bottom-1.5 left-1.5 rounded bg-black/55 px-1.5 py-0.5 text-[var(--fs-micro)] text-white">{label}</span></div><div className="px-2 py-1.5"><div className="truncate text-[var(--fs-label)] font-medium">{title}</div>{character ? <div className="mt-0.5 truncate text-[var(--fs-micro)] text-foreground/42">{character.character?.visualStatus === "ready" ? "形象就绪" : "形象待完善"} · {character.character?.voiceStatus === "ready" ? "声音已绑定" : "声音未绑定"}</div> : null}</div></button>;
}
function toInsertPayload(item: ProjectPickerItem): InsertAssetPayload {

View File

@@ -45,7 +45,7 @@ export function CanvasProjectCard({ project, projectName, variant = "library" }:
<ProjectPreview project={project} />
</button>
{!compact ? <span className={`absolute left-2.5 top-2.5 grid size-6 place-items-center rounded-md border border-black/10 bg-white/90 shadow-sm backdrop-blur transition-opacity dark:border-white/10 dark:bg-stone-900/90 ${selected ? "opacity-100" : "opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100"}`} onClick={(event) => event.stopPropagation()}><input type="checkbox" checked={selected} onChange={(event) => toggleSelected(project.id, event.target.checked)} className="app-canvas-project-checkbox size-3.5" aria-label={`选择 ${project.title}`} /></span> : null}
<span className="absolute bottom-2 right-2 rounded-md border border-white/15 bg-stone-950/80 px-1.5 py-0.5 text-[10px] font-medium text-white backdrop-blur-xl">{project.nodes.length} 节点</span>
<span className="absolute bottom-2 right-2 rounded-md border border-white/15 bg-stone-950/80 px-1.5 py-0.5 text-[var(--fs-tiny)] font-medium text-white backdrop-blur-xl">{project.nodes.length} 节点</span>
</div>
<div className={cn("app-canvas-project-body", compact ? "px-1 pb-1 pt-2.5" : "px-3 py-2.5")}>
@@ -61,7 +61,7 @@ export function CanvasProjectCard({ project, projectName, variant = "library" }:
open();
}}
>
<h2 className="truncate text-[13px] font-semibold leading-5 text-foreground">{project.title}</h2>
<h2 className="truncate text-[var(--fs-body)] font-semibold leading-5 text-foreground">{project.title}</h2>
</button>
)}
{editing ? (
@@ -88,12 +88,12 @@ export function CanvasProjectCard({ project, projectName, variant = "library" }:
</div>
)}
</div>
<div className="mt-1 flex min-w-0 items-center gap-1.5 text-[11px] leading-4 text-foreground/52">
<div className="mt-1 flex min-w-0 items-center gap-1.5 text-[var(--fs-label)] leading-4 text-foreground/52">
<span className="truncate">{projectName || "自由画布"}</span>
<span className="shrink-0 text-foreground/28" aria-hidden="true">·</span>
<span className="shrink-0 tabular-nums">{project.connections.length} 条连线</span>
</div>
<p className="mt-1 text-[10px] leading-4 tabular-nums text-foreground/38">更新于 {formatProjectTime(project.updatedAt)}</p>
<p className="mt-1 text-[var(--fs-tiny)] leading-4 tabular-nums text-foreground/38">更新于 {formatProjectTime(project.updatedAt)}</p>
</div>
</article>
);
@@ -128,7 +128,7 @@ function ProjectPreview({ project }: { project: CanvasProject }) {
return (
<span key={node.id} className="absolute flex min-w-0 items-center gap-1.5 overflow-hidden rounded-md border border-stone-300/90 bg-white/90 px-2 text-left shadow-sm backdrop-blur-sm dark:border-stone-700 dark:bg-stone-900/92" style={style}>
<span className="grid size-5 shrink-0 place-items-center text-stone-500 dark:text-stone-300">{presentation.icon}</span>
<span className="min-w-0 truncate text-[8px] font-semibold text-stone-700 dark:text-stone-200">{node.title || presentation.label}</span>
<span className="min-w-0 truncate text-[var(--fs-micro)] font-semibold text-stone-700 dark:text-stone-200">{node.title || presentation.label}</span>
</span>
);
})}

View File

@@ -133,17 +133,17 @@ export function CanvasProjectSidebar({ projectId, detail, onAddChapter, onLocate
</header>
<section className="shrink-0 border-b border-border/70 p-2">
<div className="mb-1.5 flex h-5 items-center justify-between px-1 text-[11px] font-medium text-foreground/48"><span className="flex items-center gap-1.5"><Palette className="size-3.5" />项目画风</span><Link to={`/projects/${projectId}/settings`} className="grid size-6 place-items-center rounded text-foreground/35 hover:bg-foreground/[.06] hover:text-foreground" title="编辑项目画风" aria-label="编辑项目画风"><Settings2 className="size-3.5" /></Link></div>
<div className="mb-1.5 flex h-5 items-center justify-between px-1 text-[var(--fs-label)] font-medium text-foreground/48"><span className="flex items-center gap-1.5"><Palette className="size-3.5" />项目画风</span><Link to={`/projects/${projectId}/settings`} className="grid size-6 place-items-center rounded text-foreground/35 hover:bg-foreground/[.06] hover:text-foreground" title="编辑项目画风" aria-label="编辑项目画风"><Settings2 className="size-3.5" /></Link></div>
<button type="button" disabled={!style} onClick={onLocateStyle} className="group flex h-11 w-full items-center gap-2 rounded-md px-1.5 text-left hover:bg-foreground/[.045] disabled:cursor-default disabled:hover:bg-transparent" title={style ? "定位画布中的项目画风节点" : "项目尚未设置画风"}>
{style ? <img src={style.imageUrl} alt="" width={48} height={32} className="h-8 w-12 shrink-0 rounded object-cover" /> : <span className="grid h-8 w-12 shrink-0 place-items-center rounded border border-dashed border-border text-foreground/30"><Palette className="size-3.5" /></span>}
<span className="min-w-0 flex-1"><span className="block truncate text-xs font-medium">{style?.title || "尚未设置画风"}</span><span className="mt-0.5 block truncate text-[9px] text-foreground/38">{style ? "随项目设置自动同步" : "前往项目设置选择"}</span></span>
<span className="min-w-0 flex-1"><span className="block truncate text-xs font-medium">{style?.title || "尚未设置画风"}</span><span className="mt-0.5 block truncate text-[var(--fs-micro)] text-foreground/38">{style ? "随项目设置自动同步" : "前往项目设置选择"}</span></span>
{style ? <LocateFixed className="size-3.5 shrink-0 text-foreground/25 group-hover:text-[var(--workspace-accent)]" /> : null}
</button>
</section>
<section className="flex min-h-0 flex-1 flex-col">
<div className="flex h-9 shrink-0 items-center justify-between px-3">
<span className="flex items-center gap-1.5 text-[11px] font-medium text-foreground/48"><BookOpenText className="size-3.5" />剧情章节 <span className="tabular-nums text-foreground/32">{orderedUnits.length.toLocaleString("zh-CN")}</span></span>
<span className="flex items-center gap-1.5 text-[var(--fs-label)] font-medium text-foreground/48"><BookOpenText className="size-3.5" />剧情章节 <span className="tabular-nums text-foreground/32">{orderedUnits.length.toLocaleString("zh-CN")}</span></span>
{selectedId ? <button type="button" onClick={revealSelectedChapter} className="grid size-6 place-items-center rounded text-foreground/35 hover:bg-foreground/[.06] hover:text-foreground" title="回到当前章节" aria-label="回到当前章节"><Crosshair className="size-3.5" /></button> : null}
</div>
<div className="shrink-0 px-2 pb-2">
@@ -152,7 +152,7 @@ export function CanvasProjectSidebar({ projectId, detail, onAddChapter, onLocate
<input value={query} onChange={(event) => setQuery(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") selectFirstSearchResult(); }} className="min-w-0 flex-1 bg-transparent text-xs outline-none placeholder:text-foreground/28" placeholder="搜索标题或输入章节序号" aria-label="搜索项目章节" />
{query ? <button type="button" onClick={() => setQuery("")} className="grid size-5 shrink-0 place-items-center rounded text-foreground/32 hover:bg-foreground/[.06] hover:text-foreground" aria-label="清空章节搜索"><X className="size-3" /></button> : null}
</label>
{deferredQuery ? <div className="mt-1 px-0.5 text-[9px] tabular-nums text-foreground/35">找到 {visibleUnits.length.toLocaleString("zh-CN")} 章</div> : null}
{deferredQuery ? <div className="mt-1 px-0.5 text-[var(--fs-micro)] tabular-nums text-foreground/35">找到 {visibleUnits.length.toLocaleString("zh-CN")} 章</div> : null}
</div>
<div ref={scrollRef} className="thin-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain px-1.5" aria-label="项目章节列表">
@@ -168,7 +168,7 @@ export function CanvasProjectSidebar({ projectId, detail, onAddChapter, onLocate
<div draggable onDragStart={(event) => { const payload: CanvasProjectChapterPayload = { id: unit.id, projectId, title: unit.title, position: unit.position }; event.dataTransfer.setData(CANVAS_PROJECT_CHAPTER_DND_TYPE, JSON.stringify(payload)); event.dataTransfer.effectAllowed = "copy"; }} className={`group flex h-9 cursor-grab items-center gap-1 rounded-md px-1 text-xs active:cursor-grabbing ${active ? "bg-foreground/[.08]" : "hover:bg-foreground/[.045]"}`}>
<GripVertical className="size-3.5 shrink-0 text-foreground/22" />
<button type="button" onClick={() => setSelectedId(unit.id)} className="flex min-w-0 flex-1 items-center gap-1.5 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--workspace-accent)]" aria-label={`预览第 ${index + 1} 章 ${unit.title}`}>
<span className="w-7 shrink-0 text-[9px] tabular-nums text-foreground/34">{String(index + 1).padStart(Math.max(2, String(orderedUnits.length).length), "0")}</span>
<span className="w-7 shrink-0 text-[var(--fs-micro)] tabular-nums text-foreground/34">{String(index + 1).padStart(Math.max(2, String(orderedUnits.length).length), "0")}</span>
<span className="min-w-0 flex-1 truncate text-foreground/68">{unit.title}</span>
</button>
<button type="button" disabled={Boolean(addingId)} className="grid size-6 shrink-0 place-items-center rounded text-foreground/30 opacity-0 hover:bg-[var(--workspace-accent-soft)] hover:text-[var(--workspace-accent)] group-hover:opacity-100 focus-visible:opacity-100 disabled:cursor-wait" title="添加到画布中央" aria-label={`添加${unit.title}到画布`} onClick={() => void insertChapter(unit)}>{addingId === unit.id ? <LoaderCircle className="size-3.5 animate-spin" /> : <Plus className="size-3.5" />}</button>
@@ -182,7 +182,7 @@ export function CanvasProjectSidebar({ projectId, detail, onAddChapter, onLocate
</section>
<section className="shrink-0 border-t border-border/70 p-2">
<button type="button" onClick={onOpenAssets} className="flex h-9 w-full items-center gap-2 rounded-md border border-border/80 px-2 text-xs text-foreground/65 hover:border-[var(--workspace-accent)] hover:bg-[var(--workspace-accent-soft)]"><Images className="size-3.5 text-foreground/42" /><span className="flex-1 text-left">引用项目资产</span><span className="rounded bg-foreground/[.06] px-1.5 py-0.5 text-[9px] tabular-nums text-foreground/45">{mediaAssetCount.toLocaleString("zh-CN")}</span></button>
<button type="button" onClick={onOpenAssets} className="flex h-9 w-full items-center gap-2 rounded-md border border-border/80 px-2 text-xs text-foreground/65 hover:border-[var(--workspace-accent)] hover:bg-[var(--workspace-accent-soft)]"><Images className="size-3.5 text-foreground/42" /><span className="flex-1 text-left">引用项目资产</span><span className="rounded bg-foreground/[.06] px-1.5 py-0.5 text-[var(--fs-micro)] tabular-nums text-foreground/45">{mediaAssetCount.toLocaleString("zh-CN")}</span></button>
</section>
{selectedUnit ? <ChapterPreview projectId={projectId} unit={selectedUnitQuery.data?.unit || selectedUnit} chapterNumber={orderedUnits.findIndex((unit) => unit.id === selectedUnit.id) + 1} loading={selectedUnitQuery.isLoading} loadError={selectedUnitQuery.isError} adding={addingId === selectedUnit.id} onAdd={() => insertChapter(selectedUnit, selectedUnitQuery.data?.unit.sourceText)} onClose={() => setSelectedId("")} /> : null}
@@ -194,8 +194,8 @@ function ChapterPreview({ projectId, unit, chapterNumber, loading, loadError, ad
const text = htmlToPlainText(unit.sourceText);
return (
<section className="absolute bottom-20 left-[calc(100%+8px)] top-14 z-50 flex w-[320px] flex-col overflow-hidden rounded-lg border border-border/85 bg-background/[.96] shadow-[0_18px_48px_rgba(0,0,0,.22)] backdrop-blur-xl" aria-label={`第 ${chapterNumber} 章预览`}>
<header className="flex items-start gap-2 border-b border-border/70 px-3 py-2.5"><div className="min-w-0 flex-1"><div className="text-[10px] tabular-nums text-foreground/38">第 {chapterNumber.toLocaleString("zh-CN")} 章</div><h2 className="mt-0.5 truncate text-sm font-semibold">{unit.title}</h2></div><button type="button" onClick={onClose} className="grid size-6 shrink-0 place-items-center rounded text-foreground/38 hover:bg-foreground/[.06] hover:text-foreground" aria-label="关闭章节预览"><X className="size-3.5" /></button></header>
<div className="thin-scrollbar min-h-0 flex-1 overflow-y-auto px-3 py-3"><p className="whitespace-pre-wrap text-xs leading-5 text-foreground/58">{loading ? "正在读取本章正文…" : loadError ? "正文读取失败,请稍后重试" : text || "本章还没有正文"}</p>{!loading && !loadError ? <div className="mt-2 text-[10px] tabular-nums text-foreground/35">{text.length.toLocaleString("zh-CN")} 字</div> : null}</div>
<header className="flex items-start gap-2 border-b border-border/70 px-3 py-2.5"><div className="min-w-0 flex-1"><div className="text-[var(--fs-tiny)] tabular-nums text-foreground/38">第 {chapterNumber.toLocaleString("zh-CN")} 章</div><h2 className="mt-0.5 truncate text-sm font-semibold">{unit.title}</h2></div><button type="button" onClick={onClose} className="grid size-6 shrink-0 place-items-center rounded text-foreground/38 hover:bg-foreground/[.06] hover:text-foreground" aria-label="关闭章节预览"><X className="size-3.5" /></button></header>
<div className="thin-scrollbar min-h-0 flex-1 overflow-y-auto px-3 py-3"><p className="whitespace-pre-wrap text-xs leading-5 text-foreground/58">{loading ? "正在读取本章正文…" : loadError ? "正文读取失败,请稍后重试" : text || "本章还没有正文"}</p>{!loading && !loadError ? <div className="mt-2 text-[var(--fs-tiny)] tabular-nums text-foreground/35">{text.length.toLocaleString("zh-CN")} 字</div> : null}</div>
<footer className="flex shrink-0 items-center justify-between border-t border-border/70 px-2 py-2"><button type="button" disabled={adding} onClick={() => void onAdd()} className="inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs font-medium text-[var(--workspace-accent)] hover:bg-[var(--workspace-accent-soft)] disabled:cursor-wait">{adding ? <LoaderCircle className="size-3.5 animate-spin" /> : <Plus className="size-3.5" />}添加到画布</button><Link to={`/projects/${projectId}/chapters/${unit.id}`} className="inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-foreground/62 hover:bg-foreground/[.06] hover:text-foreground">打开编辑器<ArrowUpRight className="size-3.5" /></Link></footer>
</section>
);

View File

@@ -127,7 +127,7 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
<div className="relative flex h-10 shrink-0 items-center gap-2 rounded-t-[17px] border-b px-4" style={{ borderColor: theme.node.stroke, background: theme.node.panel }}>
<Clapperboard className="size-4" />
<span className="min-w-0 flex-1 truncate text-sm font-semibold" title={node.title || "分镜脚本"}>{node.title || "分镜脚本"}</span>
{batchSummary ? <span className="min-w-0 max-w-[42%] truncate text-[11px] font-medium" title={batchSummary} style={{ color: batch?.status === "partial_failed" ? theme.accent.danger : theme.node.muted }}>{batchSummary}</span> : taskFeedback ? <span className="min-w-0 max-w-[38%] truncate text-[11px] font-medium" title={taskFeedback} style={{ color: node.metadata?.status === "error" ? theme.accent.danger : theme.node.muted }}>{taskFeedback}</span> : null}
{batchSummary ? <span className="min-w-0 max-w-[42%] truncate text-[var(--fs-label)] font-medium" title={batchSummary} style={{ color: batch?.status === "partial_failed" ? theme.accent.danger : theme.node.muted }}>{batchSummary}</span> : taskFeedback ? <span className="min-w-0 max-w-[38%] truncate text-[var(--fs-label)] font-medium" title={taskFeedback} style={{ color: node.metadata?.status === "error" ? theme.accent.danger : theme.node.muted }}>{taskFeedback}</span> : null}
<span className="text-xs font-medium" style={{ color: theme.node.muted }}>{rows.length} 镜 · {totalDuration}s</span>
{batch ? <>
{hasFailedBatchItems ? <Tooltip title="重试失败项"><button type="button" className="grid size-7 place-items-center rounded outline-none transition hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/10" style={{ "--tw-ring-color": theme.node.muted } as CSSProperties} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onRetryBatch(batch.id); }} aria-label="重试失败项"><RefreshCw className="size-3.5" /></button></Tooltip> : null}
@@ -167,7 +167,7 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
>
{rows.length ? rows.map((row) => (
<div key={row.id} className="relative grid border-b" style={{ height: STORYBOARD_ROW_HEIGHT, borderColor: theme.node.stroke, gridTemplateColumns: SCRIPT_GRID_TEMPLATE }}>
<div className="flex flex-col items-center justify-center border-r tabular-nums" style={{ color: theme.node.muted, borderColor: theme.node.stroke }}><span className="text-sm">{row.shotNumber}</span>{batchItemByRowId.get(row.id) ? <span className="max-w-16 truncate text-[9px] leading-3" title={generationBatchItemLabel(batchItemByRowId.get(row.id)!)}>{generationBatchItemLabel(batchItemByRowId.get(row.id)!)}</span> : null}</div>
<div className="flex flex-col items-center justify-center border-r tabular-nums" style={{ color: theme.node.muted, borderColor: theme.node.stroke }}><span className="text-sm">{row.shotNumber}</span>{batchItemByRowId.get(row.id) ? <span className="max-w-16 truncate text-[var(--fs-micro)] leading-3" title={generationBatchItemLabel(batchItemByRowId.get(row.id)!)}>{generationBatchItemLabel(batchItemByRowId.get(row.id)!)}</span> : null}</div>
<div className="grid grid-cols-[32px_1fr_32px] items-center border-r px-2" style={{ borderColor: theme.node.stroke }}>
<SmallButton title="减少 1 秒" onClick={() => onUpdateRow(row.id, { durationSeconds: Math.max(1, row.durationSeconds - 1) })}><Minus className="size-3" /></SmallButton>
<span className="text-center text-sm font-medium tabular-nums">{row.durationSeconds}s</span>
@@ -213,7 +213,7 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
<Tooltip title="脚本生成需要文本理解与结构化输出能力,仅展示文本模型;视频/图片模型无法生成分镜表" placement="topLeft">
<div className="mr-auto min-w-36 max-w-56 flex-1">
<ModelPicker
className="!h-7 !w-full !min-w-0 !text-[10px] !font-normal [&_img]:!size-3 [&_.lucide]:!size-3"
className="!h-7 !w-full !min-w-0 !text-[var(--fs-tiny)] !font-normal [&_img]:!size-3 [&_.lucide]:!size-3"
fullWidth
config={generationConfig}
value={generationConfig.model}
@@ -225,7 +225,7 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
/>
</div>
</Tooltip>
{simpleMode ? <span className="text-[11px]" style={{ color: theme.node.muted }}>自动拆分 · 默认时长</span> : <Select<StoryboardShotCount>
{simpleMode ? <span className="text-[var(--fs-label)]" style={{ color: theme.node.muted }}>自动拆分 · 默认时长</span> : <Select<StoryboardShotCount>
className="min-w-32"
size="small"
value={shotCount}
@@ -328,10 +328,10 @@ function PipelineStageCell({ label, stage, theme, children, last = false }: { la
return (
<div className={`flex min-w-0 items-center gap-2 px-3 ${last ? "" : "border-r"}`} style={{ borderColor: theme.node.stroke }}>
<div className="min-w-[64px] shrink-0">
<div className="text-[11px] font-semibold">{label}</div>
<div className="text-[9px] leading-3" style={{ color: stage.failed ? theme.accent.danger : theme.node.muted }}>{pipelineStatusLabel(stage)}</div>
<div className="text-[var(--fs-label)] font-semibold">{label}</div>
<div className="text-[var(--fs-micro)] leading-3" style={{ color: stage.failed ? theme.accent.danger : theme.node.muted }}>{pipelineStatusLabel(stage)}</div>
</div>
<div className="flex min-w-0 flex-1 items-center justify-end gap-1 overflow-hidden [&_.ant-btn]:!h-7 [&_.ant-btn]:!px-2 [&_.ant-btn]:!text-[10px]">{children}</div>
<div className="flex min-w-0 flex-1 items-center justify-end gap-1 overflow-hidden [&_.ant-btn]:!h-7 [&_.ant-btn]:!px-2 [&_.ant-btn]:!text-[var(--fs-tiny)]">{children}</div>
</div>
);
}

View File

@@ -12,11 +12,11 @@ export function CanvasLinkedProjectEmptyState({ projectName, hasChapter, onAddFi
return (
<div className="pointer-events-none absolute inset-0 z-20 grid place-items-center px-4 pb-16 pt-20">
<div className="pointer-events-auto w-full max-w-[440px] rounded-lg border p-3 shadow-sm backdrop-blur" data-canvas-no-zoom style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}>
<div className="flex items-center gap-2.5"><span className="grid size-8 shrink-0 place-items-center rounded-md" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}><FolderKanban className="size-4" /></span><div className="min-w-0"><h2 className="truncate text-sm font-semibold">{projectName}</h2><p className="mt-0.5 text-[11px]" style={{ color: theme.node.muted }}>项目画布为空</p></div></div>
<div className="flex items-center gap-2.5"><span className="grid size-8 shrink-0 place-items-center rounded-md" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}><FolderKanban className="size-4" /></span><div className="min-w-0"><h2 className="truncate text-sm font-semibold">{projectName}</h2><p className="mt-0.5 text-[var(--fs-label)]" style={{ color: theme.node.muted }}>项目画布为空</p></div></div>
<div className="mt-3 grid grid-cols-3 gap-1.5">
<button type="button" disabled={!hasChapter} onClick={onAddFirstChapter} className="flex h-9 min-w-0 items-center justify-center gap-1 rounded-md border px-2 text-[11px] font-medium disabled:opacity-35" style={{ borderColor: theme.node.stroke, background: theme.node.fill }}><Plus className="size-3.5 shrink-0" /><span className="truncate">添加首章</span></button>
<button type="button" onClick={onOpenAssets} className="flex h-9 min-w-0 items-center justify-center gap-1 rounded-md border px-2 text-[11px] font-medium" style={{ borderColor: theme.node.stroke, background: theme.node.fill }}><Images className="size-3.5 shrink-0" /><span className="truncate">项目资产</span></button>
<button type="button" onClick={onAddText} className="flex h-9 min-w-0 items-center justify-center gap-1 rounded-md border px-2 text-[11px] font-medium" style={{ borderColor: theme.node.stroke, background: theme.node.fill }}><Type className="size-3.5 shrink-0" /><span className="truncate">新建文本</span></button>
<button type="button" disabled={!hasChapter} onClick={onAddFirstChapter} className="flex h-9 min-w-0 items-center justify-center gap-1 rounded-md border px-2 text-[var(--fs-label)] font-medium disabled:opacity-35" style={{ borderColor: theme.node.stroke, background: theme.node.fill }}><Plus className="size-3.5 shrink-0" /><span className="truncate">添加首章</span></button>
<button type="button" onClick={onOpenAssets} className="flex h-9 min-w-0 items-center justify-center gap-1 rounded-md border px-2 text-[var(--fs-label)] font-medium" style={{ borderColor: theme.node.stroke, background: theme.node.fill }}><Images className="size-3.5 shrink-0" /><span className="truncate">项目资产</span></button>
<button type="button" onClick={onAddText} className="flex h-9 min-w-0 items-center justify-center gap-1 rounded-md border px-2 text-[var(--fs-label)] font-medium" style={{ borderColor: theme.node.stroke, background: theme.node.fill }}><Type className="size-3.5 shrink-0" /><span className="truncate">新建文本</span></button>
</div>
</div>
</div>
@@ -138,14 +138,14 @@ export function CanvasShortDramaGuide({ progress, collapsed, onToggle, onSkip, o
} as CSSProperties}
onClick={() => onStepClick(step.id)}
>
<span className="grid size-4 place-items-center rounded-full border text-[9px]" style={{ borderColor: step.status === "current" ? theme.accent.primary : theme.node.stroke, background: step.status === "completed" ? theme.accent.primary : "transparent", color: step.status === "completed" ? "#fff" : "currentColor" }}>{step.status === "completed" ? <Check className="size-2.5" /> : index + 1}</span>
<span className="grid size-4 place-items-center rounded-full border text-[var(--fs-micro)]" style={{ borderColor: step.status === "current" ? theme.accent.primary : theme.node.stroke, background: step.status === "completed" ? theme.accent.primary : "transparent", color: step.status === "completed" ? "#fff" : "currentColor" }}>{step.status === "completed" ? <Check className="size-2.5" /> : index + 1}</span>
{step.label}
</button>
</span>
))}
</div>
<span className="mx-1 h-4 w-px shrink-0" style={{ background: theme.toolbar.border }} />
{!progress.completed ? <button type="button" className="inline-flex h-8 shrink-0 items-center gap-1 rounded-md px-2 text-[11px] outline-none transition hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/10" style={{ color: theme.node.muted, "--tw-ring-color": theme.accent.primary } as CSSProperties} onClick={onSkip}><X className="size-3" />跳过导引</button> : null}
{!progress.completed ? <button type="button" className="inline-flex h-8 shrink-0 items-center gap-1 rounded-md px-2 text-[var(--fs-label)] outline-none transition hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/10" style={{ color: theme.node.muted, "--tw-ring-color": theme.accent.primary } as CSSProperties} onClick={onSkip}><X className="size-3" />跳过导引</button> : null}
<button type="button" className="grid size-8 shrink-0 place-items-center rounded-md outline-none transition hover:bg-black/5 focus-visible:ring-2 dark:hover:bg-white/10" style={{ color: theme.node.muted, "--tw-ring-color": theme.accent.primary } as CSSProperties} onClick={onToggle} aria-label="折叠短剧流程"><ChevronUp className="size-3.5" /></button>
</div>
);

View File

@@ -373,10 +373,10 @@ export function CanvasStylePickerModal({ open, value, onClose, onSelect }: { ope
<header className="flex items-center gap-3 border-b px-4 py-4 sm:px-5" style={{ borderColor: theme.node.stroke }}>
<span className="grid size-9 shrink-0 place-items-center rounded-md" style={{ background: theme.toolbar.itemHover, color: theme.node.activeStroke }}><Palette className="size-4" /></span>
<div className="min-w-0">
<div className="flex items-center gap-2 text-sm font-semibold"><span>选择项目画风</span><span className="rounded-full px-2 py-0.5 text-[10px] font-medium" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}>{canvasStylePresets.length} 套视觉预设</span></div>
<div className="mt-0.5 truncate text-[11px]" style={{ color: theme.node.muted }}>画风会统一角色三视图、项目资产、分镜和视频提示词。</div>
<div className="flex items-center gap-2 text-sm font-semibold"><span>选择项目画风</span><span className="rounded-full px-2 py-0.5 text-[var(--fs-tiny)] font-medium" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}>{canvasStylePresets.length} 套视觉预设</span></div>
<div className="mt-0.5 truncate text-[var(--fs-label)]" style={{ color: theme.node.muted }}>画风会统一角色三视图、项目资产、分镜和视频提示词。</div>
</div>
<span className="ml-auto hidden items-center gap-1 text-[11px] sm:flex" style={{ color: theme.node.faint }}><Sparkles className="size-3.5" />先看规范,再做选择</span>
<span className="ml-auto hidden items-center gap-1 text-[var(--fs-label)] sm:flex" style={{ color: theme.node.faint }}><Sparkles className="size-3.5" />先看规范,再做选择</span>
</header>
<div className="thin-scrollbar grid max-h-[76vh] grid-cols-1 gap-3 overflow-y-auto p-3 sm:grid-cols-2 sm:p-4 lg:grid-cols-3">
{canvasStylePresets.map((preset) => {
@@ -386,12 +386,12 @@ export function CanvasStylePickerModal({ open, value, onClose, onSelect }: { ope
<button type="button" className="h-full w-full overflow-hidden rounded-lg border text-left transition hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2" style={{ background: theme.node.panel, borderColor: active ? theme.node.activeStroke : theme.node.stroke, boxShadow: active ? `0 0 0 1px ${theme.node.activeStroke}` : undefined, "--tw-ring-color": theme.node.activeStroke } as CSSProperties} onClick={() => onSelect(preset)}>
<span className="relative block aspect-[16/9] overflow-hidden" style={{ background: theme.canvas.background }}>
<img src={preset.imageUrl} width="960" height="540" alt={`${preset.title}画风示意`} className="h-full w-full object-cover transition duration-300 group-hover:scale-[1.03]" style={preset.id === "black-white-noir" ? { filter: "grayscale(1) contrast(1.08)" } : undefined} loading="lazy" />
<span className="absolute inset-x-0 bottom-0 flex items-center justify-between gap-2 px-3 py-2 text-white" style={{ background: "linear-gradient(180deg, transparent, rgba(0,0,0,.72))" }}><span className="text-[10px] font-medium tracking-wide">{preset.category}</span>{active ? <span className="grid size-5 place-items-center rounded-full bg-white text-black"><Check className="size-3.5" /></span> : null}</span>
<span className="absolute inset-x-0 bottom-0 flex items-center justify-between gap-2 px-3 py-2 text-white" style={{ background: "linear-gradient(180deg, transparent, rgba(0,0,0,.72))" }}><span className="text-[var(--fs-tiny)] font-medium tracking-wide">{preset.category}</span>{active ? <span className="grid size-5 place-items-center rounded-full bg-white text-black"><Check className="size-3.5" /></span> : null}</span>
</span>
<span className="block p-3">
<span className="flex items-center gap-2"><span className="text-sm font-semibold">{preset.title}</span>{active ? <span className="text-[10px]" style={{ color: theme.node.activeStroke }}>已选择</span> : null}</span>
<span className="flex items-center gap-2"><span className="text-sm font-semibold">{preset.title}</span>{active ? <span className="text-[var(--fs-tiny)]" style={{ color: theme.node.activeStroke }}>已选择</span> : null}</span>
<span className="mt-1 block text-xs leading-5" style={{ color: theme.node.muted }}>{preset.description}</span>
<span className="mt-2 flex flex-wrap gap-1">{preset.tags.map((tag) => <span key={tag} className="rounded px-1.5 py-0.5 text-[10px]" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}>{tag}</span>)}</span>
<span className="mt-2 flex flex-wrap gap-1">{preset.tags.map((tag) => <span key={tag} className="rounded px-1.5 py-0.5 text-[var(--fs-tiny)]" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}>{tag}</span>)}</span>
</span>
</button>
<button type="button" className="absolute right-2 top-2 grid size-8 place-items-center rounded-md bg-black/60 text-white shadow-sm backdrop-blur transition-colors hover:bg-black/75 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white" onClick={() => setDetailPreset(preset)} aria-label={`查看${preset.title}详情`} title="查看画风详情"><Eye className="size-4" /></button>
@@ -399,7 +399,7 @@ export function CanvasStylePickerModal({ open, value, onClose, onSelect }: { ope
);
})}
</div>
<footer className="flex items-center gap-2 border-t px-4 py-3 text-[11px]" style={{ borderColor: theme.node.stroke, color: theme.node.faint }}><Clapperboard className="size-3.5" />图片是风格示意,实际效果取决于模型、角色参考图与分镜提示词。</footer>
<footer className="flex items-center gap-2 border-t px-4 py-3 text-[var(--fs-label)]" style={{ borderColor: theme.node.stroke, color: theme.node.faint }}><Clapperboard className="size-3.5" />图片是风格示意,实际效果取决于模型、角色参考图与分镜提示词。</footer>
</div>
</Modal>
<CanvasStyleDetailModal open={Boolean(detailPreset)} preset={detailPreset} selected={detailPreset?.id === value} onClose={() => setDetailPreset(null)} onSelect={(preset) => { setDetailPreset(null); onSelect(preset); }} />
@@ -418,15 +418,15 @@ export function CanvasStyleDetailModal({ open, preset, selected = false, onClose
</aside>
<section className="flex min-h-0 flex-1 flex-col overflow-hidden">
<header className="border-b px-5 py-4" style={{ borderColor: theme.node.stroke }}>
<div className="text-[10px] font-medium" style={{ color: theme.node.activeStroke }}>{preset.category}</div>
<div className="text-[var(--fs-tiny)] font-medium" style={{ color: theme.node.activeStroke }}>{preset.category}</div>
<h2 className="mt-1 text-xl font-semibold">{preset.title}</h2>
<p className="mt-2 text-xs leading-5" style={{ color: theme.node.muted }}>{preset.description}</p>
<div className="mt-3 flex flex-wrap gap-1.5">{preset.tags.map((tag) => <span key={tag} className="rounded px-2 py-1 text-[10px]" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}>{tag}</span>)}</div>
<div className="mt-3 flex flex-wrap gap-1.5">{preset.tags.map((tag) => <span key={tag} className="rounded px-2 py-1 text-[var(--fs-tiny)]" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}>{tag}</span>)}</div>
</header>
<div className="thin-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain px-5 py-2">
{sections.map((section) => <div key={section.title} className="border-b py-3 last:border-b-0" style={{ borderColor: theme.node.stroke }}><h3 className="text-xs font-semibold">{section.title}</h3><p className="mt-1.5 text-xs leading-5" style={{ color: theme.node.muted }}>{section.content}</p></div>)}
</div>
<footer className="flex flex-col gap-2 border-t px-5 py-3 sm:flex-row sm:items-center sm:justify-between" style={{ borderColor: theme.node.stroke }}><span className="text-[10px]" style={{ color: theme.node.faint }}>角色三视图会自动提取其中的色彩、造型和材质规范</span><div className="flex shrink-0 justify-end gap-2"><Button onClick={onClose}>关闭</Button>{onSelect ? <Button type="primary" disabled={selected} icon={selected ? <Check className="size-3.5" /> : <Palette className="size-3.5" />} onClick={() => onSelect(preset)}>{selected ? "当前画风" : "选择该画风"}</Button> : null}</div></footer>
<footer className="flex flex-col gap-2 border-t px-5 py-3 sm:flex-row sm:items-center sm:justify-between" style={{ borderColor: theme.node.stroke }}><span className="text-[var(--fs-tiny)]" style={{ color: theme.node.faint }}>角色三视图会自动提取其中的色彩、造型和材质规范</span><div className="flex shrink-0 justify-end gap-2"><Button onClick={onClose}>关闭</Button>{onSelect ? <Button type="primary" disabled={selected} icon={selected ? <Check className="size-3.5" /> : <Palette className="size-3.5" />} onClick={() => onSelect(preset)}>{selected ? "当前画风" : "选择该画风"}</Button> : null}</div></footer>
</section>
</div> : null}
</Modal>

View File

@@ -51,7 +51,7 @@ export function CanvasTextEditorModal({ node, open, onClose, onSave }: CanvasTex
content: emptyTextDocument(),
editorProps: {
attributes: {
class: "min-h-full px-8 py-7 text-[15px] leading-7 outline-none sm:px-12 lg:px-16",
class: "min-h-full px-8 py-7 text-[var(--fs-body-lg)] leading-7 outline-none sm:px-12 lg:px-16",
"aria-label": "文本节点富文本编辑区",
},
},
@@ -129,8 +129,8 @@ export function CanvasTextEditorModal({ node, open, onClose, onSave }: CanvasTex
placeholder="文本节点标题"
aria-label="文本节点标题"
/>
<span className="hidden shrink-0 text-[11px] sm:inline" style={{ color: theme.node.muted }}>{characterCount.toLocaleString("zh-CN")} 字 · {wordCount.toLocaleString("zh-CN")} 词</span>
<span className="ml-auto hidden text-[11px] sm:inline" style={{ color: dirty ? theme.accent.primary : theme.node.muted }}>{dirty ? "有未保存修改" : "已保存"}</span>
<span className="hidden shrink-0 text-[var(--fs-label)] sm:inline" style={{ color: theme.node.muted }}>{characterCount.toLocaleString("zh-CN")} 字 · {wordCount.toLocaleString("zh-CN")} 词</span>
<span className="ml-auto hidden text-[var(--fs-label)] sm:inline" style={{ color: dirty ? theme.accent.primary : theme.node.muted }}>{dirty ? "有未保存修改" : "已保存"}</span>
<Button size="small" type="primary" icon={<Save className="size-3.5" />} loading={saving} disabled={!dirty} onClick={() => void save()}>保存</Button>
<Tooltip title="关闭">
<Button size="small" type="text" icon={<X className="size-4" />} aria-label="关闭文本编辑器" onClick={close} />
@@ -147,7 +147,7 @@ export function CanvasTextEditorModal({ node, open, onClose, onSave }: CanvasTex
</div>
</div>
<footer className="flex h-8 shrink-0 items-center gap-3 border-t px-3 text-[10px]" style={{ borderColor: theme.node.stroke, color: theme.node.muted }}>
<footer className="flex h-8 shrink-0 items-center gap-3 border-t px-3 text-[var(--fs-tiny)]" style={{ borderColor: theme.node.stroke, color: theme.node.muted }}>
<span className="hidden sm:inline">支持标题、列表、引用、链接、代码和颜色格式</span>
<span className="ml-auto">Ctrl/⌘S 保存</span>
</footer>
@@ -253,7 +253,7 @@ function TextEditorToolbar({ editor }: { editor: Editor | null }) {
menu={{
items: [
{ key: "code", icon: <Code2 className="size-3.5" />, label: "行内代码" },
{ key: "codeBlock", icon: <span className="text-[10px] font-bold">{"<>"}</span>, label: "代码块" },
{ key: "codeBlock", icon: <span className="text-[var(--fs-tiny)] font-bold">{"<>"}</span>, label: "代码块" },
{ key: "rule", icon: <Minus className="size-3.5" />, label: "插入分隔线" },
{ type: "divider" },
{ key: "clear", icon: <Eraser className="size-3.5" />, label: "清除格式" },

View File

@@ -143,16 +143,16 @@ export function CanvasToolbar({
<AnimatePresence>
{appearanceOpen ? (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: aceternityMotion.duration.instant }} className="pointer-events-auto absolute bottom-[50px] z-30 w-[224px] -translate-x-1/2" style={{ left: panelX || "50%" }}>
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ y: 6, scale: 0.97 }} animate={{ y: 0, scale: 1 }} exit={{ y: 4, scale: 0.98 }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[17px] border p-2.5 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.toolbar.item, boxShadow: `0 24px 64px ${theme.spatial.shadow}` }} onWheel={(event) => event.stopPropagation()}>
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ y: 6, scale: 0.97 }} animate={{ y: 0, scale: 1 }} exit={{ y: 4, scale: 0.98 }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[var(--panel-radius)] border p-2.5 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.toolbar.item, boxShadow: `0 24px 64px ${theme.spatial.shadow}` }} onWheel={(event) => event.stopPropagation()}>
<PanelHeading icon={<Palette className="size-4" />} title="画布外观" subtitle="调整整个创作空间" theme={theme} />
<div className="mt-3 text-[9px] font-semibold uppercase opacity-45">主题模式</div>
<div className="mt-1 grid grid-cols-2 gap-1 rounded-[11px] border p-1" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
<div className="mt-3 text-[var(--fs-micro)] font-semibold uppercase opacity-45">主题模式</div>
<div className="mt-1 grid grid-cols-2 gap-1 rounded-[var(--dock-item-radius-labeled)] border p-1" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
<CanvasThemeButton colorTheme={colorTheme} targetTheme="light" onThemeChange={setTheme}><Sun className="size-3.5" />浅色</CanvasThemeButton>
<CanvasThemeButton colorTheme={colorTheme} targetTheme="dark" onThemeChange={setTheme}><Moon className="size-3.5" />深色</CanvasThemeButton>
</div>
<div className="mt-3 text-[9px] font-semibold uppercase opacity-45">空间网格</div>
<div className="mt-3 text-[var(--fs-micro)] font-semibold uppercase opacity-45">空间网格</div>
<Segmented
className="mt-1 w-full !rounded-[11px] !p-0.5 [&_.ant-segmented-group]:!flex [&_.ant-segmented-item]:!min-h-7 [&_.ant-segmented-item]:!flex-1 [&_.ant-segmented-item-label]:!min-h-7 [&_.ant-segmented-item-label]:!text-[10px] [&_.ant-segmented-item-label]:!leading-7"
className="mt-1 w-full !rounded-[var(--dock-item-radius-labeled)] !p-0.5 [&_.ant-segmented-group]:!flex [&_.ant-segmented-item]:!min-h-7 [&_.ant-segmented-item]:!flex-1 [&_.ant-segmented-item-label]:!min-h-7 [&_.ant-segmented-item-label]:!text-[var(--fs-tiny)] [&_.ant-segmented-item-label]:!leading-7"
value={backgroundMode}
onChange={(value) => onBackgroundModeChange(value as CanvasBackgroundMode)}
options={[
@@ -161,8 +161,8 @@ export function CanvasToolbar({
{ value: "blank", label: <span className="inline-flex items-center gap-1.5"><Square className="size-3.5" />空白</span> },
]}
/>
<div className="mt-2.5 flex items-center justify-between gap-2 rounded-[11px] border px-2.5 py-2" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
<span className="inline-flex min-w-0 items-center gap-1.5 text-[10px] font-semibold"><Info className="size-3" />图片信息</span>
<div className="mt-2.5 flex items-center justify-between gap-2 rounded-[var(--dock-item-radius-labeled)] border px-2.5 py-2" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
<span className="inline-flex min-w-0 items-center gap-1.5 text-[var(--fs-tiny)] font-semibold"><Info className="size-3" />图片信息</span>
<Switch size="small" checked={showImageInfo} onChange={onShowImageInfoChange} />
</div>
</SpotlightSurface>
@@ -214,7 +214,7 @@ function AddNodeMenu({ x, theme, workspaceMode, isProjectLinked, onAddText, onCh
];
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: aceternityMotion.duration.instant }} className="pointer-events-auto absolute bottom-[50px] z-40 w-[260px] max-w-[calc(100vw-24px)] -translate-x-1/2" style={{ left: x || "50%" }}>
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ y: 6, scale: 0.97 }} animate={{ y: 0, scale: 1 }} exit={{ y: 4, scale: 0.98 }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[16px] border p-2 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 24px 64px ${theme.spatial.shadow}` }} onWheel={(event) => event.stopPropagation()}>
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ y: 6, scale: 0.97 }} animate={{ y: 0, scale: 1 }} exit={{ y: 4, scale: 0.98 }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[var(--panel-radius)] border p-2 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 24px 64px ${theme.spatial.shadow}` }} onWheel={(event) => event.stopPropagation()}>
<PanelHeading icon={<Plus className="size-4" />} title="创建内容" subtitle="选择节点类型" theme={theme} />
<MenuSection title="创作节点" />
<CanvasCreateCommandGrid commands={nodeCommands} />
@@ -228,14 +228,14 @@ function AddNodeMenu({ x, theme, workspaceMode, isProjectLinked, onAddText, onCh
function PanelHeading({ icon, title, subtitle, theme }: { icon: ReactNode; title: string; subtitle: string; theme: CanvasTheme }) {
return (
<div className="flex items-center gap-2">
<span className="grid size-8 shrink-0 place-items-center rounded-[10px] border opacity-75 [&_svg]:size-3.5" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>{icon}</span>
<span className="min-w-0"><span className="block text-xs font-semibold">{title}</span><span className="mt-0.5 block text-[9px]" style={{ color: theme.node.muted }}>{subtitle}</span></span>
<span className="grid size-8 shrink-0 place-items-center rounded-[var(--dock-item-radius)] border opacity-75 [&_svg]:size-3.5" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>{icon}</span>
<span className="min-w-0"><span className="block text-xs font-semibold">{title}</span><span className="mt-0.5 block text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>{subtitle}</span></span>
</div>
);
}
function MenuSection({ title }: { title: string }) {
return <div className="mb-1 mt-3 px-1 text-[9px] font-semibold uppercase opacity-42">{title}</div>;
return <div className="mb-1 mt-3 px-1 text-[var(--fs-micro)] font-semibold uppercase opacity-42">{title}</div>;
}
function CanvasThemeButton({ colorTheme, targetTheme, onThemeChange, children }: { colorTheme: CanvasColorTheme; targetTheme: CanvasColorTheme; onThemeChange: (theme: CanvasColorTheme) => void; children: ReactNode }) {
@@ -246,7 +246,7 @@ function CanvasThemeButton({ colorTheme, targetTheme, onThemeChange, children }:
theme={colorTheme}
targetTheme={targetTheme}
onThemeChange={onThemeChange}
className="inline-flex h-8 min-w-0 items-center justify-center gap-1.5 rounded-[10px] px-2 text-xs font-semibold transition-colors"
className="inline-flex h-8 min-w-0 items-center justify-center gap-1.5 rounded-[var(--dock-item-radius)] px-2 text-xs font-semibold transition-colors"
style={active ? { background: theme.node.text, color: theme.node.panel } : { color: theme.toolbar.item }}
aria-label={`切换到${targetTheme === "dark" ? "深色" : "浅色"}主题`}
title={`切换到${targetTheme === "dark" ? "深色" : "浅色"}主题`}

View File

@@ -15,12 +15,12 @@ export function CanvasVersionCompareModal({ open, versions, onClose, onSetPrimar
<article key={node.id} className="overflow-hidden rounded-lg border" style={{ borderColor: node.metadata?.versionPrimary ? theme.node.activeStroke : theme.node.stroke, background: theme.node.panel }}>
<div className="flex h-11 items-center justify-between gap-3 border-b px-3" style={{ borderColor: theme.node.stroke }}>
<div className="flex min-w-0 flex-1 items-center gap-2"><span className="grid size-6 shrink-0 place-items-center rounded-md text-xs font-bold" style={{ background: theme.toolbar.itemHover }}>{node.metadata?.versionLabel || "-"}</span><span className="min-w-0 truncate text-xs font-semibold" title={node.title}>{node.title}</span></div>
{node.metadata?.versionPrimary ? <span className="inline-flex shrink-0 items-center gap-1 whitespace-nowrap text-[10px] font-medium" style={{ color: theme.node.activeStroke }}><Check className="size-3" />主版本</span> : null}
{node.metadata?.versionPrimary ? <span className="inline-flex shrink-0 items-center gap-1 whitespace-nowrap text-[var(--fs-tiny)] font-medium" style={{ color: theme.node.activeStroke }}><Check className="size-3" />主版本</span> : null}
</div>
<button type="button" className="block h-52 w-full overflow-hidden" style={{ background: theme.node.fill }} onClick={() => onFocus(node.id)}>
{node.type === CanvasNodeType.Image && node.metadata?.content ? <img src={node.metadata.content} alt={node.title || "版本图片"} className="size-full object-contain" /> : node.type === CanvasNodeType.Video && node.metadata?.content ? <video src={node.metadata.content} className="size-full object-contain" muted preload="metadata" aria-label={node.title || "版本视频"} /> : <span className="grid size-full place-items-center px-4 text-center text-xs" style={{ color: theme.node.muted }}>点击定位到画布节点</span>}
</button>
<div className="space-y-2 p-3 text-[11px]">
<div className="space-y-2 p-3 text-[var(--fs-label)]">
<Info label="模型" value={node.metadata?.model || "默认模型"} />
<Info label="尺寸" value={node.metadata?.size || "默认尺寸"} />
<div><div className="whitespace-nowrap opacity-45">提示词</div><div className="mt-1 line-clamp-5 whitespace-pre-wrap break-words leading-5">{node.metadata?.composerContent || node.metadata?.prompt || "未填写"}</div></div>

View File

@@ -161,7 +161,7 @@ function CompactMenuButton({
<div
ref={menuRef}
data-canvas-no-zoom
className="fixed z-[1500] overflow-hidden rounded-[12px] border p-1 backdrop-blur-xl"
className="fixed z-[var(--z-toast)] overflow-hidden rounded-[var(--r-lg)] border p-1 backdrop-blur-xl"
style={menuStyle}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
@@ -204,7 +204,7 @@ function CompactMenuButton({
<button
ref={triggerRef}
type="button"
className="inline-flex h-6 w-full min-w-0 items-center gap-1 rounded-[10px] border-0 bg-transparent px-1.5 shadow-none transition hover:opacity-80 focus-visible:outline-none focus-visible:ring-1"
className="inline-flex h-6 w-full min-w-0 items-center gap-1 rounded-[var(--dock-item-radius)] border-0 bg-transparent px-1.5 shadow-none transition hover:opacity-80 focus-visible:outline-none focus-visible:ring-1"
style={buttonStyle}
title={title}
onClick={() => setOpen((value) => !value)}

View File

@@ -79,7 +79,7 @@ export function CanvasSelectionToolbar({ anchorRef, containerRef, count, childre
onPointerDown={(event) => event.stopPropagation()}
>
<motion.div initial={reducedMotion ? { opacity: 0 } : { opacity: 0, scale: 0.9, y: anchor.placement === "above" ? 8 : -8 }} animate={{ opacity: 1, scale: 1, y: 0 }} transition={aceternityMotion.spring.panel} className="flex items-center gap-2">
<span className="aceternity-floating-panel shrink-0 rounded-full border px-2.5 py-1.5 text-[10px] font-semibold tabular-nums backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.spatial.glowStrong, color: theme.accent.primary, boxShadow: `0 14px 36px ${theme.spatial.shadow}` }}>已选 {count}</span>
<span className="aceternity-floating-panel shrink-0 rounded-full border px-2.5 py-1.5 text-[var(--fs-tiny)] font-semibold tabular-nums backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.spatial.glowStrong, color: theme.accent.primary, boxShadow: `0 14px 36px ${theme.spatial.shadow}` }}>已选 {count}</span>
<div className="max-w-[min(560px,calc(100vw-90px))]">{children}</div>
</motion.div>
</div>
@@ -154,7 +154,7 @@ export function CanvasConnectionCreateMenu({ pending, viewport, viewportSize, co
initial={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 6, scale: 0.97, rotateX: 2 }}
animate={{ opacity: 1, y: 0, scale: 1, rotateX: 0 }}
transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }}
className="aceternity-floating-panel absolute z-[120] w-[248px] origin-top-left overflow-hidden rounded-[16px] border p-2 backdrop-blur-2xl"
className="aceternity-floating-panel absolute z-[120] w-[248px] origin-top-left overflow-hidden rounded-[var(--r-2xl)] border p-2 backdrop-blur-2xl"
data-canvas-no-zoom
data-connection-create-menu
style={{ left: initialPosition.left, top: initialPosition.top, background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 30px 90px ${theme.spatial.shadow}` }}
@@ -164,8 +164,8 @@ export function CanvasConnectionCreateMenu({ pending, viewport, viewportSize, co
<div className="absolute inset-x-8 top-0 h-px" style={{ background: `linear-gradient(90deg, transparent, ${theme.toolbar.border}, transparent)` }} />
<div className="mb-1.5 flex items-center justify-between gap-2 px-1 py-0.5">
<span className="flex min-w-0 items-center gap-2">
<span className="grid size-8 shrink-0 place-items-center rounded-[10px] border opacity-75" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}><WandSparkles className="size-3.5" /></span>
<span className="min-w-0"><span className="block truncate text-[11px] font-semibold">创建下一步</span><span className="mt-0.5 block truncate text-[8px]" style={{ color: theme.node.muted }}>引用当前节点</span></span>
<span className="grid size-8 shrink-0 place-items-center rounded-[var(--dock-item-radius)] border opacity-75" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}><WandSparkles className="size-3.5" /></span>
<span className="min-w-0"><span className="block truncate text-[var(--fs-label)] font-semibold">创建下一步</span><span className="mt-0.5 block truncate text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>引用当前节点</span></span>
</span>
<button type="button" className="grid size-6 shrink-0 place-items-center rounded-full border opacity-55 transition-opacity hover:opacity-100" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }} onClick={onClose} aria-label="关闭连线创建菜单"><X className="size-3" /></button>
</div>
@@ -184,11 +184,11 @@ export function CanvasConnectionCreateMenu({ pending, viewport, viewportSize, co
function ConnectionCreateOption({ motionEnabled, icon, title, description, onClick }: { motionEnabled: boolean; icon: ReactNode; title: string; description?: string; onClick: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
return (
<motion.button type="button" whileHover={motionEnabled ? { x: 2 } : undefined} whileTap={motionEnabled ? { scale: 0.98 } : undefined} transition={aceternityMotion.spring.dock} className="group flex min-h-10 w-full cursor-pointer items-center gap-2 rounded-[9px] border border-transparent px-2 py-1.5 text-left outline-none hover:border-black/10 hover:bg-black/5 focus-visible:ring-2 dark:hover:border-white/10 dark:hover:bg-white/8" style={{ color: theme.node.text, "--tw-ring-color": theme.node.muted } as CSSProperties} onClick={onClick}>
<span className="grid size-7 shrink-0 place-items-center rounded-[8px] opacity-65 transition-opacity group-hover:opacity-100 [&_svg]:size-3.5" style={{ background: theme.toolbar.itemHover }}>{icon}</span>
<motion.button type="button" whileHover={motionEnabled ? { x: 2 } : undefined} whileTap={motionEnabled ? { scale: 0.98 } : undefined} transition={aceternityMotion.spring.dock} className="group flex min-h-10 w-full cursor-pointer items-center gap-2 rounded-[var(--dock-item-radius)] border border-transparent px-2 py-1.5 text-left outline-none hover:border-black/10 hover:bg-black/5 focus-visible:ring-2 dark:hover:border-white/10 dark:hover:bg-white/8" style={{ color: theme.node.text, "--tw-ring-color": theme.node.muted } as CSSProperties} onClick={onClick}>
<span className="grid size-7 shrink-0 place-items-center rounded-[var(--r-md)] opacity-65 transition-opacity group-hover:opacity-100 [&_svg]:size-3.5" style={{ background: theme.toolbar.itemHover }}>{icon}</span>
<span className="min-w-0 flex-1">
<span className="flex items-center gap-2 text-[10px] font-semibold leading-4">{title}</span>
{description ? <span className="mt-0.5 block truncate text-[8px]" style={{ color: theme.node.muted }}>{description}</span> : null}
<span className="flex items-center gap-2 text-[var(--fs-tiny)] font-semibold leading-4">{title}</span>
{description ? <span className="mt-0.5 block truncate text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>{description}</span> : null}
</span>
<ChevronRight className="size-3.5 shrink-0 opacity-35 transition-transform group-hover:translate-x-0.5" />
</motion.button>

View File

@@ -76,7 +76,7 @@ export function CanvasZoomControls({ scale, onScaleChange, onReset, isMiniMapOpe
{
id: "zoom-precision",
label: "精确缩放",
icon: <span className="flex items-baseline text-[9px] font-bold tabular-nums"><span ref={dockLabelRef}>{Math.round(scale * 100)}</span><span className="ml-px text-[7px] opacity-55">%</span></span>,
icon: <span className="flex items-baseline text-[var(--fs-micro)] font-bold tabular-nums"><span ref={dockLabelRef}>{Math.round(scale * 100)}</span><span className="ml-px text-[7px] opacity-55">%</span></span>,
active: precisionOpen,
onClick: () => setPrecisionOpen((value) => !value),
},
@@ -94,16 +94,16 @@ export function CanvasZoomControls({ scale, onScaleChange, onReset, isMiniMapOpe
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 9, scale: 0.96 }}
transition={aceternityMotion.spring.panel}
className="aceternity-floating-panel absolute bottom-[50px] left-0 w-[220px] overflow-hidden rounded-[17px] border p-2.5 backdrop-blur-2xl"
className="aceternity-floating-panel absolute bottom-[50px] left-0 w-[220px] overflow-hidden rounded-[var(--panel-radius)] border p-2.5 backdrop-blur-2xl"
style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 28px 80px ${theme.spatial.shadow}` }}
>
<div className="absolute inset-x-10 top-0 h-px" style={{ background: `linear-gradient(90deg, transparent, ${theme.spatial.glowStrong}, transparent)` }} />
<div className="flex items-center justify-between gap-3">
<span>
<span className="block text-[10px] font-semibold">画布尺度</span>
<span className="mt-0.5 block text-[9px]" style={{ color: theme.node.muted }}>精确控制视野密度</span>
<span className="block text-[var(--fs-tiny)] font-semibold">画布尺度</span>
<span className="mt-0.5 block text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>精确控制视野密度</span>
</span>
<span ref={panelLabelRef} className="rounded-full border px-2 py-0.5 text-[10px] font-semibold tabular-nums" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.accent.primary }}>
<span ref={panelLabelRef} className="rounded-full border px-2 py-0.5 text-[var(--fs-tiny)] font-semibold tabular-nums" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.accent.primary }}>
{Math.round(scale * 100)}%
</span>
</div>
@@ -127,7 +127,7 @@ export function CanvasZoomControls({ scale, onScaleChange, onReset, isMiniMapOpe
whileHover={{ y: -1 }}
whileTap={{ scale: 0.95 }}
transition={aceternityMotion.spring.dock}
className="h-7 rounded-[9px] border text-[9px] font-semibold tabular-nums outline-none focus-visible:ring-2"
className="h-7 rounded-[var(--dock-item-radius)] border text-[var(--fs-micro)] font-semibold tabular-nums outline-none focus-visible:ring-2"
style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.node.muted }}
onClick={() => commitScale(level)}
>

View File

@@ -17,10 +17,10 @@ export function CanvasDirectorNodePanel({ node, scene, previewUrl, onOpen, profe
<span className="grid size-7 shrink-0 place-items-center rounded-md" style={{ background: theme.toolbar.itemHover }}><Clapperboard className="size-3.5" /></span>
<div className="min-w-0">
<div className="truncate text-sm font-semibold">{node.metadata?.workflowTitle || node.title}</div>
<div className="truncate text-[10px]" style={{ color: theme.node.muted }}>{shot?.name || "未设置镜头"}</div>
<div className="truncate text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>{shot?.name || "未设置镜头"}</div>
</div>
</div>
<span className="shrink-0 text-[10px] font-semibold" style={{ color: theme.accent.primary }}>3D</span>
<span className="shrink-0 text-[var(--fs-tiny)] font-semibold" style={{ color: theme.accent.primary }}>3D</span>
</div>
<button
@@ -38,7 +38,7 @@ export function CanvasDirectorNodePanel({ node, scene, previewUrl, onOpen, profe
<span className={`absolute inset-x-0 bottom-0 flex h-10 items-center justify-center gap-1.5 text-xs font-semibold backdrop-blur-sm transition-opacity ${professional ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : "opacity-100"}`} style={{ background: `${theme.toolbar.panel}dd`, color: theme.node.text }}>{professional ? <><Move3d className="size-3.5" />进入导演台</> : <><LockKeyhole className="size-3.5" />专业模式可编辑</>}</span>
</button>
<div className="mt-2 grid grid-cols-3 gap-1 text-[10px]" style={{ color: theme.node.muted }}>
<div className="mt-2 grid grid-cols-3 gap-1 text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>
<Stat icon={<Box className="size-3" />} value={scene?.objects.length || 0} label="对象" />
<Stat icon={<Camera className="size-3" />} value={scene?.cameras.length || 0} label="机位" />
<Stat icon={<Lightbulb className="size-3" />} value={scene?.lights.length || 0} label="灯光" />
@@ -61,7 +61,7 @@ function SceneSchematic({ scene }: { scene: DirectorScene | null }) {
})}
<Camera className="absolute bottom-[14%] left-[12%] size-5 text-white/75" />
<span className="absolute left-[18%] top-[18%] size-16 rounded-full bg-white/10 blur-xl" />
<span className="absolute inset-x-3 top-3 text-[10px] font-medium text-white/70">{scene ? "场景预览" : "正在准备场景"}</span>
<span className="absolute inset-x-3 top-3 text-[var(--fs-tiny)] font-medium text-white/70">{scene ? "场景预览" : "正在准备场景"}</span>
</div>
);
}

View File

@@ -238,7 +238,7 @@ export function CanvasDirectorWorkbench({ open, scene, imageNodes, onClose, onCh
<main className="relative min-h-0 overflow-hidden bg-neutral-900">
<DirectorViewport ref={viewportRef} scene={draft} selectedObjectId={selectedObjectId} transformMode={transformMode} renderMode={renderMode} playhead={playhead} onSelectObject={setSelectedObjectId} onObjectTransform={(id, transform) => updateObject(id, { transform })} />
<div className="pointer-events-none absolute left-3 top-3 text-[10px] font-medium text-white/70">{activeShot.name} · {activeCamera?.name || "无摄影机"} · {activeShot.duration}s</div>
<div className="pointer-events-none absolute left-3 top-3 text-[var(--fs-tiny)] font-medium text-white/70">{activeShot.name} · {activeCamera?.name || "无摄影机"} · {activeShot.duration}s</div>
</main>
<aside className="thin-scrollbar min-h-0 overflow-y-auto border-l max-lg:hidden" style={{ background: theme.node.panel, borderColor: theme.toolbar.border }}>
@@ -249,12 +249,12 @@ export function CanvasDirectorWorkbench({ open, scene, imageNodes, onClose, onCh
<footer className="shrink-0 border-t px-3 py-2" style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border }}>
<div className="flex items-center gap-2">
<IconButton label={playing ? "暂停" : "播放"} onClick={() => setPlaying(!playing)}>{playing ? <Pause className="size-4" /> : <Play className="size-4" />}</IconButton>
<span className="w-12 text-right text-[11px] tabular-nums">{playhead.toFixed(1)}s</span>
<span className="w-12 text-right text-[var(--fs-label)] tabular-nums">{playhead.toFixed(1)}s</span>
<Slider className="min-w-0 flex-1" min={0} max={activeShot.duration} step={0.05} value={playhead} onChange={setPlayhead} />
<Button size="small" type="text" icon={<Focus className="size-3.5" />} onClick={selectedObject ? addObjectKeyframe : addCameraKeyframe}>记录关键帧</Button>
</div>
<div className="mt-1.5 flex gap-1 overflow-x-auto">
{draft.shots.map((shot, index) => <button key={shot.id} type="button" className="h-8 min-w-28 shrink-0 border-l-2 px-2 text-left text-[11px] transition" style={{ borderColor: draft.activeShotId === shot.id ? theme.node.activeStroke : theme.toolbar.border, background: draft.activeShotId === shot.id ? theme.toolbar.itemHover : "transparent" }} onClick={() => { commit((current) => ({ ...current, activeShotId: shot.id })); setPlayhead(0); }}><span className="block truncate">{index + 1}. {shot.name}</span><span className="block opacity-45">{shot.duration}s</span></button>)}
{draft.shots.map((shot, index) => <button key={shot.id} type="button" className="h-8 min-w-28 shrink-0 border-l-2 px-2 text-left text-[var(--fs-label)] transition" style={{ borderColor: draft.activeShotId === shot.id ? theme.node.activeStroke : theme.toolbar.border, background: draft.activeShotId === shot.id ? theme.toolbar.itemHover : "transparent" }} onClick={() => { commit((current) => ({ ...current, activeShotId: shot.id })); setPlayhead(0); }}><span className="block truncate">{index + 1}. {shot.name}</span><span className="block opacity-45">{shot.duration}s</span></button>)}
<button type="button" className="grid h-8 w-9 shrink-0 place-items-center" title="新增镜头" onClick={addShot}><Plus className="size-4" /></button>
</div>
</footer>
@@ -263,7 +263,7 @@ export function CanvasDirectorWorkbench({ open, scene, imageNodes, onClose, onCh
}
function ObjectInspector({ object, playhead, onUpdate, onAddKeyframe, onDelete }: { object: DirectorObject; playhead: number; onUpdate: (patch: Partial<DirectorObject>) => void; onAddKeyframe: () => void; onDelete: () => void }) {
return <Inspector title={object.name} onTitleChange={(name) => onUpdate({ name })} onDelete={onDelete}><TransformFields transform={object.transform} onChange={(transform) => onUpdate({ transform })} /><Field label="颜色"><ColorPicker value={object.color} onChange={(_, color) => onUpdate({ color })} /></Field>{object.primitive === "character" ? <Field label="姿势"><Select className="w-full" value={object.pose} options={poseOptions} onChange={(pose) => onUpdate({ pose })} /></Field> : null}<Field label="可见"><Switch checked={object.visible} onChange={(visible) => onUpdate({ visible })} /></Field><Field label="投射阴影"><Switch checked={object.castShadow} onChange={(castShadow) => onUpdate({ castShadow })} /></Field><Button block icon={<Focus className="size-3.5" />} onClick={onAddKeyframe}>在 {playhead.toFixed(1)}s 记录关键帧</Button><div className="text-[10px] opacity-50">已记录 {object.keyframes.length} 个关键帧</div></Inspector>;
return <Inspector title={object.name} onTitleChange={(name) => onUpdate({ name })} onDelete={onDelete}><TransformFields transform={object.transform} onChange={(transform) => onUpdate({ transform })} /><Field label="颜色"><ColorPicker value={object.color} onChange={(_, color) => onUpdate({ color })} /></Field>{object.primitive === "character" ? <Field label="姿势"><Select className="w-full" value={object.pose} options={poseOptions} onChange={(pose) => onUpdate({ pose })} /></Field> : null}<Field label="可见"><Switch checked={object.visible} onChange={(visible) => onUpdate({ visible })} /></Field><Field label="投射阴影"><Switch checked={object.castShadow} onChange={(castShadow) => onUpdate({ castShadow })} /></Field><Button block icon={<Focus className="size-3.5" />} onClick={onAddKeyframe}>在 {playhead.toFixed(1)}s 记录关键帧</Button><div className="text-[var(--fs-tiny)] opacity-50">已记录 {object.keyframes.length} 个关键帧</div></Inspector>;
}
function LightInspector({ light, onUpdate, onDelete }: { light: DirectorLight; onUpdate: (patch: Partial<DirectorLight>) => void; onDelete: () => void }) {
@@ -286,10 +286,10 @@ function Vec3Field({ label, value, step = 0.1, onChange }: { label: string; valu
return <Field label={label}><div className="grid grid-cols-3 gap-1">{value.map((item, index) => <InputNumber key={index} className="w-full" size="small" step={step} value={Number(item.toFixed(2))} onChange={(next) => onChange(value.map((entry, itemIndex) => itemIndex === index ? next || 0 : entry) as DirectorVec3)} />)}</div></Field>;
}
function Field({ label, children }: { label: string; children: ReactNode }) { return <label className="block"><span className="mb-1 block text-[11px] opacity-55">{label}</span>{children}</label>; }
function PanelTitle({ title, action }: { title: string; action?: ReactNode }) { return <div className="flex h-9 items-center px-3 text-[10px] font-semibold uppercase opacity-55"><span className="flex-1">{title}</span>{action}</div>; }
function Field({ label, children }: { label: string; children: ReactNode }) { return <label className="block"><span className="mb-1 block text-[var(--fs-label)] opacity-55">{label}</span>{children}</label>; }
function PanelTitle({ title, action }: { title: string; action?: ReactNode }) { return <div className="flex h-9 items-center px-3 text-[var(--fs-tiny)] font-semibold uppercase opacity-55"><span className="flex-1">{title}</span>{action}</div>; }
function SceneRow({ active, icon, label, onClick }: { active?: boolean; icon: ReactElement; label: string; onClick: () => void }) { return <button type="button" className={`flex h-8 w-full items-center gap-2 px-2 text-left text-xs transition ${active ? "bg-black/10 dark:bg-white/10" : "hover:bg-black/5 dark:hover:bg-white/5"}`} onClick={onClick}><span className="[&>svg]:size-3.5">{icon}</span><span className="truncate">{label}</span></button>; }
function QuickAdd({ label, icon, onClick }: { label: string; icon: ReactElement; onClick: () => void }) { return <button type="button" className="flex h-8 items-center gap-1.5 border px-2 text-[10px] transition hover:bg-black/5 dark:hover:bg-white/5" onClick={onClick}><span className="[&>svg]:size-3.5">{icon}</span><span className="truncate">{label}</span></button>; }
function QuickAdd({ label, icon, onClick }: { label: string; icon: ReactElement; onClick: () => void }) { return <button type="button" className="flex h-8 items-center gap-1.5 border px-2 text-[var(--fs-tiny)] transition hover:bg-black/5 dark:hover:bg-white/5" onClick={onClick}><span className="[&>svg]:size-3.5">{icon}</span><span className="truncate">{label}</span></button>; }
function IconButton({ label, disabled, children, onClick }: { label: string; disabled?: boolean; children: ReactNode; onClick: () => void }) { return <button type="button" aria-label={label} title={label} disabled={disabled} className="grid size-8 shrink-0 place-items-center rounded-md transition hover:bg-black/5 disabled:opacity-30 dark:hover:bg-white/10" onClick={onClick}>{children}</button>; }
function ToolButton({ label, active, children, onClick }: { label: string; active: boolean; children: ReactNode; onClick: () => void }) { return <button type="button" aria-label={label} title={label} className={`grid size-8 place-items-center rounded-md transition ${active ? "bg-black text-white dark:bg-white dark:text-black" : "hover:bg-black/5 dark:hover:bg-white/10"}`} onClick={onClick}>{children}</button>; }

View File

@@ -27,7 +27,7 @@ export function ImageGenerationPending({ className, label, compact = false }: {
maskImage: "radial-gradient(ellipse at 38% 68%, black 0%, black 28%, transparent 60%)",
}}
/>
<div className="absolute left-4 top-4 flex items-center gap-2 text-[15px] font-medium text-stone-500 dark:text-stone-300">
<div className="absolute left-4 top-4 flex items-center gap-2 text-[var(--fs-body-lg)] font-medium text-stone-500 dark:text-stone-300">
<LoaderCircle className="size-4 animate-spin" />
<span>{label || pendingMessages[index]}</span>
</div>

View File

@@ -84,7 +84,7 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle =
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<SettingTitle color={theme.node.muted}>透明背景</SettingTitle>
<div className="mt-1 text-[11px]" style={{ color: theme.node.muted }}>
<div className="mt-1 text-[var(--fs-label)]" style={{ color: theme.node.muted }}>
请求模型输出保留 Alpha 通道的 PNG
</div>
</div>
@@ -121,7 +121,7 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle =
<button
key={item.value}
type="button"
className="flex h-[52px] cursor-pointer flex-col items-center justify-center gap-0.5 rounded-lg bg-transparent text-[11px] transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
className="flex h-[52px] cursor-pointer flex-col items-center justify-center gap-0.5 rounded-lg bg-transparent text-[var(--fs-label)] transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
style={{ background: selectedAspect?.value === item.value ? theme.toolbar.activeBg : "transparent", color: theme.node.text, outlineColor: theme.node.muted }}
onMouseDown={(event) => event.stopPropagation()}
onClick={() => selectAspect(item.value)}

View File

@@ -64,7 +64,7 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
>
<Link to="/" className={cn("min-w-0 items-center gap-2", mobileSidebarExpanded || spatialWorkbench ? "flex" : "hidden", "lg:flex")} title="影策">
<span className="app-workspace-brand-mark grid size-7 shrink-0 place-items-center rounded-md bg-foreground text-background"><InfinityIcon className="size-4" /></span>
{spatialWorkbench ? <span className="min-w-0"><span className="block truncate text-[13px] font-semibold">影策</span><span className="block truncate text-[9px] text-foreground/36">AI 叙事工作台</span></span> : <span className="truncate text-[13px] font-semibold">影策</span>}
{spatialWorkbench ? <span className="min-w-0"><span className="block truncate text-[var(--fs-body)] font-semibold">影策</span><span className="block truncate text-[var(--fs-micro)] text-foreground/36">AI 叙事工作台</span></span> : <span className="truncate text-[var(--fs-body)] font-semibold">影策</span>}
</Link>
</div>
@@ -83,7 +83,7 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
const showSection = index === 0 || tool.section !== visibleNavigationTools[index - 1]?.section;
return (
<Fragment key={tool.slug}>
{showSection ? <div className={cn("mb-2 px-2 text-[10px] font-medium text-foreground/34", index > 0 && "mt-4", mobileSidebarExpanded ? "block" : "hidden", "lg:hidden")}>{tool.section}</div> : null}
{showSection ? <div className={cn("mb-2 px-2 text-[var(--fs-tiny)] font-medium text-foreground/34", index > 0 && "mt-4", mobileSidebarExpanded ? "block" : "hidden", "lg:hidden")}>{tool.section}</div> : null}
<Link
to={`/${tool.slug}`}
title={tool.label}
@@ -91,7 +91,7 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
if (window.innerWidth < 1024) setMobileSidebarExpanded(false);
}}
className={cn(
"app-workspace-nav-link relative mb-1 flex shrink-0 items-center rounded-md text-[13px] transition-colors",
"app-workspace-nav-link relative mb-1 flex shrink-0 items-center rounded-md text-[var(--fs-body)] transition-colors",
spatialWorkbench ? "h-11" : "h-9",
mobileSidebarExpanded ? "gap-3 px-2.5" : "justify-center px-0",
"lg:justify-center lg:px-0",

View File

@@ -12,7 +12,7 @@ export function IdentityProviderBadge({ user, compact = false, className }: { us
<span
className={cn(
"inline-flex shrink-0 items-center justify-center border border-border bg-background font-semibold text-foreground/65",
compact ? "size-3.5 rounded-full text-[8px] leading-none" : "h-4 rounded px-1 text-[9px] leading-none",
compact ? "size-3.5 rounded-full text-[var(--fs-micro)] leading-none" : "h-4 rounded px-1 text-[var(--fs-micro)] leading-none",
className,
)}
title={user.identityUsername ? `Linux.do · @${user.identityUsername}` : "Linux.do 用户"}

View File

@@ -114,7 +114,7 @@ export function SystemAnnouncementCenter({ userId, className, style, showLabel =
{showLabel ? (
<span className={`min-w-0 flex-1 items-center justify-between gap-2 whitespace-nowrap ${labelClassName || ""}`}>
<span>系统公告</span>
<Tag color={unreadCount > 0 ? "gold" : undefined} className="!m-0 !min-w-6 !px-1.5 !text-center !text-[9px] !font-medium !leading-[18px] tabular-nums">{announcements.length}</Tag>
<Tag color={unreadCount > 0 ? "gold" : undefined} className="!m-0 !min-w-6 !px-1.5 !text-center !text-[var(--fs-micro)] !font-medium !leading-[18px] tabular-nums">{announcements.length}</Tag>
</span>
) : null}
</motion.button>

View File

@@ -20,7 +20,7 @@ export function PageHeader({ title, description, meta, actions, icon }: { title:
{icon ? <WorkspaceSignalIcon variant={icon} /> : null}
<div className="min-w-0">
<div className="flex min-w-0 flex-wrap items-baseline gap-2.5">
<h1 className="truncate text-[22px] font-semibold leading-7">{title}</h1>
<h1 className="truncate text-[var(--fs-title)] font-semibold leading-7">{title}</h1>
{meta}
</div>
{description ? <p className="mt-1 text-xs leading-5 text-foreground/58">{description}</p> : null}

View File

@@ -69,7 +69,7 @@ export function WorkspaceSidebarFooter({ expandedClassName, collapsedClassName,
<UserAvatar user={user} className="size-8" />
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-1.5"><span className="truncate text-sm font-medium">{user.displayName || user.username}</span><IdentityProviderBadge user={user} /></div>
<div className="mt-0.5 truncate text-[11px] tabular-nums text-foreground/45">可用 {balance} 积分</div>
<div className="mt-0.5 truncate text-[var(--fs-label)] tabular-nums text-foreground/45">可用 {balance} 积分</div>
</div>
</div>
@@ -80,7 +80,7 @@ export function WorkspaceSidebarFooter({ expandedClassName, collapsedClassName,
) : null}
<div className="border-y border-border/65 py-2">
<AppChangelogButton className="flex h-8 w-full items-center gap-2 rounded px-2 text-[11px] text-foreground/58 hover:bg-foreground/[.055] hover:text-foreground [&_svg]:size-3.5" showLabel showVersion versionClassName="ml-auto text-[9px] tabular-nums text-foreground/32" />
<AppChangelogButton className="flex h-8 w-full items-center gap-2 rounded px-2 text-[var(--fs-label)] text-foreground/58 hover:bg-foreground/[.055] hover:text-foreground [&_svg]:size-3.5" showLabel showVersion versionClassName="ml-auto text-[var(--fs-micro)] tabular-nums text-foreground/32" />
</div>
<div className="flex h-10 items-center px-2">
@@ -96,7 +96,7 @@ export function WorkspaceSidebarFooter({ expandedClassName, collapsedClassName,
<UserAvatar user={user} className="size-7" />
<span className={cn("min-w-0 flex-1 flex-col", expandedClassName)}>
<span className="truncate text-xs font-medium">{user.displayName || user.username}</span>
<span className="mt-0.5 block truncate text-[9px] tabular-nums text-foreground/42">{balance} 积分</span>
<span className="mt-0.5 block truncate text-[var(--fs-micro)] tabular-nums text-foreground/42">{balance} 积分</span>
</span>
<ChevronRight className={cn("size-3.5 shrink-0 text-foreground/30", expandedClassName)} />
</button>

View File

@@ -8,7 +8,7 @@ export function WorkspaceState({ icon = "empty", title, description, action, com
return (
<section className={cn("workspace-state flex flex-col items-center justify-center text-center", compact ? "min-h-44 py-8" : "min-h-[320px] py-12", className)}>
<WorkspaceSignalIcon variant={icon} size={compact ? "md" : "lg"} />
<h2 className="mt-4 text-[15px] font-semibold leading-6 text-foreground">{title}</h2>
<h2 className="mt-4 text-[var(--fs-body-lg)] font-semibold leading-6 text-foreground">{title}</h2>
{description ? <p className="mt-1.5 max-w-[42ch] text-xs leading-5 text-foreground/58">{description}</p> : null}
{action ? <div className="mt-5">{action}</div> : null}
</section>

View File

@@ -195,11 +195,11 @@ function ModelLabel({ config, model, capability, theme, creationVariant }: { con
<ModelIcon model={model} />
</span>
<span className="min-w-0 flex-1 overflow-hidden">
<span className="block min-w-0 truncate text-[11px] font-medium leading-none">{modelDisplayName(config, model)}</span>
<span className="mt-1 block truncate text-[10px]" style={{ color: theme.node.muted }} title={meta.description}>{meta.description}</span>
<span className="block min-w-0 truncate text-[var(--fs-label)] font-medium leading-none">{modelDisplayName(config, model)}</span>
<span className="mt-1 block truncate text-[var(--fs-tiny)]" style={{ color: theme.node.muted }} title={meta.description}>{meta.description}</span>
</span>
<ModelPrice price={modelMenuPrice(config, model)} />
{!creationVariant && meta.time ? <span className="shrink-0 rounded-full px-1.5 py-0.5 text-[10px] tabular-nums" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}>{meta.time}</span> : null}
{!creationVariant && meta.time ? <span className="shrink-0 rounded-full px-1.5 py-0.5 text-[var(--fs-tiny)] tabular-nums" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}>{meta.time}</span> : null}
</span>
);
}
@@ -214,9 +214,9 @@ function modelMenuPrice(config: AiConfig, model: string): { value: number; unit:
function ModelPrice({ price, compact = false }: { price: { value: number; unit: "次" | "秒" } | null | undefined; compact?: boolean }) {
if (price === undefined) return null;
if (price === null) return compact ? null : <span className="shrink-0 text-[10px] text-foreground/40">未配置</span>;
if (price === null) return compact ? null : <span className="shrink-0 text-[var(--fs-tiny)] text-foreground/40">未配置</span>;
return (
<span className="inline-flex shrink-0 items-center gap-0.5 text-[10px] font-bold tabular-nums text-amber-600 dark:text-amber-300" title={`每${price.unit}消耗 ${price.value.toLocaleString("zh-CN", { maximumFractionDigits: 6 })} 积分`}>
<span className="inline-flex shrink-0 items-center gap-0.5 text-[var(--fs-tiny)] font-bold tabular-nums text-amber-600 dark:text-amber-300" title={`每${price.unit}消耗 ${price.value.toLocaleString("zh-CN", { maximumFractionDigits: 6 })} 积分`}>
<Coins className="size-3" />
{price.value.toLocaleString("zh-CN", { maximumFractionDigits: compact ? 3 : 6 })}/{price.unit}
</span>

View File

@@ -106,10 +106,10 @@ function AnnouncementTimelineItem({ announcement, last, reducedMotion }: { annou
</div>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<h3 className="text-[15px] font-semibold leading-6 tracking-normal text-foreground sm:text-base">{announcement.title}</h3>
<span className="inline-flex items-center gap-1 text-[11px] font-medium text-foreground/45"><Icon className="size-3" />{meta.label}</span>
<h3 className="text-[var(--fs-body-lg)] font-semibold leading-6 tracking-normal text-foreground sm:text-base">{announcement.title}</h3>
<span className="inline-flex items-center gap-1 text-[var(--fs-label)] font-medium text-foreground/45"><Icon className="size-3" />{meta.label}</span>
</div>
<AnnouncementContent content={announcement.content} className="mt-1 text-sm leading-6 text-foreground/75 sm:text-[15px]" />
<AnnouncementContent content={announcement.content} className="mt-1 text-sm leading-6 text-foreground/75 sm:text-[var(--fs-body-lg)]" />
<time dateTime={announcement.publishedAt} className="mt-2 block text-xs tabular-nums text-foreground/40">{relativeTime(announcement.publishedAt)} · {formatDateTime(announcement.publishedAt)}</time>
</div>
</motion.article>

View File

@@ -78,10 +78,10 @@ export const FloatingDock = forwardRef<HTMLDivElement, FloatingDockProps>(functi
: coarsePointer
? embedded
? size === "compact" ? "h-10 gap-1 px-0.5" : "h-11 gap-1 px-0.5"
: size === "compact" ? "h-11 gap-1 rounded-[15px] px-1.5 pb-1" : "h-12 gap-1 rounded-[17px] px-2 pb-1"
: size === "compact" ? "h-11 gap-1 rounded-[15px] px-1.5 pb-1" : "h-12 gap-1 rounded-[var(--panel-radius)] px-2 pb-1"
: embedded
? size === "compact" ? "h-8 gap-0.5 px-0.5 pb-0.5" : "h-9 gap-0.5 px-0.5 pb-0.5"
: size === "compact" ? "h-8 gap-0.5 rounded-[12px] px-1 pb-1" : "h-10 gap-0.5 rounded-[14px] px-1.5 pb-1",
: size === "compact" ? "h-8 gap-0.5 rounded-[var(--r-lg)] px-1 pb-1" : "h-10 gap-0.5 rounded-[var(--dock-radius)] px-1.5 pb-1",
className,
)}
style={style}
@@ -118,7 +118,7 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s
aria-label={command.label}
aria-pressed={command.active || undefined}
disabled={command.disabled}
className={cn("aceternity-dock-command is-labeled group inline-flex h-8 items-center justify-center gap-1.5 whitespace-nowrap rounded-[9px] border-0 px-2.5 outline-none", command.active && "is-active", command.danger && "is-danger")}
className={cn("aceternity-dock-command is-labeled group inline-flex h-8 items-center justify-center gap-1.5 whitespace-nowrap rounded-[var(--dock-item-radius)] border-0 px-2.5 outline-none", command.active && "is-active", command.danger && "is-danger")}
whileTap={!command.disabled ? { scale: 0.96 } : undefined}
transition={aceternityMotion.spring.dock}
onMouseEnter={() => setHovered(true)}
@@ -128,7 +128,7 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s
onClick={command.onClick}
>
<span className="grid size-3.5 shrink-0 place-items-center">{command.icon}</span>
<span className="inline-flex h-4 items-center text-[11px] font-medium leading-none">{command.displayLabel || command.label}</span>
<span className="inline-flex h-4 items-center text-[var(--fs-label)] font-medium leading-none">{command.displayLabel || command.label}</span>
</motion.button>
</motion.span>
);
@@ -161,7 +161,7 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 4, scale: 0.96 }}
transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }}
className={cn("aceternity-dock-tooltip pointer-events-none absolute left-1/2 z-[140] -translate-x-1/2 whitespace-nowrap border font-medium shadow-xl backdrop-blur-xl", compact ? "-top-7 rounded-md px-1.5 py-0.5 text-[9px]" : "-top-8 rounded-md px-2 py-1 text-[10px]")}
className={cn("aceternity-dock-tooltip pointer-events-none absolute left-1/2 z-[140] -translate-x-1/2 whitespace-nowrap border font-medium shadow-xl backdrop-blur-xl", compact ? "-top-7 rounded-md px-1.5 py-0.5 text-[var(--fs-micro)]" : "-top-8 rounded-md px-2 py-1 text-[var(--fs-tiny)]")}
>
{command.label}
</motion.span>

View File

@@ -71,7 +71,7 @@ export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle =
<button
key={item.value}
type="button"
className="flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-md px-1 text-[11px] font-medium transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
className="flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-md px-1 text-[var(--fs-label)] font-medium transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
style={{ background: size === item.value ? theme.toolbar.activeBg : "transparent", color: theme.node.text, outlineColor: theme.node.muted }}
onMouseDown={(event) => event.stopPropagation()}
onClick={() => onConfigChange("size", item.value)}
@@ -141,7 +141,7 @@ function SeedanceVideoSettingsPanel({ config, onConfigChange, theme, showTitle,
);
})}
</div>
{isSeedanceFastModel(model) ? <div className="text-[10px] leading-4 opacity-55">fast 模型自动使用 720P</div> : null}
{isSeedanceFastModel(model) ? <div className="text-[var(--fs-tiny)] leading-4 opacity-55">fast 模型自动使用 720P</div> : null}
</SettingGroup>
<SettingGroup title="比例" color={theme.node.muted}>
<div className="grid grid-cols-4 gap-1.5">
@@ -149,7 +149,7 @@ function SeedanceVideoSettingsPanel({ config, onConfigChange, theme, showTitle,
<button
key={item.value}
type="button"
className="flex h-11 min-w-0 cursor-pointer flex-col items-center justify-center gap-0.5 rounded-md px-1 text-[10px] font-medium leading-none transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
className="flex h-11 min-w-0 cursor-pointer flex-col items-center justify-center gap-0.5 rounded-md px-1 text-[var(--fs-tiny)] font-medium leading-none transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
style={{ background: ratio === item.value ? theme.toolbar.activeBg : "transparent", color: theme.node.text, outlineColor: theme.node.muted }}
onMouseDown={(event) => event.stopPropagation()}
onClick={() => onConfigChange("size", item.value)}
@@ -211,7 +211,7 @@ export function normalizeVideoResolutionValue(value: string) {
function OptionPill({ selected, disabled = false, theme, onClick, children }: { selected: boolean; disabled?: boolean; theme: CanvasTheme; onClick: () => void; children: ReactNode }) {
return (
<button type="button" disabled={disabled} className="h-8 cursor-pointer whitespace-nowrap rounded-md px-1 text-[11px] font-medium leading-none transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 disabled:cursor-not-allowed disabled:opacity-35" style={{ background: selected ? theme.toolbar.activeBg : "transparent", color: theme.node.text, outlineColor: theme.node.muted }} onMouseDown={(event) => event.stopPropagation()} onClick={onClick}>
<button type="button" disabled={disabled} className="h-8 cursor-pointer whitespace-nowrap rounded-md px-1 text-[var(--fs-label)] font-medium leading-none transition-colors hover:brightness-110 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 disabled:cursor-not-allowed disabled:opacity-35" style={{ background: selected ? theme.toolbar.activeBg : "transparent", color: theme.node.text, outlineColor: theme.node.muted }} onMouseDown={(event) => event.stopPropagation()} onClick={onClick}>
{children}
</button>
);
@@ -220,7 +220,7 @@ function OptionPill({ selected, disabled = false, theme, onClick, children }: {
function SettingGroup({ title, color, children }: { title: string; color: string; children: ReactNode }) {
return (
<div className="space-y-1.5">
<div className="text-[10px] font-semibold" style={{ color }}>
<div className="text-[var(--fs-tiny)] font-semibold" style={{ color }}>
{title}
</div>
{children}
@@ -230,7 +230,7 @@ function SettingGroup({ title, color, children }: { title: string; color: string
function DimensionInput({ prefix, value, disabled, theme, onChange }: { prefix: string; value: number; disabled: boolean; theme: CanvasTheme; onChange: (value: number | null) => void }) {
return (
<label className="flex h-8 overflow-hidden rounded-md text-[11px]" style={{ background: theme.toolbar.itemHover, color: theme.node.text, opacity: disabled ? 0.55 : 1 }}>
<label className="flex h-8 overflow-hidden rounded-md text-[var(--fs-label)]" style={{ background: theme.toolbar.itemHover, color: theme.node.text, opacity: disabled ? 0.55 : 1 }}>
<span className="grid w-7 place-items-center" style={{ color: theme.node.muted }}>
{prefix}
</span>
@@ -247,7 +247,7 @@ function DurationInput({ value, min, theme, onChange }: { value: number; min: nu
};
return (
<label className="flex h-8 min-w-0 items-center overflow-hidden rounded-md border text-[11px]" style={{ background: theme.toolbar.itemHover, borderColor: theme.toolbar.border, color: theme.node.text }}>
<label className="flex h-8 min-w-0 items-center overflow-hidden rounded-md border text-[var(--fs-label)]" style={{ background: theme.toolbar.itemHover, borderColor: theme.toolbar.border, color: theme.node.text }}>
<input
key={`${min}-${value}`}
type="number"
@@ -288,7 +288,7 @@ function ratioPreview(ratio: string) {
function SwitchRow({ label, checked, theme, onChange }: { label: string; checked: boolean; theme: CanvasTheme; onChange: (checked: boolean) => void }) {
return (
<div className="flex h-8 items-center justify-between gap-2">
<span className="min-w-0 whitespace-nowrap text-[11px]" style={{ color: theme.node.text }}>
<span className="min-w-0 whitespace-nowrap text-[var(--fs-label)]" style={{ color: theme.node.text }}>
{label}
</span>
<span className="shrink-0" onMouseDown={(event) => event.stopPropagation()}>