feat(style): 全站无彩灰阶收尾——清理 legacy 别名与硬编码色漂移 (HEL-685 S6)

- app.css 删除 8 个无引用 legacy 别名,新增 --id-1..5 身份灰阶实色阶
- 画布去紫罗兰二级 palette(tokens.css 删除,改用全站 token)
- 画布/镜头台/智能体/omni/视频组件硬编码色全部归位 token
- agentPalette 彩色身份色改为显影灰阶 --id 阶;修复 3 处相纸白底白字不可读
- WorkflowLibrary 试运行抽屉焦点还原(异步禁用触发键丢焦)修复
- e2e: fpages/import 色值断言对齐现行 token;新增 hel685 走查脚本
  (1440/1024/390 截图 + 控制台错误 + 减动效 + 键盘焦点 + 对比度探针);
  pagination 空库自举;fpages 增加逐页控制台错误检查

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
施工员
2026-10-11 17:26:12 +08:00
co-authored by multica-agent
parent 2f267a751f
commit bc24370233
32 changed files with 405 additions and 156 deletions
+11 -11
View File
@@ -98,8 +98,8 @@
position: relative;
}
.artifact-node.running {
border-color: rgba(125, 211, 252, 0.6);
box-shadow: 0 0 18px rgba(125, 211, 252, 0.25);
border-color: var(--run);
box-shadow: 0 0 18px var(--run-soft);
}
:global(.svelte-flow__node.selected) .artifact-node {
border-color: var(--accent);
@@ -117,9 +117,9 @@
font-size: 9px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #bae6fd;
background: rgba(12, 74, 110, 0.55);
border: 1px solid rgba(125, 211, 252, 0.35);
color: var(--text-2);
background: var(--bg-4);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 2px 6px;
flex-shrink: 0;
@@ -141,12 +141,12 @@
flex-shrink: 0;
}
.running-badge {
color: #bae6fd;
background: rgba(12, 74, 110, 0.6);
color: var(--run);
background: var(--run-soft);
}
.failed-badge {
color: #fecaca;
background: rgba(127, 29, 29, 0.5);
color: var(--err);
background: var(--err-soft);
}
.play-badge {
font-size: 9px;
@@ -205,9 +205,9 @@
font-weight: 600;
letter-spacing: 0.04em;
text-transform: lowercase;
color: var(--text-muted);
color: var(--on-media);
pointer-events: none;
background: rgba(0, 0, 0, 0.55);
background: var(--media-scrim);
padding: 1px 5px;
border-radius: 999px;
}
@@ -108,7 +108,7 @@
transition: border-color 0.15s;
}
.entity-node:hover {
border-color: #3a3a44;
border-color: var(--line-2);
}
/* Svelte Flow marks the selected node wrapper with .selected */
:global(.svelte-flow__node.selected) .entity-node {
@@ -176,9 +176,9 @@
font-weight: 600;
letter-spacing: 0.04em;
text-transform: lowercase;
color: var(--text-muted);
color: var(--on-media);
pointer-events: none;
background: rgba(0, 0, 0, 0.55);
background: var(--media-scrim);
padding: 1px 5px;
border-radius: 999px;
}
-13
View File
@@ -1,13 +0,0 @@
/* Canvas v2.1 scoped visual tokens (spec v2.0 §2) — canvas surfaces only.
The global --accent stays untouched for v1 pages. */
.canvas-root {
--accent: #4f46a5;
--accent-hover: #6157c9;
--accent-glow: rgba(99, 102, 241, 0.3);
--edge: #7dd3fc;
/* Edge-chip surfaces (LinkEdge label) — fallbacks match the dark theme. */
--panel: #17171f;
--panel-border: #2a2a35;
--fg: #f4f4f5;
--fg-dim: #a1a1aa;
}
@@ -522,7 +522,7 @@
}
.role {
font-weight: 600;
color: var(--accent, #a78bfa);
color: var(--accent);
font-size: 11px;
margin-left: 6px;
font-family: ui-monospace, monospace;
@@ -567,7 +567,7 @@
input.invalid:focus-visible,
textarea.invalid:focus-visible,
select.invalid:focus-visible {
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.25);
box-shadow: 0 0 0 3px var(--err-soft);
}
.error-msg {
font-size: 11px;
@@ -607,7 +607,7 @@
}
.clear:hover {
color: var(--error);
border-color: rgba(239, 68, 68, 0.4);
border-color: var(--err-line);
}
.clear:focus-visible {
outline: 2px solid var(--accent);
@@ -30,7 +30,7 @@
position: absolute;
inset: 0;
z-index: 30;
background: rgba(18, 18, 24, 0.78);
background: var(--overlay);
backdrop-filter: blur(2px);
border-radius: var(--radius-md);
display: flex;
@@ -237,7 +237,7 @@ aria-label={zoomed ? t('lightbox.fitToScreen') : t('lightbox.zoom100')}
}
.counter {
font-size: 12px;
color: rgba(255, 255, 255, 0.65);
color: color-mix(in srgb, var(--on-media) 65%, transparent);
margin-right: 4px;
font-variant-numeric: tabular-nums;
}
@@ -249,13 +249,13 @@ aria-label={zoomed ? t('lightbox.fitToScreen') : t('lightbox.zoom100')}
height: 40px;
border: none;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
color: #fff;
background: color-mix(in srgb, var(--on-media) 12%, transparent);
color: var(--on-media);
cursor: pointer;
transition: background 0.15s;
}
.tool:hover {
background: rgba(255, 255, 255, 0.22);
background: color-mix(in srgb, var(--on-media) 22%, transparent);
}
.tool:focus-visible {
outline: 2px solid var(--accent);
@@ -272,13 +272,13 @@ aria-label={zoomed ? t('lightbox.fitToScreen') : t('lightbox.zoom100')}
height: 44px;
border: none;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
color: #fff;
background: color-mix(in srgb, var(--on-media) 10%, transparent);
color: var(--on-media);
cursor: pointer;
transition: background 0.15s;
}
.nav:hover {
background: rgba(255, 255, 255, 0.22);
background: color-mix(in srgb, var(--on-media) 22%, transparent);
}
.nav:focus-visible {
outline: 2px solid var(--accent);
@@ -334,16 +334,16 @@ aria-label={zoomed ? t('lightbox.fitToScreen') : t('lightbox.zoom100')}
.caption {
margin: 0;
font-size: 13px;
color: rgba(255, 255, 255, 0.85);
color: color-mix(in srgb, var(--on-media) 85%, transparent);
max-width: min(96vw, 720px);
text-align: center;
}
.hint {
margin: 0;
font-size: 12px;
color: rgba(255, 255, 255, 0.55);
color: color-mix(in srgb, var(--on-media) 55%, transparent);
}
.hint a {
color: rgba(255, 255, 255, 0.85);
color: color-mix(in srgb, var(--on-media) 85%, transparent);
}
</style>
@@ -504,9 +504,9 @@
height: 36px;
padding: 0 20px;
border-radius: 9999px;
background: var(--success, #22c55e);
background: var(--ok);
border: none;
color: #000;
color: var(--accent-ink);
font-size: 14px;
font-weight: 700;
font-family: var(--font-body);
@@ -95,10 +95,10 @@
rgba(255, 255, 255, 0.03) 6px,
rgba(255, 255, 255, 0.03) 12px
),
var(--bg-elevated, #16161d);
border: 1px dashed var(--border, #2a2a35);
var(--bg-elevated);
border: 1px dashed var(--border);
border-radius: inherit;
color: var(--text-muted, #8b8b9a);
color: var(--text-muted);
font-size: 12px;
font-weight: 500;
letter-spacing: 0.02em;
@@ -132,7 +132,7 @@
border-color: color-mix(in srgb, var(--success) 30%, var(--border));
}
.artifact.failed {
border-color: rgba(239, 68, 68, 0.4);
border-color: var(--err-line);
}
.meta {
display: flex;
@@ -249,12 +249,12 @@
width: 44px;
height: 44px;
border-radius: 9999px;
background: rgba(0, 0, 0, 0.55);
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.18);
background: var(--media-scrim);
box-shadow: 0 0 0 1px var(--on-media-line);
}
.play-badge :global(svg) {
position: absolute;
color: #fff;
color: var(--on-media);
fill: currentColor;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
margin-left: 2px;
@@ -668,8 +668,8 @@ let {
justify-content: center;
border: none;
border-radius: 999px;
background: rgba(0, 0, 0, 0.65);
color: #fff;
background: var(--media-scrim);
color: var(--on-media);
cursor: pointer;
padding: 0;
}
@@ -772,7 +772,7 @@ let {
border-radius: var(--radius-md);
border: 1px solid var(--accent);
background: var(--accent);
color: #fff;
color: var(--accent-ink);
font-size: 13px;
font-weight: 600;
cursor: pointer;
@@ -785,7 +785,7 @@ let {
.send.stop {
background: transparent;
color: var(--error);
border-color: rgba(239, 68, 68, 0.5);
border-color: var(--err-line);
}
.send.steer {
color: var(--accent);
@@ -431,6 +431,6 @@
}
.del:hover {
color: var(--error);
background: rgba(239, 68, 68, 0.12);
background: var(--err-soft);
}
</style>
@@ -60,7 +60,7 @@
overflow: hidden;
}
.tool-card.error {
border-color: rgba(239, 68, 68, 0.45);
border-color: var(--err-line);
}
.head {
display: flex;
@@ -1,35 +1,42 @@
/**
* Agent identity palette: a stable display name + accent color per agent.
* Agent identity palette: a stable display name + identity step per agent.
*
* Identity colors answer "who is speaking" only (avatar / name / bubble
* tint) — they never express task status (spec v2 §3.6). The curated set
* reuses the spec §3.6 avatar five-color family plus one neutral slate, so
* no tailwind rainbow hues leak into the fusion palette; the default
* main-loop agent is neutral `--text-2` slate.
* Identity answers "who is speaking" only (avatar dot / name / bubble tint)
* — it never expresses task status. HEL-685 无彩化:全部改为 app.css 的
* 显影灰阶实色阶 --id-1..5(与全站 --avatar-1..5 同一身份语言),
* 不再使用彩色身份色;默认主循环智能体保持中性 --id-4。
*/
export const DEFAULT_AGENT_NAME = 'Calliope';
// Fixed identity map (keyed by the raw agent_name the backend emits).
// Step numbers mirror the avatar five-set used across the app
// (models/agents/home project cards).
const IDENTITIES: Record<string, { label: string; color: string }> = {
planner: { label: 'Planner', color: '#5b8def' }, // avatar-2 blue
'story-agent': { label: 'Story Agent', color: '#3fb97f' }, // avatar-3 green
'script-agent': { label: 'Script Agent', color: '#e8a33d' }, // avatar-1 amber
'assets-agent': { label: 'Assets Agent', color: '#b98be0' }, // avatar-4 violet
'video-agent': { label: 'Video Agent', color: '#d98e6a' }, // avatar-5 clay
'research-agent': { label: 'Research Agent', color: '#8fa3c8' }, // neutral slate
planner: { label: 'Planner', color: 'var(--id-2)' },
'story-agent': { label: 'Story Agent', color: 'var(--id-3)' },
'script-agent': { label: 'Script Agent', color: 'var(--id-1)' },
'assets-agent': { label: 'Assets Agent', color: 'var(--id-4)' },
'video-agent': { label: 'Video Agent', color: 'var(--id-5)' },
'research-agent': { label: 'Research Agent', color: 'var(--id-4)' },
// The five named agents (HEL-613) — the runner stamps their zh names on
// named-session replies; colors mirror the spec §2.1 avatar five-set.
剧本创作: { label: '剧本创作', color: '#e8a33d' },
视觉设计: { label: '视觉设计', color: '#5b8def' },
分镜设计: { label: '分镜设计', color: '#3fb97f' },
素材资产: { label: '素材资产', color: '#b98be0' },
镜头脚本: { label: '镜头脚本', color: '#d98e6a' },
// named-session replies; steps mirror the avatar five-set.
剧本创作: { label: '剧本创作', color: 'var(--id-1)' },
视觉设计: { label: '视觉设计', color: 'var(--id-2)' },
分镜设计: { label: '分镜设计', color: 'var(--id-3)' },
素材资产: { label: '素材资产', color: 'var(--id-4)' },
镜头脚本: { label: '镜头脚本', color: 'var(--id-5)' },
};
// Fallback palette for unknown names (stable hash → deterministic color),
// narrowed to the avatar identity family + neutral slate (spec v2 §十.4).
const FALLBACK_COLORS = ['#5b8def', '#3fb97f', '#e8a33d', '#b98be0', '#d98e6a', '#8fa3c8'];
// Fallback palette for unknown names (stable hash → deterministic step),
// spanning the same five grayscale identity steps.
const FALLBACK_COLORS = [
'var(--id-1)',
'var(--id-2)',
'var(--id-3)',
'var(--id-4)',
'var(--id-5)',
];
function hashString(s: string): number {
let h = 0;
@@ -39,8 +46,8 @@ function hashString(s: string): number {
return h;
}
/** Neutral slate (--text-2) — the default agent borrows no signal color. */
export const DEFAULT_AGENT_COLOR = '#aab2c5';
/** Neutral step (--id-4) — the default agent borrows no signal color. */
export const DEFAULT_AGENT_COLOR = 'var(--id-4)';
export function agentDisplayName(name: string | null | undefined): string {
const key = (name || '').trim().toLowerCase();
@@ -231,7 +231,7 @@
.tabs button.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
color: var(--accent-ink);
}
.tabs button:focus-visible {
@@ -341,7 +341,7 @@
height: 20px;
border-radius: 9999px;
background: var(--accent);
color: #fff;
color: var(--accent-ink);
display: flex;
align-items: center;
justify-content: center;
@@ -271,9 +271,9 @@
width: 20px;
height: 20px;
border-radius: 9999px;
background: rgba(0, 0, 0, 0.65);
background: var(--media-scrim);
border: none;
color: white;
color: var(--on-media);
cursor: pointer;
opacity: 0;
transition: opacity 0.15s;
@@ -173,7 +173,7 @@
padding: 0 4px;
border-radius: 9999px;
background: var(--accent);
color: white;
color: var(--accent-ink);
font-size: 10px;
font-weight: 700;
}
@@ -90,7 +90,7 @@
.step-btn:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.05);
background: var(--bg-4);
}
.step-btn:focus-visible {
@@ -272,7 +272,7 @@
height: 20px;
border-radius: 9999px;
background: var(--accent);
color: #fff;
color: var(--accent-ink);
display: flex;
align-items: center;
justify-content: center;
@@ -278,6 +278,9 @@
let testOpen = $state(false);
let testWf = $state<Workflow | null>(null);
let openingTestId = $state<number | null>(null);
// HEL-685 键盘焦点:openTest 会在异步加载期间禁用触发按钮,焦点先掉回
// body,抽屉关闭时就还原不到触发键。这里记下触发元素,关闭后兜底还原。
let testTriggerEl: HTMLElement | null = null;
async function openTest(row: WorkflowMeta) {
openingTestId = row.id;
@@ -289,6 +292,14 @@
}
}
$effect(() => {
if (!testOpen && testTriggerEl) {
const el = testTriggerEl;
testTriggerEl = null;
if (document.activeElement === document.body && el.isConnected) el.focus();
}
});
let wfRecheckId = $state<number | null>(null);
type ConnectionCheck = Awaited<ReturnType<typeof workflows.check>>;
const wfRecheckResult = $state<Record<number, ConnectionCheck>>({});
@@ -716,7 +727,10 @@
class="link-accent wf-test-btn"
title={t('wft.title')}
disabled={openingTestId === row.id}
onclick={() => void openTest(row)}
onclick={(e) => {
testTriggerEl = e.currentTarget;
void openTest(row);
}}
>
<Icon name="play" size={13} />
{openingTestId === row.id ? t('common.loading') : t('conn.wfTestBtn')}
@@ -85,7 +85,7 @@
align-items: center;
gap: 12px;
padding: 8px 12px;
border-top: 1px solid var(--border, #232733);
border-top: 1px solid var(--border);
min-height: 74px;
}
.label {
@@ -108,7 +108,7 @@
.open {
display: block;
position: relative;
border: 1px solid var(--border, #2a2f3a);
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
padding: 0;
@@ -128,8 +128,8 @@
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.05em;
background: rgba(0, 0, 0, 0.65);
color: #fff;
background: var(--media-scrim);
color: var(--on-media);
border-radius: 4px;
padding: 1px 5px;
pointer-events: none;
@@ -153,7 +153,7 @@
opacity: 1;
}
.danger {
color: #e5697a;
color: var(--err);
}
.empty {
font-size: 12px;
@@ -123,7 +123,7 @@
gap: 4px;
}
.btn {
border: 1px solid var(--border, #2a2f3a);
border: 1px solid var(--border);
background: transparent;
color: inherit;
border-radius: 6px;
@@ -133,10 +133,10 @@
line-height: 1.4;
}
.btn:hover {
background: rgba(255, 255, 255, 0.06);
background: var(--bg-4);
}
.danger {
color: #e5697a;
color: var(--err);
}
.add-grid {
display: grid;
@@ -145,7 +145,7 @@
padding: 0 12px 10px;
}
.add-item {
border: 1px solid var(--border, #2a2f3a);
border: 1px solid var(--border);
background: transparent;
color: inherit;
border-radius: 6px;
@@ -154,7 +154,7 @@
cursor: pointer;
}
.add-item:hover {
background: rgba(255, 255, 255, 0.06);
background: var(--bg-4);
}
.objects {
flex: 1;
@@ -171,10 +171,10 @@
padding: 2px 4px;
}
.row:hover {
background: rgba(255, 255, 255, 0.04);
background: var(--bg-3);
}
.row.selected {
background: rgba(99, 102, 241, 0.22);
background: var(--accent-soft);
}
.name {
flex: 1;
@@ -217,8 +217,8 @@
}
.rename {
flex: 1;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border, #2a2f3a);
background: var(--bg);
border: 1px solid var(--border);
color: inherit;
border-radius: 4px;
padding: 4px 6px;
@@ -233,6 +233,6 @@
padding: 8px 12px;
font-size: 11px;
opacity: 0.45;
border-top: 1px solid var(--border, #232733);
border-top: 1px solid var(--border);
}
</style>
@@ -235,7 +235,7 @@
opacity: 0.6;
}
section {
border-top: 1px solid var(--border, #232733);
border-top: 1px solid var(--border);
padding-top: 12px;
}
.collapse {
@@ -257,8 +257,8 @@
input[type='number'],
select {
width: 100%;
background: rgba(0, 0, 0, 0.25);
border: 1px solid var(--border, #2a2f3a);
background: var(--bg);
border: 1px solid var(--border);
color: inherit;
border-radius: 4px;
padding: 3px 5px;
@@ -279,7 +279,7 @@
gap: 6px;
}
.joint {
border: 1px solid var(--border, #232733);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 8px;
}
@@ -298,8 +298,8 @@
}
.capture {
margin-top: auto;
border: 1px solid var(--border, #2a2f3a);
background: rgba(99, 102, 241, 0.18);
border: 1px solid var(--accent-line);
background: var(--accent-soft);
color: inherit;
border-radius: 8px;
padding: 8px;
@@ -307,6 +307,6 @@
cursor: pointer;
}
.capture:hover {
background: rgba(99, 102, 241, 0.3);
background: color-mix(in srgb, var(--accent) 18%, transparent);
}
</style>
@@ -255,7 +255,7 @@
background: var(--bg-elevated);
}
.item.active {
background: rgba(99, 102, 241, 0.22);
background: var(--accent-soft);
}
.dot {
width: 7px;
@@ -265,7 +265,7 @@
flex-shrink: 0;
}
.dot.run {
background: var(--accent, #6366f1);
background: var(--run);
animation: dot-pulse 1.4s ease-in-out infinite;
}
@keyframes dot-pulse {
@@ -295,6 +295,6 @@
display: flex;
}
.del:hover {
color: #e5697a;
color: var(--err);
}
</style>
@@ -841,7 +841,7 @@ let exporting = false;
height: 100%;
min-height: 320px;
overflow: hidden;
background: #14161c;
background: var(--bg);
position: relative;
}
.viewport :global(canvas) {
@@ -866,7 +866,7 @@ let exporting = false;
top: 0;
bottom: 0;
width: 1px;
background: rgba(255, 255, 255, 0.14);
background: color-mix(in srgb, var(--on-media) 14%, transparent);
}
.composition-guide .thirds-v:nth-of-type(1) {
left: 33.33%;
@@ -879,7 +879,7 @@ let exporting = false;
left: 0;
right: 0;
height: 1px;
background: rgba(255, 255, 255, 0.14);
background: color-mix(in srgb, var(--on-media) 14%, transparent);
}
.composition-guide .thirds-h:nth-of-type(3) {
top: 33.33%;
@@ -894,7 +894,7 @@ let exporting = false;
width: 14px;
height: 1px;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.35);
background: color-mix(in srgb, var(--on-media) 35%, transparent);
}
.composition-guide .cross-v {
position: absolute;
@@ -903,6 +903,6 @@ let exporting = false;
width: 1px;
height: 14px;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.35);
background: color-mix(in srgb, var(--on-media) 35%, transparent);
}
</style>
@@ -194,7 +194,7 @@
align-items: center;
gap: 10px;
padding: 6px 12px;
border-top: 1px solid var(--border, #232733);
border-top: 1px solid var(--border);
}
.controls {
display: flex;
@@ -207,7 +207,7 @@
justify-content: center;
min-width: 26px;
height: 26px;
border: 1px solid var(--border, #2a2f3a);
border: 1px solid var(--border);
background: transparent;
color: inherit;
border-radius: 6px;
@@ -216,7 +216,7 @@
padding: 0 6px;
}
.icon-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.06);
background: var(--bg-4);
}
.icon-btn:disabled {
opacity: 0.45;
@@ -227,10 +227,10 @@
gap: 4px;
}
.obj-add {
color: #7dd88a;
color: var(--ok);
}
.export {
border: 1px solid var(--border, #2a2f3a);
border: 1px solid var(--border);
background: transparent;
color: inherit;
border-radius: 6px;
@@ -240,7 +240,7 @@
white-space: nowrap;
}
.export:hover:not(:disabled) {
border-color: var(--accent, #6366f1);
border-color: var(--accent-line);
}
.export:disabled {
opacity: 0.45;
@@ -255,8 +255,8 @@
}
.dur input {
width: 46px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border, #2a2f3a);
background: var(--bg);
border: 1px solid var(--border);
color: inherit;
border-radius: 4px;
padding: 3px 5px;
@@ -280,14 +280,14 @@
position: absolute;
inset: 11px 0;
border-radius: 3px;
background: rgba(255, 255, 255, 0.08);
background: var(--bg-4);
}
.playhead {
position: absolute;
top: 2px;
bottom: 2px;
width: 2px;
background: var(--accent, #6366f1);
background: var(--accent);
transform: translateX(-1px);
pointer-events: none;
}
@@ -298,8 +298,8 @@
height: 10px;
border-radius: 2px;
transform: translate(-5px, -50%) rotate(45deg);
background: #e0b34d;
border: 1px solid rgba(0, 0, 0, 0.5);
background: var(--warn);
border: 1px solid var(--bg);
cursor: grab;
padding: 0;
z-index: 2;
@@ -89,8 +89,8 @@
display: flex;
align-items: center;
gap: 6px;
border: 1px solid var(--border, #2a2f3a);
background: rgba(16, 18, 24, 0.82);
border: 1px solid var(--line);
background: color-mix(in srgb, var(--bg-2) 85%, transparent);
border-radius: 999px;
padding: 4px 8px;
backdrop-filter: blur(4px);
@@ -103,7 +103,7 @@
.divider {
width: 1px;
height: 16px;
background: var(--border, #2a2f3a);
background: var(--border);
margin: 0 2px;
}
.btn {
@@ -119,12 +119,12 @@
opacity: 0.85;
}
.btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.08);
background: var(--bg-4);
opacity: 1;
}
.btn.on {
background: rgba(99, 102, 241, 0.25);
border-color: var(--accent, #6366f1);
background: var(--accent-soft);
border-color: var(--accent-line);
opacity: 1;
}
.btn:disabled {
@@ -132,9 +132,9 @@
cursor: not-allowed;
}
.capture {
border-color: var(--border, #2a2f3a);
border-color: var(--border);
}
.capture:hover {
border-color: var(--accent, #6366f1);
border-color: var(--accent-line);
}
</style>
+11 -8
View File
@@ -132,9 +132,20 @@
--avatar-4: linear-gradient(135deg, #55544f, #424140);
--avatar-5: linear-gradient(135deg, #424140, #2e2e2c);
/* Agent identity solid steps (HEL-685 无彩化):--avatar-1..5 的实色对应
阶,供 color/border/dot 等不能用渐变值的场合。五阶全部落在深底可读
带内(最暗一阶 ≥ --text-3 对比度下限),身份仍以明度阶区分。 */
--id-1: #f0efe9;
--id-2: #d8d6cd;
--id-3: #b8b6ac;
--id-4: #a8a69c;
--id-5: #94938c;
/* ------------------------------------------------------------------
* Legacy token names (§六 映射表) — aliases only, never new values,
* so every existing component renders in the fusion palette.
* HEL-685 清理:无引用别名(--accent-hover/--accent-contrast/--accent-focus/
* --error-border/--font-ui/--space-xs/--space-lg/--space-xl)已删除。
* ------------------------------------------------------------------ */
--surface: var(--bg-2);
--surface-2: var(--bg-3);
@@ -142,17 +153,13 @@
--border: var(--line);
--border-strong: var(--line-2);
--border-hover: var(--line-2);
--accent-hover: var(--accent-hi);
--accent-contrast: var(--accent-ink);
--accent-dim: var(--accent-soft);
--accent-focus: var(--accent-line);
--success: var(--ok);
--success-dim: var(--ok-soft);
--info: var(--run);
--info-dim: var(--run-soft);
--error: var(--err);
--error-dim: var(--err-soft);
--error-border: var(--err-line);
--warning: var(--warn);
--bg-primary: var(--bg);
--bg-surface: var(--bg-2);
@@ -160,7 +167,6 @@
--text-primary: var(--text);
--text-secondary: var(--text-2);
--text-muted: var(--text-3);
--font-ui: var(--font);
--font-a-mono: var(--mono);
--font-display: var(--font);
--font-body: var(--font);
@@ -168,11 +174,8 @@
--radius-sm: var(--r-sm);
--radius-md: var(--r-md);
--radius-lg: var(--r-lg);
--space-xs: var(--sp-1);
--space-sm: var(--sp-3);
--space-md: var(--sp-6);
--space-lg: var(--sp-7);
--space-xl: var(--sp-8);
/* Omni Composer pill tokens */
--pill-bg: var(--bg-3);
@@ -15,7 +15,6 @@
type Viewport,
} from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
import '$lib/canvas/tokens.css';
import AppHeader from '$lib/components/AppHeader.svelte';
import AgentChat from '$lib/components/agent/AgentChat.svelte';
import AgentComposer from '$lib/components/agent/AgentComposer.svelte';
+10 -3
View File
@@ -96,7 +96,7 @@ const editedColor = await page
.first()
.evaluate((el) => getComputedStyle(el).color)
.catch(() => '');
ok('已编辑标签用琥珀 rgb(240,180,92)', editedColor === 'rgb(240, 180, 92)', editedColor);
ok('已编辑标签用琥珀例外 rgb(163,145,95)', editedColor === 'rgb(163, 145, 95)', editedColor);
// 复制提示词(镜头段 prompt 复制按钮存在性在 UI 层检查 ShotStagePanel)
await page.goto(`${BASE}/workbench/${pid}`, { waitUntil: 'networkidle' });
@@ -115,7 +115,12 @@ const railLibrary = await page.locator('.rail a[href="/library"], .nav a[href="/
ok('图标轨/更多含素材库入口', railLibrary >= 1, `count=${railLibrary}`);
await shot(page, 'library-1440');
// ---------- per-page 3-width walk + overflow ----------
// ---------- per-page 3-width walk + overflow + console errors ----------
let consoleErrors = [];
page.on('console', (m) => {
if (m.type() === 'error') consoleErrors.push(m.text().slice(0, 160));
});
page.on('pageerror', (e) => consoleErrors.push(String(e).slice(0, 160)));
const PAGES = [
['home', '/'],
['workbench', `/workbench/${pid}`],
@@ -131,8 +136,10 @@ const PAGES = [
for (const [name, route] of PAGES) {
for (const [wname, w] of WIDTHS) {
await page.setViewportSize({ width: w, height: 900 });
consoleErrors = [];
await page.goto(`${BASE}${route}`, { waitUntil: 'networkidle' }).catch(() => {});
await page.waitForTimeout(700);
ok(`${name} ${wname} 无控制台错误`, consoleErrors.length === 0, consoleErrors[0] ?? '');
if (wname !== '1440') await shot(page, `${name}-${wname}`);
if (w === 390) {
const overflow = await page.evaluate(
@@ -155,7 +162,7 @@ await page.waitForTimeout(600);
const runGradBar = await page.locator('.a-progress').count();
ok('任务页进度条组件(a-progress)', runGradBar >= 0);
const testTagNeutral = await page.locator('.tag-test').first().evaluate((el) => getComputedStyle(el).color).catch(() => '');
ok('测试标签为中性灰', testTagNeutral === '' || testTagNeutral === 'rgb(170, 178, 197)', testTagNeutral);
ok('测试标签为中性灰阶', testTagNeutral === '' || testTagNeutral === 'rgb(168, 166, 156)', testTagNeutral);
// ---------- agent chat bubbles ----------
await page.goto(`${BASE}/agents`, { waitUntil: 'networkidle' });
+210
View File
@@ -0,0 +1,210 @@
/* HEL-685 S6 全站走查:1440/1024/390 截图 + 减动效 / 键盘焦点 / 对比度探针。
* Runs against the isolated stack (vite 5173 → backend 8247 → fake ComfyUI
* 18188). Screenshots land in e2e/shots/hel685-*.png. */
import { chromium } from 'playwright';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.dirname(fileURLToPath(import.meta.url));
const SHOTS = path.join(ROOT, 'shots');
fs.mkdirSync(SHOTS, { recursive: true });
const BASE = 'http://127.0.0.1:5173';
const API = 'http://127.0.0.1:8247';
let failures = 0;
function ok(name, cond, extra = '') {
const line = `${cond ? 'PASS' : 'FAIL'} ${name}${extra ? ' — ' + extra : ''}`;
console.log(line);
if (!cond) failures++;
}
const api = async (p, init) => {
const r = await fetch(API + p, { headers: { 'Content-Type': 'application/json' }, ...init });
const body = await r.text();
try {
return JSON.parse(body);
} catch {
return { raw: body, status: r.status };
}
};
// ---------- fixtures: project + confirmed script + canvas ----------
const proj = await api('/api/projects', {
method: 'POST',
body: JSON.stringify({ title: 'S6走查·灯塔', idea: '灯塔看守人的短篇' }),
});
const pid = proj.id;
await api(`/api/workbench/projects/${pid}/stages/script/import`, {
method: 'POST',
body: JSON.stringify({
text: '灯塔看守人老周在暴风雨夜发现一封三十年前的信。\n\n他修好旧船,横过海峡,把回信送到对岸。',
}),
});
const canvas = await api('/api/canvas', { method: 'POST', body: JSON.stringify({ project_id: pid }) });
const cid = canvas.canvas?.id ?? canvas.id;
ok('走查项目与画布就绪', !!pid && !!cid, `pid=${pid} cid=${cid}`);
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const consoleErrors = [];
page.on('console', (m) => {
if (m.type() === 'error') consoleErrors.push(m.text().slice(0, 160));
});
page.on('pageerror', (e) => consoleErrors.push(String(e).slice(0, 160)));
const shot = (name) => page.screenshot({ path: `${SHOTS}/hel685-${name}.png` });
// ---------- 1440 desktop set (compare against HEL-679 / theme-1) ----------
const DESK = [
['home-1440', '/'],
['workbench-1440', `/workbench/${pid}`],
['image-desk-1440', '/image'],
['video-desk-1440', '/video'],
['agents-1440', '/agents'],
['models-1440', '/models'],
['connections-1440', '/connections'],
['tasks-1440', '/tasks'],
['library-1440', '/library'],
['canvas-1440', `/canvas/${cid}`],
['build-scene-1440', '/build-scene'],
];
for (const [name, route] of DESK) {
consoleErrors.length = 0;
await page.goto(`${BASE}${route}`, { waitUntil: 'networkidle' }).catch(() => {});
await page.waitForTimeout(700);
ok(`${name} 无控制台错误`, consoleErrors.length === 0, consoleErrors[0] ?? '');
await shot(name);
}
// ---------- 1024 / 390 usability set ----------
const NARROW = [
['home', '/'],
['workbench', `/workbench/${pid}`],
['image-desk', '/image'],
['video-desk', '/video'],
['agents', '/agents'],
['tasks', '/tasks'],
['canvas', `/canvas/${cid}`],
];
for (const [name, route] of NARROW) {
for (const w of [1024, 390]) {
await page.setViewportSize({ width: w, height: 900 });
await page.goto(`${BASE}${route}`, { waitUntil: 'networkidle' }).catch(() => {});
await page.waitForTimeout(600);
await shot(`${name}-${w}`);
if (w === 390) {
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
);
ok(`${name} 390 无横向溢出`, overflow <= 1, `overflow=${overflow}px`);
}
}
}
await page.setViewportSize({ width: 1440, height: 900 });
// ---------- reduce-motion probes (顶栏开关 data-reduce=1) ----------
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' }).catch(() => {});
await page.waitForTimeout(400);
await page.evaluate(() => (document.documentElement.dataset.reduce = '1'));
const dur = await page.locator('.a-btn').first().evaluate((el) => getComputedStyle(el).transitionDuration);
ok('减动效:过渡时长塌缩到 ≤120ms', parseFloat(dur) <= 0.12, dur);
const spinRule = await page.evaluate(() => {
const ring = document.createElement('div');
ring.className = 'a-tile-ring';
ring.innerHTML = '<i></i>';
document.body.appendChild(ring);
const name = getComputedStyle(ring.firstChild).animationName;
ring.remove();
return name;
});
ok('减动效:上传旋转动画禁用(animation-name=none)', spinRule === 'none', spinRule);
await page.evaluate(() => delete document.documentElement.dataset.reduce);
// ---------- keyboard focus probe ----------
await page.goto(`${BASE}/tasks`, { waitUntil: 'networkidle' }).catch(() => {});
await page.waitForTimeout(400);
await page.keyboard.press('Tab');
const focusRing = await page.evaluate(() => {
const el = document.activeElement;
if (!el) return null;
const cs = getComputedStyle(el);
return cs.outlineStyle !== 'none' && parseFloat(cs.outlineWidth) >= 1.5 ? cs.outlineColor : null;
});
ok('键盘焦点显示相纸白焦点圈', focusRing === 'rgb(245, 244, 238)', String(focusRing));
// ---------- text contrast probes (WCAG AA on real elements) ----------
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' }).catch(() => {});
await page.waitForTimeout(600);
const lum = (rgbStr) => {
const m = rgbStr.match(/\d+/g).map(Number);
const [r, g, b] = m.map((v) => {
const c = v / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const ratio = (a, b) => {
const l1 = lum(a);
const l2 = lum(b);
const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
return (hi + 0.05) / (lo + 0.05);
};
const probeText = async (sel, label, min = 4.5) => {
const el = page.locator(sel).first();
if (!(await el.count())) return ok(`${label} 探针元素存在`, false, sel);
const { color, bg } = await el.evaluate((e) => {
const bgOf = (node) => {
let n = node;
while (n && n !== document.documentElement) {
const bg = getComputedStyle(n).backgroundColor;
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') return bg;
n = n.parentElement;
}
return 'rgb(33, 33, 32)';
};
return { color: getComputedStyle(e).color, bg: bgOf(e) };
});
const r = ratio(color, bg);
ok(`${label} 对比度 ${r.toFixed(2)}:1 ≥ ${min}`, r >= min, `${color} on ${bg}`);
};
await probeText('body', '首页正文文字');
await page.goto(`${BASE}/settings`, { waitUntil: 'networkidle' }).catch(() => {});
await page.waitForTimeout(600);
await probeText('.a-panel-lead', '面板次要文字(--text-2 级)');
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' }).catch(() => {});
await page.waitForTimeout(400);
// .a-btn.primary 用渐变背景(backgroundColor 透明),对 --accent 实色计算
const btnContrast = await page.evaluate(() => {
const cs = getComputedStyle(document.documentElement);
const hex = (h) => {
const s = h.replace('#', '');
const n = parseInt(s.length === 3 ? s.split('').map((c) => c + c).join('') : s, 16);
return `rgb(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255})`;
};
return { ink: hex(cs.getPropertyValue('--accent-ink').trim()), accent: hex(cs.getPropertyValue('--accent').trim()) };
});
const btnRatio = ratio(btnContrast.ink, btnContrast.accent);
ok(`主按钮文字对比度 ${btnRatio.toFixed(2)}:1 ≥ 4.5`, btnRatio >= 4.5, `${btnContrast.ink} on ${btnContrast.accent}`);
// agent identity steps all ≥ text-3 floor (computed from :root tokens)
const idSteps = await page.evaluate(() =>
['--id-1', '--id-2', '--id-3', '--id-4', '--id-5', '--text-3'].map((v) =>
getComputedStyle(document.documentElement).getPropertyValue(v).trim(),
),
);
const bg2 = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--bg-2').trim());
const toRgb = (hex) => {
const h = hex.replace('#', '');
const n = parseInt(h.length === 3 ? h.split('').map((c) => c + c).join('') : h, 16);
return `rgb(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255})`;
};
for (let i = 0; i < idSteps.length; i++) {
const r = ratio(toRgb(idSteps[i]), toRgb(bg2));
const label = i < 5 ? `身份灰阶 --id-${i + 1}` : '--text-3 下限';
ok(`${label} 在面板底上 ≥4.5:1(${r.toFixed(2)}:1)`, r >= 4.5);
}
await browser.close();
console.log(failures === 0 ? 'ALL HEL-685 WALKTHROUGH CHECKS PASSED' : `${failures} CHECKS FAILED`);
process.exit(failures === 0 ? 0 : 1);
+7 -7
View File
@@ -213,8 +213,8 @@ const run = async () => {
})),
);
ok(
"① 解析 JSON 绿 ✓",
states[0].ok && states[0].color === "rgb(73, 203, 144)",
"① 解析 JSON 亮灰 ✓",
states[0].ok && states[0].color === "rgb(221, 221, 215)",
states[0].color,
);
ok(
@@ -227,8 +227,8 @@ const run = async () => {
.textContent(),
);
ok(
"③ 核对模型全绿(假 object_info 装齐)",
states[2].ok && states[2].color === "rgb(73, 203, 144)",
"③ 核对模型全亮灰(假 object_info 装齐)",
states[2].ok && states[2].color === "rgb(221, 221, 215)",
states[2].color,
);
ok("④ 建立输入槽(1 参数 0 媒体)", states[3].ok);
@@ -311,7 +311,7 @@ const run = async () => {
.evaluate((el) => getComputedStyle(el).color);
ok(
"无法核查 = 琥珀(非绿非红)",
warnColor === "rgb(240, 180, 92)",
warnColor === "rgb(163, 145, 95)",
warnColor,
);
ok(
@@ -392,7 +392,7 @@ const run = async () => {
const chipColor = await page
.locator(".drawer-panel .wft-active .chip.run")
.evaluate((el) => getComputedStyle(el).color);
ok("进行中状态为信号青", chipColor === "rgb(84, 223, 210)", chipColor);
ok("进行中状态为运行灰", chipColor === "rgb(184, 182, 172)", chipColor);
ok(
"进行中显示进度条",
(await page.locator(".drawer-panel .wft-active .track").count()) === 1,
@@ -450,7 +450,7 @@ const run = async () => {
.evaluate((el) => getComputedStyle(el).color);
ok(
"失败判定为红 + 中文标题",
badColor === "rgb(240, 100, 90)",
badColor === "rgb(208, 122, 107)",
await page
.locator(".drawer-panel .wft-result.bad .wft-verdict")
.textContent(),
+22
View File
@@ -16,6 +16,28 @@ const run = async () => {
const base = 'http://127.0.0.1:5173';
let realJobs = [];
// HEL-685 harness fix: fan-out needs ≥1 real take to clone; on a fresh DB
// the list is empty — kick one quick fake-ComfyUI image job, wait for it,
// and only then start intercepting.
{
const probe = await fetch('http://127.0.0.1:8247/api/playground/jobs').then((r) => r.json());
if (!Array.isArray(probe) || probe.length === 0) {
await fetch('http://127.0.0.1:8247/api/playground/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
workflow_id: 1,
input_values: { 1: { text: 'pagination 基准' }, 2: 42 },
source: 'studio-image',
}),
});
for (let i = 0; i < 60; i++) {
await page.waitForTimeout(500);
const jobs = await fetch('http://127.0.0.1:8247/api/playground/jobs').then((r) => r.json());
if (Array.isArray(jobs) && jobs.some((j) => j.status === 'done')) break;
}
}
}
await page.route('**/api/playground/jobs**', async (route) => {
const r = await route.fetch();
realJobs = await r.json();