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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user