refactor(canvas): 硬编码字号/圆角清理 → 语义 token(审计 D1/P1-3) (#131)
清理画布组件散落的硬编码字号与圆角,统一走 token 体系: 字号: - 新增 --fs-nano(7px) 覆盖原 text-[6px]/text-[7px] 子微型标注 (角标计数、节点预览、% 后缀) - text-[19px] → --fs-heading-lg(18px) 圆角(就近映射,±1~2px): - rounded-[3px] → --r-xs | rounded-[5px] → --r-sm - rounded-[6px] → --r-sm | rounded-[7px] → --r-md - rounded-[18px] → --r-2xl 涉及 11 个组件文件。rounded-[1px](minimap 视口指示框,刻意尖角)保留。 颜色项(bg-white/border-stone)经核实均带 dark: 变体,属标准 Tailwind 双主题模式非误用,不在本轮处理。 Co-authored-by: sikongyue <3155157671@qq.com>
This commit is contained in:
@@ -257,7 +257,7 @@ export function AgentPanelTabs<T extends string>({ value, items, theme, right, o
|
||||
<div className="flex min-h-9 items-center justify-between gap-2 rounded-md p-1" style={{ background: theme.spatial.surface }}>
|
||||
<nav className="grid min-w-0 flex-1 grid-flow-col auto-cols-fr items-center gap-0.5 text-xs" role="tablist" aria-label="Agent 面板">
|
||||
{items.map((item) => (
|
||||
<button key={item.value} type="button" role="tab" aria-selected={value === item.value} className={`inline-flex h-8 min-w-0 items-center justify-center gap-1 rounded-[5px] px-1.5 transition-colors ${value === item.value ? "font-medium" : "font-normal"}`} style={{ background: value === item.value ? theme.node.fill : "transparent", color: value === item.value ? theme.node.text : theme.node.muted, boxShadow: value === item.value ? `0 1px 5px ${theme.spatial.shadow}` : "none" }} onClick={() => onChange(item.value)}>
|
||||
<button key={item.value} type="button" role="tab" aria-selected={value === item.value} className={`inline-flex h-8 min-w-0 items-center justify-center gap-1 rounded-[var(--r-sm)] px-1.5 transition-colors ${value === item.value ? "font-medium" : "font-normal"}`} style={{ background: value === item.value ? theme.node.fill : "transparent", color: value === item.value ? theme.node.text : theme.node.muted, boxShadow: value === item.value ? `0 1px 5px ${theme.spatial.shadow}` : "none" }} onClick={() => onChange(item.value)}>
|
||||
<span className="shrink-0">{item.icon}</span>
|
||||
<span className="min-w-0 truncate">{item.label}</span>
|
||||
{item.count ? <span className="shrink-0 tabular-nums opacity-60">{item.count}</span> : null}
|
||||
|
||||
@@ -73,7 +73,7 @@ function AgentModeSwitch({ value, theme, onChange }: { value: CanvasAgentMode; t
|
||||
const active = value === item;
|
||||
const Icon = item === "online" ? Globe2 : Laptop;
|
||||
return (
|
||||
<button key={item} type="button" className="inline-flex h-7 items-center gap-1 rounded-[5px] px-2 transition-colors" style={{ background: active ? theme.node.fill : "transparent", color: active ? theme.node.text : theme.node.muted, boxShadow: active ? `0 1px 5px ${theme.spatial.shadow}` : "none" }} onClick={() => onChange(item)} aria-pressed={active}>
|
||||
<button key={item} type="button" className="inline-flex h-7 items-center gap-1 rounded-[var(--r-sm)] px-2 transition-colors" style={{ background: active ? theme.node.fill : "transparent", color: active ? theme.node.text : theme.node.muted, boxShadow: active ? `0 1px 5px ${theme.spatial.shadow}` : "none" }} onClick={() => onChange(item)} aria-pressed={active}>
|
||||
<Icon className="size-3" />
|
||||
{item === "online" ? "网站" : "本机"}
|
||||
</button>
|
||||
@@ -100,7 +100,7 @@ export function AgentChatEmptyState({ theme, nodeCount, onSelect }: { theme: Can
|
||||
<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-[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>
|
||||
<h2 className="mt-3 text-[var(--fs-heading-lg)] font-semibold leading-6" style={{ color: theme.node.text }}>从当前画布开始</h2>
|
||||
<div className="mt-4 grid grid-cols-1 gap-1">
|
||||
{visibleStarterActions.map(({ label, icon: Icon }) => (
|
||||
<button key={label} type="button" className="group flex min-h-11 min-w-0 items-center gap-2.5 rounded-md px-2.5 text-left text-xs font-medium transition-colors" style={{ color: theme.node.text }} onMouseEnter={(event) => { event.currentTarget.style.background = theme.spatial.surface; }} onMouseLeave={(event) => { event.currentTarget.style.background = "transparent"; }} onFocus={(event) => { event.currentTarget.style.background = theme.spatial.surface; }} onBlur={(event) => { event.currentTarget.style.background = "transparent"; }} onClick={() => onSelect(label)}>
|
||||
|
||||
@@ -118,7 +118,7 @@ export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true,
|
||||
{
|
||||
id: "asset-tray-toggle",
|
||||
label: open ? "收起素材空间" : `打开素材空间,共 ${(showLibrary ? assetImages.length : 0) + canvasImages.length} 项`,
|
||||
icon: <span className="relative"><Images /><span className="absolute -right-1.5 -top-1.5 min-w-3 rounded-full px-0.5 text-center text-[6px] font-bold leading-3" style={{ background: theme.accent.primary, color: "#ffffff" }}>{(showLibrary ? assetImages.length : 0) + canvasImages.length}</span></span>,
|
||||
icon: <span className="relative"><Images /><span className="absolute -right-1.5 -top-1.5 min-w-3 rounded-full px-0.5 text-center text-[var(--fs-nano)] font-bold leading-3" style={{ background: theme.accent.primary, color: "#ffffff" }}>{(showLibrary ? assetImages.length : 0) + canvasImages.length}</span></span>,
|
||||
active: open,
|
||||
onClick: () => setOpen((value) => !value),
|
||||
},
|
||||
@@ -133,7 +133,7 @@ export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true,
|
||||
animate={{ opacity: 1, y: 0, scale: 1, rotateX: 0 }}
|
||||
exit={{ opacity: 0, y: 14, scale: 0.95, rotateX: 3 }}
|
||||
transition={aceternityMotion.spring.panel}
|
||||
className="aceternity-floating-panel absolute bottom-[50px] left-0 flex w-[min(88vw,312px)] origin-bottom-left flex-col overflow-hidden rounded-[18px] border p-2.5 backdrop-blur-2xl"
|
||||
className="aceternity-floating-panel absolute bottom-[50px] left-0 flex w-[min(88vw,312px)] origin-bottom-left flex-col overflow-hidden rounded-[var(--r-2xl)] border p-2.5 backdrop-blur-2xl"
|
||||
style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, height: safeTrayHeight, minHeight: Math.min(TRAY_MIN_HEIGHT, getMaxTrayHeight()), maxHeight: "calc(100vh - 6rem)", boxShadow: `0 32px 100px ${theme.spatial.shadow}` }}
|
||||
>
|
||||
<div className="absolute inset-x-10 top-0 h-px" style={{ background: `linear-gradient(90deg, transparent, ${theme.spatial.glowStrong}, transparent)` }} />
|
||||
|
||||
@@ -317,7 +317,7 @@ 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-[var(--fs-micro)]" style={{ color: theme.node.muted }}>{detail}</span> : null}</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-[var(--fs-nano)] 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>
|
||||
|
||||
@@ -37,7 +37,7 @@ export function CanvasCreateCommandGrid({ commands, variant = "node" }: { comman
|
||||
>
|
||||
<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-[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}
|
||||
{command.badge ? <span className="absolute right-1 top-1 rounded-full border px-1 py-0.5 text-[var(--fs-nano)] font-bold leading-none" style={{ background: theme.toolbar.activeBg, borderColor: theme.toolbar.border, color: theme.node.muted }}>{command.badge}</span> : null}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -255,7 +255,7 @@ function FaceSelectionOverlay({ node, viewport, containerRef, imageWidth, imageH
|
||||
key={face.id}
|
||||
type="button"
|
||||
aria-label={selected ? `选择${selected.name}` : "选择此人脸"}
|
||||
className={`absolute rounded-[7px] border-2 ${interactive ? "pointer-events-auto" : "pointer-events-none"}`}
|
||||
className={`absolute rounded-[var(--r-md)] border-2 ${interactive ? "pointer-events-auto" : "pointer-events-none"}`}
|
||||
style={{ left: `${(face.x / imageWidth) * 100}%`, top: `${(face.y / imageHeight) * 100}%`, width: `${(face.width / imageWidth) * 100}%`, height: `${(face.height / imageHeight) * 100}%`, borderColor: active ? theme.accent.primary : "rgba(255,255,255,.94)", boxShadow: active ? `0 0 0 3px ${theme.accent.primarySoft}, 0 8px 24px rgba(0,0,0,.24)` : "0 8px 20px rgba(0,0,0,.18)" }}
|
||||
whileHover={{ scale: 1.025 }}
|
||||
whileTap={{ scale: 0.985 }}
|
||||
@@ -267,7 +267,7 @@ function FaceSelectionOverlay({ node, viewport, containerRef, imageWidth, imageH
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
{manualDraft ? <div className="pointer-events-none absolute rounded-[7px] border-2 border-dashed border-white" style={{ left: `${(manualDraft.x / imageWidth) * 100}%`, top: `${(manualDraft.y / imageHeight) * 100}%`, width: `${(manualDraft.width / imageWidth) * 100}%`, height: `${(manualDraft.height / imageHeight) * 100}%`, boxShadow: "0 0 0 3px rgba(255,255,255,.16)" }} /> : null}
|
||||
{manualDraft ? <div className="pointer-events-none absolute rounded-[var(--r-md)] border-2 border-dashed border-white" style={{ left: `${(manualDraft.x / imageWidth) * 100}%`, top: `${(manualDraft.y / imageHeight) * 100}%`, width: `${(manualDraft.width / imageWidth) * 100}%`, height: `${(manualDraft.height / imageHeight) * 100}%`, boxShadow: "0 0 0 3px rgba(255,255,255,.16)" }} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -263,12 +263,12 @@ function FramePreview({ nodes, frame, theme }: { nodes: CanvasNodeData[]; frame:
|
||||
<div className="pointer-events-none absolute inset-x-2 bottom-2 overflow-hidden rounded-md" style={{ top: FRAME_HEADER_HEIGHT, background: theme.frame.preview }}>
|
||||
{layout.length ? (
|
||||
layout.map(({ node, ...style }) => (
|
||||
<div key={node.id} className="absolute overflow-hidden rounded-[3px] border" style={{ ...style, background: theme.node.fill, borderColor: theme.node.stroke }}>
|
||||
<div key={node.id} className="absolute overflow-hidden rounded-[var(--r-xs)] border" style={{ ...style, background: theme.node.fill, borderColor: theme.node.stroke }}>
|
||||
{node.type === CanvasNodeType.Image && node.metadata?.content ? <img src={node.metadata.content} alt="" className="h-full w-full object-cover" loading="lazy" decoding="async" draggable={false} /> : null}
|
||||
{node.type === CanvasNodeType.Video && node.metadata?.content ? <video src={node.metadata.content} className="h-full w-full object-cover" muted playsInline preload="metadata" /> : null}
|
||||
{node.type === CanvasNodeType.Video && !node.metadata?.content ? <Video className="m-auto size-4 h-full opacity-40" /> : null}
|
||||
{node.type === CanvasNodeType.Text ? <div className="line-clamp-3 p-1 text-[7px] leading-[9px]" style={{ color: theme.node.text }}>{node.metadata?.content || node.title}</div> : null}
|
||||
{node.type === CanvasNodeType.Script ? <div className="p-1 text-[7px] leading-[9px]" style={{ color: theme.node.text }}>分镜脚本 · {node.metadata?.storyboard?.rows.length || 0} 镜</div> : null}
|
||||
{node.type === CanvasNodeType.Text ? <div className="line-clamp-3 p-1 text-[var(--fs-nano)] leading-[9px]" style={{ color: theme.node.text }}>{node.metadata?.content || node.title}</div> : null}
|
||||
{node.type === CanvasNodeType.Script ? <div className="p-1 text-[var(--fs-nano)] leading-[9px]" style={{ color: theme.node.text }}>分镜脚本 · {node.metadata?.storyboard?.rows.length || 0} 镜</div> : null}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -72,9 +72,9 @@ export function ImageToolSettingsModal({ open, tools, selectedIds, showLabels, o
|
||||
<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 }}>
|
||||
<label key={tool.id} className="flex h-8 min-w-0 cursor-pointer items-center gap-1 rounded-[var(--r-md)] 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="grid size-5 shrink-0 place-items-center rounded-[var(--r-sm)] [&_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-[var(--fs-tiny)] font-medium leading-none">{tool.label}</span>
|
||||
</label>
|
||||
))}
|
||||
|
||||
@@ -122,7 +122,7 @@ function FaceThumbnail({ dataUrl, imageWidth, imageHeight, box }: { dataUrl: str
|
||||
const scaleX = imageWidth / Math.max(1, box.width);
|
||||
const scaleY = imageHeight / Math.max(1, box.height);
|
||||
return (
|
||||
<span className="relative block size-6 shrink-0 overflow-hidden rounded-[6px] bg-black/20">
|
||||
<span className="relative block size-6 shrink-0 overflow-hidden rounded-[var(--r-sm)] bg-black/20">
|
||||
<img src={dataUrl} alt="" draggable={false} className="pointer-events-none absolute max-w-none" style={{ width: `${scaleX * 100}%`, height: `${scaleY * 100}%`, left: `${-(box.x / Math.max(1, box.width)) * 100}%`, top: `${-(box.y / Math.max(1, box.height)) * 100}%` }} />
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -76,7 +76,7 @@ export function CanvasZoomControls({ scale, onScaleChange, onReset, isMiniMapOpe
|
||||
{
|
||||
id: "zoom-precision",
|
||||
label: "精确缩放",
|
||||
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>,
|
||||
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-[var(--fs-nano)] opacity-55">%</span></span>,
|
||||
active: precisionOpen,
|
||||
onClick: () => setPrecisionOpen((value) => !value),
|
||||
},
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
--space-24: 96px;
|
||||
|
||||
/* --- 3. 字号(10 档)--- */
|
||||
--fs-nano: 7px; /* 纳米标注(角标计数、节点预览、% 后缀)*/
|
||||
--fs-micro: 9px; /* 极小标注(分镜行号辅助)*/
|
||||
--fs-tiny: 10px; /* 小标注(徽章、tag)*/
|
||||
--fs-label: 11px; /* 标签文字 */
|
||||
|
||||
Reference in New Issue
Block a user