diff --git a/docs/architecture-audit-2026-10.md b/docs/architecture-audit-2026-10.md index 84bc009..f33fec4 100644 --- a/docs/architecture-audit-2026-10.md +++ b/docs/architecture-audit-2026-10.md @@ -145,17 +145,19 @@ round-семантика `clampByte` и trunc-семантика `clampInt` со ## Low -### L1. Флаг `running` общий для `run()` и `download()` +### L1. Флаг `running` общий для `run()` и `download()` — СДЕЛАНО (2026-10) -`SchemaToolView.svelte:124,433,469`: параллельный auto-run и клик по download -гоняют один `$state`; индикация и `disabled` ведут себя некорректно. +В `schema-tool-runner.svelte.ts` флаги разделены: `running` (run) и +`downloading` (download); `download()` больше не затирает индикацию +параллельного auto-run. Компонент передаёт в превью +`busy = running || downloading`. -### L2. Дублирование dismiss-логики поповеров +### L2. Дублирование dismiss-логики поповеров — СДЕЛАНО (2026-10) -`SchemaDownload.svelte:62-76` и `ToolPickerButton.svelte:25-39` — побуквенно -одинаковые ~15 строк: pointerdown-outside + Escape + подписка на `window` в -`$effect`. Кандидат на shared action (`clickOutside`/`popoverDismiss`). Сами -эффекты корректны. +Общий хелпер `onDismiss(root, close)` в `web/src/lib/components/ui/dismiss.ts` +(pointerdown-outside + Escape + отписка); `SchemaDownload.svelte` и +`ToolPickerButton.svelte` переведены на него — по ~15 одинаковых строк удалено +из каждого. ### L3. Несогласованный API `Toggle` @@ -178,10 +180,11 @@ round-семантика `clampByte` и trunc-семантика `clampInt` со `["linear-gradient-png","resize-png","quantize-png"]` (kit — витрина, не production). -### L6. Дублирование CSS-значения превью-тайла +### L6. Дублирование CSS-значения превью-тайла — СДЕЛАНО (2026-10) -`clamp(var(--space-brand), 30vh, 60vh)` в `PreviewTile.svelte:96` и -`SchemaResultTile.svelte:129`; магические `30vh/60vh` не в токенах. +Новый токен `--size-tile-canvas-min: clamp(var(--space-brand), 30vh, 60vh)` в +`app.css`; `PreviewTile.svelte` и `SchemaResultTile.svelte` используют +`var(--size-tile-canvas-min)`. ### L7. `localStorage.setItem` без try/catch вне app.html — СДЕЛАНО (2026-10) diff --git a/web/src/app.css b/web/src/app.css index 96a3827..4633ea0 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -151,6 +151,7 @@ --size-preview-canvas-min-height-s: 180px; --size-preview-canvas-min-height-l: 360px; + --size-tile-canvas-min: clamp(var(--space-brand), 30vh, 60vh); --size-panel-min-height: 220px; --size-tool-icon: 42px; diff --git a/web/src/lib/components/schema/PreviewTile.svelte b/web/src/lib/components/schema/PreviewTile.svelte index 79c6359..7fc657e 100644 --- a/web/src/lib/components/schema/PreviewTile.svelte +++ b/web/src/lib/components/schema/PreviewTile.svelte @@ -93,7 +93,7 @@ display: flex; align-items: center; justify-content: center; - min-height: clamp(var(--space-brand), 30vh, 60vh); + min-height: var(--size-tile-canvas-min); border: var(--size-border) solid var(--color-border); border-radius: var(--radius-s); overflow: hidden; diff --git a/web/src/lib/components/schema/SchemaDownload.svelte b/web/src/lib/components/schema/SchemaDownload.svelte index ebd72aa..ca7a444 100644 --- a/web/src/lib/components/schema/SchemaDownload.svelte +++ b/web/src/lib/components/schema/SchemaDownload.svelte @@ -1,6 +1,7 @@ diff --git a/web/src/lib/components/schema/SchemaResultTile.svelte b/web/src/lib/components/schema/SchemaResultTile.svelte index 69d62f8..f6b6490 100644 --- a/web/src/lib/components/schema/SchemaResultTile.svelte +++ b/web/src/lib/components/schema/SchemaResultTile.svelte @@ -126,7 +126,7 @@ ломал parts-grid в одну колонку. */ width: 100%; align-self: stretch; - min-height: clamp(var(--space-brand), 30vh, 60vh); + min-height: var(--size-tile-canvas-min); border: var(--size-border) solid var(--color-border); border-radius: var(--radius-s); overflow: auto; diff --git a/web/src/lib/components/schema/SchemaToolView.svelte b/web/src/lib/components/schema/SchemaToolView.svelte index 9fbba8e..00ec979 100644 --- a/web/src/lib/components/schema/SchemaToolView.svelte +++ b/web/src/lib/components/schema/SchemaToolView.svelte @@ -142,6 +142,8 @@ }); const { run, download, copyText, downloadText } = runner; const running = $derived(runner.running); + // Display-level busy: either a run or a download is in flight. + const busy = $derived(running || runner.downloading); const result = $derived(runner.result); const fileResult = $derived(runner.fileResult); const textResult = $derived(runner.textResult); @@ -499,7 +501,7 @@ toolId={lastTool.id} {inputMode} resultKind={displayResultKind} - {running} + running={busy} error={errorText} {aligned} ontogglealign={() => (aligned = !aligned)} @@ -561,7 +563,7 @@ {source} {sourceFiles} {textSource} - {running} + running={busy} resultKind={displayResultKind} {result} {resultNote} diff --git a/web/src/lib/components/schema/ToolPickerButton.svelte b/web/src/lib/components/schema/ToolPickerButton.svelte index b4a00dd..6e3048a 100644 --- a/web/src/lib/components/schema/ToolPickerButton.svelte +++ b/web/src/lib/components/schema/ToolPickerButton.svelte @@ -1,5 +1,6 @@ diff --git a/web/src/lib/components/schema/schema-tool-runner.svelte.ts b/web/src/lib/components/schema/schema-tool-runner.svelte.ts index d16067c..febcf2e 100644 --- a/web/src/lib/components/schema/schema-tool-runner.svelte.ts +++ b/web/src/lib/components/schema/schema-tool-runner.svelte.ts @@ -40,7 +40,10 @@ export interface SchemaToolRunnerCtx { } interface RunnerState { + // run() and download() have separate flags: a parallel auto-run and a + // download click must not clobber each other's indicator/disabled state. running: boolean; + downloading: boolean; // Generation of the latest run(): a stale in-flight chain (params changed // while the worker was busy) must not overwrite newer results. runGen: number; @@ -138,7 +141,7 @@ async function run(s: RunnerState, ctx: SchemaToolRunnerCtx) { async function download(s: RunnerState, ctx: SchemaToolRunnerCtx) { if (!ctx.schema()) return; - s.running = true; + s.downloading = true; s.displayError = null; try { if (s.fileResult) { @@ -157,7 +160,7 @@ async function download(s: RunnerState, ctx: SchemaToolRunnerCtx) { } catch (e) { s.displayError = toDisplayError(e); } finally { - s.running = false; + s.downloading = false; } } @@ -198,6 +201,7 @@ function clearResults(s: RunnerState) { export function createSchemaToolRunner(ctx: SchemaToolRunnerCtx) { const s = $state({ running: false, + downloading: false, runGen: 0, result: null, fileResult: null, @@ -213,6 +217,9 @@ export function createSchemaToolRunner(ctx: SchemaToolRunnerCtx) { get running() { return s.running; }, + get downloading() { + return s.downloading; + }, get result() { return s.result; }, diff --git a/web/src/lib/components/ui/dismiss.ts b/web/src/lib/components/ui/dismiss.ts new file mode 100644 index 0000000..f27890f --- /dev/null +++ b/web/src/lib/components/ui/dismiss.ts @@ -0,0 +1,19 @@ +// Popover dismiss: close on pointerdown outside the root and on Escape. +// Subscribe inside `$effect` while the popover is open; returns unsubscribe. +export function onDismiss( + root: () => HTMLElement | null, + close: () => void, +): () => void { + function onPointerDown(e: PointerEvent) { + if (!root()?.contains(e.target as Node)) close(); + } + function onKey(e: KeyboardEvent) { + if (e.key === "Escape") close(); + } + window.addEventListener("pointerdown", onPointerDown); + window.addEventListener("keydown", onKey); + return () => { + window.removeEventListener("pointerdown", onPointerDown); + window.removeEventListener("keydown", onKey); + }; +}