diff --git a/web/.prettierignore b/web/.prettierignore index 1d0c748..b9f4fe2 100644 --- a/web/.prettierignore +++ b/web/.prettierignore @@ -6,3 +6,4 @@ pnpm-lock.yaml static *.log .DS_Store +audit diff --git a/web/README.md b/web/README.md index 67a2c60..301604e 100644 --- a/web/README.md +++ b/web/README.md @@ -1,6 +1,7 @@ # sv -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). +Everything you need to build a Svelte project, powered by +[`sv`](https://github.com/sveltejs/cli). ## Creating a project @@ -20,7 +21,8 @@ pnpm dlx sv@0.17.0 create --template minimal --types ts --install pnpm web ## Developing -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: +Once you've created a project and installed dependencies with `npm install` (or +`pnpm install` or `yarn`), start a development server: ```sh npm run dev @@ -39,4 +41,5 @@ npm run build You can preview the production build with `npm run preview`. -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. +> To deploy your app, you may need to install an +> [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/web/scripts/audit.mjs b/web/scripts/audit.mjs index abbc6f5..1567d87 100644 --- a/web/scripts/audit.mjs +++ b/web/scripts/audit.mjs @@ -38,7 +38,7 @@ const FIELDS = [ "lineHeight", "borderRadius", "display", - "gap" + "gap", ]; const waitFor = async (url, ms = 60000) => { @@ -58,7 +58,8 @@ const snapshot = (page, url) => const readTokens = () => { const s = getComputedStyle(document.documentElement); const out = {}; - for (const k of s) if (k.startsWith("--")) out[k] = s.getPropertyValue(k).trim(); + for (const k of s) + if (k.startsWith("--")) out[k] = s.getPropertyValue(k).trim(); return out; }; const tokensLight = readTokens(); @@ -85,14 +86,19 @@ const snapshot = (page, url) => "lineHeight", "borderRadius", "display", - "gap" + "gap", ]) o[f] = s[f]; return o; }; const rect = (el) => { const r = el.getBoundingClientRect(); - return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) }; + return { + x: Math.round(r.x), + y: Math.round(r.y), + w: Math.round(r.width), + h: Math.round(r.height), + }; }; const walk = (el, path) => { if (["SCRIPT", "STYLE", "NOSCRIPT"].includes(el.tagName)) return []; @@ -101,12 +107,18 @@ const snapshot = (page, url) => out.push({ path, tag: el.tagName.toLowerCase(), - text: el.children.length === 0 ? el.textContent.trim().slice(0, 40) : "", + text: + el.children.length === 0 ? el.textContent.trim().slice(0, 40) : "", rect: rect(el), - style: sig(el) + style: sig(el), }); for (let i = 0; i < kids.length; i++) - out.push(...walk(kids[i], `${path}>${kids[i].tagName.toLowerCase()}:nth-child(${i + 1})`)); + out.push( + ...walk( + kids[i], + `${path}>${kids[i].tagName.toLowerCase()}:nth-child(${i + 1})`, + ), + ); return out; }; return { tokensLight, tokensDark, tree: walk(document.body, "body") }; @@ -120,13 +132,16 @@ async function main() { cwd: WEB, stdio: "ignore", shell: true, - detached: process.platform !== "win32" + detached: process.platform !== "win32", }); await waitFor(OURS); } const browser = await chromium.launch(); - const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 }); + const page = await browser.newPage({ + viewport: { width: 1440, height: 900 }, + deviceScaleFactor: 1, + }); try { await page.goto(OURS, { waitUntil: "load" }); await page.evaluate(() => document.fonts.ready); @@ -154,7 +169,7 @@ async function main() { }; const tokenDiffs = { light: tokDiff(ours.tokensLight, ref.tokensLight), - dark: tokDiff(ours.tokensDark, ref.tokensDark) + dark: tokDiff(ours.tokensDark, ref.tokensDark), }; // element diff: совпадающие по тексту элементы (структуры разные, @@ -183,9 +198,15 @@ async function main() { } const dr = o.rect; const rr = r.rect; - if (Math.abs(dr.x - rr.x) > 2 || Math.abs(dr.y - rr.y) > 2 || Math.abs(dr.w - rr.w) > 2 || Math.abs(dr.h - rr.h) > 2) + if ( + Math.abs(dr.x - rr.x) > 2 || + Math.abs(dr.y - rr.y) > 2 || + Math.abs(dr.w - rr.w) > 2 || + Math.abs(dr.h - rr.h) > 2 + ) deltas.rect = { ours: dr, ref: rr }; - if (Object.keys(deltas).length) elementDiffs.push({ path: o.path, tag: o.tag, text: k, deltas }); + if (Object.keys(deltas).length) + elementDiffs.push({ path: o.path, tag: o.tag, text: k, deltas }); } for (const [k] of txtR) if (!txtO.has(k)) onlyRef.push(k); @@ -202,16 +223,22 @@ async function main() { tokenDark: tokenDiffs.dark.length, elements: elementDiffs.length, onlyOurs: onlyOurs.length, - onlyRef: onlyRef.length - } + onlyRef: onlyRef.length, + }, }; mkdirSync(resolve(WEB, "audit"), { recursive: true }); - writeFileSync(resolve(WEB, "audit/audit-report.json"), JSON.stringify(report, null, 2)); - writeFileSync(resolve(WEB, "audit/audit-report.md"), toMarkdown(report, ours, ref)); + writeFileSync( + resolve(WEB, "audit/audit-report.json"), + JSON.stringify(report, null, 2), + ); + writeFileSync( + resolve(WEB, "audit/audit-report.md"), + toMarkdown(report, ours, ref), + ); console.log( `audit done: tokens light/dark=${tokenDiffs.light.length}/${tokenDiffs.dark.length}, ` + - `elements=${elementDiffs.length}, onlyOurs=${onlyOurs.length}, onlyRef=${onlyRef.length}` + `elements=${elementDiffs.length}, onlyOurs=${onlyOurs.length}, onlyRef=${onlyRef.length}`, ); console.log(`report: web/audit/audit-report.md`); } finally { @@ -219,7 +246,9 @@ async function main() { if (server) { try { if (process.platform === "win32") - spawn("taskkill", ["/pid", String(server.pid), "/f", "/t"], { stdio: "ignore" }); + spawn("taskkill", ["/pid", String(server.pid), "/f", "/t"], { + stdio: "ignore", + }); else server.kill("SIGTERM"); } catch {} } @@ -233,24 +262,25 @@ function toMarkdown(report, ours, ref) { rows.map((r) => `| \`${r.key}\` | ${r.ours} | ${r.ref} |`).join("\n") : "_все совпадают_"; const sorted = [...report.elementDiffs].sort( - (x, y) => Object.keys(y.deltas).length - Object.keys(x.deltas).length + (x, y) => Object.keys(y.deltas).length - Object.keys(x.deltas).length, ); const elRows = sorted .slice(0, 100) .map((e) => { - const d = Object.entries(e.deltas) - .map(([k, v]) => { - if (k === "rect") { - const f = (r) => `${r.x},${r.y} ${r.w}x${r.h}`; - return ` - **rect**: \`${f(v.ours)}\` → \`${f(v.ref)}\``; - } - return ` - **${k}**: \`${v.ours}\` → \`${v.ref}\``; - }) - .join("\n"); + const d = Object.entries(e.deltas) + .map(([k, v]) => { + if (k === "rect") { + const f = (r) => `${r.x},${r.y} ${r.w}x${r.h}`; + return ` - **rect**: \`${f(v.ours)}\` → \`${f(v.ref)}\``; + } + return ` - **${k}**: \`${v.ours}\` → \`${v.ref}\``; + }) + .join("\n"); return `### \`${e.path}\`${e.text ? ` — "${e.text}"` : ""}\n${d}`; }) .join("\n\n"); - return `# Style audit: ${report.ours}\n\nvs ${report.ref}\n\n_${report.generatedAt}_\n\n` + + return ( + `# Style audit: ${report.ours}\n\nvs ${report.ref}\n\n_${report.generatedAt}_\n\n` + `## Сводка\n\n- Токены light: **${report.counts.tokenLight}** расх.\n` + `- Токены dark: **${report.counts.tokenDark}** расх.\n` + `- Элементы (стиль/геометрия): **${report.counts.elements}** расх.\n` + @@ -259,10 +289,15 @@ function toMarkdown(report, ours, ref) { `## Токены — Dark\n\n${tokTable(report.tokenDiffs.dark)}\n\n` + `## Расхождения элементов (топ ${Math.min(100, sorted.length)})\n\n${elRows}\n\n` + `## Только у нас (структурно)\n\n` + - (report.onlyOurs.length ? report.onlyOurs.map((p) => `- \`${p}\``).join("\n") : "_—_") + + (report.onlyOurs.length + ? report.onlyOurs.map((p) => `- \`${p}\``).join("\n") + : "_—_") + `\n\n## Только в рефе (структурно)\n\n` + - (report.onlyRef.length ? report.onlyRef.map((p) => `- \`${p}\``).join("\n") : "_—_") + - `\n`; + (report.onlyRef.length + ? report.onlyRef.map((p) => `- \`${p}\``).join("\n") + : "_—_") + + `\n` + ); } main().catch((e) => { diff --git a/web/src/app.css b/web/src/app.css index 6c65077..5bb615e 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -39,11 +39,12 @@ --control-max-width: 16rem; --font-sans: - system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - --font-mono: ui-monospace, 'Cascadia Code', Consolas, monospace; + system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, + sans-serif; + --font-mono: ui-monospace, "Cascadia Code", Consolas, monospace; } -[data-theme='dark'] { +[data-theme="dark"] { color-scheme: dark; --bg: #12151a; @@ -150,7 +151,7 @@ select { max-width: var(--control-max-width); } -input.control[type='number'] { +input.control[type="number"] { font-family: var(--font-mono); width: 100%; } diff --git a/web/src/app.d.ts b/web/src/app.d.ts index a5fcf12..86f05a3 100644 --- a/web/src/app.d.ts +++ b/web/src/app.d.ts @@ -10,7 +10,7 @@ declare global { } } -declare module '*.css?raw' { +declare module "*.css?raw" { const content: string; export default content; } diff --git a/web/src/app.html b/web/src/app.html index 4d88af5..2ed93b2 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -7,16 +7,16 @@ diff --git a/web/src/lib/categories.ts b/web/src/lib/categories.ts index 443a19c..b943124 100644 --- a/web/src/lib/categories.ts +++ b/web/src/lib/categories.ts @@ -1,24 +1,24 @@ export type CategoryId = - | 'convert' - | 'alpha' - | 'color' - | 'geometry' - | 'analyze' - | 'generate' - | 'filters' - | 'text'; + | "convert" + | "alpha" + | "color" + | "geometry" + | "analyze" + | "generate" + | "filters" + | "text"; /** * Порядок категорий в каталоге. Человекочитаемые названия живут в словарях * i18n: секция categories, ключ = CategoryId. */ export const CATEGORIES: readonly CategoryId[] = [ - 'convert', - 'alpha', - 'color', - 'geometry', - 'filters', - 'text', - 'analyze', - 'generate' + "convert", + "alpha", + "color", + "geometry", + "filters", + "text", + "analyze", + "generate", ]; diff --git a/web/src/lib/components/DownloadButton.svelte b/web/src/lib/components/DownloadButton.svelte index 62836f9..9b8a46d 100644 --- a/web/src/lib/components/DownloadButton.svelte +++ b/web/src/lib/components/DownloadButton.svelte @@ -1,9 +1,9 @@
{#if hasMask} - + {/if} {#each params as param (param.id)}
- {#if param.type === 'checkbox'} - - {:else if param.type === 'number'} + {#if param.type === "checkbox"} + + {:else if param.type === "number"} - {:else if param.type === 'slider'} + {:else if param.type === "slider"} - {:else if param.type === 'select'} + {:else if param.type === "select"} ({ value: o.value, - label: optionLabel(tool, param, o.value) + label: optionLabel(tool, param, o.value), }))} bind:value={values[param.id]} /> - {:else if param.type === 'color'} + {:else if param.type === "color"} onPipetteToggle?.(param.id)} /> - {:else if param.type === 'text'} + {:else if param.type === "text"} - import { rgbToHex } from '$lib/core/color'; - import type { PixelImage } from '$lib/core/types'; - import PipetteLoupe from './tool/PipetteLoupe.svelte'; + import { rgbToHex } from "$lib/core/color"; + import type { PixelImage } from "$lib/core/types"; + import PipetteLoupe from "./tool/PipetteLoupe.svelte"; interface Props { image: PixelImage | null; @@ -12,27 +12,42 @@ let { image, pipetteActive = false, onPickColor }: Props = $props(); let canvas = $state(); - let hover = $state<{ clientX: number; clientY: number; px: number; py: number; hex: string } | null>( - null - ); + let hover = $state<{ + clientX: number; + clientY: number; + px: number; + py: number; + hex: string; + } | null>(null); $effect(() => { if (!canvas || !image) return; canvas.width = image.width; canvas.height = image.height; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); if (!ctx) return; - ctx.putImageData(new ImageData(image.data, image.width, image.height), 0, 0); + ctx.putImageData( + new ImageData(image.data, image.width, image.height), + 0, + 0, + ); }); - function pixelAt(event: MouseEvent): { px: number; py: number; hex: string } | null { + function pixelAt( + event: MouseEvent, + ): { px: number; py: number; hex: string } | null { if (!canvas || !image) return null; const rect = canvas.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return null; - const px = Math.floor((event.clientX - rect.left) * (canvas.width / rect.width)); - const py = Math.floor((event.clientY - rect.top) * (canvas.height / rect.height)); - if (px < 0 || py < 0 || px >= canvas.width || py >= canvas.height) return null; - const ctx = canvas.getContext('2d'); + const px = Math.floor( + (event.clientX - rect.left) * (canvas.width / rect.width), + ); + const py = Math.floor( + (event.clientY - rect.top) * (canvas.height / rect.height), + ); + if (px < 0 || py < 0 || px >= canvas.width || py >= canvas.height) + return null; + const ctx = canvas.getContext("2d"); if (!ctx) return null; const [r, g, b] = ctx.getImageData(px, py, 1, 1).data; return { px, py, hex: rgbToHex(r, g, b) }; @@ -53,7 +68,13 @@ } const pixel = pixelAt(event); hover = pixel - ? { clientX: event.clientX, clientY: event.clientY, px: pixel.px, py: pixel.py, hex: pixel.hex } + ? { + clientX: event.clientX, + clientY: event.clientY, + px: pixel.px, + py: pixel.py, + hex: pixel.hex, + } : null; } @@ -90,8 +111,10 @@ display: block; max-width: 100%; max-height: 32rem; - background: - repeating-conic-gradient(var(--check-a) 0% 25%, var(--check-b) 0% 50%); + background: repeating-conic-gradient( + var(--check-a) 0% 25%, + var(--check-b) 0% 50% + ); background-size: 16px 16px; border: 1px solid var(--border); } diff --git a/web/src/lib/components/ToolPage.svelte b/web/src/lib/components/ToolPage.svelte index 870c11a..f8d2577 100644 --- a/web/src/lib/components/ToolPage.svelte +++ b/web/src/lib/components/ToolPage.svelte @@ -1,60 +1,81 @@ -
+
{#if header} {@render header()} {/if} {#if isChain}
- +
{:else if !isSourceless}
- +
{#if isTextSource && !source} onTextSubmit?.(text)} /> @@ -162,34 +178,50 @@ {#if showParams}
- +
- handlePipetteToggle?.(id)} {hasMask} bind:showMask /> + handlePipetteToggle?.(id)} + {hasMask} + bind:showMask + />
{/if}
- +
{#if isChain} {#if busy && !result} - + {:else} {#if busy} - {t('resultCard.recalc')} + {t("resultCard.recalc")} {/if}
showError?.(e)} /> -
{/if} @@ -199,11 +231,11 @@ sourceLoaded={isSourceless ? true : !!source} {status} {result} - displayImage={displayImage} + {displayImage} {info} {isInfo} params={sanitized} - textResult={textResult} + {textResult} onChainToggle={canChainBase ? toggleChain : undefined} {hasChain} onDownloadError={(e) => showError?.(e)} @@ -276,7 +308,7 @@ text-align: left; } - .pane-params :global(input[type='range']) { + .pane-params :global(input[type="range"]) { order: -1; flex: 1 1 100%; min-width: 0; @@ -306,7 +338,10 @@ @media (min-width: 75rem) { .stage-grid:not(.no-params):not(.single) { - grid-template-columns: minmax(0, 1fr) clamp(13rem, 17vw, 18rem) minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr) clamp(13rem, 17vw, 18rem) minmax( + 0, + 1fr + ); } .stage-grid.no-params:not(.single), diff --git a/web/src/lib/components/stage/ToolStageClassic.svelte b/web/src/lib/components/stage/ToolStageClassic.svelte index 3673a63..14aa94b 100644 --- a/web/src/lib/components/stage/ToolStageClassic.svelte +++ b/web/src/lib/components/stage/ToolStageClassic.svelte @@ -1,10 +1,10 @@
{#if !isSourceless} - +
{#if isTextSource && !source} @@ -56,7 +58,7 @@
{/if}
0 || hasMask)}
- +
- import { t } from '$lib/i18n/t'; - import type { PixelImage } from '$lib/core/types'; - import DropZone from '../DropZone.svelte'; - import Preview from '../Preview.svelte'; - import Button from '../ui/Button.svelte'; + import { t } from "$lib/i18n/t"; + import type { PixelImage } from "$lib/core/types"; + import DropZone from "../DropZone.svelte"; + import Preview from "../Preview.svelte"; + import Button from "../ui/Button.svelte"; interface Props { overlay: PixelImage | null; @@ -16,13 +16,17 @@
- + {#if !overlay} - + {:else}
- +
{/if}
diff --git a/web/src/lib/components/tool/ParamsCard.svelte b/web/src/lib/components/tool/ParamsCard.svelte index be1f7dc..caa7bc4 100644 --- a/web/src/lib/components/tool/ParamsCard.svelte +++ b/web/src/lib/components/tool/ParamsCard.svelte @@ -1,7 +1,7 @@ @@ -36,7 +36,7 @@ bind:showMask /> {:else} -

{t('paramsCard.noParams')}

+

{t("paramsCard.noParams")}

{/if}
diff --git a/web/src/lib/components/tool/PipetteLoupe.svelte b/web/src/lib/components/tool/PipetteLoupe.svelte index cca4d88..211c0ee 100644 --- a/web/src/lib/components/tool/PipetteLoupe.svelte +++ b/web/src/lib/components/tool/PipetteLoupe.svelte @@ -1,5 +1,5 @@ @@ -77,7 +92,8 @@ style="left:{clientX}px;top:{clientY}px" aria-hidden="true" > - +

{hex}

diff --git a/web/src/lib/components/tool/ResultCard.svelte b/web/src/lib/components/tool/ResultCard.svelte index d1fc3e1..1e3ccd6 100644 --- a/web/src/lib/components/tool/ResultCard.svelte +++ b/web/src/lib/components/tool/ResultCard.svelte @@ -1,16 +1,16 @@
{#if !sourceLoaded}
- +
- {:else if !isInfo && status === 'processing' && !result} + {:else if !isInfo && status === "processing" && !result}
{:else if isInfo} @@ -61,7 +64,7 @@ {/if}
- {:else if tool.resultType === 'text'} + {:else if tool.resultType === "text"}
{#if textResult !== null} @@ -70,8 +73,8 @@ {:else}
- {#if status === 'processing'} - {t('resultCard.recalc')} + {#if status === "processing"} + {t("resultCard.recalc")} {/if}
@@ -84,7 +87,7 @@ /> {#if onChainToggle} {/if}
diff --git a/web/src/lib/components/tool/SourceCard.svelte b/web/src/lib/components/tool/SourceCard.svelte index 362c152..64d403e 100644 --- a/web/src/lib/components/tool/SourceCard.svelte +++ b/web/src/lib/components/tool/SourceCard.svelte @@ -1,10 +1,10 @@
@@ -24,9 +31,11 @@ {:else}
- +
- +
{/if}
diff --git a/web/src/lib/components/tool/TextInputCard.svelte b/web/src/lib/components/tool/TextInputCard.svelte index d9f4828..bc13a5d 100644 --- a/web/src/lib/components/tool/TextInputCard.svelte +++ b/web/src/lib/components/tool/TextInputCard.svelte @@ -1,6 +1,6 @@
-

{t('textInput.heading')}

+

{t("textInput.heading")}

-
diff --git a/web/src/lib/components/tool/TextResult.svelte b/web/src/lib/components/tool/TextResult.svelte index edf155c..a664565 100644 --- a/web/src/lib/components/tool/TextResult.svelte +++ b/web/src/lib/components/tool/TextResult.svelte @@ -1,7 +1,7 @@
- +
- +
diff --git a/web/src/lib/components/ui/Button.svelte b/web/src/lib/components/ui/Button.svelte index 5e868f0..f87de44 100644 --- a/web/src/lib/components/ui/Button.svelte +++ b/web/src/lib/components/ui/Button.svelte @@ -1,9 +1,9 @@ @@ -29,7 +29,7 @@ class={fullWidth ? `${variant} fullwidth` : variant} aria-busy={busy} disabled={disabled || busy} - onclick={onclick} + {onclick} > {#if busy && busyText} {busyText} diff --git a/web/src/lib/components/ui/CheckboxField.svelte b/web/src/lib/components/ui/CheckboxField.svelte index a9c8cbb..5ffb737 100644 --- a/web/src/lib/components/ui/CheckboxField.svelte +++ b/web/src/lib/components/ui/CheckboxField.svelte @@ -11,7 +11,7 @@
{#if hint} @@ -38,7 +38,7 @@ color: var(--text-muted); } - input[type='checkbox'] { + input[type="checkbox"] { width: 1rem; height: 1rem; cursor: pointer; diff --git a/web/src/lib/components/ui/ColorField.svelte b/web/src/lib/components/ui/ColorField.svelte index 5f41c73..57c08ce 100644 --- a/web/src/lib/components/ui/ColorField.svelte +++ b/web/src/lib/components/ui/ColorField.svelte @@ -1,6 +1,6 @@
- + {#if onPipetteToggle} - - + + +