Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
225fcb49ea | ||
|
|
70a860ce2a | ||
|
|
e42212a16f | ||
|
|
f5f19f0366 | ||
|
|
bfe7b20b73 | ||
|
|
48e16ae443 |
@@ -52,15 +52,16 @@
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* §四.5 Toast:右下角堆叠,220ms 上移入场;成功绿点 / 失败红点。 */
|
||||
.toast-host {
|
||||
position: fixed;
|
||||
top: 72px;
|
||||
right: 20px;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: min(360px, calc(100vw - 32px));
|
||||
max-width: min(400px, calc(100vw - 32px));
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast {
|
||||
@@ -69,23 +70,10 @@
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-surface);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
||||
animation: toast-in 0.22s ease-out;
|
||||
}
|
||||
.toast.success {
|
||||
border-color: rgba(34, 197, 94, 0.45);
|
||||
box-shadow:
|
||||
0 12px 32px rgba(0, 0, 0, 0.45),
|
||||
0 0 0 1px rgba(34, 197, 94, 0.12);
|
||||
}
|
||||
.toast.error {
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
.toast.info {
|
||||
border-color: rgba(59, 130, 246, 0.45);
|
||||
border-radius: var(--r-lg);
|
||||
background: var(--float-bg);
|
||||
box-shadow: var(--shadow-float);
|
||||
animation: toast-in var(--d-enter) var(--ease-out);
|
||||
}
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
@@ -101,17 +89,17 @@
|
||||
background: currentColor;
|
||||
}
|
||||
.toast.success .badge {
|
||||
color: var(--success);
|
||||
color: var(--ok);
|
||||
}
|
||||
.toast.error .badge {
|
||||
color: var(--error);
|
||||
color: var(--err);
|
||||
}
|
||||
.toast.info .badge {
|
||||
color: var(--info);
|
||||
color: var(--text-2);
|
||||
}
|
||||
.msg {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-size: var(--fs-body);
|
||||
line-height: 1.4;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
@@ -127,10 +115,12 @@
|
||||
padding: 0 2px;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--r-sm);
|
||||
}
|
||||
.dismiss:hover {
|
||||
color: var(--text-primary);
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.dismiss:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
.dismiss:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
@@ -139,7 +129,7 @@
|
||||
@keyframes toast-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
@@ -151,9 +141,11 @@
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
:global(:root[data-reduce='1']) .toast {
|
||||
animation: none;
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.toast-host {
|
||||
top: auto;
|
||||
bottom: 20px;
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
function segClass(key: string): string {
|
||||
const state = stats.stage_status?.[key];
|
||||
if (state === 'confirmed') return 'seg-confirmed';
|
||||
if (state === 'running' || state === 'awaiting_confirm') return 'seg-draft';
|
||||
if (state === 'running') return 'seg-running';
|
||||
if (state === 'awaiting_confirm') return 'seg-draft';
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -70,7 +71,7 @@
|
||||
return { cls: 'st-progress', label: `${stageName} · ${t('workbench.state.running')}` };
|
||||
}
|
||||
if (state === 'awaiting_confirm') {
|
||||
return { cls: 'st-progress', label: `${stageName} · ${t('workbench.state.draft')}` };
|
||||
return { cls: 'st-pending', label: `${stageName} · ${t('workbench.state.draft')}` };
|
||||
}
|
||||
if (stats.stage_confirmed > 0) {
|
||||
return { cls: 'st-progress', label: `${stageName} · ${t('home.statusProgress')}` };
|
||||
@@ -329,7 +330,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(22, 19, 10, 0.82);
|
||||
color: color-mix(in srgb, var(--accent-ink) 82%, transparent);
|
||||
opacity: 0.9;
|
||||
}
|
||||
.status-tag {
|
||||
@@ -340,7 +341,7 @@
|
||||
border-radius: 999px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
background: rgba(13, 14, 17, 0.78);
|
||||
background: var(--media-scrim);
|
||||
border: 1px solid var(--border-strong);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
@@ -350,7 +351,10 @@
|
||||
color: var(--text-2);
|
||||
}
|
||||
.st-progress {
|
||||
color: var(--accent);
|
||||
color: var(--run);
|
||||
}
|
||||
.st-pending {
|
||||
color: var(--warn);
|
||||
}
|
||||
.st-done {
|
||||
color: var(--success);
|
||||
@@ -409,8 +413,11 @@
|
||||
border-radius: 2px;
|
||||
background: var(--surface-3);
|
||||
}
|
||||
.seg-running {
|
||||
background: var(--run);
|
||||
}
|
||||
.seg-draft {
|
||||
background: var(--accent);
|
||||
background: var(--warn);
|
||||
}
|
||||
.seg-confirmed {
|
||||
background: var(--success);
|
||||
@@ -425,8 +432,8 @@
|
||||
margin-left: auto;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-dim);
|
||||
color: var(--accent);
|
||||
background: var(--run-soft);
|
||||
color: var(--run);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
@@ -462,7 +469,7 @@
|
||||
height: 28px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
background: rgba(13, 14, 17, 0.6);
|
||||
background: var(--media-scrim);
|
||||
color: var(--text-2);
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
|
||||
@@ -341,14 +341,14 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 进行数徽章 (spec §3): accent-dim capsule with amber count */
|
||||
/* 进行数徽章:进行中计数用信号青 */
|
||||
.nav-badge {
|
||||
margin-left: auto;
|
||||
min-width: 20px;
|
||||
padding: 1px 7px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-dim);
|
||||
color: var(--accent);
|
||||
background: var(--run-soft);
|
||||
color: var(--run);
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
@@ -384,7 +384,7 @@
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* §5 连接状态点: green = probe passed, amber breathing = testing,
|
||||
/* 连接状态点: cyan = online, cyan breathing = testing,
|
||||
red = failed, gray = no connection configured. */
|
||||
.dot {
|
||||
width: 7px;
|
||||
@@ -396,13 +396,13 @@
|
||||
margin-top: 5px;
|
||||
}
|
||||
.dot.ok {
|
||||
background: var(--success);
|
||||
background: var(--run);
|
||||
}
|
||||
.dot.fail {
|
||||
background: var(--error);
|
||||
}
|
||||
.dot.testing {
|
||||
background: var(--accent);
|
||||
background: var(--run);
|
||||
animation: shell-breathe 2s cubic-bezier(0.16, 1, 0.3, 1) infinite;
|
||||
}
|
||||
@keyframes shell-breathe {
|
||||
|
||||
@@ -258,7 +258,7 @@
|
||||
color: var(--accent);
|
||||
}
|
||||
.slot-empty.invalid {
|
||||
border-color: rgba(229, 72, 77, 0.65);
|
||||
border-color: var(--err-line);
|
||||
}
|
||||
.slot-empty:disabled {
|
||||
cursor: default;
|
||||
|
||||
@@ -749,6 +749,28 @@
|
||||
const selectedEntry = $derived(
|
||||
doneEntries.find((e) => e.key === selectedKey) ?? doneEntries[0] ?? null,
|
||||
);
|
||||
const selectedJobId = $derived(selectedEntry?.job.id ?? null);
|
||||
|
||||
/**
|
||||
* Clicking a history row: a finished job loads into the preview
|
||||
* (player / image), anything else just reports its live status —
|
||||
* never a silent jump to the newest finished clip.
|
||||
*/
|
||||
function selectFromHistory(job: Job) {
|
||||
const st = jobStatusInfo(job);
|
||||
if (st.tone === 'ok' && job.output_paths.length) {
|
||||
selectedKey = `${job.id}:${job.output_paths[0]}`;
|
||||
return;
|
||||
}
|
||||
toast.info(st.detail && st.detail !== st.label ? `${st.label} · ${st.detail}` : st.label);
|
||||
}
|
||||
|
||||
function histRowKey(e: KeyboardEvent, job: Job) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
selectFromHistory(job);
|
||||
}
|
||||
}
|
||||
|
||||
function summaryOf(job: Job): string {
|
||||
const parts: string[] = [];
|
||||
@@ -801,6 +823,26 @@
|
||||
let curSec = $state(0);
|
||||
let durSec = $state(0);
|
||||
|
||||
// Switching the selected video must stop the old clip and rewind it,
|
||||
// never keep playing the previous audio/progress into the new one.
|
||||
// Runs BEFORE the DOM swaps the src so the reset cannot clobber the
|
||||
// duration/metadata events of the clip being loaded in.
|
||||
$effect.pre(() => {
|
||||
void selectedEntry?.key;
|
||||
const v = videoEl;
|
||||
if (v) {
|
||||
try {
|
||||
v.pause();
|
||||
v.currentTime = 0;
|
||||
} catch {
|
||||
/* no loaded metadata yet — src change already reset it */
|
||||
}
|
||||
}
|
||||
playing = false;
|
||||
curSec = 0;
|
||||
durSec = 0;
|
||||
});
|
||||
|
||||
function fmtClock(s: number): string {
|
||||
if (!Number.isFinite(s)) return '0:00';
|
||||
const m = Math.floor(s / 60);
|
||||
@@ -828,13 +870,16 @@
|
||||
else void playerBox?.requestFullscreen();
|
||||
}
|
||||
|
||||
// First/last frame strip for the selected video job
|
||||
// First/last frame + reference strip for the selected video job.
|
||||
// Ref workflows come in two shapes: an autogrow image_list, or several
|
||||
// plain image inputs — both are shown as reference images.
|
||||
const selectedFrames = $derived.by(() => {
|
||||
if (kind !== 'video' || !selectedEntry) return [];
|
||||
const wf = ($workflowsQuery.data ?? []).find((w) => w.id === selectedEntry.job.workflow_id);
|
||||
if (!wf) return [];
|
||||
const vals = (selectedEntry.job.payload?.input_values ?? {}) as Record<string, unknown>;
|
||||
const out: Array<{ label: string; path: string; ref?: boolean }> = [];
|
||||
let refIdx = 0;
|
||||
for (const inp of wf.input_schema ?? []) {
|
||||
const h = `${inp.role ?? ''} ${inp.label}`.toLowerCase();
|
||||
const v = vals[inp.nodeId];
|
||||
@@ -848,6 +893,10 @@
|
||||
if (typeof v !== 'string' || !v) continue;
|
||||
if (/first|start|首帧/.test(h)) out.push({ label: t('studio.slotFirst'), path: v });
|
||||
else if (/last|end|尾帧/.test(h)) out.push({ label: t('studio.slotLast'), path: v });
|
||||
else if (isMedia(inp) && inp.kind === 'image' && !/mask|遮罩/.test(h)) {
|
||||
refIdx += 1;
|
||||
out.push({ label: t('studio.slotRefN', { n: refIdx }), path: v, ref: true });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
});
|
||||
@@ -895,12 +944,31 @@
|
||||
}
|
||||
|
||||
const deleteTarget = $derived(kindJobs.find((j) => j.id === deleteJobId) ?? null);
|
||||
// The dialog names the exact job and — when it produced several
|
||||
// outputs — says plainly that ALL of them go away with it (the
|
||||
// backend only deletes whole jobs, never a single output file).
|
||||
const deleteTitle = $derived(
|
||||
deleteTarget ? t('studio.delTitleJob', { name: wfName(deleteTarget) }) : t('studio.delTitle'),
|
||||
);
|
||||
const deleteMessage = $derived.by(() => {
|
||||
const n = deleteTarget?.output_paths?.length ?? 0;
|
||||
return n > 1 ? t('studio.delConfirmMulti', { n }) : t('studio.delConfirm');
|
||||
});
|
||||
|
||||
function askDeleteJob(jobId: number) {
|
||||
deleteJobId = jobId;
|
||||
deleteOpen = true;
|
||||
}
|
||||
|
||||
async function doDelete() {
|
||||
if (!deleteJobId) return;
|
||||
try {
|
||||
await playgroundApi.deleteJob(deleteJobId);
|
||||
submittedIds.delete(deleteJobId);
|
||||
if (selectedEntry?.job.id === deleteJobId) selectedKey = null;
|
||||
toast.success(t('studio.deletedToast'));
|
||||
await client.invalidateQueries({ queryKey: ['playground-jobs'] });
|
||||
await client.invalidateQueries({ queryKey: ['jobs'] });
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
@@ -1556,11 +1624,20 @@
|
||||
<Icon name="upload" size={13} />
|
||||
{t('studio.sendToImg2Img')}
|
||||
</Button>
|
||||
<a class="a-btn secondary sm" href={assetUrl(selectedEntry.path)} download={downloadName(selectedEntry.path)}>
|
||||
<Icon name="download" size={13} />
|
||||
{t('studio.download')}
|
||||
</a>
|
||||
</span>
|
||||
<a class="a-btn secondary sm" href={assetUrl(selectedEntry.path)} download={downloadName(selectedEntry.path)}>
|
||||
<Icon name="download" size={13} />
|
||||
{t('studio.download')}
|
||||
</a>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
title={t('studio.deleteBtn')}
|
||||
onclick={() => askDeleteJob(selectedEntry!.job.id)}
|
||||
>
|
||||
<Icon name="trash" size={13} />
|
||||
{t('studio.deleteBtn')}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -1575,7 +1652,8 @@
|
||||
<h2>{t('studio.video.resultsTitle')}</h2>
|
||||
{#if selectedEntry}
|
||||
<span class="panel-hint">
|
||||
{t('studio.lastDone', {
|
||||
{wfName(selectedEntry.job)} ·
|
||||
{t('studio.doneAt', {
|
||||
time: fmtTime(parseDbTs(selectedEntry.job.completed_at ?? selectedEntry.job.created_at)),
|
||||
})}
|
||||
</span>
|
||||
@@ -1648,11 +1726,20 @@
|
||||
<Icon name="retry" size={13} />
|
||||
{t('studio.retryLink')}
|
||||
</Button>
|
||||
<a class="a-btn secondary sm" href={assetUrl(selectedEntry.path)} download={downloadName(selectedEntry.path)}>
|
||||
<Icon name="download" size={13} />
|
||||
{t('studio.downloadMp4')}
|
||||
</a>
|
||||
</span>
|
||||
<a class="a-btn secondary sm" href={assetUrl(selectedEntry.path)} download={downloadName(selectedEntry.path)}>
|
||||
<Icon name="download" size={13} />
|
||||
{t('studio.downloadMp4')}
|
||||
</a>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
title={t('studio.deleteBtn')}
|
||||
onclick={() => askDeleteJob(selectedEntry!.job.id)}
|
||||
>
|
||||
<Icon name="trash" size={13} />
|
||||
{t('studio.deleteBtn')}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="empty-line">{t('studio.historyEmptyVideo')}</p>
|
||||
@@ -1673,59 +1760,73 @@
|
||||
{kind === 'image' ? t('studio.historyEmptyImage') : t('studio.historyEmptyVideo')}
|
||||
</p>
|
||||
{:else}
|
||||
<div class="history-list">
|
||||
{#each kindJobs as job (job.id)}
|
||||
{@const st = jobStatusInfo(job)}
|
||||
<div class="hist-row" class:failed={job.status === 'failed'} class:video={kind === 'video'}>
|
||||
<div class="hist-thumb" class:video={kind === 'video'}>
|
||||
{#if job.output_paths?.[0]}
|
||||
{#if kind === 'image'}
|
||||
<img src={assetUrl(job.output_paths[0])} alt={wfName(job)} loading="lazy" />
|
||||
{:else}
|
||||
<video
|
||||
src={assetUrl(job.output_paths[0])}
|
||||
preload="metadata"
|
||||
muted
|
||||
onloadedmetadata={(e) => noteDuration(job.output_paths![0], e)}
|
||||
></video>
|
||||
{#if durations[job.output_paths[0]]}
|
||||
<span class="dur mono">{fmtClock(durations[job.output_paths[0]])}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if job.status === 'failed'}
|
||||
<span class="thumb-fallback failed"><Icon name="alert" size={14} /></span>
|
||||
<div class="history-list">
|
||||
{#each kindJobs as job (job.id)}
|
||||
{@const st = jobStatusInfo(job)}
|
||||
<div
|
||||
class="hist-row"
|
||||
class:failed={job.status === 'failed'}
|
||||
class:video={kind === 'video'}
|
||||
class:selected={selectedJobId === job.id}
|
||||
role="button"
|
||||
tabindex={0}
|
||||
aria-label={`${wfName(job)} · ${st.label}`}
|
||||
aria-current={selectedJobId === job.id ? 'true' : undefined}
|
||||
onclick={() => selectFromHistory(job)}
|
||||
onkeydown={(e) => histRowKey(e, job)}
|
||||
>
|
||||
<div class="hist-thumb" class:video={kind === 'video'}>
|
||||
{#if job.output_paths?.[0]}
|
||||
{#if kind === 'image'}
|
||||
<img src={assetUrl(job.output_paths[0])} alt={wfName(job)} loading="lazy" />
|
||||
{:else}
|
||||
<span class="thumb-fallback"><Icon name={kind === 'image' ? 'image' : 'film'} size={14} /></span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="hist-body">
|
||||
<div class="hist-name" title={wfName(job)}>{wfName(job)}</div>
|
||||
<div class="hist-meta mono">{summaryOf(job)}</div>
|
||||
<div class="hist-status">
|
||||
{#if job.status !== 'failed'}
|
||||
<span class="st {st.tone}" title={st.detail}>{st.label}</span>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="st fail-link"
|
||||
disabled={retryingId === job.id}
|
||||
onclick={() => retryJob(job)}
|
||||
>
|
||||
{t('studio.retryLink')}
|
||||
</button>
|
||||
<video
|
||||
src={assetUrl(job.output_paths[0])}
|
||||
preload="metadata"
|
||||
muted
|
||||
onloadedmetadata={(e) => noteDuration(job.output_paths![0], e)}
|
||||
></video>
|
||||
{#if durations[job.output_paths[0]]}
|
||||
<span class="dur mono">{fmtClock(durations[job.output_paths[0]])}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if job.status === 'failed'}
|
||||
<span class="thumb-fallback failed"><Icon name="alert" size={14} /></span>
|
||||
{:else}
|
||||
<span class="thumb-fallback"><Icon name={kind === 'image' ? 'image' : 'film'} size={14} /></span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="hist-body">
|
||||
<div class="hist-name" title={wfName(job)}>{wfName(job)}</div>
|
||||
<div class="hist-meta mono">{summaryOf(job)}</div>
|
||||
<div class="hist-status">
|
||||
{#if job.status !== 'failed'}
|
||||
<span class="st {st.tone}" title={st.detail}>{st.label}</span>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="hist-del"
|
||||
title={t('studio.deleteBtn')}
|
||||
onclick={() => {
|
||||
deleteJobId = job.id;
|
||||
deleteOpen = true;
|
||||
class="st fail-link"
|
||||
disabled={retryingId === job.id}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
retryJob(job);
|
||||
}}
|
||||
>
|
||||
<Icon name="trash" size={13} />
|
||||
{t('studio.retryLink')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="hist-del"
|
||||
title={t('studio.deleteBtn')}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
askDeleteJob(job.id);
|
||||
}}
|
||||
>
|
||||
<Icon name="trash" size={13} />
|
||||
</button>
|
||||
</div>
|
||||
{#if job.status === 'failed' && job.error}
|
||||
<div class="hist-error">{job.error}</div>
|
||||
{:else if st.tone === 'fail' || (job.status === 'done' && job.phase === 'dry_run')}
|
||||
@@ -1765,9 +1866,10 @@
|
||||
|
||||
<ConfirmDialog
|
||||
bind:open={deleteOpen}
|
||||
title={t('studio.delTitle')}
|
||||
message={t('studio.delConfirm')}
|
||||
title={deleteTitle}
|
||||
message={deleteMessage}
|
||||
confirmLabel={t('studio.deleteBtn')}
|
||||
cancelLabel={t('common.cancel')}
|
||||
danger
|
||||
onconfirm={doDelete}
|
||||
oncancel={() => {
|
||||
@@ -1795,7 +1897,7 @@
|
||||
color: var(--text-3);
|
||||
}
|
||||
.h3-hint p.warn {
|
||||
color: var(--accent);
|
||||
color: var(--warn);
|
||||
}
|
||||
.studio-page {
|
||||
display: grid;
|
||||
@@ -1888,10 +1990,10 @@
|
||||
}
|
||||
.f-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(232, 163, 61, 0.6);
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.f-input.invalid {
|
||||
border-color: rgba(229, 72, 77, 0.65);
|
||||
border-color: var(--err-line);
|
||||
}
|
||||
.f-error {
|
||||
display: block;
|
||||
@@ -2037,13 +2139,10 @@
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tag.info {
|
||||
color: var(--info);
|
||||
background: var(--info-dim);
|
||||
}
|
||||
.tag.info,
|
||||
.tag.cloud {
|
||||
color: var(--cloud);
|
||||
background: var(--cloud-dim);
|
||||
color: var(--text-2);
|
||||
background: var(--bg-4);
|
||||
}
|
||||
.task-body {
|
||||
margin-top: 12px;
|
||||
@@ -2066,9 +2165,9 @@
|
||||
.progress .fill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 10px var(--accent-glow);
|
||||
transition: width 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
background: var(--run-grad);
|
||||
box-shadow: 0 0 8px var(--run-soft);
|
||||
transition: width var(--d-progress) linear;
|
||||
}
|
||||
.task-meta {
|
||||
font-size: 11px;
|
||||
@@ -2136,7 +2235,7 @@
|
||||
display: block;
|
||||
}
|
||||
.thumb:hover {
|
||||
border-color: rgba(255, 255, 255, 0.24);
|
||||
border-color: var(--line-2);
|
||||
}
|
||||
.thumb.selected {
|
||||
border: 2px solid var(--accent);
|
||||
@@ -2369,10 +2468,23 @@
|
||||
border-radius: 8px;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.hist-row:hover {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.hist-row:focus-visible {
|
||||
outline: 1px solid var(--accent);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
/* The row loaded into the preview stays marked, and its delete stays visible. */
|
||||
.hist-row.selected {
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 2px 0 0 var(--accent);
|
||||
}
|
||||
.hist-row.selected .hist-del {
|
||||
opacity: 1;
|
||||
}
|
||||
.hist-row.failed {
|
||||
background: var(--error-dim);
|
||||
}
|
||||
@@ -2456,7 +2568,7 @@
|
||||
color: var(--success);
|
||||
}
|
||||
.st.run {
|
||||
color: var(--accent);
|
||||
color: var(--run);
|
||||
}
|
||||
.st.queue {
|
||||
color: var(--text-3);
|
||||
@@ -2498,6 +2610,15 @@
|
||||
.hist-row:hover .hist-del {
|
||||
opacity: 1;
|
||||
}
|
||||
.hist-del:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
/* Touch devices have no hover to reveal the delete affordance. */
|
||||
@media (hover: none) {
|
||||
.hist-del {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.hist-del:hover {
|
||||
color: var(--error);
|
||||
background: var(--error-dim);
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import Spinner from './Spinner.svelte';
|
||||
|
||||
interface Props {
|
||||
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
size?: 'sm' | 'md';
|
||||
/** `danger` = outlined (list rows); `danger-solid` = confirm dialogs only. */
|
||||
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'danger-solid';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
/** Stretch to the container width (e.g. a panel's main action). */
|
||||
block?: boolean;
|
||||
disabled?: boolean;
|
||||
@@ -32,6 +33,7 @@
|
||||
{type}
|
||||
class="a-btn btn {variant}"
|
||||
class:sm={size === 'sm'}
|
||||
class:lg={size === 'lg'}
|
||||
class:block
|
||||
class:loading
|
||||
disabled={disabled || loading}
|
||||
@@ -49,7 +51,7 @@
|
||||
</button>
|
||||
|
||||
<style>
|
||||
/* Look comes from the shared `.a-btn` rules in app.css (§5). */
|
||||
/* Look comes from the shared `.a-btn` rules in app.css (§四.1). */
|
||||
.btn {
|
||||
position: relative;
|
||||
}
|
||||
@@ -72,10 +74,14 @@
|
||||
justify-content: center;
|
||||
}
|
||||
.btn.primary .btn-spinner {
|
||||
--spinner-track: rgba(22, 19, 10, 0.3);
|
||||
--spinner-arc: var(--accent-contrast);
|
||||
--spinner-track: color-mix(in srgb, var(--accent-ink) 30%, transparent);
|
||||
--spinner-arc: var(--accent-ink);
|
||||
}
|
||||
.btn.danger .btn-spinner {
|
||||
--spinner-arc: var(--error);
|
||||
--spinner-arc: var(--err);
|
||||
}
|
||||
.btn.danger-solid .btn-spinner {
|
||||
--spinner-track: color-mix(in srgb, var(--on-media) 30%, transparent);
|
||||
--spinner-arc: var(--on-media);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -46,16 +46,18 @@
|
||||
.card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
border-radius: var(--r-lg);
|
||||
}
|
||||
.card.hoverable {
|
||||
transition:
|
||||
border-color 150ms ease,
|
||||
transform 150ms ease;
|
||||
border-color var(--d-state) ease,
|
||||
transform var(--d-state) var(--ease-out);
|
||||
}
|
||||
.card.hoverable:hover {
|
||||
border-color: #3f3f46;
|
||||
transform: translateY(-1px);
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.card.hoverable:hover {
|
||||
border-color: var(--line-2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{#snippet footer()}
|
||||
<Button variant="ghost" onclick={() => (open = false)}>{cancelLabel}</Button
|
||||
>
|
||||
<Button variant={danger ? 'danger' : 'primary'} onclick={handleConfirm}
|
||||
<Button variant={danger ? 'danger-solid' : 'primary'} onclick={handleConfirm}
|
||||
>{confirmLabel}</Button
|
||||
>
|
||||
{/snippet}
|
||||
|
||||
@@ -144,22 +144,19 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(5, 5, 8, 0.6);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
animation: modal-fade 150ms ease-out;
|
||||
background: var(--overlay);
|
||||
animation: modal-fade var(--d-state) ease-out;
|
||||
}
|
||||
.modal-panel {
|
||||
width: min(520px, 100%);
|
||||
max-height: calc(100vh - 96px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
|
||||
background: var(--float-bg);
|
||||
border-radius: var(--r-lg);
|
||||
box-shadow: var(--shadow-float);
|
||||
outline: none;
|
||||
animation: modal-rise 180ms ease-out;
|
||||
animation: modal-rise var(--d-pop) var(--ease-out);
|
||||
}
|
||||
.modal-panel.size-lg {
|
||||
width: min(920px, 100%);
|
||||
@@ -177,7 +174,7 @@
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
font-family: var(--font-display);
|
||||
font-size: 16px;
|
||||
font-size: var(--fs-title);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
@@ -194,12 +191,14 @@
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 150ms ease,
|
||||
color 150ms ease;
|
||||
background-color var(--d-state) ease,
|
||||
color var(--d-state) ease;
|
||||
}
|
||||
.modal-close:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-primary);
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.modal-close:hover {
|
||||
background: var(--bg-4);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
.modal-close:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
indeterminate?: boolean;
|
||||
label?: string;
|
||||
size?: 'sm' | 'md';
|
||||
/** §5 进度条: running = amber, done = green, failed = red (static). */
|
||||
tone?: 'accent' | 'success' | 'error';
|
||||
/** 进度条: running = 信号青, done = green, failed = red (static). */
|
||||
tone?: 'run' | 'success' | 'error';
|
||||
/** Track only — no label / percentage row. */
|
||||
bare?: boolean;
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
indeterminate = false,
|
||||
label,
|
||||
size = 'md',
|
||||
tone = 'accent',
|
||||
tone = 'run',
|
||||
bare = false,
|
||||
}: Props = $props();
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
{#if indeterminate}
|
||||
<div class="fill indeterminate"></div>
|
||||
{:else}
|
||||
<div class="fill" style:width={pct + '%'}></div>
|
||||
<div class="fill" style:transform="scaleX({pct / 100})"></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,8 +75,8 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--surface-3);
|
||||
border-radius: 999px;
|
||||
background: var(--bg-4);
|
||||
border-radius: var(--r-pill);
|
||||
}
|
||||
.progress-sm .track {
|
||||
height: 4px;
|
||||
@@ -86,15 +86,18 @@
|
||||
}
|
||||
.fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
border-radius: 999px;
|
||||
transition: width 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
background: var(--run-grad);
|
||||
box-shadow: 0 0 8px var(--run-soft);
|
||||
transform-origin: left;
|
||||
transition: transform var(--d-progress) linear;
|
||||
}
|
||||
.tone-success .fill {
|
||||
background: var(--success);
|
||||
background: var(--ok);
|
||||
box-shadow: none;
|
||||
}
|
||||
.tone-error .fill {
|
||||
background: var(--error);
|
||||
background: var(--err);
|
||||
box-shadow: none;
|
||||
}
|
||||
.fill.indeterminate {
|
||||
position: absolute;
|
||||
@@ -104,10 +107,11 @@
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
var(--accent) 40%,
|
||||
var(--accent) 60%,
|
||||
var(--run) 40%,
|
||||
var(--run) 60%,
|
||||
transparent
|
||||
);
|
||||
box-shadow: none;
|
||||
transition: none;
|
||||
animation: progress-slide 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -19,36 +19,26 @@
|
||||
></div>
|
||||
|
||||
<style>
|
||||
/* §四.9 骨架屏:--bg-3 圆角块,1.4s 呼吸透明度;减少动态时静态。 */
|
||||
.skeleton {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--bg-elevated);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-3);
|
||||
border-radius: var(--r-sm);
|
||||
animation: skeleton-breath 1.4s ease-in-out infinite;
|
||||
}
|
||||
.skeleton.circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.skeleton::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.06),
|
||||
transparent
|
||||
);
|
||||
transform: translateX(-100%);
|
||||
animation: skeleton-shimmer 1.6s ease-in-out infinite;
|
||||
}
|
||||
@keyframes skeleton-shimmer {
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
@keyframes skeleton-breath {
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.skeleton::after {
|
||||
display: none;
|
||||
.skeleton {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
:global(:root[data-reduce='1']) .skeleton {
|
||||
animation: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,27 +6,27 @@
|
||||
|
||||
let { status, label }: Props = $props();
|
||||
|
||||
type Tone = 'success' | 'info' | 'muted' | 'error' | 'warning' | 'accent';
|
||||
type Tone = 'ok' | 'run' | 'muted' | 'err' | 'warn';
|
||||
|
||||
const toneByStatus: Record<string, Tone> = {
|
||||
ready: 'success',
|
||||
done: 'success',
|
||||
completed: 'success',
|
||||
running: 'info',
|
||||
generating: 'info',
|
||||
queued: 'info',
|
||||
ready: 'ok',
|
||||
done: 'ok',
|
||||
completed: 'ok',
|
||||
running: 'run',
|
||||
generating: 'run',
|
||||
in_progress: 'run',
|
||||
queued: 'muted',
|
||||
pending: 'muted',
|
||||
idle: 'muted',
|
||||
draft: 'muted',
|
||||
failed: 'error',
|
||||
error: 'error',
|
||||
paused: 'warning',
|
||||
in_progress: 'accent',
|
||||
failed: 'err',
|
||||
error: 'err',
|
||||
paused: 'warn',
|
||||
};
|
||||
|
||||
const key = $derived(status.trim().toLowerCase());
|
||||
const tone: Tone = $derived(toneByStatus[key] ?? 'muted');
|
||||
const pulsing = $derived(key === 'running');
|
||||
const pulsing = $derived(tone === 'run');
|
||||
</script>
|
||||
|
||||
<span class="chip chip-{tone}">
|
||||
@@ -35,59 +35,48 @@
|
||||
</span>
|
||||
|
||||
<style>
|
||||
/* §四.2 状态标签:20px 高、--r-sm、12px;语义色文字 + 13% 软底。 */
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 9px;
|
||||
border: 1px solid;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
gap: var(--sp-2);
|
||||
height: 20px;
|
||||
padding: 0 var(--sp-2);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: var(--fs-aux);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.chip-success {
|
||||
color: var(--success);
|
||||
background: color-mix(in srgb, var(--success) 12%, transparent);
|
||||
border-color: color-mix(in srgb, var(--success) 35%, transparent);
|
||||
.chip-ok {
|
||||
color: var(--ok);
|
||||
background: var(--ok-soft);
|
||||
}
|
||||
.chip-info {
|
||||
color: var(--info);
|
||||
background: color-mix(in srgb, var(--info) 12%, transparent);
|
||||
border-color: color-mix(in srgb, var(--info) 35%, transparent);
|
||||
.chip-run {
|
||||
color: var(--run);
|
||||
background: var(--run-soft);
|
||||
}
|
||||
.chip-error {
|
||||
color: var(--error);
|
||||
background: color-mix(in srgb, var(--error) 12%, transparent);
|
||||
border-color: color-mix(in srgb, var(--error) 35%, transparent);
|
||||
.chip-err {
|
||||
color: var(--err);
|
||||
background: var(--err-soft);
|
||||
}
|
||||
.chip-warning {
|
||||
color: var(--warning);
|
||||
background: color-mix(in srgb, var(--warning) 12%, transparent);
|
||||
border-color: color-mix(in srgb, var(--warning) 35%, transparent);
|
||||
}
|
||||
.chip-accent {
|
||||
color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
border-color: color-mix(in srgb, var(--accent) 35%, transparent);
|
||||
.chip-warn {
|
||||
color: var(--warn);
|
||||
background: var(--warn-soft);
|
||||
}
|
||||
.chip-muted {
|
||||
color: var(--text-secondary);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: var(--border);
|
||||
color: var(--text-2);
|
||||
background: var(--bg-4);
|
||||
}
|
||||
.dot.pulsing {
|
||||
animation: chip-pulse 1.2s ease-in-out infinite;
|
||||
animation: chip-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes chip-pulse {
|
||||
0%,
|
||||
@@ -103,4 +92,7 @@
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
:global(:root[data-reduce='1']) .dot.pulsing {
|
||||
animation: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
let { checked, label, disabled = false, onchange }: Props = $props();
|
||||
</script>
|
||||
|
||||
<!-- §5 开关: 34×19 capsule; on = amber track + dark knob, off = --surface-3. -->
|
||||
<!-- §3.10 开关: 34×19 capsule; on = --accent-lo track + ink knob, off = --bg-4. -->
|
||||
<button
|
||||
type="button"
|
||||
class="switch"
|
||||
@@ -28,38 +28,38 @@
|
||||
flex-shrink: 0;
|
||||
width: 34px;
|
||||
height: 19px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: var(--surface-3);
|
||||
border-radius: var(--r-pill);
|
||||
border: 1px solid var(--line-2);
|
||||
background: var(--bg-4);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition:
|
||||
background-color 150ms,
|
||||
border-color 150ms;
|
||||
background-color var(--d-state) ease,
|
||||
border-color var(--d-state) ease;
|
||||
}
|
||||
.switch:disabled {
|
||||
opacity: 0.4;
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.knob {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--r-pill);
|
||||
background: var(--text-3);
|
||||
transition:
|
||||
transform 150ms cubic-bezier(0.16, 1, 0.3, 1),
|
||||
background-color 150ms;
|
||||
transform var(--d-state) var(--ease-out),
|
||||
background-color var(--d-state) ease;
|
||||
}
|
||||
.switch.on {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-lo);
|
||||
border-color: var(--accent-lo);
|
||||
}
|
||||
.switch.on .knob {
|
||||
transform: translateX(15px);
|
||||
background: var(--accent-contrast);
|
||||
background: var(--accent-ink);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -369,7 +369,7 @@
|
||||
}
|
||||
.wb-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(232, 163, 61, 0.6);
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.wb-input::placeholder {
|
||||
color: var(--text-3);
|
||||
@@ -438,7 +438,7 @@
|
||||
}
|
||||
.btn.ghost:hover:not(:disabled) {
|
||||
color: var(--text);
|
||||
border-color: rgba(255, 255, 255, 0.24);
|
||||
border-color: var(--line-2);
|
||||
}
|
||||
.btn.gen {
|
||||
color: var(--accent);
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
background: var(--surface-3);
|
||||
}
|
||||
.st-running {
|
||||
color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
color: var(--run);
|
||||
background: var(--run-soft);
|
||||
}
|
||||
.st-done {
|
||||
color: var(--success);
|
||||
|
||||
@@ -258,7 +258,7 @@
|
||||
}
|
||||
.wb-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(232, 163, 61, 0.6);
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.wb-input::placeholder {
|
||||
color: var(--text-3);
|
||||
@@ -294,7 +294,7 @@
|
||||
}
|
||||
.btn.ghost:hover:not(:disabled) {
|
||||
color: var(--text);
|
||||
border-color: rgba(255, 255, 255, 0.24);
|
||||
border-color: var(--line-2);
|
||||
}
|
||||
.btn.sm {
|
||||
padding: 6px 12px;
|
||||
|
||||
@@ -341,7 +341,7 @@
|
||||
}
|
||||
.wb-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(232, 163, 61, 0.6);
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.wb-input::placeholder {
|
||||
color: var(--text-3);
|
||||
@@ -412,7 +412,7 @@
|
||||
}
|
||||
.btn.ghost:hover:not(:disabled) {
|
||||
color: var(--text);
|
||||
border-color: rgba(255, 255, 255, 0.24);
|
||||
border-color: var(--line-2);
|
||||
}
|
||||
.btn.gen {
|
||||
color: var(--accent);
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { t } from '$lib/i18n.svelte';
|
||||
|
||||
/**
|
||||
* Five-stage stepper (a2): one step per agent with the spec §2.1 semantic
|
||||
* colors — gray=待开始, amber=进行中/待确认 (+breath while running),
|
||||
* green=已确认, blue=用户改过草稿. A step unlocks when itself or any
|
||||
* Five-stage stepper (a2): one step per agent with the fusion status
|
||||
* colors — gray=待开始, cyan=进行中 (+breath), amber=待确认,
|
||||
* green=已确认, --info=用户改过草稿. A step unlocks when itself or any
|
||||
* earlier stage has content; later steps stay clickable only after the
|
||||
* previous one is confirmed.
|
||||
*/
|
||||
@@ -159,7 +159,7 @@
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #edeef0;
|
||||
color: var(--text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -188,12 +188,14 @@
|
||||
.st-todo {
|
||||
color: var(--text-3);
|
||||
}
|
||||
.st-running,
|
||||
.st-running {
|
||||
color: var(--run);
|
||||
}
|
||||
.st-draft {
|
||||
color: var(--accent);
|
||||
color: var(--warn);
|
||||
}
|
||||
.st-edited {
|
||||
color: var(--info);
|
||||
color: var(--warn);
|
||||
}
|
||||
.st-confirmed {
|
||||
color: var(--success);
|
||||
@@ -201,14 +203,14 @@
|
||||
|
||||
.step-btn.active[data-state='running'] .avatar,
|
||||
.step-btn[data-state='running'] .avatar {
|
||||
box-shadow: 0 0 10px rgba(232, 163, 61, 0.35);
|
||||
box-shadow: 0 0 10px var(--run-soft);
|
||||
}
|
||||
|
||||
.breath {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
background: var(--run);
|
||||
animation: wb-breath 2s cubic-bezier(0.16, 1, 0.3, 1) infinite;
|
||||
}
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@
|
||||
}
|
||||
.wb-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(232, 163, 61, 0.6);
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.wb-input::placeholder {
|
||||
color: var(--text-3);
|
||||
@@ -386,7 +386,7 @@
|
||||
}
|
||||
.btn.ghost:hover:not(:disabled) {
|
||||
color: var(--text);
|
||||
border-color: rgba(255, 255, 255, 0.24);
|
||||
border-color: var(--line-2);
|
||||
}
|
||||
.btn:disabled {
|
||||
opacity: 0.4;
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
}
|
||||
.dir-card.chosen {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 10px rgba(232, 163, 61, 0.35);
|
||||
box-shadow: 0 0 10px var(--accent-glow);
|
||||
}
|
||||
.dir-card input[type='radio'] {
|
||||
position: absolute;
|
||||
@@ -310,7 +310,7 @@
|
||||
}
|
||||
.wb-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(232, 163, 61, 0.6);
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.wb-input::placeholder {
|
||||
color: var(--text-3);
|
||||
@@ -375,7 +375,7 @@
|
||||
}
|
||||
.btn.ghost:hover:not(:disabled) {
|
||||
color: var(--text);
|
||||
border-color: rgba(255, 255, 255, 0.24);
|
||||
border-color: var(--line-2);
|
||||
}
|
||||
.btn:disabled {
|
||||
opacity: 0.4;
|
||||
|
||||
@@ -1462,7 +1462,7 @@ export const de: Dict = {
|
||||
'studio.image.historyTitle': 'Bildverlauf',
|
||||
'studio.video.historyTitle': 'Videoverlauf',
|
||||
'studio.historyCount': '{n} gesamt',
|
||||
'studio.lastDone': 'Zuletzt fertig · {time}',
|
||||
'studio.doneAt': 'Fertiggestellt um {time}',
|
||||
'studio.kindImageText': 'Bild',
|
||||
'studio.kindVideoText': 'Video',
|
||||
'studio.loading': 'Lädt…',
|
||||
@@ -1578,7 +1578,10 @@ export const de: Dict = {
|
||||
'studio.cancelToast': 'Abbruch angefordert',
|
||||
'studio.deleteBtn': 'Löschen',
|
||||
'studio.delTitle': 'Aufgabeneintrag löschen',
|
||||
'studio.delConfirm': 'Die Ergebnisdateien werden mitgelöscht. Löschen?',
|
||||
'studio.delConfirm': 'Beim Löschen werden auch die Ergebnisdateien dieses Auftrags entfernt; der Verlauf ist nicht wiederherstellbar. Löschen?',
|
||||
'studio.delTitleJob': 'Auftrag „{name}“ löschen',
|
||||
'studio.delConfirmMulti': 'Dieser Auftrag hat {n} Ergebnisse. Beim Löschen werden der gesamte Auftragsdatensatz und alle {n} Ergebnisdateien entfernt; dies ist unwiderruflich. Löschen?',
|
||||
'studio.deletedToast': 'Auftrag und zugehörige Ergebnisdateien gelöscht',
|
||||
'studio.wfEmptyTitle': 'Noch keine {kind}-Workflows',
|
||||
'studio.wfEmptyBody': 'Entwicklermodus in ComfyUI aktivieren, Workflow als API-JSON exportieren und auf der Seite ComfyUI-Verbindungen als {kind} importieren.',
|
||||
'studio.wfEmptyGo': 'In ComfyUI-Verbindungen importieren',
|
||||
|
||||
@@ -1461,7 +1461,7 @@ export const en = {
|
||||
'studio.image.historyTitle': 'Image history',
|
||||
'studio.video.historyTitle': 'Video history',
|
||||
'studio.historyCount': '{n} total',
|
||||
'studio.lastDone': 'Last done · {time}',
|
||||
'studio.doneAt': 'Done at {time}',
|
||||
'studio.kindImageText': 'image',
|
||||
'studio.kindVideoText': 'video',
|
||||
'studio.loading': 'Loading…',
|
||||
@@ -1577,7 +1577,10 @@ export const en = {
|
||||
'studio.cancelToast': 'Cancel requested',
|
||||
'studio.deleteBtn': 'Delete',
|
||||
'studio.delTitle': 'Delete task record',
|
||||
'studio.delConfirm': 'The result files will be removed together with the record. Delete it?',
|
||||
'studio.delConfirm': 'Deleting also removes this job’s result files; the history cannot be restored. Delete?',
|
||||
'studio.delTitleJob': 'Delete job “{name}”',
|
||||
'studio.delConfirmMulti': 'This job produced {n} results. Deleting removes the whole job record and all {n} result files; this cannot be undone. Delete?',
|
||||
'studio.deletedToast': 'Job deleted together with its result files',
|
||||
'studio.wfEmptyTitle': 'No {kind} workflows yet',
|
||||
'studio.wfEmptyBody': "Enable ComfyUI's developer mode, export the workflow as API JSON, then import it on the ComfyUI Connections page as {kind}.",
|
||||
'studio.wfEmptyGo': 'Import in ComfyUI Connections',
|
||||
|
||||
@@ -1462,7 +1462,7 @@ export const es: Dict = {
|
||||
'studio.image.historyTitle': 'Historial de imágenes',
|
||||
'studio.video.historyTitle': 'Historial de vídeos',
|
||||
'studio.historyCount': '{n} en total',
|
||||
'studio.lastDone': 'Último · {time}',
|
||||
'studio.doneAt': 'Completado a las {time}',
|
||||
'studio.kindImageText': 'imagen',
|
||||
'studio.kindVideoText': 'vídeo',
|
||||
'studio.loading': 'Cargando…',
|
||||
@@ -1578,7 +1578,10 @@ export const es: Dict = {
|
||||
'studio.cancelToast': 'Cancelación solicitada',
|
||||
'studio.deleteBtn': 'Eliminar',
|
||||
'studio.delTitle': 'Eliminar registro de tarea',
|
||||
'studio.delConfirm': 'Los archivos de resultados se eliminarán junto con el registro. ¿Eliminar?',
|
||||
'studio.delConfirm': 'Al eliminar también se quitan los archivos de resultados de esta tarea; el historial no se puede restaurar. ¿Eliminar?',
|
||||
'studio.delTitleJob': 'Eliminar tarea «{name}»',
|
||||
'studio.delConfirmMulti': 'Esta tarea produjo {n} resultados. Al eliminarla se quita el registro completo y los {n} archivos de resultados; no se puede deshacer. ¿Eliminar?',
|
||||
'studio.deletedToast': 'Tarea eliminada junto con sus archivos de resultados',
|
||||
'studio.wfEmptyTitle': 'Aún no hay flujos de {kind}',
|
||||
'studio.wfEmptyBody': 'Activa el modo desarrollador de ComfyUI, exporta el flujo como JSON API e impórtalo en Conexiones ComfyUI como {kind}.',
|
||||
'studio.wfEmptyGo': 'Importar en Conexiones ComfyUI',
|
||||
|
||||
@@ -1462,7 +1462,7 @@ export const fr: Dict = {
|
||||
'studio.image.historyTitle': 'Historique images',
|
||||
'studio.video.historyTitle': 'Historique vidéos',
|
||||
'studio.historyCount': '{n} au total',
|
||||
'studio.lastDone': 'Dernier · {time}',
|
||||
'studio.doneAt': 'Terminé à {time}',
|
||||
'studio.kindImageText': 'image',
|
||||
'studio.kindVideoText': 'vidéo',
|
||||
'studio.loading': 'Chargement…',
|
||||
@@ -1578,7 +1578,10 @@ export const fr: Dict = {
|
||||
'studio.cancelToast': 'Annulation demandée',
|
||||
'studio.deleteBtn': 'Supprimer',
|
||||
'studio.delTitle': 'Supprimer la tâche',
|
||||
'studio.delConfirm': 'Les fichiers de résultats seront supprimés avec la tâche. Supprimer ?',
|
||||
'studio.delConfirm': 'La suppression retire aussi les fichiers de résultats de cette tâche ; l’historique est irrécupérable. Supprimer ?',
|
||||
'studio.delTitleJob': 'Supprimer la tâche « {name} »',
|
||||
'studio.delConfirmMulti': 'Cette tâche a produit {n} résultats. La suppression retire la tâche entière et ses {n} fichiers de résultats ; action irréversible. Supprimer ?',
|
||||
'studio.deletedToast': 'Tâche supprimée avec ses fichiers de résultats',
|
||||
'studio.wfEmptyTitle': 'Pas encore de workflows {kind}',
|
||||
'studio.wfEmptyBody': 'Activez le mode développeur de ComfyUI, exportez le workflow en JSON API, puis importez-le dans Connexions ComfyUI comme {kind}.',
|
||||
'studio.wfEmptyGo': 'Importer dans Connexions ComfyUI',
|
||||
|
||||
@@ -1462,7 +1462,7 @@ export const ja: Dict = {
|
||||
'studio.image.historyTitle': '画像生成履歴',
|
||||
'studio.video.historyTitle': '動画生成履歴',
|
||||
'studio.historyCount': '計 {n} 回',
|
||||
'studio.lastDone': '最終完了 · {time}',
|
||||
'studio.doneAt': '完了 {time}',
|
||||
'studio.kindImageText': '画像',
|
||||
'studio.kindVideoText': '動画',
|
||||
'studio.loading': '読み込み中…',
|
||||
@@ -1578,7 +1578,10 @@ export const ja: Dict = {
|
||||
'studio.cancelToast': 'キャンセルを要求しました',
|
||||
'studio.deleteBtn': '削除',
|
||||
'studio.delTitle': 'タスク記録を削除',
|
||||
'studio.delConfirm': '結果ファイルも一緒に削除され、履歴は復元できません。削除しますか?',
|
||||
'studio.delConfirm': '削除するとこのタスクの結果ファイルも削除され、履歴は元に戻せません。削除しますか?',
|
||||
'studio.delTitleJob': 'タスク「{name}」を削除',
|
||||
'studio.delConfirmMulti': 'このタスクには結果が {n} 件あります。削除するとタスク記録と {n} 件すべての結果ファイルが失われ、元に戻せません。削除しますか?',
|
||||
'studio.deletedToast': 'タスクと結果ファイルを削除しました',
|
||||
'studio.wfEmptyTitle': '{kind}ワークフローがまだありません',
|
||||
'studio.wfEmptyBody': 'ComfyUI の開発者モードで API 形式 JSON を書き出し、「ComfyUI 接続」ページで{kind}として取り込んでください。',
|
||||
'studio.wfEmptyGo': '「ComfyUI 接続」で取り込む',
|
||||
|
||||
@@ -1462,7 +1462,7 @@ export const ko: Dict = {
|
||||
'studio.image.historyTitle': '이미지 생성 기록',
|
||||
'studio.video.historyTitle': '영상 생성 기록',
|
||||
'studio.historyCount': '총 {n}회',
|
||||
'studio.lastDone': '최근 완료 · {time}',
|
||||
'studio.doneAt': '완료 {time}',
|
||||
'studio.kindImageText': '이미지',
|
||||
'studio.kindVideoText': '비디오',
|
||||
'studio.loading': '불러오는 중…',
|
||||
@@ -1578,7 +1578,10 @@ export const ko: Dict = {
|
||||
'studio.cancelToast': '취소를 요청했습니다',
|
||||
'studio.deleteBtn': '삭제',
|
||||
'studio.delTitle': '작업 기록 삭제',
|
||||
'studio.delConfirm': '결과 파일도 함께 삭제되며 기록은 복구할 수 없습니다. 삭제할까요?',
|
||||
'studio.delConfirm': '삭제 시 이 작업의 결과 파일도 함께 제거되며 기록은 복구할 수 없습니다. 삭제할까요?',
|
||||
'studio.delTitleJob': '작업 «{name}» 삭제',
|
||||
'studio.delConfirmMulti': '이 작업에는 결과가 {n}개 있습니다. 삭제하면 작업 기록과 결과 파일 {n}개 모두 제거되며 복구할 수 없습니다. 삭제할까요?',
|
||||
'studio.deletedToast': '작업과 결과 파일을 삭제했습니다',
|
||||
'studio.wfEmptyTitle': '{kind} 워크플로가 아직 없습니다',
|
||||
'studio.wfEmptyBody': 'ComfyUI 개발자 모드를 켜고 API 형식 JSON으로 내보낸 뒤, ComfyUI 연결 페이지에서 {kind}(으)로 가져오세요.',
|
||||
'studio.wfEmptyGo': 'ComfyUI 연결에서 가져오기',
|
||||
|
||||
@@ -1461,7 +1461,7 @@ export const zh: Dict = {
|
||||
'studio.image.historyTitle': '生图历史',
|
||||
'studio.video.historyTitle': '生视频历史',
|
||||
'studio.historyCount': '共 {n} 次',
|
||||
'studio.lastDone': '最近完成 · {time}',
|
||||
'studio.doneAt': '完成于 {time}',
|
||||
'studio.kindImageText': '图片',
|
||||
'studio.kindVideoText': '视频',
|
||||
'studio.loading': '正在加载…',
|
||||
@@ -1579,6 +1579,9 @@ export const zh: Dict = {
|
||||
'studio.deleteBtn': '删除',
|
||||
'studio.delTitle': '删除任务记录',
|
||||
'studio.delConfirm': '删除后该任务的结果文件会一并移除,历史不可恢复。确定删除?',
|
||||
'studio.delTitleJob': '删除任务「{name}」',
|
||||
'studio.delConfirmMulti': '该任务共生成 {n} 个结果,删除将移除整条任务记录及其全部 {n} 个结果文件,历史不可恢复。确定删除?',
|
||||
'studio.deletedToast': '已删除该任务及其结果文件',
|
||||
'studio.wfEmptyTitle': '还没有{kind}工作流',
|
||||
'studio.wfEmptyBody': '在 ComfyUI 里启用开发者模式,用「导出(API 格式)」得到 JSON,再到「ComfyUI 连接」页导入并分类为{kind}。',
|
||||
'studio.wfEmptyGo': '去「ComfyUI 连接」导入',
|
||||
|
||||
@@ -245,11 +245,11 @@
|
||||
return parts.filter(Boolean).join(' · ');
|
||||
}
|
||||
|
||||
function jobBar(job: Job): { value: number; tone: 'accent' | 'success' | 'error' } {
|
||||
function jobBar(job: Job): { value: number; tone: 'run' | 'success' | 'error' } {
|
||||
if (job.status === 'done') return { value: 100, tone: 'success' };
|
||||
if (job.status === 'failed') return { value: 100, tone: 'error' };
|
||||
if (job.status === 'running') return { value: progressFor(job.id)?.progress ?? 3, tone: 'accent' };
|
||||
return { value: 0, tone: 'accent' };
|
||||
if (job.status === 'running') return { value: progressFor(job.id)?.progress ?? 3, tone: 'run' };
|
||||
return { value: 0, tone: 'run' };
|
||||
}
|
||||
|
||||
function jobStatusLabel(status: string): string {
|
||||
@@ -493,7 +493,7 @@
|
||||
<aside class="tasks">
|
||||
<div class="tasks-head">
|
||||
<h2>{t('home.tasksTitle')}</h2>
|
||||
<span class="chip" class:amber={activeJobCount > 0}>
|
||||
<span class="chip" class:run={activeJobCount > 0}>
|
||||
{t('home.activeJobs', { n: activeJobCount })}
|
||||
</span>
|
||||
</div>
|
||||
@@ -580,9 +580,9 @@
|
||||
font-size: 11.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip.amber {
|
||||
background: var(--accent-dim);
|
||||
color: var(--accent);
|
||||
.chip.run {
|
||||
background: var(--run-soft);
|
||||
color: var(--run);
|
||||
}
|
||||
|
||||
.entry-btn {
|
||||
@@ -616,7 +616,7 @@
|
||||
color: var(--text);
|
||||
}
|
||||
.entry-btn.secondary:hover {
|
||||
border-color: rgba(255, 255, 255, 0.24);
|
||||
border-color: var(--line-2);
|
||||
}
|
||||
.entry-btn:active {
|
||||
transform: scale(0.98);
|
||||
@@ -651,7 +651,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--accent);
|
||||
color: var(--text-2);
|
||||
}
|
||||
.entry-card h2 {
|
||||
margin: 4px 0 0;
|
||||
@@ -736,10 +736,10 @@
|
||||
}
|
||||
.f-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(232, 163, 61, 0.6);
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.f-input.invalid {
|
||||
border-color: rgba(229, 72, 77, 0.65);
|
||||
border-color: var(--err-line);
|
||||
}
|
||||
.f-hint {
|
||||
display: block;
|
||||
@@ -908,13 +908,10 @@
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tag-local {
|
||||
background: var(--info-dim);
|
||||
color: var(--info);
|
||||
}
|
||||
.tag-local,
|
||||
.tag-cloud {
|
||||
background: var(--cloud-dim);
|
||||
color: var(--cloud);
|
||||
background: var(--bg-4);
|
||||
color: var(--text-2);
|
||||
}
|
||||
.job-name {
|
||||
flex: 1;
|
||||
@@ -950,7 +947,7 @@
|
||||
color: var(--success);
|
||||
}
|
||||
.running-text {
|
||||
color: var(--accent);
|
||||
color: var(--run);
|
||||
}
|
||||
.queued-text {
|
||||
color: var(--text-3);
|
||||
@@ -966,8 +963,8 @@
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 10px var(--accent-glow);
|
||||
background: var(--run);
|
||||
box-shadow: 0 0 10px var(--run-soft);
|
||||
}
|
||||
.dot.breathe {
|
||||
animation: breathe 2s ease-in-out infinite;
|
||||
|
||||
@@ -1133,11 +1133,11 @@
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #edeef0;
|
||||
color: var(--text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.avatar.dark {
|
||||
color: #16130a;
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
.avatar.lg {
|
||||
width: 42px;
|
||||
@@ -1294,8 +1294,8 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-tag.busy {
|
||||
background: var(--accent-dim);
|
||||
color: var(--accent);
|
||||
background: var(--run-soft);
|
||||
color: var(--run);
|
||||
}
|
||||
.chat-head-actions {
|
||||
display: flex;
|
||||
@@ -1618,7 +1618,7 @@
|
||||
background: var(--success);
|
||||
}
|
||||
.dot.amber {
|
||||
background: var(--accent);
|
||||
background: var(--run);
|
||||
}
|
||||
.dot.amber.breathing {
|
||||
animation: breathe 2s ease-in-out infinite;
|
||||
|
||||
+401
-149
@@ -4,84 +4,201 @@
|
||||
|
||||
:root {
|
||||
/* ------------------------------------------------------------------
|
||||
* A「片场暗房」design tokens — spec-v2 §2 (HEL-603 confirmed).
|
||||
* Single source of truth for the whole app: do not hardcode colors in
|
||||
* pages or components, and do not override these with ad-hoc CSS.
|
||||
* 融合视觉 design tokens — HEL-649 整站施工规范 §一(流水台结构 ×
|
||||
* 放映厅配色)。Single source of truth for the whole app: pages and
|
||||
* components reference these variables only — no hardcoded colors,
|
||||
* no ad-hoc overrides. Gold is for key actions and the current
|
||||
* selection only; status colors each carry exactly one meaning.
|
||||
* ------------------------------------------------------------------ */
|
||||
--bg: #0d0e11;
|
||||
--surface: #14161b;
|
||||
--surface-2: #1b1e25;
|
||||
--surface-3: #22262f;
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--border-strong: rgba(255, 255, 255, 0.14);
|
||||
--border-hover: rgba(255, 255, 255, 0.24);
|
||||
--text: #edeef0;
|
||||
--text-2: #9ba1ab;
|
||||
--text-3: #5c626c;
|
||||
--accent: #e8a33d;
|
||||
--accent-hover: #f1b155; /* +8% brightness (§5 主按钮) */
|
||||
--accent-contrast: #16130a;
|
||||
--accent-dim: rgba(232, 163, 61, 0.14);
|
||||
--accent-glow: rgba(232, 163, 61, 0.35);
|
||||
--accent-focus: rgba(232, 163, 61, 0.6);
|
||||
--success: #3fb97f;
|
||||
--success-dim: rgba(63, 185, 127, 0.14);
|
||||
--info: #5b8def;
|
||||
--info-dim: rgba(91, 141, 239, 0.14);
|
||||
--error: #e5484d;
|
||||
--error-dim: rgba(229, 72, 77, 0.14);
|
||||
--error-border: rgba(229, 72, 77, 0.4);
|
||||
--cloud: #b98be0;
|
||||
--cloud-dim: rgba(185, 139, 224, 0.14);
|
||||
|
||||
/* Avatar five-color set (§2.1) — five agents only */
|
||||
/* 底色:蓝黑四阶,逐层浮起 (§1.1) */
|
||||
--bg: #07090e;
|
||||
--bg-2: #0c0f16;
|
||||
--bg-3: #121724;
|
||||
--bg-4: #1a2133;
|
||||
--line: #1b2233;
|
||||
--line-2: #2a3450;
|
||||
--page-glow: radial-gradient(1200px 500px at 70% -10%, rgba(38, 52, 92, 0.25), transparent 60%);
|
||||
--float-bg: linear-gradient(180deg, #10152a, var(--bg-2));
|
||||
--overlay: rgba(4, 6, 10, 0.6);
|
||||
--media-scrim: rgba(7, 9, 14, 0.65);
|
||||
--on-media: #fff;
|
||||
--on-media-line: rgba(255, 255, 255, 0.05);
|
||||
|
||||
/* 放映金:主按钮、当前选中、改过的参数、聚焦框、Toast 动作 (§1.2) */
|
||||
--accent: #f2c784;
|
||||
--accent-ink: #23170a;
|
||||
--accent-soft: rgba(242, 199, 132, 0.13);
|
||||
--accent-hi: #f7d9a2;
|
||||
--accent-lo: #ecaf63;
|
||||
--accent-glow: rgba(236, 175, 99, 0.28);
|
||||
--accent-line: rgba(242, 199, 132, 0.55);
|
||||
--accent-grad: linear-gradient(180deg, var(--accent-hi), var(--accent-lo));
|
||||
--accent-grad-hover: linear-gradient(180deg, #fbe3b6, #f0b968);
|
||||
--accent-ring: 0 0 0 1px rgba(242, 199, 132, 0.7), 0 0 22px var(--accent-glow);
|
||||
--btn-glow: 0 2px 14px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, 0.35);
|
||||
--btn-glow-hover: 0 4px 18px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
||||
|
||||
/* 信号色:绿=完成/已确认/通过,青=进行中/在线,红=失败/危险,
|
||||
琥珀=待确认/文件缺失/限额 (§1.3) */
|
||||
--ok: #49cb90;
|
||||
--ok-soft: rgba(73, 203, 144, 0.13);
|
||||
--run: #54dfd2;
|
||||
--run-soft: rgba(84, 223, 210, 0.13);
|
||||
--run-grad: linear-gradient(90deg, #3bbfb4, var(--run));
|
||||
--err: #f0645a;
|
||||
--err-soft: rgba(240, 100, 90, 0.13);
|
||||
--err-line: rgba(240, 100, 90, 0.6);
|
||||
--warn: #f0b45c;
|
||||
--warn-soft: rgba(240, 180, 92, 0.13);
|
||||
|
||||
/* 文字与字体 (§1.4);--text-2 是深底可读性下限,不得再暗 */
|
||||
--text: #f4f6fa;
|
||||
--text-2: #aab2c5;
|
||||
--text-3: #6d7690;
|
||||
--font: 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', system-ui, sans-serif;
|
||||
--mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||
--fs-title: 15px;
|
||||
--fs-section: 14px;
|
||||
--fs-body: 13px;
|
||||
--fs-body-sm: 12.5px;
|
||||
--fs-aux: 12px;
|
||||
--fs-mini: 11px;
|
||||
--lh-body: 1.55;
|
||||
|
||||
/* 间距阶梯 4/6/8/10/12/16/20/28,不得出现阶梯外数值 (§1.5) */
|
||||
--sp-1: 4px;
|
||||
--sp-2: 6px;
|
||||
--sp-3: 8px;
|
||||
--sp-4: 10px;
|
||||
--sp-5: 12px;
|
||||
--sp-6: 16px;
|
||||
--sp-7: 20px;
|
||||
--sp-8: 28px;
|
||||
|
||||
/* 圆角与唯一一级阴影 (§1.5);卡片不投影,用底色差分层 */
|
||||
--r-sm: 6px;
|
||||
--r-md: 10px;
|
||||
--r-lg: 14px;
|
||||
--r-pill: 999px;
|
||||
--shadow-float: 0 14px 44px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--line-2);
|
||||
|
||||
/* 专属符号 (§1.7):齿孔只在记录流/输入台分隔带,光晕只在播放舞台 */
|
||||
--sprocket: radial-gradient(circle 2px, var(--line-2) 98%, transparent);
|
||||
--stage-bg:
|
||||
radial-gradient(60% 45% at 50% 38%, rgba(242, 199, 132, 0.07), transparent 70%),
|
||||
radial-gradient(80% 70% at 50% 50%, #0d1220, #05070c);
|
||||
|
||||
/* 动效 (§1.6,取 HEL-648 实测值);只动 transform / opacity */
|
||||
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
||||
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
||||
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
--ease-spring: cubic-bezier(0.34, 1.45, 0.44, 1);
|
||||
--d-press: 120ms;
|
||||
--d-state: 150ms;
|
||||
--d-pop: 180ms;
|
||||
--d-leave: 200ms;
|
||||
--d-enter: 220ms;
|
||||
--d-drawer: 280ms;
|
||||
--d-flash: 900ms;
|
||||
--d-progress: 400ms;
|
||||
--press-scale: 0.97;
|
||||
|
||||
/* Avatar five-color set — five agents only; the one colorful gradient
|
||||
family the spec keeps (§3.6). */
|
||||
--avatar-1: linear-gradient(135deg, #e8a33d, #b96a1b);
|
||||
--avatar-2: linear-gradient(135deg, #5b8def, #3a5fa9);
|
||||
--avatar-3: linear-gradient(135deg, #3fb97f, #2a7a54);
|
||||
--avatar-4: linear-gradient(135deg, #b98be0, #7a4fa9);
|
||||
--avatar-5: linear-gradient(135deg, #d98e6a, #a95535);
|
||||
|
||||
/* Typography (§2.2) */
|
||||
--font-ui: 'Noto Sans SC', 'Noto Sans CJK SC', sans-serif;
|
||||
--font-a-mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace;
|
||||
|
||||
/* Radius / spacing (§2.3) */
|
||||
--radius-sm: 7px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 10px;
|
||||
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 16px;
|
||||
--space-lg: 24px;
|
||||
--space-xl: 32px;
|
||||
|
||||
/* Legacy token names used by shared primitives — aliases of the A set,
|
||||
so every component renders in the same darkroom palette. */
|
||||
/* ------------------------------------------------------------------
|
||||
* Legacy token names (§六 映射表) — aliases only, never new values,
|
||||
* so every existing component renders in the fusion palette.
|
||||
* ------------------------------------------------------------------ */
|
||||
--surface: var(--bg-2);
|
||||
--surface-2: var(--bg-3);
|
||||
--surface-3: var(--bg-4);
|
||||
--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(--surface);
|
||||
--bg-elevated: var(--surface-2);
|
||||
--bg-surface: var(--bg-2);
|
||||
--bg-elevated: var(--bg-3);
|
||||
--text-primary: var(--text);
|
||||
--text-secondary: var(--text-2);
|
||||
--text-muted: var(--text-3);
|
||||
--warning: var(--accent);
|
||||
--font-display: var(--font-ui);
|
||||
--font-body: var(--font-ui);
|
||||
--font-mono: var(--font-a-mono);
|
||||
--font-ui: var(--font);
|
||||
--font-a-mono: var(--mono);
|
||||
--font-display: var(--font);
|
||||
--font-body: var(--font);
|
||||
--font-mono: var(--mono);
|
||||
--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(--surface-2);
|
||||
--pill-border: var(--border-strong);
|
||||
--pill-radius: 9999px;
|
||||
--pill-bg: var(--bg-3);
|
||||
--pill-border: var(--line-2);
|
||||
--pill-radius: var(--r-pill);
|
||||
--pill-height: 36px;
|
||||
--pill-padding: 0 14px;
|
||||
--pill-font-size: 13px;
|
||||
--pill-font-size: var(--fs-body);
|
||||
}
|
||||
|
||||
/* 减少动态效果 (§五):系统设置自动生效,顶栏开关写 data-reduce="1"。
|
||||
位移/缩放/弹入/光晕全部取消,只留 ≤120ms 透明度,进度条直接跳变。 */
|
||||
:root[data-reduce='1'] {
|
||||
--d-press: 0ms;
|
||||
--d-state: 120ms;
|
||||
--d-pop: 120ms;
|
||||
--d-leave: 120ms;
|
||||
--d-enter: 120ms;
|
||||
--d-drawer: 120ms;
|
||||
--d-flash: 0ms;
|
||||
--d-progress: 0ms;
|
||||
--press-scale: 1;
|
||||
--ease-spring: ease;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root {
|
||||
--d-press: 0ms;
|
||||
--d-state: 120ms;
|
||||
--d-pop: 120ms;
|
||||
--d-leave: 120ms;
|
||||
--d-enter: 120ms;
|
||||
--d-drawer: 120ms;
|
||||
--d-flash: 0ms;
|
||||
--d-progress: 0ms;
|
||||
--press-scale: 1;
|
||||
--ease-spring: ease;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
background: var(--page-glow), var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-ui);
|
||||
font-family: var(--font);
|
||||
font-size: var(--fs-body);
|
||||
line-height: var(--lh-body);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
@@ -89,20 +206,20 @@ body {
|
||||
.a-darkroom {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-ui);
|
||||
font-size: 13.5px;
|
||||
line-height: 1.6;
|
||||
font-family: var(--font);
|
||||
font-size: var(--fs-body);
|
||||
line-height: var(--lh-body);
|
||||
}
|
||||
|
||||
code,
|
||||
.mono,
|
||||
.kh {
|
||||
font-family: var(--font-a-mono);
|
||||
font-family: var(--mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Shared A controls (§5 组件状态全表). The only definitions of these
|
||||
* Shared controls (§四 组件状态通用规范). The only definitions of these
|
||||
* looks — `ui/Button.svelte` renders `.a-btn`, pages use the classes
|
||||
* directly for links / selects. Page styles may only tweak layout
|
||||
* (width / margin), never re-declare colors.
|
||||
@@ -111,91 +228,120 @@ code,
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
gap: var(--sp-2);
|
||||
min-height: 34px;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
padding: 0 var(--sp-6);
|
||||
border-radius: var(--r-md);
|
||||
border: 1px solid transparent;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
background-color 150ms cubic-bezier(0.16, 1, 0.3, 1),
|
||||
border-color 150ms,
|
||||
color 150ms,
|
||||
transform 100ms;
|
||||
transform var(--d-press) var(--ease-out),
|
||||
background-color var(--d-state) ease,
|
||||
border-color var(--d-state) ease,
|
||||
box-shadow var(--d-state) ease,
|
||||
color var(--d-state) ease,
|
||||
opacity var(--d-state) ease;
|
||||
}
|
||||
.a-btn.lg {
|
||||
min-height: 38px;
|
||||
padding: 0 var(--sp-7);
|
||||
}
|
||||
.a-btn.sm {
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
min-height: 28px;
|
||||
padding: 0 var(--sp-4);
|
||||
font-size: var(--fs-aux);
|
||||
border-radius: var(--r-sm);
|
||||
}
|
||||
.a-btn.xs {
|
||||
min-height: 24px;
|
||||
padding: 0 9px;
|
||||
font-size: 11px;
|
||||
padding: 0 var(--sp-3);
|
||||
font-size: var(--fs-mini);
|
||||
border-radius: var(--r-sm);
|
||||
}
|
||||
.a-btn.block {
|
||||
width: 100%;
|
||||
}
|
||||
.a-btn.primary {
|
||||
background: var(--accent);
|
||||
color: var(--accent-contrast);
|
||||
}
|
||||
.a-btn.primary:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
background: var(--accent-grad);
|
||||
color: var(--accent-ink);
|
||||
box-shadow: var(--btn-glow);
|
||||
}
|
||||
.a-btn.secondary {
|
||||
background: var(--surface-2);
|
||||
border-color: var(--border-strong);
|
||||
background: var(--bg-3);
|
||||
border-color: var(--line);
|
||||
color: var(--text);
|
||||
}
|
||||
.a-btn.secondary:hover:not(:disabled) {
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
.a-btn.ghost {
|
||||
background: transparent;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.a-btn.ghost:hover:not(:disabled) {
|
||||
background: var(--surface-3);
|
||||
color: var(--text);
|
||||
}
|
||||
/* 危险两级制:列表行内只用描边红 danger;实心红 danger-solid 只在确认弹层内。 */
|
||||
.a-btn.danger {
|
||||
background: transparent;
|
||||
border-color: var(--error-border);
|
||||
color: var(--error);
|
||||
border-color: var(--err-line);
|
||||
color: var(--err);
|
||||
}
|
||||
.a-btn.danger:hover:not(:disabled) {
|
||||
background: var(--error-dim);
|
||||
.a-btn.danger-solid {
|
||||
background: var(--err);
|
||||
color: var(--on-media);
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.a-btn.primary:hover:not(:disabled) {
|
||||
background: var(--accent-grad-hover);
|
||||
box-shadow: var(--btn-glow-hover);
|
||||
}
|
||||
.a-btn.secondary:hover:not(:disabled) {
|
||||
border-color: var(--line-2);
|
||||
background: var(--bg-4);
|
||||
}
|
||||
.a-btn.ghost:hover:not(:disabled) {
|
||||
background: var(--bg-4);
|
||||
color: var(--text);
|
||||
}
|
||||
.a-btn.danger:hover:not(:disabled) {
|
||||
background: var(--err-soft);
|
||||
}
|
||||
.a-btn.danger-solid:hover:not(:disabled) {
|
||||
box-shadow:
|
||||
0 0 0 1px var(--err-line),
|
||||
0 0 18px var(--err-soft);
|
||||
}
|
||||
}
|
||||
.a-btn:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
transform: scale(var(--press-scale));
|
||||
}
|
||||
/* 禁用必须在旁边给出中文原因(§1.2),由调用方渲染。 */
|
||||
.a-btn:disabled,
|
||||
.a-btn[aria-disabled='true'] {
|
||||
opacity: 0.4;
|
||||
opacity: 0.45;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Top-bar status capsule (counts / save state / connection). */
|
||||
/* Top-bar status capsule (queue / connection / save state) — §二 顶栏胶囊. */
|
||||
.status-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-3);
|
||||
gap: var(--sp-2);
|
||||
height: 30px;
|
||||
padding: 0 var(--sp-5);
|
||||
border-radius: var(--r-pill);
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg-2);
|
||||
color: var(--text-2);
|
||||
font-size: 11.5px;
|
||||
font-size: var(--fs-aux);
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.status-chip.accent {
|
||||
background: var(--accent-dim);
|
||||
background: var(--accent-soft);
|
||||
border-color: var(--accent-line);
|
||||
color: var(--accent);
|
||||
}
|
||||
.status-chip-dot {
|
||||
@@ -206,33 +352,122 @@ code,
|
||||
background: currentColor;
|
||||
}
|
||||
.status-chip-dot.ok {
|
||||
background: var(--success);
|
||||
background: var(--ok);
|
||||
}
|
||||
.status-chip-dot.run {
|
||||
background: var(--run);
|
||||
}
|
||||
.status-chip-dot.fail {
|
||||
background: var(--error);
|
||||
background: var(--err);
|
||||
}
|
||||
.status-chip-dot.idle {
|
||||
background: var(--text-3);
|
||||
}
|
||||
|
||||
/* Card / panel surface (§2.3: 10px radius, 1px border, no shadow). */
|
||||
/* Status label (§四.2):语义色文字 + 13% 软底;默认为中性(排队 / 未开始)。 */
|
||||
.a-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-1);
|
||||
height: 20px;
|
||||
padding: 0 var(--sp-2);
|
||||
border-radius: var(--r-sm);
|
||||
background: var(--bg-4);
|
||||
color: var(--text-2);
|
||||
font-size: var(--fs-aux);
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
color var(--d-state) ease,
|
||||
background-color var(--d-state) ease;
|
||||
}
|
||||
.a-tag.accent {
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
.a-tag.ok {
|
||||
color: var(--ok);
|
||||
background: var(--ok-soft);
|
||||
}
|
||||
.a-tag.run {
|
||||
color: var(--run);
|
||||
background: var(--run-soft);
|
||||
}
|
||||
.a-tag.err {
|
||||
color: var(--err);
|
||||
background: var(--err-soft);
|
||||
}
|
||||
.a-tag.warn {
|
||||
color: var(--warn);
|
||||
background: var(--warn-soft);
|
||||
}
|
||||
|
||||
/* Real-progress bar (§1.3):<div class="a-progress" style="--p:.42"><i></i></div>.
|
||||
Only for true progress values — never fake progress. */
|
||||
.a-progress {
|
||||
position: relative;
|
||||
height: 4px;
|
||||
border-radius: var(--r-pill);
|
||||
background: var(--bg-4);
|
||||
overflow: hidden;
|
||||
}
|
||||
.a-progress > i {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--run-grad);
|
||||
box-shadow: 0 0 8px var(--run-soft);
|
||||
transform-origin: left;
|
||||
transform: scaleX(var(--p, 0));
|
||||
transition: transform var(--d-progress) linear;
|
||||
}
|
||||
|
||||
/* Film sprocket divider — only between the take feed and the composer (§1.7). */
|
||||
.a-sprocket {
|
||||
height: 6px;
|
||||
background-image: var(--sprocket);
|
||||
background-size: 18px 6px;
|
||||
background-repeat: repeat-x;
|
||||
background-position: center;
|
||||
opacity: 0.55;
|
||||
}
|
||||
:root[data-reduce='1'] .a-sprocket {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Projection stage — only behind media in the inspector (§1.7). */
|
||||
.a-stage {
|
||||
position: relative;
|
||||
border-radius: var(--r-lg);
|
||||
overflow: hidden;
|
||||
background: var(--stage-bg);
|
||||
box-shadow: 0 0 0 1px var(--line);
|
||||
}
|
||||
|
||||
/* Floating surface for pop / drawer / confirm / toast / lightbox (§四.3). */
|
||||
.a-float {
|
||||
background: var(--float-bg);
|
||||
border-radius: var(--r-lg);
|
||||
box-shadow: var(--shadow-float);
|
||||
}
|
||||
|
||||
/* Card / panel surface (§1.5:卡片不投影,用底色差分层). */
|
||||
.a-panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 20px 22px;
|
||||
background: var(--bg-2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r-lg);
|
||||
padding: var(--sp-7);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.a-panel-title {
|
||||
margin: 0;
|
||||
font-size: 14.5px;
|
||||
font-size: var(--fs-section);
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.a-panel-lead {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
margin: var(--sp-1) 0 0;
|
||||
font-size: var(--fs-aux);
|
||||
line-height: var(--lh-body);
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
@@ -243,39 +478,47 @@ code,
|
||||
.field-textarea,
|
||||
.field-select {
|
||||
box-sizing: border-box;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 7px;
|
||||
background: var(--bg-3);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r-md);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 12.5px;
|
||||
transition: border-color 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
font-size: var(--fs-body-sm);
|
||||
transition: border-color var(--d-state) ease;
|
||||
}
|
||||
.a-select {
|
||||
padding: 7px 9px;
|
||||
padding: var(--sp-2) var(--sp-4);
|
||||
cursor: pointer;
|
||||
}
|
||||
.a-input,
|
||||
.field-input,
|
||||
.field-select {
|
||||
width: 100%;
|
||||
padding: 8px 11px;
|
||||
padding: var(--sp-3) var(--sp-5);
|
||||
}
|
||||
.a-textarea,
|
||||
.field-textarea {
|
||||
width: 100%;
|
||||
padding: 9px 11px;
|
||||
padding: var(--sp-4) var(--sp-5);
|
||||
resize: vertical;
|
||||
min-height: 96px;
|
||||
line-height: 1.6;
|
||||
line-height: var(--lh-body);
|
||||
}
|
||||
.a-select:hover,
|
||||
.a-input:hover,
|
||||
.a-textarea:hover,
|
||||
.field-input:hover,
|
||||
.field-textarea:hover,
|
||||
.field-select:hover {
|
||||
border-color: var(--border-hover);
|
||||
.a-input::placeholder,
|
||||
.a-textarea::placeholder,
|
||||
.field-input::placeholder,
|
||||
.field-textarea::placeholder {
|
||||
color: var(--text-3);
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.a-select:hover,
|
||||
.a-input:hover,
|
||||
.a-textarea:hover,
|
||||
.field-input:hover,
|
||||
.field-textarea:hover,
|
||||
.field-select:hover {
|
||||
border-color: var(--line-2);
|
||||
}
|
||||
}
|
||||
.a-select:focus,
|
||||
.a-input:focus,
|
||||
@@ -284,12 +527,12 @@ code,
|
||||
.field-textarea:focus,
|
||||
.field-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-focus);
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.a-input.invalid,
|
||||
.a-textarea.invalid,
|
||||
.field-input.invalid {
|
||||
border-color: var(--error);
|
||||
border-color: var(--err);
|
||||
}
|
||||
.a-select option.unavailable {
|
||||
color: var(--text-3);
|
||||
@@ -297,30 +540,30 @@ code,
|
||||
|
||||
.field {
|
||||
display: block;
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: var(--sp-5);
|
||||
}
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-size: var(--fs-aux);
|
||||
font-weight: 500;
|
||||
color: var(--text-2);
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: var(--sp-2);
|
||||
}
|
||||
.field-hint {
|
||||
margin: 6px 0 0;
|
||||
font-size: 11.5px;
|
||||
margin: var(--sp-2) 0 0;
|
||||
font-size: var(--fs-aux);
|
||||
color: var(--text-3);
|
||||
}
|
||||
.field-error {
|
||||
margin: 6px 0 0;
|
||||
font-size: 11.5px;
|
||||
color: var(--error);
|
||||
margin: var(--sp-2) 0 0;
|
||||
font-size: var(--fs-aux);
|
||||
color: var(--err);
|
||||
}
|
||||
|
||||
/* Dark theme scrollbars (Firefox + Chromium) */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--surface-3) transparent;
|
||||
scrollbar-color: var(--bg-4) transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
@@ -333,8 +576,8 @@ code,
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: var(--surface-3);
|
||||
border-radius: 999px;
|
||||
background: var(--bg-4);
|
||||
border-radius: var(--r-pill);
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
@@ -349,17 +592,19 @@ code,
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Global keyboard focus fallback: every natively focusable element gets a ring,
|
||||
even before the ui/ primitives are adopted. Component-scoped :focus-visible
|
||||
rules (e.g. .field-input) override this via higher specificity. */
|
||||
/* Global keyboard focus (§四.8): every natively focusable element gets the
|
||||
gold ring. Component-scoped :focus-visible rules override this via higher
|
||||
specificity. */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--r-sm);
|
||||
}
|
||||
|
||||
/* Global reduced-motion baseline. Components that need a styled static fallback
|
||||
(Skeleton shimmer, ProgressBar indeterminate, StatusChip pulse) also handle
|
||||
this in their own scoped styles. */
|
||||
/* Reduced-motion baseline for components that still carry their own
|
||||
durations. Components that need a styled static fallback (Skeleton,
|
||||
ProgressBar indeterminate, StatusChip pulse) also handle this in their
|
||||
scoped styles. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
@@ -369,3 +614,10 @@ code,
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
:root[data-reduce='1'] *,
|
||||
:root[data-reduce='1'] *::before,
|
||||
:root[data-reduce='1'] *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
@@ -1654,7 +1654,7 @@
|
||||
transition: border-color 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.card.failed {
|
||||
border-color: rgba(229, 72, 77, 0.35);
|
||||
border-color: color-mix(in srgb, var(--err) 35%, transparent);
|
||||
}
|
||||
.card-row {
|
||||
display: flex;
|
||||
@@ -1671,7 +1671,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--accent);
|
||||
color: var(--text-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.card-name {
|
||||
@@ -1688,13 +1688,10 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
.tag-local,
|
||||
.tag-lan {
|
||||
color: var(--info);
|
||||
background: var(--info-dim);
|
||||
}
|
||||
.tag-lan,
|
||||
.tag-cloud {
|
||||
color: var(--cloud);
|
||||
background: var(--cloud-dim);
|
||||
color: var(--text-2);
|
||||
background: var(--bg-4);
|
||||
}
|
||||
.tag-auth {
|
||||
color: var(--text-2);
|
||||
@@ -1743,8 +1740,8 @@
|
||||
background: var(--text-3);
|
||||
}
|
||||
.dot.amber {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 10px var(--accent-glow);
|
||||
background: var(--run);
|
||||
box-shadow: 0 0 10px var(--run-soft);
|
||||
}
|
||||
.dot.breathing {
|
||||
animation: breathe 2s ease-in-out infinite;
|
||||
@@ -1769,7 +1766,7 @@
|
||||
color: var(--error);
|
||||
}
|
||||
.status-text.amber {
|
||||
color: var(--accent);
|
||||
color: var(--run);
|
||||
}
|
||||
.status-text.gray {
|
||||
color: var(--text-3);
|
||||
@@ -1959,7 +1956,7 @@
|
||||
border-color: var(--error);
|
||||
}
|
||||
.wf-conn-check.warn {
|
||||
border-color: var(--accent);
|
||||
border-color: var(--warn);
|
||||
}
|
||||
.wf-conn-head {
|
||||
display: flex;
|
||||
@@ -1977,7 +1974,7 @@
|
||||
color: var(--error);
|
||||
}
|
||||
.wf-conn-check.warn .wf-conn-head :global(.icon) {
|
||||
color: var(--accent);
|
||||
color: var(--warn);
|
||||
}
|
||||
.wf-conn-title {
|
||||
font-size: 12px;
|
||||
@@ -2067,7 +2064,7 @@
|
||||
cursor: help;
|
||||
}
|
||||
.wf-meta-item.recheck.warn .wf-recheck-text {
|
||||
color: var(--accent);
|
||||
color: var(--warn);
|
||||
}
|
||||
.wf-evidence {
|
||||
margin: 0 0 12px;
|
||||
@@ -2198,15 +2195,15 @@
|
||||
color: var(--error);
|
||||
}
|
||||
.tag-usage {
|
||||
color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
color: var(--text-2);
|
||||
background: var(--bg-4);
|
||||
}
|
||||
.tag-disabled {
|
||||
color: var(--text-3);
|
||||
background: var(--surface-3);
|
||||
}
|
||||
.wf-pending-note {
|
||||
color: var(--accent);
|
||||
color: var(--warn);
|
||||
}
|
||||
.wf-row.pending .wf-name {
|
||||
color: var(--text-3);
|
||||
@@ -2252,7 +2249,7 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
.wf-json.invalid {
|
||||
border-color: rgba(229, 72, 77, 0.65);
|
||||
border-color: var(--err-line);
|
||||
}
|
||||
.file-btn {
|
||||
display: inline-flex;
|
||||
@@ -2271,7 +2268,7 @@
|
||||
}
|
||||
.file-btn:hover {
|
||||
color: var(--text);
|
||||
border-color: rgba(255, 255, 255, 0.24);
|
||||
border-color: var(--line-2);
|
||||
}
|
||||
.file-btn input {
|
||||
display: none;
|
||||
@@ -2302,13 +2299,10 @@
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.wf-kind.image {
|
||||
color: var(--info);
|
||||
background: var(--info-dim);
|
||||
}
|
||||
.wf-kind.image,
|
||||
.wf-kind.video {
|
||||
color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
color: var(--text-2);
|
||||
background: var(--bg-4);
|
||||
}
|
||||
.wf-name {
|
||||
flex: 1;
|
||||
@@ -2460,14 +2454,14 @@
|
||||
}
|
||||
.f-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(232, 163, 61, 0.6);
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.f-input:disabled {
|
||||
color: var(--text-3);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.f-input.invalid {
|
||||
border-color: rgba(229, 72, 77, 0.65);
|
||||
border-color: var(--err-line);
|
||||
}
|
||||
.f-hint {
|
||||
display: block;
|
||||
@@ -2610,7 +2604,7 @@
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 790;
|
||||
background: rgba(5, 5, 8, 0.5);
|
||||
background: var(--overlay);
|
||||
}
|
||||
.wf-import-grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -929,7 +929,7 @@
|
||||
transition: border-color 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.card.failed {
|
||||
border-color: rgba(229, 72, 77, 0.35);
|
||||
border-color: color-mix(in srgb, var(--err) 35%, transparent);
|
||||
}
|
||||
.card-row {
|
||||
display: flex;
|
||||
@@ -946,7 +946,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--accent);
|
||||
color: var(--text-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.card-name {
|
||||
@@ -963,13 +963,10 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
.tag-local,
|
||||
.tag-lan {
|
||||
color: var(--info);
|
||||
background: var(--info-dim);
|
||||
}
|
||||
.tag-lan,
|
||||
.tag-cloud {
|
||||
color: var(--cloud);
|
||||
background: var(--cloud-dim);
|
||||
color: var(--text-2);
|
||||
background: var(--bg-4);
|
||||
}
|
||||
.tag-default {
|
||||
color: var(--accent);
|
||||
@@ -1010,8 +1007,8 @@
|
||||
background: var(--text-3);
|
||||
}
|
||||
.dot.amber {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 10px var(--accent-glow);
|
||||
background: var(--run);
|
||||
box-shadow: 0 0 10px var(--run-soft);
|
||||
}
|
||||
.dot.breathing {
|
||||
animation: breathe 2s ease-in-out infinite;
|
||||
@@ -1036,7 +1033,7 @@
|
||||
color: var(--error);
|
||||
}
|
||||
.status-text.amber {
|
||||
color: var(--accent);
|
||||
color: var(--run);
|
||||
}
|
||||
.status-text.gray {
|
||||
color: var(--text-3);
|
||||
@@ -1179,7 +1176,7 @@
|
||||
color: var(--text);
|
||||
}
|
||||
.avatar.dark {
|
||||
color: #16130a;
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
.agent-name {
|
||||
font-size: 12.5px;
|
||||
@@ -1282,14 +1279,14 @@
|
||||
}
|
||||
.f-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(232, 163, 61, 0.6);
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.f-input:disabled {
|
||||
color: var(--text-3);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.f-input.invalid {
|
||||
border-color: rgba(229, 72, 77, 0.65);
|
||||
border-color: var(--err-line);
|
||||
}
|
||||
.f-hint {
|
||||
display: block;
|
||||
@@ -1433,7 +1430,7 @@
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 790;
|
||||
background: rgba(5, 5, 8, 0.5);
|
||||
background: var(--overlay);
|
||||
}
|
||||
.agents-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
@@ -669,7 +669,7 @@
|
||||
color: var(--text-2);
|
||||
}
|
||||
.note.warn :global(.icon) {
|
||||
color: var(--accent);
|
||||
color: var(--warn);
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@
|
||||
min-height: 56px;
|
||||
}
|
||||
.trow.failed {
|
||||
border-color: rgba(229, 72, 77, 0.35);
|
||||
border-color: color-mix(in srgb, var(--err) 35%, transparent);
|
||||
}
|
||||
|
||||
.tkind {
|
||||
@@ -324,13 +324,10 @@
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tkind.image {
|
||||
color: var(--info);
|
||||
background: var(--info-dim);
|
||||
}
|
||||
.tkind.image,
|
||||
.tkind.video {
|
||||
color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
color: var(--text-2);
|
||||
background: var(--bg-4);
|
||||
}
|
||||
|
||||
.tthumb {
|
||||
@@ -412,7 +409,7 @@
|
||||
color: var(--success);
|
||||
}
|
||||
.tst.run {
|
||||
color: var(--accent);
|
||||
color: var(--run);
|
||||
}
|
||||
.tst.queue {
|
||||
color: var(--text-3);
|
||||
|
||||
@@ -670,10 +670,13 @@
|
||||
color: var(--text-2);
|
||||
background: var(--surface-3);
|
||||
}
|
||||
.st-running,
|
||||
.st-running {
|
||||
color: var(--run);
|
||||
background: var(--run-soft);
|
||||
}
|
||||
.st-draft {
|
||||
color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
color: var(--warn);
|
||||
background: var(--warn-soft);
|
||||
}
|
||||
.st-confirmed {
|
||||
color: var(--success);
|
||||
@@ -684,8 +687,8 @@
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--info);
|
||||
background: var(--info-dim);
|
||||
color: var(--warn);
|
||||
background: var(--warn-soft);
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
@@ -695,7 +698,7 @@
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--error-dim);
|
||||
border: 1px solid rgba(229, 72, 77, 0.35);
|
||||
border: 1px solid color-mix(in srgb, var(--err) 35%, transparent);
|
||||
color: var(--error);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.6;
|
||||
@@ -706,9 +709,9 @@
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--accent-dim);
|
||||
border: 1px solid rgba(232, 163, 61, 0.3);
|
||||
color: var(--accent);
|
||||
background: var(--warn-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
|
||||
color: var(--warn);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.running-banner {
|
||||
@@ -766,7 +769,7 @@
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #edeef0;
|
||||
color: var(--text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.agent-copy {
|
||||
@@ -833,13 +836,10 @@
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.kind-image {
|
||||
background: var(--info-dim);
|
||||
color: var(--info);
|
||||
}
|
||||
.kind-image,
|
||||
.kind-video {
|
||||
background: var(--cloud-dim);
|
||||
color: var(--cloud);
|
||||
background: var(--bg-4);
|
||||
color: var(--text-2);
|
||||
}
|
||||
.job-body {
|
||||
display: flex;
|
||||
@@ -872,7 +872,7 @@
|
||||
color: var(--success);
|
||||
}
|
||||
.job-state.st-running {
|
||||
color: var(--accent);
|
||||
color: var(--run);
|
||||
}
|
||||
.job-state.st-pending {
|
||||
color: var(--text-2);
|
||||
@@ -904,9 +904,11 @@
|
||||
.flow-dot.st-confirmed {
|
||||
background: var(--success);
|
||||
}
|
||||
.flow-dot.st-running,
|
||||
.flow-dot.st-running {
|
||||
background: var(--run);
|
||||
}
|
||||
.flow-dot.st-awaiting_confirm {
|
||||
background: var(--accent);
|
||||
background: var(--warn);
|
||||
}
|
||||
.flow-name {
|
||||
font-size: 12px;
|
||||
@@ -953,7 +955,7 @@
|
||||
}
|
||||
.wb-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(232, 163, 61, 0.6);
|
||||
border-color: var(--accent-line);
|
||||
}
|
||||
.wb-input::placeholder {
|
||||
color: var(--text-3);
|
||||
@@ -990,7 +992,7 @@
|
||||
}
|
||||
.btn.ghost:hover:not(:disabled) {
|
||||
color: var(--text);
|
||||
border-color: rgba(255, 255, 255, 0.24);
|
||||
border-color: var(--line-2);
|
||||
}
|
||||
.btn.sm {
|
||||
padding: 6px 11px;
|
||||
@@ -1024,7 +1026,7 @@
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
background: var(--run);
|
||||
animation: wb-page-breath 2s cubic-bezier(0.16, 1, 0.3, 1) infinite;
|
||||
}
|
||||
.breath.big {
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
// Colors and radii resolve to the design tokens in src/routes/app.css so the
|
||||
// Tailwind palette can never drift from the single source of truth.
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#0a0a0c',
|
||||
surface: '#141418',
|
||||
elevated: '#1e1e24',
|
||||
border: '#2a2a32',
|
||||
muted: '#71717a',
|
||||
accent: '#8b5cf6',
|
||||
'accent-hover': '#7c3aed',
|
||||
primary: 'var(--bg)',
|
||||
surface: 'var(--bg-2)',
|
||||
elevated: 'var(--bg-3)',
|
||||
border: 'var(--line)',
|
||||
muted: 'var(--text-3)',
|
||||
accent: 'var(--accent)',
|
||||
'accent-hover': 'var(--accent-hi)',
|
||||
ok: 'var(--ok)',
|
||||
run: 'var(--run)',
|
||||
err: 'var(--err)',
|
||||
warn: 'var(--warn)',
|
||||
},
|
||||
borderRadius: {
|
||||
sm: '6px',
|
||||
md: '10px',
|
||||
lg: '16px',
|
||||
sm: 'var(--r-sm)',
|
||||
md: 'var(--r-md)',
|
||||
lg: 'var(--r-lg)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user