chore: update refs audit
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
# Plan: `/preview/list-tools` → design2 (catalog)
|
||||||
|
|
||||||
|
Ветка `feat/redesign`. Цель — привести каталог инструментов к рефу
|
||||||
|
`refs-html/list-tools.html`. Сейчас страница вообще не на design2: по
|
||||||
|
мульти-аудиту `onlyOurs=257, onlyRef=54` (демо-страница — `18/11`).
|
||||||
|
|
||||||
|
## Текущее состояние
|
||||||
|
|
||||||
|
- `src/routes/preview/list-tools/+page.svelte` использует собственную вёрстку
|
||||||
|
(`.catalog` / `.cat-head` / `.cat-grid`) + `Panel` / `PanelHeading` /
|
||||||
|
`ToolCard` (kit). Нет поиска, фильтров, счётчика, навигация по категориям
|
||||||
|
сделана через `Panel`-блоки, а не через `.catalog-groups`.
|
||||||
|
- Реф (`list-tools.html`) строит:
|
||||||
|
- `.catalog-page` → `.catalog-head` (`.eyebrow`, `h1` «Tool catalog»,
|
||||||
|
`p` lede, `.catalog-total` «NN TOOLS AVAILABLE» с большим числом),
|
||||||
|
- `.catalog-toolbar` (`.catalog-search` + `.catalog-filters` с кнопками),
|
||||||
|
- `.catalog-groups` (grid `repeat(2,minmax(0,1fr))`, gap `56px 28px`) →
|
||||||
|
`.catalog-group` (`.group-title` «CAT · NN TOOLS») → `.tool-cards` →
|
||||||
|
`.tool-card`.
|
||||||
|
- `.tool-card` (реф) — `display:grid`, колонки `42px minmax(0,1fr) 24px 18px`,
|
||||||
|
`min-height:106px`, `gap:14px`, `padding:16px`: `.tool-icon` (42×42
|
||||||
|
синий размытый квадрат), `.tool-copy` (`strong` 13px mono + `span` 12px mono
|
||||||
|
muted), `.tool-index` (10px mono, сверху), `.tool-arrow` (появляется при hover).
|
||||||
|
- Токены `design2.css` уже совпадают с рефом (`--background #eef1f4`,
|
||||||
|
`--panel #f8fafb`, `--foreground #17212b`, `--muted #6d7883`, `--line #cbd3da`,
|
||||||
|
`--blue #1769d2`, `--radius 4px`) — править токены не нужно.
|
||||||
|
|
||||||
|
## Шаги
|
||||||
|
|
||||||
|
1. **Restyle `kit/ToolCard.svelte`** под реф (меняется «на месте» — используется
|
||||||
|
ещё в `preview/+page` и `preview/kit`, стиль унифицируется, ок):
|
||||||
|
- grid-колонки `42px minmax(0,1fr) 24px 18px`, `align-items:center`,
|
||||||
|
`gap:14px`, `min-height:106px`, `padding:16px`, `border:1px solid line`,
|
||||||
|
`background:panel`, `border-radius:radius`.
|
||||||
|
- `.tool-icon`: 42×42 `place-items:center`, `background:color-mix(in srgb,
|
||||||
|
var(--blue) 12%, transparent)`, `color:var(--blue)` (реф использует
|
||||||
|
`--panel-strong`; вместо нового токена берём этот `color-mix`).
|
||||||
|
- `.tool-copy strong`: `600 13px var(--font-mono)`; `span`: `12px/1.5 mono`
|
||||||
|
`muted`.
|
||||||
|
- `.tool-index`: `10px mono muted`, `align-self:start`.
|
||||||
|
- `.tool-arrow`: `color:var(--blue)`, `opacity:0` → `1` + `translate(2px,-2px)`
|
||||||
|
на `.tool-card:hover`.
|
||||||
|
- DOM-порядок как в рефе: `icon, copy, index, arrow`.
|
||||||
|
- `href` через `resolve` из `$app/paths` (убрать `svelte-ignore`).
|
||||||
|
|
||||||
|
2. **`kit/CatalogHeader.svelte`**: `.eyebrow` «EASY-PNG-TOOLS / CATALOG»,
|
||||||
|
`h1` «Tool catalog», lede; `.catalog-total` с динамическим `TOOLS.length`
|
||||||
|
(реф показывает `32`).
|
||||||
|
|
||||||
|
3. **`kit/CatalogToolbar.svelte`**: `.catalog-search` (иконка + `<input>`) и
|
||||||
|
`.catalog-filters` (кнопки по категориям). Состояние `query` / `category`
|
||||||
|
поднимается наружу через `bind:` (или callback-пропы), чтобы страница
|
||||||
|
фильтровала список. Фильтры функциональные (клиентский фильтр по названию +
|
||||||
|
категории), не декоративные.
|
||||||
|
|
||||||
|
4. **`kit/CatalogGroup.svelte`**: `.group-title` (uppercase label категории +
|
||||||
|
«NN TOOLS») + слот со списком карточек; сетка `.tool-cards` — `gap:10px`
|
||||||
|
(реф), на мобиле (`@media max-width:800px`) всё схлопывается в 1 колонку.
|
||||||
|
|
||||||
|
5. **Переписать `preview/list-tools/+page.svelte`** как тонкую композицию:
|
||||||
|
- обёртка `.catalog-page` (`padding:60px clamp(24px,4vw,72px) 72px`);
|
||||||
|
- `<CatalogHeader />`;
|
||||||
|
- `<CatalogToolbar bind:query bind:category />`;
|
||||||
|
- `.catalog-groups` из отфильтрованных групп (по `CATEGORIES`, пересчитав
|
||||||
|
`tools` с учётом `query`/`category`);
|
||||||
|
- внутри `<CatalogGroup>` — `<ToolCard>` на каждый tool (`href=
|
||||||
|
"/preview/tools/{id}"`, `icon` из `tool-icons`, `index` по порядку,
|
||||||
|
`title`/`description` из registry);
|
||||||
|
- убрать `Panel`/`PanelHeading` из этого маршрута.
|
||||||
|
|
||||||
|
6. **Категории/локализация**: labels привести к рефу (реф: ALL / CONVERT /
|
||||||
|
TRANSPARENCY / COLOR / GEOMETRY / FILTERS / ANALYZE). Наши `CATEGORIES`
|
||||||
|
(`convert/alpha/color/geometry/filters/text/analyze/generate`) — сопоставить
|
||||||
|
(`alpha`→TRANSPARENCY и т.п.); точное сопоставление уточнить по рефу и
|
||||||
|
`i18n/ru.ts`. Фильтры = ALL + те же label'ы. Тексты заголовков/lede — по
|
||||||
|
рефу (англ. «Tool catalog» / «Focused utilities for working with PNG…»),
|
||||||
|
либо оставить RU-вариант, если это сознательное отклонение (зафиксировать).
|
||||||
|
|
||||||
|
7. **Адаптив**: перенести правила рефа (`@media max-width:800px`) —
|
||||||
|
`.catalog-groups` → 1fr, `.tool-card` → `38px minmax(0,1fr) 18px`, скрыть
|
||||||
|
`.tool-index`/показать `.tool-arrow`, `.catalog-head` в колонку и т.д.
|
||||||
|
|
||||||
|
## Верификация
|
||||||
|
|
||||||
|
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`,
|
||||||
|
`pnpm build` — без ошибок.
|
||||||
|
- `pnpm refs-audit` (теперь мульти-маршрутный) → глянуть секцию
|
||||||
|
`/preview/list-tools`: `onlyOurs`/`onlyRef` должны упасть с `257/54`
|
||||||
|
примерно до уровня demo (`18/11`). Остаточный diff (если будет) зафиксировать
|
||||||
|
в этом файле.
|
||||||
|
|
||||||
|
## Примечания
|
||||||
|
|
||||||
|
- `ToolCard` правится на месте — влияет на `preview/+page` и `preview/kit`
|
||||||
|
(там тоже карточки каталога, унификация уместна).
|
||||||
|
- Старый дизайн (`routes/(old)/**`, `components/search/**`) не трогать.
|
||||||
|
- Коммиты делает автор (агент не коммитит).
|
||||||
+11
-6
@@ -6,12 +6,17 @@
|
|||||||
|
|
||||||
## Файлы
|
## Файлы
|
||||||
|
|
||||||
| Файл | Маршрут в refs | Что показывает |
|
| Файл | Маршрут в нашем приложении (audit) | Что показывает |
|
||||||
| ------------------------- | ------------------------------------ | --------------------------------------------------- |
|
| ------------------------- | -------------------------------------- | --------------------------------------------------- |
|
||||||
| `index.html` | `/` | Главная: пайплайн-воркспейс + панель превью |
|
| `demo.html` | `/preview/demo` | Пайплайн-воркспейс + панель превью |
|
||||||
| `demo.html` | `/easy-png-tools/demo` | То же, что главная (зеркало) |
|
| `list-tools.html` | `/preview/list-tools` | Каталог инструментов (группы, поиск, фильтры) |
|
||||||
| `gradient.html` | `/easy-png-tools/gradient` | Инструмент «Градиент»: настройки + превью + CSS |
|
| `gradient.html` | `/preview/tools/linear-gradient-png` | Инструмент «Градиент»: настройки + превью + CSS |
|
||||||
| `background-remover.html` | `/easy-png-tools/background-remover` | Инструмент «Удаление фона»: сравнение source/result |
|
| `background-remover.html` | `/preview/tools/remove-background-png` | Инструмент «Удаление фона»: сравнение source/result |
|
||||||
|
|
||||||
|
> `index.html` не используется: в Next-рефе это зеркало `demo` (главная == `/easy-png-tools/demo`),
|
||||||
|
> а в нашем приложении `/preview` — своя страница (воркспейс). Файл не генерируется
|
||||||
|
> экстрактором (`scripts/extract-static.mjs` пропускает `index.html`) и исключён
|
||||||
|
> из аудита (`EXCLUDE` в `web/scripts/audit.mjs`).
|
||||||
|
|
||||||
## Особенности
|
## Особенности
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+58
-43
@@ -1,5 +1,5 @@
|
|||||||
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
||||||
import { join, dirname, basename } from 'node:path';
|
import { join, dirname, basename } from "node:path";
|
||||||
|
|
||||||
// Usage: node extract-static.mjs <source-out-dir> <dest-dir>
|
// Usage: node extract-static.mjs <source-out-dir> <dest-dir>
|
||||||
// e.g. node extract-static.mjs .next\server\app ..\refs-html
|
// e.g. node extract-static.mjs .next\server\app ..\refs-html
|
||||||
@@ -25,40 +25,50 @@ const THEME_TOGGLE = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function injectThemeToggle(html) {
|
function injectThemeToggle(html) {
|
||||||
if (html.includes('__themeToggleInjected')) return html;
|
if (html.includes("__themeToggleInjected")) return html;
|
||||||
const tag = '</body>';
|
const tag = "</body>";
|
||||||
if (html.includes(tag)) return html.replace(tag, THEME_TOGGLE + '\n' + tag);
|
if (html.includes(tag)) return html.replace(tag, THEME_TOGGLE + "\n" + tag);
|
||||||
if (html.includes('</html>')) return html.replace('</html>', THEME_TOGGLE + '\n</html>');
|
if (html.includes("</html>")) return html.replace("</html>", THEME_TOGGLE + "\n</html>");
|
||||||
return html + '\n' + THEME_TOGGLE;
|
return html + "\n" + THEME_TOGGLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CSS: find the compiled chunk and pretty-print it ---
|
// --- CSS: find the compiled chunk and pretty-print it ---
|
||||||
const cssChunks = join(root, '..', '..', 'static', 'chunks');
|
const cssChunks = join(root, "..", "..", "static", "chunks");
|
||||||
const cssFile = readdirSync(cssChunks)
|
const cssFile = readdirSync(cssChunks)
|
||||||
.filter(f => f.endsWith('.css'))
|
.filter((f) => f.endsWith(".css"))
|
||||||
.sort((a, b) => statSync(join(cssChunks, b)).size - statSync(join(cssChunks, a)).size)[0];
|
.sort((a, b) => statSync(join(cssChunks, b)).size - statSync(join(cssChunks, a)).size)[0];
|
||||||
const css = readFileSync(join(cssChunks, cssFile), 'utf8');
|
const css = readFileSync(join(cssChunks, cssFile), "utf8");
|
||||||
|
|
||||||
const prettyCss = css
|
const prettyCss = css
|
||||||
.replace(/\r/g, '')
|
.replace(/\r/g, "")
|
||||||
.replace(/\{/g, ' {\n ')
|
.replace(/\{/g, " {\n ")
|
||||||
.replace(/;/g, ';\n ')
|
.replace(/;/g, ";\n ")
|
||||||
.replace(/\}/g, '\n}\n')
|
.replace(/\}/g, "\n}\n")
|
||||||
.replace(/\n{3,}/g, '\n\n')
|
.replace(/\n{3,}/g, "\n\n")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// --- conservative html formatter ---
|
// --- conservative html formatter ---
|
||||||
|
|
||||||
const VOID = new Set([
|
const VOID = new Set([
|
||||||
'area','base','br','col','embed','hr','img','input',
|
"area",
|
||||||
'link','meta','param','source','track','wbr',
|
"base",
|
||||||
|
"br",
|
||||||
|
"col",
|
||||||
|
"embed",
|
||||||
|
"hr",
|
||||||
|
"img",
|
||||||
|
"input",
|
||||||
|
"link",
|
||||||
|
"meta",
|
||||||
|
"param",
|
||||||
|
"source",
|
||||||
|
"track",
|
||||||
|
"wbr",
|
||||||
]);
|
]);
|
||||||
const RAW = new Set(['pre','textarea','script','style']);
|
const RAW = new Set(["pre", "textarea", "script", "style"]);
|
||||||
|
|
||||||
function fmtHtml(html) {
|
function fmtHtml(html) {
|
||||||
const tokens = html.match(
|
const tokens = html.match(/<!--[\s\S]*?-->|<!doctype[^>]*>|<\/?[a-zA-Z][^>]*>|[^<]+|</gi) || [];
|
||||||
/<!--[\s\S]*?-->|<!doctype[^>]*>|<\/?[a-zA-Z][^>]*>|[^<]+|</gi
|
|
||||||
) || [];
|
|
||||||
const out = [];
|
const out = [];
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
let rawTag = null;
|
let rawTag = null;
|
||||||
@@ -66,7 +76,7 @@ function fmtHtml(html) {
|
|||||||
for (const tk of tokens) {
|
for (const tk of tokens) {
|
||||||
if (rawTag) {
|
if (rawTag) {
|
||||||
out.push(tk);
|
out.push(tk);
|
||||||
if (new RegExp(`</${rawTag}\\s*>`, 'i').test(tk)) {
|
if (new RegExp(`</${rawTag}\\s*>`, "i").test(tk)) {
|
||||||
depth--;
|
depth--;
|
||||||
rawTag = null;
|
rawTag = null;
|
||||||
}
|
}
|
||||||
@@ -75,39 +85,45 @@ function fmtHtml(html) {
|
|||||||
|
|
||||||
const isOpen = /^<[a-zA-Z]/.test(tk);
|
const isOpen = /^<[a-zA-Z]/.test(tk);
|
||||||
const isClose = /^<\//.test(tk);
|
const isClose = /^<\//.test(tk);
|
||||||
let name = '';
|
let name = "";
|
||||||
if (isOpen || isClose) {
|
if (isOpen || isClose) {
|
||||||
name = (tk.match(/^<\/?([a-zA-Z0-9-]+)/) || [])[1]?.toLowerCase() || '';
|
name = (tk.match(/^<\/?([a-zA-Z0-9-]+)/) || [])[1]?.toLowerCase() || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOpen && RAW.has(name)) {
|
if (isOpen && RAW.has(name)) {
|
||||||
out.push('\n' + ' '.repeat(depth) + tk);
|
out.push("\n" + " ".repeat(depth) + tk);
|
||||||
if (!/\/>$/.test(tk)) { depth++; rawTag = name; }
|
if (!/\/>$/.test(tk)) {
|
||||||
|
depth++;
|
||||||
|
rawTag = name;
|
||||||
|
}
|
||||||
} else if (isOpen && !RAW.has(name)) {
|
} else if (isOpen && !RAW.has(name)) {
|
||||||
out.push('\n' + ' '.repeat(depth) + tk);
|
out.push("\n" + " ".repeat(depth) + tk);
|
||||||
if (!/\/>$/.test(tk) && !VOID.has(name)) depth++;
|
if (!/\/>$/.test(tk) && !VOID.has(name)) depth++;
|
||||||
} else if (isClose) {
|
} else if (isClose) {
|
||||||
depth = Math.max(0, depth - 1);
|
depth = Math.max(0, depth - 1);
|
||||||
out.push('\n' + ' '.repeat(depth) + tk);
|
out.push("\n" + " ".repeat(depth) + tk);
|
||||||
} else if (/^</.test(tk)) {
|
} else if (/^</.test(tk)) {
|
||||||
out.push('\n' + ' '.repeat(depth) + tk);
|
out.push("\n" + " ".repeat(depth) + tk);
|
||||||
} else {
|
} else {
|
||||||
const t = tk.replace(/\s+/g, ' ');
|
const t = tk.replace(/\s+/g, " ");
|
||||||
if (t.trim()) out.push(t);
|
if (t.trim()) out.push(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out.join('').replace(/^\n/, '').replace(/\n{3,}/g, '\n\n');
|
return out
|
||||||
|
.join("")
|
||||||
|
.replace(/^\n/, "")
|
||||||
|
.replace(/\n{3,}/g, "\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- auto-discover pages by walking the export dir for *.html ---
|
// --- auto-discover pages by walking the export dir for *.html ---
|
||||||
function walk(dir, base = '') {
|
function walk(dir, base = "") {
|
||||||
const found = [];
|
const found = [];
|
||||||
for (const entry of readdirSync(dir)) {
|
for (const entry of readdirSync(dir)) {
|
||||||
const full = join(dir, entry);
|
const full = join(dir, entry);
|
||||||
const rel = base ? join(base, entry) : entry;
|
const rel = base ? join(base, entry) : entry;
|
||||||
if (statSync(full).isDirectory()) {
|
if (statSync(full).isDirectory()) {
|
||||||
found.push(...walk(full, rel));
|
found.push(...walk(full, rel));
|
||||||
} else if (entry.endsWith('.html')) {
|
} else if (entry.endsWith(".html")) {
|
||||||
found.push(rel);
|
found.push(rel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,23 +132,22 @@ function walk(dir, base = '') {
|
|||||||
|
|
||||||
const pages = walk(root)
|
const pages = walk(root)
|
||||||
// skip Next internals
|
// skip Next internals
|
||||||
.filter((rel) => !basename(rel).startsWith('_') && !basename(rel).startsWith('404'))
|
.filter((rel) => !basename(rel).startsWith("_") && !basename(rel).startsWith("404"))
|
||||||
|
// `index.html` нам не нужен как реф, поэтому не генерируем его.
|
||||||
|
.filter((rel) => basename(rel).toLowerCase() !== "index.html")
|
||||||
.map((rel) => [rel, basename(rel)]);
|
.map((rel) => [rel, basename(rel)]);
|
||||||
|
|
||||||
console.log(`discovered ${pages.length} page(s):`, pages.map((p) => p[1]).join(', '));
|
console.log(`discovered ${pages.length} page(s):`, pages.map((p) => p[1]).join(", "));
|
||||||
|
|
||||||
for (const [src, out] of pages) {
|
for (const [src, out] of pages) {
|
||||||
let html = readFileSync(join(root, src), 'utf8');
|
let html = readFileSync(join(root, src), "utf8");
|
||||||
|
|
||||||
// strip scripts — pure static snapshot
|
// strip scripts — pure static snapshot
|
||||||
html = html.replace(/<script[\s\S]*?<\/script>/g, '');
|
html = html.replace(/<script[\s\S]*?<\/script>/g, "");
|
||||||
html = html.replace(/<link[^>]+rel="preload"[^>]+as="script"[^>]*>/g, '');
|
html = html.replace(/<link[^>]+rel="preload"[^>]+as="script"[^>]*>/g, "");
|
||||||
|
|
||||||
// inline compiled css instead of linking /_next/...
|
// inline compiled css instead of linking /_next/...
|
||||||
html = html.replace(
|
html = html.replace(/<link[^>]+rel="stylesheet"[^>]*>/g, () => "<style>\n" + prettyCss + "\n</style>");
|
||||||
/<link[^>]+rel="stylesheet"[^>]*>/g,
|
|
||||||
() => '<style>\n' + prettyCss + '\n</style>',
|
|
||||||
);
|
|
||||||
|
|
||||||
html = fmtHtml(html);
|
html = fmtHtml(html);
|
||||||
html = injectThemeToggle(html);
|
html = injectThemeToggle(html);
|
||||||
@@ -140,5 +155,5 @@ for (const [src, out] of pages) {
|
|||||||
const file = join(dest, out);
|
const file = join(dest, out);
|
||||||
mkdirSync(dirname(file), { recursive: true });
|
mkdirSync(dirname(file), { recursive: true });
|
||||||
writeFileSync(file, html);
|
writeFileSync(file, html);
|
||||||
console.log('written:', file);
|
console.log("written:", file);
|
||||||
}
|
}
|
||||||
|
|||||||
+1985
-3
File diff suppressed because it is too large
Load Diff
+681
-13
@@ -1,17 +1,22 @@
|
|||||||
# Style audit: http://127.0.0.1:5179/preview/demo
|
# Style audit (multi-route)
|
||||||
|
|
||||||
vs C:\+XBOCTuK\+life_projects\easy-png-tools\refs-html\demo.html
|
_2026-08-28T15:27:27.893Z_
|
||||||
|
|
||||||
_2026-08-28T14:54:53.564Z_
|
|
||||||
|
|
||||||
## Сводка
|
## Сводка
|
||||||
|
|
||||||
- Токены light: **10** расх.
|
| route | ref | tokL | tokD | els | onlyOurs | onlyRef |
|
||||||
- Токены dark: **16** расх.
|
|---|---|---|---|---|---|---|
|
||||||
- Элементы (стиль/геометрия): **47** расх.
|
| `/preview/demo` | demo.html | 10 | 16 | 47 | 18 | 11 |
|
||||||
- Только у нас: 18, только в рефе: 11
|
| `/preview/list-tools` | list-tools.html | 10 | 16 | 30 | 257 | 54 |
|
||||||
|
| `/preview/tools/linear-gradient-png` | gradient.html | 10 | 16 | 7 | 111 | 30 |
|
||||||
|
| `/preview/tools/remove-background-png` | background-remover.html | 10 | 16 | 8 | 110 | 28 |
|
||||||
|
|
||||||
## Токены — Light
|
## /preview/demo (vs demo.html)
|
||||||
|
|
||||||
|
- Токены light: **10**, dark: **16**
|
||||||
|
- Элементы: **47**, только у нас: 18, только в рефе: 11
|
||||||
|
|
||||||
|
### Токены — Light
|
||||||
|
|
||||||
| token | ours | ref |
|
| token | ours | ref |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -26,7 +31,7 @@ _2026-08-28T14:54:53.564Z_
|
|||||||
| `--radius-lg` | — | .5rem |
|
| `--radius-lg` | — | .5rem |
|
||||||
| `--text-sm` | — | .875rem |
|
| `--text-sm` | — | .875rem |
|
||||||
|
|
||||||
## Токены — Dark
|
### Токены — Dark
|
||||||
|
|
||||||
| token | ours | ref |
|
| token | ours | ref |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -47,7 +52,7 @@ _2026-08-28T14:54:53.564Z_
|
|||||||
| `--radius-lg` | — | .5rem |
|
| `--radius-lg` | — | .5rem |
|
||||||
| `--text-sm` | — | .875rem |
|
| `--text-sm` | — | .875rem |
|
||||||
|
|
||||||
## Расхождения элементов (топ 47)
|
### Расхождения элементов (топ 47)
|
||||||
|
|
||||||
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "/"
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "/"
|
||||||
- **fontFamily**: `"IBM Plex Mono", monospace` → `"IBM Plex Sans", sans-serif`
|
- **fontFamily**: `"IBM Plex Mono", monospace` → `"IBM Plex Sans", sans-serif`
|
||||||
@@ -259,7 +264,7 @@ _2026-08-28T14:54:53.564Z_
|
|||||||
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(1)>p:nth-child(3)` — "Output is generated in-browser. Your fil"
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(1)>p:nth-child(3)` — "Output is generated in-browser. Your fil"
|
||||||
- **rect**: `799,2489 565x17` → `799,1425 565x17`
|
- **rect**: `799,2489 565x17` → `799,1425 565x17`
|
||||||
|
|
||||||
## Только у нас (структурно)
|
### Только у нас
|
||||||
|
|
||||||
- `/ DEMO`
|
- `/ DEMO`
|
||||||
- `+ Add tool`
|
- `+ Add tool`
|
||||||
@@ -280,7 +285,7 @@ _2026-08-28T14:54:53.564Z_
|
|||||||
- `v0.1.0`
|
- `v0.1.0`
|
||||||
- `© 2026`
|
- `© 2026`
|
||||||
|
|
||||||
## Только в рефе (структурно)
|
### Только в рефе
|
||||||
|
|
||||||
- `/
|
- `/
|
||||||
WORKSPACE`
|
WORKSPACE`
|
||||||
@@ -297,3 +302,666 @@ _2026-08-28T14:54:53.564Z_
|
|||||||
px`
|
px`
|
||||||
- `v2.4.0`
|
- `v2.4.0`
|
||||||
- `© 2024`
|
- `© 2024`
|
||||||
|
|
||||||
|
|
||||||
|
## /preview/list-tools (vs list-tools.html)
|
||||||
|
|
||||||
|
- Токены light: **10**, dark: **16**
|
||||||
|
- Элементы: **30**, только у нас: 257, только в рефе: 54
|
||||||
|
|
||||||
|
### Токены — Light
|
||||||
|
|
||||||
|
| token | ours | ref |
|
||||||
|
|---|---|---|
|
||||||
|
| `--danger` | #e5484d | — |
|
||||||
|
| `--success` | #25a96a | — |
|
||||||
|
| `--text-sm--line-height` | — | calc(1.25 / .875) |
|
||||||
|
| `--text-xs--line-height` | — | calc(1 / .75) |
|
||||||
|
| `--font-weight-medium` | — | 500 |
|
||||||
|
| `--spacing` | — | .25rem |
|
||||||
|
| `--text-xs` | — | .75rem |
|
||||||
|
| `--radius-md` | — | .375rem |
|
||||||
|
| `--radius-lg` | — | .5rem |
|
||||||
|
| `--text-sm` | — | .875rem |
|
||||||
|
|
||||||
|
### Токены — Dark
|
||||||
|
|
||||||
|
| token | ours | ref |
|
||||||
|
|---|---|---|
|
||||||
|
| `--panel` | #182129 | #f8fafb |
|
||||||
|
| `--blue` | #54a2ff | #1769d2 |
|
||||||
|
| `--muted` | #91a0ac | #6d7883 |
|
||||||
|
| `--background` | #11171d | #eef1f4 |
|
||||||
|
| `--danger` | #ff7479 | — |
|
||||||
|
| `--foreground` | #e8eef2 | #17212b |
|
||||||
|
| `--success` | #25a96a | — |
|
||||||
|
| `--line` | #33414c | #cbd3da |
|
||||||
|
| `--text-sm--line-height` | — | calc(1.25 / .875) |
|
||||||
|
| `--text-xs--line-height` | — | calc(1 / .75) |
|
||||||
|
| `--font-weight-medium` | — | 500 |
|
||||||
|
| `--spacing` | — | .25rem |
|
||||||
|
| `--text-xs` | — | .75rem |
|
||||||
|
| `--radius-md` | — | .375rem |
|
||||||
|
| `--radius-lg` | — | .5rem |
|
||||||
|
| `--text-sm` | — | .875rem |
|
||||||
|
|
||||||
|
### Расхождения элементов (топ 30)
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(2)` — "Catalog"
|
||||||
|
- **fontSize**: `12px` → `10px`
|
||||||
|
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
|
||||||
|
- **letterSpacing**: `0.48px` → `normal`
|
||||||
|
- **rect**: `621,24 54x15` → `832,26 39x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(1)` — "Workspace"
|
||||||
|
- **fontSize**: `12px` → `10px`
|
||||||
|
- **letterSpacing**: `0.48px` → `normal`
|
||||||
|
- **rect**: `534,24 69x15` → `761,26 49x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(3)` — "Gradient"
|
||||||
|
- **fontSize**: `12px` → `10px`
|
||||||
|
- **letterSpacing**: `0.48px` → `normal`
|
||||||
|
- **rect**: `693,24 61x15` → `893,26 44x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(4)` — "Background remover"
|
||||||
|
- **fontSize**: `12px` → `10px`
|
||||||
|
- **letterSpacing**: `0.48px` → `normal`
|
||||||
|
- **rect**: `773,24 138x15` → `959,26 99x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(2)>div:nth-child(2)>a:nth-child(1)>span:nth-child(2)>span:nth-child(1)` — "Convert JPG to PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `104,302 175x19` → `131,494 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(2)>div:nth-child(2)>a:nth-child(2)>span:nth-child(2)>span:nth-child(1)` — "Convert WebP to PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `364,311 175x19` → `131,610 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(2)>div:nth-child(2)>a:nth-child(7)>span:nth-child(2)>span:nth-child(1)` — "PNG to Base64"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `364,429 175x19` → `131,726 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(2)>div:nth-child(2)>a:nth-child(9)>span:nth-child(2)>span:nth-child(1)` — "PNG to Data URI"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `885,429 175x19` → `131,842 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(2)>div:nth-child(2)>a:nth-child(17)>span:nth-child(2)>span:nth-child(1)` — "Convert PNG to JPG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `364,717 175x19` → `131,958 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(3)>div:nth-child(2)>a:nth-child(8)>span:nth-child(2)>span:nth-child(1)` — "Round corners PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `625,1239 175x19` → `807,726 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(3)>div:nth-child(2)>a:nth-child(11)>span:nth-child(2)>span:nth-child(1)` — "Outline PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `104,1384 175x19` → `807,842 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(4)>div:nth-child(2)>a:nth-child(7)>span:nth-child(2)>span:nth-child(1)` — "Grayscale PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `364,2096 175x19` → `131,1276 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(4)>div:nth-child(2)>a:nth-child(8)>span:nth-child(2)>span:nth-child(1)` — "Invert colors PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `625,2104 175x19` → `131,1392 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(4)>div:nth-child(2)>a:nth-child(10)>span:nth-child(2)>span:nth-child(1)` — "Change PNG opacity"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `1146,2096 175x19` → `807,958 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(4)>div:nth-child(2)>a:nth-child(20)>span:nth-child(2)>span:nth-child(1)` — "Temperature PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `1146,2361 175x19` → `131,1624 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(5)>div:nth-child(2)>a:nth-child(1)>span:nth-child(2)>span:nth-child(1)` — "Resize PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `104,2731 175x19` → `807,1160 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(5)>div:nth-child(2)>a:nth-child(2)>span:nth-child(2)>span:nth-child(1)` — "Crop PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `364,2740 175x19` → `807,1276 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(5)>div:nth-child(2)>a:nth-child(3)>span:nth-child(2)>span:nth-child(1)` — "Rotate PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `625,2748 175x19` → `807,1392 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(5)>div:nth-child(2)>a:nth-child(4)>span:nth-child(2)>span:nth-child(1)` — "Flip PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `885,2757 175x19` → `807,1508 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(5)>div:nth-child(2)>a:nth-child(5)>span:nth-child(2)>span:nth-child(1)` — "Add padding to PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `1146,2748 175x19` → `807,1624 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(6)>div:nth-child(2)>a:nth-child(1)>span:nth-child(2)>span:nth-child(1)` — "Blur PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `104,3396 175x19` → `131,1826 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(6)>div:nth-child(2)>a:nth-child(2)>span:nth-child(2)>span:nth-child(1)` — "Sharpen PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `364,3387 175x19` → `131,1942 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(6)>div:nth-child(2)>a:nth-child(3)>span:nth-child(2)>span:nth-child(1)` — "Vignette PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `625,3404 175x19` → `131,2058 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(6)>div:nth-child(2)>a:nth-child(8)>span:nth-child(2)>span:nth-child(1)` — "JPEG artifacts"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `625,3540 175x19` → `131,2174 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(8)>div:nth-child(2)>a:nth-child(9)>span:nth-child(2)>span:nth-child(1)` — "PNG info"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `885,4142 175x19` → `807,1826 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(8)>div:nth-child(2)>a:nth-child(13)>span:nth-child(2)>span:nth-child(1)` — "PNG orientation"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `625,4287 175x19` → `807,2174 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(9)>div:nth-child(2)>a:nth-child(7)>span:nth-child(2)>span:nth-child(1)` — "Create gradient PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `13px`
|
||||||
|
- **rect**: `364,4632 175x19` → `131,1160 488x15`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(3)>div:nth-child(4)>button:nth-child(2)` — "EN"
|
||||||
|
- **letterSpacing**: `0.6px` → `normal`
|
||||||
|
- **rect**: `1349,19 32x25` → `1352,20 29x24`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(1)>span:nth-child(2)` — "easy-png-tools"
|
||||||
|
- **rect**: `98,23 118x18` → `98,23 108x17`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(3)>div:nth-child(4)>button:nth-child(1)` — "RU"
|
||||||
|
- **letterSpacing**: `0.6px` → `normal`
|
||||||
|
|
||||||
|
### Только у нас
|
||||||
|
|
||||||
|
- `/ LIST-TOOLS`
|
||||||
|
- `CATALOG`
|
||||||
|
- `All tools`
|
||||||
|
- `Каталог всех PNG-инструментов, сгруппиро`
|
||||||
|
- `convert`
|
||||||
|
- `Convert`
|
||||||
|
- `Opens a JPEG and saves it as lossless PN`
|
||||||
|
- `Re-encodes a WebP image into universal P`
|
||||||
|
- `Convert GIF to PNG`
|
||||||
|
- `Extracts the first frame of a GIF animat`
|
||||||
|
- `Convert BMP to PNG`
|
||||||
|
- `Re-encodes BMP into compact lossless PNG`
|
||||||
|
- `Convert ICO to PNG`
|
||||||
|
- `Turns an .ico icon into a regular PNG of`
|
||||||
|
- `Convert PNG to BMP`
|
||||||
|
- `Saves the image as 24-bit BMP without an`
|
||||||
|
- `Encodes the image into a base64 string f`
|
||||||
|
- `Base64 to PNG`
|
||||||
|
- `Decodes a base64 string or data-uri back`
|
||||||
|
- `Builds a full data-uri (data:image/png;b`
|
||||||
|
- `Data URI to PNG`
|
||||||
|
- `Decodes data:image/…;base64,… back into `
|
||||||
|
- `PNG to HEX pixels`
|
||||||
|
- `Shows all pixels as rrggbbaa hex values `
|
||||||
|
- `HEX pixels to PNG`
|
||||||
|
- `Assembles an image from rrggbbaa hex val`
|
||||||
|
- `PNG to Bytes`
|
||||||
|
- `Lists every pixel as four decimal bytes `
|
||||||
|
- `Bytes to PNG`
|
||||||
|
- `Assembles an image from decimal RGBA byt`
|
||||||
|
- `PNG to RGB Values`
|
||||||
|
- `Lists every pixel as rgba(r, g, b, a), o`
|
||||||
|
- `RGB Values to PNG`
|
||||||
|
- `Assembles an image from rgba(r, g, b, a)`
|
||||||
|
- `Transparency is composited over the chos`
|
||||||
|
- `Convert PNG to WebP`
|
||||||
|
- `Re-encodes the image into WebP with adju`
|
||||||
|
- `Compress PNG`
|
||||||
|
- `Shrinks the PNG by reducing its palette `
|
||||||
|
- `Reduce PNG to Size`
|
||||||
|
- `Binary-searches the palette size until t`
|
||||||
|
- `SVG to PNG`
|
||||||
|
- `Decodes SVG markup into a raster image. `
|
||||||
|
- `alpha`
|
||||||
|
- `Alpha & transparency`
|
||||||
|
- `Circle Mask PNG`
|
||||||
|
- `Cuts the image into a circle. Diameter i`
|
||||||
|
- `Square Mask PNG`
|
||||||
|
- `Cuts the image into a rectangle with sid`
|
||||||
|
- `Star Mask PNG`
|
||||||
|
- `Cuts the image into an n-pointed star wi`
|
||||||
|
- `Wavy Mask PNG`
|
||||||
|
- `Cuts the image into a wavy-edged circle:`
|
||||||
|
- `Remove alpha channel PNG`
|
||||||
|
- `Composites the image over a white backgr`
|
||||||
|
- `Set alpha channel PNG`
|
||||||
|
- `Assigns the same opacity to all pixels; `
|
||||||
|
- `Extract alpha mask PNG`
|
||||||
|
- `Turns transparency into a black-and-whit`
|
||||||
|
- `Clips corners by a radius set as a perce`
|
||||||
|
|
||||||
|
### Только в рефе
|
||||||
|
|
||||||
|
- `/
|
||||||
|
CATALOG`
|
||||||
|
- `/`
|
||||||
|
- `EASY-PNG-TOOLS / CATALOG`
|
||||||
|
- `Tool catalog`
|
||||||
|
- `Focused utilities for working with PNG. `
|
||||||
|
- `32`
|
||||||
|
- `ALL`
|
||||||
|
- `CONVERT`
|
||||||
|
- `TRANSPARENCY`
|
||||||
|
- `COLOR`
|
||||||
|
- `GEOMETRY`
|
||||||
|
- `FILTERS`
|
||||||
|
- `ANALYZE`
|
||||||
|
- `05
|
||||||
|
TOOLS`
|
||||||
|
- `Re-encode JPEG files as lossless PNG whi`
|
||||||
|
- `01`
|
||||||
|
- `Turn WebP images into a universal PNG fo`
|
||||||
|
- `02`
|
||||||
|
- `Encode an image as a base64 string for e`
|
||||||
|
- `03`
|
||||||
|
- `Build a complete data URI ready for HTML`
|
||||||
|
- `04`
|
||||||
|
- `Composite transparency over a selected b`
|
||||||
|
- `05`
|
||||||
|
- `Remove background PNG`
|
||||||
|
- `Remove a solid background by color, tole`
|
||||||
|
- `Extract alpha mask`
|
||||||
|
- `Turn the alpha channel into a clean blac`
|
||||||
|
- `Clip the image corners by a precise radi`
|
||||||
|
- `Add a colored ring around opaque content`
|
||||||
|
- `Multiply the alpha channel while keeping`
|
||||||
|
- `Generate a smooth transition between two`
|
||||||
|
- `Convert the image to luminance-based sha`
|
||||||
|
- `Invert every color channel while leaving`
|
||||||
|
- `Brightness & contrast`
|
||||||
|
- `Adjust brightness and contrast across a `
|
||||||
|
- `Make an image warmer or cooler with a si`
|
||||||
|
- `Scale an image with bilinear interpolati`
|
||||||
|
- `Cut a rectangular area with exact coordi`
|
||||||
|
- `Rotate by 90, 180, or 270 degrees withou`
|
||||||
|
- `Mirror the image horizontally or vertica`
|
||||||
|
- `Expand the canvas on all sides by a chos`
|
||||||
|
- `04
|
||||||
|
TOOLS`
|
||||||
|
- `Apply a fast Gaussian-style blur with tr`
|
||||||
|
- `Emphasize edges with an adjustable sharp`
|
||||||
|
- `Smoothly darken the image edges while pr`
|
||||||
|
- `Simulate low-quality JPEG recompression `
|
||||||
|
- `Inspect dimensions, alpha presence, and `
|
||||||
|
- `Check grayscale`
|
||||||
|
- `Report whether the image contains only s`
|
||||||
|
- `Check transparency`
|
||||||
|
- `Detect transparent and semi-transparent `
|
||||||
|
- `Classify the image as portrait, landscap`
|
||||||
|
- `•`
|
||||||
|
|
||||||
|
|
||||||
|
## /preview/tools/linear-gradient-png (vs gradient.html)
|
||||||
|
|
||||||
|
- Токены light: **10**, dark: **16**
|
||||||
|
- Элементы: **7**, только у нас: 111, только в рефе: 30
|
||||||
|
|
||||||
|
### Токены — Light
|
||||||
|
|
||||||
|
| token | ours | ref |
|
||||||
|
|---|---|---|
|
||||||
|
| `--danger` | #e5484d | — |
|
||||||
|
| `--success` | #25a96a | — |
|
||||||
|
| `--text-sm--line-height` | — | calc(1.25 / .875) |
|
||||||
|
| `--text-xs--line-height` | — | calc(1 / .75) |
|
||||||
|
| `--font-weight-medium` | — | 500 |
|
||||||
|
| `--spacing` | — | .25rem |
|
||||||
|
| `--text-xs` | — | .75rem |
|
||||||
|
| `--radius-md` | — | .375rem |
|
||||||
|
| `--radius-lg` | — | .5rem |
|
||||||
|
| `--text-sm` | — | .875rem |
|
||||||
|
|
||||||
|
### Токены — Dark
|
||||||
|
|
||||||
|
| token | ours | ref |
|
||||||
|
|---|---|---|
|
||||||
|
| `--panel` | #182129 | #f8fafb |
|
||||||
|
| `--blue` | #54a2ff | #1769d2 |
|
||||||
|
| `--muted` | #91a0ac | #6d7883 |
|
||||||
|
| `--background` | #11171d | #eef1f4 |
|
||||||
|
| `--danger` | #ff7479 | — |
|
||||||
|
| `--foreground` | #e8eef2 | #17212b |
|
||||||
|
| `--success` | #25a96a | — |
|
||||||
|
| `--line` | #33414c | #cbd3da |
|
||||||
|
| `--text-sm--line-height` | — | calc(1.25 / .875) |
|
||||||
|
| `--text-xs--line-height` | — | calc(1 / .75) |
|
||||||
|
| `--font-weight-medium` | — | 500 |
|
||||||
|
| `--spacing` | — | .25rem |
|
||||||
|
| `--text-xs` | — | .75rem |
|
||||||
|
| `--radius-md` | — | .375rem |
|
||||||
|
| `--radius-lg` | — | .5rem |
|
||||||
|
| `--text-sm` | — | .875rem |
|
||||||
|
|
||||||
|
### Расхождения элементов (топ 7)
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(3)` — "Gradient"
|
||||||
|
- **fontSize**: `12px` → `10px`
|
||||||
|
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
|
||||||
|
- **letterSpacing**: `0.48px` → `normal`
|
||||||
|
- **rect**: `754,24 61x15` → `930,26 44x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(1)` — "Workspace"
|
||||||
|
- **fontSize**: `12px` → `10px`
|
||||||
|
- **letterSpacing**: `0.48px` → `normal`
|
||||||
|
- **rect**: `596,24 69x15` → `798,26 49x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(2)` — "Catalog"
|
||||||
|
- **fontSize**: `12px` → `10px`
|
||||||
|
- **letterSpacing**: `0.48px` → `normal`
|
||||||
|
- **rect**: `683,24 54x15` → `870,26 39x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(4)` — "Background remover"
|
||||||
|
- **fontSize**: `12px` → `10px`
|
||||||
|
- **letterSpacing**: `0.48px` → `normal`
|
||||||
|
- **rect**: `834,24 138x15` → `996,26 99x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(3)>div:nth-child(4)>button:nth-child(2)` — "EN"
|
||||||
|
- **letterSpacing**: `0.6px` → `normal`
|
||||||
|
- **rect**: `1349,19 32x25` → `1352,20 29x24`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(1)>span:nth-child(2)` — "easy-png-tools"
|
||||||
|
- **rect**: `98,23 118x18` → `98,23 108x17`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(3)>div:nth-child(4)>button:nth-child(1)` — "RU"
|
||||||
|
- **letterSpacing**: `0.6px` → `normal`
|
||||||
|
|
||||||
|
### Только у нас
|
||||||
|
|
||||||
|
- `/ TOOLS / LINEAR-GRADIENT-PNG`
|
||||||
|
- `STEPS`
|
||||||
|
- `Pipeline`
|
||||||
|
- `AUTO`
|
||||||
|
- `01`
|
||||||
|
- `generate`
|
||||||
|
- `Create gradient PNG`
|
||||||
|
- `Width`
|
||||||
|
- `Height`
|
||||||
|
- `Start color`
|
||||||
|
- `End color`
|
||||||
|
- `Direction`
|
||||||
|
- `Horizontal`
|
||||||
|
- `Vertical`
|
||||||
|
- `Add step`
|
||||||
|
- `Split PNG into HSL`
|
||||||
|
- `Split PNG into HSV`
|
||||||
|
- `Split PNG into HSI`
|
||||||
|
- `Convert PNG to CMYK Colors`
|
||||||
|
- `Convert PNG to YCbCr Colors`
|
||||||
|
- `Convert PNG to LAB Colors`
|
||||||
|
- `Show Transparent Areas PNG`
|
||||||
|
- `Show Grayscale Pixels PNG`
|
||||||
|
- `Show Color Pixels PNG`
|
||||||
|
- `Light Pixel Mask PNG`
|
||||||
|
- `Dark Pixel Mask PNG`
|
||||||
|
- `Unique Color Mask PNG`
|
||||||
|
- `Convert JPG to PNG`
|
||||||
|
- `Convert WebP to PNG`
|
||||||
|
- `Convert GIF to PNG`
|
||||||
|
- `Convert BMP to PNG`
|
||||||
|
- `Convert ICO to PNG`
|
||||||
|
- `Convert PNG to BMP`
|
||||||
|
- `Resize PNG`
|
||||||
|
- `Crop PNG`
|
||||||
|
- `Rotate PNG`
|
||||||
|
- `Flip PNG`
|
||||||
|
- `Add padding to PNG`
|
||||||
|
- `Add border to PNG`
|
||||||
|
- `Fit PNG onto background`
|
||||||
|
- `Tile PNG`
|
||||||
|
- `Circle Mask PNG`
|
||||||
|
- `Square Mask PNG`
|
||||||
|
- `Star Mask PNG`
|
||||||
|
- `Wavy Mask PNG`
|
||||||
|
- `Center PNG by content`
|
||||||
|
- `Blur PNG`
|
||||||
|
- `Sharpen PNG`
|
||||||
|
- `Grayscale PNG`
|
||||||
|
- `Invert colors PNG`
|
||||||
|
- `Brightness & contrast PNG`
|
||||||
|
- `Change PNG opacity`
|
||||||
|
- `Sepia effect`
|
||||||
|
- `Change hue PNG`
|
||||||
|
- `Extract channel PNG`
|
||||||
|
- `Swap channels PNG`
|
||||||
|
- `Black & white threshold PNG`
|
||||||
|
- `Posterize PNG`
|
||||||
|
- `Two colors PNG`
|
||||||
|
- `Convert PNG to JPG`
|
||||||
|
|
||||||
|
### Только в рефе
|
||||||
|
|
||||||
|
- `/
|
||||||
|
GRADIENT`
|
||||||
|
- `/`
|
||||||
|
- `Gradient background.`
|
||||||
|
- `Create a clean, export-ready gradient wi`
|
||||||
|
- `GRADIENT SETTINGS`
|
||||||
|
- `Configure output`
|
||||||
|
- `TOOL 01`
|
||||||
|
- `GRADIENT TYPE`
|
||||||
|
- `Linear`
|
||||||
|
- `Radial`
|
||||||
|
- `COLOR STOPS`
|
||||||
|
- `→`
|
||||||
|
- `135
|
||||||
|
°`
|
||||||
|
- `0° →`
|
||||||
|
- `45° ↗`
|
||||||
|
- `90° ↑`
|
||||||
|
- `135° ↖`
|
||||||
|
- `180° ←`
|
||||||
|
- `225° ↙`
|
||||||
|
- `270° ↓`
|
||||||
|
- `315° ↘`
|
||||||
|
- `100
|
||||||
|
%`
|
||||||
|
- `OUTPUT PREVIEW`
|
||||||
|
- `gradient.png`
|
||||||
|
- `PNG`
|
||||||
|
- `GENERATED CSS`
|
||||||
|
- `background: linear-gradient(135deg, #176`
|
||||||
|
- `v2.4.0`
|
||||||
|
- `gradient tool · local-only`
|
||||||
|
- `© 2024`
|
||||||
|
|
||||||
|
|
||||||
|
## /preview/tools/remove-background-png (vs background-remover.html)
|
||||||
|
|
||||||
|
- Токены light: **10**, dark: **16**
|
||||||
|
- Элементы: **8**, только у нас: 110, только в рефе: 28
|
||||||
|
|
||||||
|
### Токены — Light
|
||||||
|
|
||||||
|
| token | ours | ref |
|
||||||
|
|---|---|---|
|
||||||
|
| `--danger` | #e5484d | — |
|
||||||
|
| `--success` | #25a96a | — |
|
||||||
|
| `--text-sm--line-height` | — | calc(1.25 / .875) |
|
||||||
|
| `--text-xs--line-height` | — | calc(1 / .75) |
|
||||||
|
| `--font-weight-medium` | — | 500 |
|
||||||
|
| `--spacing` | — | .25rem |
|
||||||
|
| `--text-xs` | — | .75rem |
|
||||||
|
| `--radius-md` | — | .375rem |
|
||||||
|
| `--radius-lg` | — | .5rem |
|
||||||
|
| `--text-sm` | — | .875rem |
|
||||||
|
|
||||||
|
### Токены — Dark
|
||||||
|
|
||||||
|
| token | ours | ref |
|
||||||
|
|---|---|---|
|
||||||
|
| `--panel` | #182129 | #f8fafb |
|
||||||
|
| `--blue` | #54a2ff | #1769d2 |
|
||||||
|
| `--muted` | #91a0ac | #6d7883 |
|
||||||
|
| `--background` | #11171d | #eef1f4 |
|
||||||
|
| `--danger` | #ff7479 | — |
|
||||||
|
| `--foreground` | #e8eef2 | #17212b |
|
||||||
|
| `--success` | #25a96a | — |
|
||||||
|
| `--line` | #33414c | #cbd3da |
|
||||||
|
| `--text-sm--line-height` | — | calc(1.25 / .875) |
|
||||||
|
| `--text-xs--line-height` | — | calc(1 / .75) |
|
||||||
|
| `--font-weight-medium` | — | 500 |
|
||||||
|
| `--spacing` | — | .25rem |
|
||||||
|
| `--text-xs` | — | .75rem |
|
||||||
|
| `--radius-md` | — | .375rem |
|
||||||
|
| `--radius-lg` | — | .5rem |
|
||||||
|
| `--text-sm` | — | .875rem |
|
||||||
|
|
||||||
|
### Расхождения элементов (топ 8)
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(4)` — "Background remover"
|
||||||
|
- **fontSize**: `12px` → `10px`
|
||||||
|
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
|
||||||
|
- **letterSpacing**: `0.48px` → `normal`
|
||||||
|
- **rect**: `841,24 138x15` → `978,26 99x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(1)` — "Workspace"
|
||||||
|
- **fontSize**: `12px` → `10px`
|
||||||
|
- **letterSpacing**: `0.48px` → `normal`
|
||||||
|
- **rect**: `603,24 69x15` → `780,26 49x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(2)` — "Catalog"
|
||||||
|
- **fontSize**: `12px` → `10px`
|
||||||
|
- **letterSpacing**: `0.48px` → `normal`
|
||||||
|
- **rect**: `690,24 54x15` → `851,26 39x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(3)` — "Gradient"
|
||||||
|
- **fontSize**: `12px` → `10px`
|
||||||
|
- **letterSpacing**: `0.48px` → `normal`
|
||||||
|
- **rect**: `762,24 61x15` → `912,26 44x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>div:nth-child(2)>section:nth-child(1)>header:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "RESULT"
|
||||||
|
- **letterSpacing**: `1px` → `normal`
|
||||||
|
- **lineHeight**: `12px` → `normal`
|
||||||
|
- **rect**: `726,125 108x12` → `954,467 33x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(3)>div:nth-child(4)>button:nth-child(2)` — "EN"
|
||||||
|
- **letterSpacing**: `0.6px` → `normal`
|
||||||
|
- **rect**: `1349,19 32x25` → `1352,20 29x24`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(1)>span:nth-child(2)` — "easy-png-tools"
|
||||||
|
- **rect**: `98,23 118x18` → `98,23 108x17`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(3)>div:nth-child(4)>button:nth-child(1)` — "RU"
|
||||||
|
- **letterSpacing**: `0.6px` → `normal`
|
||||||
|
|
||||||
|
### Только у нас
|
||||||
|
|
||||||
|
- `/ TOOLS / REMOVE-BACKGROUND-PNG`
|
||||||
|
- `STEPS`
|
||||||
|
- `Pipeline`
|
||||||
|
- `AUTO`
|
||||||
|
- `01`
|
||||||
|
- `alpha`
|
||||||
|
- `Remove background PNG (smart)`
|
||||||
|
- `Background color`
|
||||||
|
- `Similarity tolerance, %`
|
||||||
|
- `10`
|
||||||
|
- `Outer regions only`
|
||||||
|
- `Edge smoothing, passes`
|
||||||
|
- `1`
|
||||||
|
- `Add step`
|
||||||
|
- `Split PNG into HSL`
|
||||||
|
- `Split PNG into HSV`
|
||||||
|
- `Split PNG into HSI`
|
||||||
|
- `Convert PNG to CMYK Colors`
|
||||||
|
- `Convert PNG to YCbCr Colors`
|
||||||
|
- `Convert PNG to LAB Colors`
|
||||||
|
- `Show Transparent Areas PNG`
|
||||||
|
- `Show Grayscale Pixels PNG`
|
||||||
|
- `Show Color Pixels PNG`
|
||||||
|
- `Light Pixel Mask PNG`
|
||||||
|
- `Dark Pixel Mask PNG`
|
||||||
|
- `Unique Color Mask PNG`
|
||||||
|
- `Convert JPG to PNG`
|
||||||
|
- `Convert WebP to PNG`
|
||||||
|
- `Convert GIF to PNG`
|
||||||
|
- `Convert BMP to PNG`
|
||||||
|
- `Convert ICO to PNG`
|
||||||
|
- `Convert PNG to BMP`
|
||||||
|
- `Resize PNG`
|
||||||
|
- `Crop PNG`
|
||||||
|
- `Rotate PNG`
|
||||||
|
- `Flip PNG`
|
||||||
|
- `Add padding to PNG`
|
||||||
|
- `Add border to PNG`
|
||||||
|
- `Fit PNG onto background`
|
||||||
|
- `Tile PNG`
|
||||||
|
- `Circle Mask PNG`
|
||||||
|
- `Square Mask PNG`
|
||||||
|
- `Star Mask PNG`
|
||||||
|
- `Wavy Mask PNG`
|
||||||
|
- `Center PNG by content`
|
||||||
|
- `Blur PNG`
|
||||||
|
- `Sharpen PNG`
|
||||||
|
- `Grayscale PNG`
|
||||||
|
- `Invert colors PNG`
|
||||||
|
- `Brightness & contrast PNG`
|
||||||
|
- `Change PNG opacity`
|
||||||
|
- `Sepia effect`
|
||||||
|
- `Change hue PNG`
|
||||||
|
- `Extract channel PNG`
|
||||||
|
- `Swap channels PNG`
|
||||||
|
- `Black & white threshold PNG`
|
||||||
|
- `Posterize PNG`
|
||||||
|
- `Two colors PNG`
|
||||||
|
- `Convert PNG to JPG`
|
||||||
|
- `Convert PNG to WebP`
|
||||||
|
|
||||||
|
### Только в рефе
|
||||||
|
|
||||||
|
- `/
|
||||||
|
BACKGROUND REMOVER`
|
||||||
|
- `/`
|
||||||
|
- `Remove background.`
|
||||||
|
- `Select a background color and tune the e`
|
||||||
|
- `REMOVER SETTINGS`
|
||||||
|
- `Configure detection`
|
||||||
|
- `TOOL 02`
|
||||||
|
- `BACKGROUND COLOR`
|
||||||
|
- `72
|
||||||
|
%`
|
||||||
|
- `strict edges`
|
||||||
|
- `more removal`
|
||||||
|
- `OUTER COLOR ONLY`
|
||||||
|
- `Only remove connected background pixels `
|
||||||
|
- `SHOW MASK`
|
||||||
|
- `Preview the detected transparency mask.`
|
||||||
|
- `SOURCE / RESULT`
|
||||||
|
- `comparison.png`
|
||||||
|
- `SOURCE`
|
||||||
|
- `original.png`
|
||||||
|
- `OBJECT`
|
||||||
|
- `1200 × 800`
|
||||||
|
- `removed-bg.png`
|
||||||
|
- `PNG`
|
||||||
|
- `PNG-24`
|
||||||
|
- `ENABLED`
|
||||||
|
- `v2.4.0`
|
||||||
|
- `background remover · local-only`
|
||||||
|
- `© 2024`
|
||||||
|
|||||||
+217
-99
@@ -1,11 +1,12 @@
|
|||||||
// Стиль-аудит: снимает design-токены (light/dark) и дерево вычисленных стилей
|
// Стиль-аудит: для каждого preview-маршрута снимает design-токены (light/dark)
|
||||||
// нашей preview-страницы и refs-html страницы, сравнивает и пишет отчёт.
|
// и дерево вычисленных стилей нашей страницы и соответствующей refs-html
|
||||||
// Запуск: pnpm audit (опции: --ours <url> --ref <file> --no-serve --route <path>)
|
// страницы, сравнивает и пишет сводный отчёт.
|
||||||
|
// Запуск: pnpm audit (опции: --route <path> --ref <file> --no-serve --ours <url>)
|
||||||
import { chromium } from "playwright";
|
import { chromium } from "playwright";
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
import { resolve } from "node:path";
|
import { resolve, join } from "node:path";
|
||||||
|
|
||||||
const args = (() => {
|
const args = (() => {
|
||||||
const a = {};
|
const a = {};
|
||||||
@@ -21,9 +22,7 @@ const args = (() => {
|
|||||||
|
|
||||||
const WEB = process.cwd();
|
const WEB = process.cwd();
|
||||||
const PORT = 5179;
|
const PORT = 5179;
|
||||||
const ROUTE = args.route || "/preview/demo";
|
const REFS_DIR = resolve(WEB, "../refs-html");
|
||||||
const OURS = args.ours || `http://127.0.0.1:${PORT}${ROUTE}`;
|
|
||||||
const REF = args.ref || resolve(WEB, "../refs-html/demo.html");
|
|
||||||
|
|
||||||
const FIELDS = [
|
const FIELDS = [
|
||||||
"fontFamily",
|
"fontFamily",
|
||||||
@@ -41,6 +40,104 @@ const FIELDS = [
|
|||||||
"gap",
|
"gap",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Динамические сегменты: явный список значений + сопоставление id → файл рефа
|
||||||
|
// (id инструмента в registry.ts не совпадает с именем refs-html-файла).
|
||||||
|
const DYNAMIC = {
|
||||||
|
"tools/[id]": {
|
||||||
|
ids: ["linear-gradient-png", "remove-background-png"],
|
||||||
|
refFor: (id) =>
|
||||||
|
id === "linear-gradient-png"
|
||||||
|
? "gradient.html"
|
||||||
|
: id === "remove-background-png"
|
||||||
|
? "background-remover.html"
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Маршруты, которые не с чем сравнивать (нет релевантного рефа). Например,
|
||||||
|
// `/preview` (индекс/воркспейс) в нашем приложении — своя страница, а
|
||||||
|
// `refs-html/index.html` — это зеркало demo из Next-рефа, не его реф.
|
||||||
|
const EXCLUDE = new Set(["/preview"]);
|
||||||
|
|
||||||
|
const refNameFor = (route) => {
|
||||||
|
if (route === "/preview") return "index.html";
|
||||||
|
const last = route.split("/").filter(Boolean).pop();
|
||||||
|
return `${last}.html`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function discoverRoutes() {
|
||||||
|
const base = resolve(WEB, "src/routes/preview");
|
||||||
|
const out = [];
|
||||||
|
const walk = (dir) => {
|
||||||
|
let ents;
|
||||||
|
try {
|
||||||
|
ents = readdirSync(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const e of ents) {
|
||||||
|
const p = join(dir, e.name);
|
||||||
|
if (e.isDirectory()) walk(p);
|
||||||
|
else if (e.name === "+page.svelte") {
|
||||||
|
const rel = p.slice(base.length).replace(/\\/g, "/");
|
||||||
|
const parts = rel.split("/").filter(Boolean);
|
||||||
|
parts.pop(); // +page.svelte
|
||||||
|
out.push(parts.length ? `/preview/${parts.join("/")}` : "/preview");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(base);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTargets() {
|
||||||
|
if (args.route) {
|
||||||
|
const ref = args.ref || refNameFor(args.route);
|
||||||
|
return [{ route: args.route, ref }];
|
||||||
|
}
|
||||||
|
const targets = [];
|
||||||
|
const seen = new Set();
|
||||||
|
const add = (route, ref) => {
|
||||||
|
if (!ref || seen.has(route)) return;
|
||||||
|
if (!refExists(ref)) {
|
||||||
|
console.warn(`[audit] ref not found, skipped: ${route} -> ${ref}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seen.add(route);
|
||||||
|
targets.push({ route, ref });
|
||||||
|
};
|
||||||
|
for (const route of discoverRoutes()) {
|
||||||
|
if (route.includes("[")) continue; // динамика — ниже
|
||||||
|
if (EXCLUDE.has(route)) {
|
||||||
|
console.log(`[audit] excluded (no relevant ref): ${route}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
add(route, refNameFor(route));
|
||||||
|
}
|
||||||
|
for (const [seg, cfg] of Object.entries(DYNAMIC)) {
|
||||||
|
for (const id of cfg.ids) {
|
||||||
|
add(`/preview/${seg.replace("[id]", id)}`, cfg.refFor(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Рефы без маршрута — просто сообщаем, не падаем.
|
||||||
|
const routed = new Set(targets.map((t) => t.ref));
|
||||||
|
for (const f of refsList()) {
|
||||||
|
if (!routed.has(f)) console.warn(`[audit] ref без маршрута: ${f}`);
|
||||||
|
}
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refsList() {
|
||||||
|
try {
|
||||||
|
return readdirSync(REFS_DIR).filter((f) => f.endsWith(".html"));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function refExists(name) {
|
||||||
|
return refsList().includes(name);
|
||||||
|
}
|
||||||
|
|
||||||
const waitFor = async (url, ms = 60000) => {
|
const waitFor = async (url, ms = 60000) => {
|
||||||
const t = Date.now();
|
const t = Date.now();
|
||||||
while (Date.now() - t < ms) {
|
while (Date.now() - t < ms) {
|
||||||
@@ -133,38 +230,9 @@ const snapshot = (page, url) =>
|
|||||||
return { tokensLight, tokensDark, tree: walk(document.body, "body") };
|
return { tokensLight, tokensDark, tree: walk(document.body, "body") };
|
||||||
}, url);
|
}, url);
|
||||||
|
|
||||||
async function main() {
|
|
||||||
let server;
|
|
||||||
if (!args.noServe) {
|
|
||||||
const bin = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
|
||||||
server = spawn(bin, ["dev", "--port", String(PORT), "--strictPort"], {
|
|
||||||
cwd: WEB,
|
|
||||||
stdio: "ignore",
|
|
||||||
shell: true,
|
|
||||||
detached: process.platform !== "win32",
|
|
||||||
});
|
|
||||||
await waitFor(OURS);
|
|
||||||
}
|
|
||||||
|
|
||||||
const browser = await chromium.launch();
|
|
||||||
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);
|
|
||||||
await page.waitForTimeout(400);
|
|
||||||
const ours = await snapshot(page, OURS);
|
|
||||||
|
|
||||||
await page.goto(pathToFileURL(REF).href, { waitUntil: "load" });
|
|
||||||
await page.evaluate(() => document.fonts.ready);
|
|
||||||
await page.waitForTimeout(400);
|
|
||||||
const ref = await snapshot(page, pathToFileURL(REF).href);
|
|
||||||
|
|
||||||
// token diff (игнорируем служебный мусор Tailwind/lightningcss — он
|
|
||||||
// ref-only и не относится к нашим смысловым токенам)
|
|
||||||
const TOK_NOISE = /^(--tw-|--lightningcss-|--default-)/;
|
const TOK_NOISE = /^(--tw-|--lightningcss-|--default-)/;
|
||||||
|
|
||||||
|
function diffPair(ours, ref) {
|
||||||
const tokDiff = (a, b) => {
|
const tokDiff = (a, b) => {
|
||||||
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
||||||
const rows = [];
|
const rows = [];
|
||||||
@@ -181,8 +249,6 @@ async function main() {
|
|||||||
dark: tokDiff(ours.tokensDark, ref.tokensDark),
|
dark: tokDiff(ours.tokensDark, ref.tokensDark),
|
||||||
};
|
};
|
||||||
|
|
||||||
// element diff: совпадающие по тексту элементы (структуры разные,
|
|
||||||
// поэтому nth-child-пути не совпадают — матчим по видимому тексту)
|
|
||||||
const byText = (tree) => {
|
const byText = (tree) => {
|
||||||
const m = new Map();
|
const m = new Map();
|
||||||
for (const e of tree) if (e.text) if (!m.has(e.text)) m.set(e.text, e);
|
for (const e of tree) if (e.text) if (!m.has(e.text)) m.set(e.text, e);
|
||||||
@@ -219,10 +285,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
for (const [k] of txtR) if (!txtO.has(k)) onlyRef.push(k);
|
for (const [k] of txtR) if (!txtO.has(k)) onlyRef.push(k);
|
||||||
|
|
||||||
const report = {
|
return {
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
ours: OURS,
|
|
||||||
ref: REF,
|
|
||||||
tokenDiffs,
|
tokenDiffs,
|
||||||
elementDiffs,
|
elementDiffs,
|
||||||
onlyOurs: onlyOurs.slice(0, 60),
|
onlyOurs: onlyOurs.slice(0, 60),
|
||||||
@@ -235,20 +298,120 @@ async function main() {
|
|||||||
onlyRef: onlyRef.length,
|
onlyRef: onlyRef.length,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function auditOne(page, target) {
|
||||||
|
const oursUrl = args.ours || `http://127.0.0.1:${PORT}${target.route}`;
|
||||||
|
const refUrl = pathToFileURL(resolve(REFS_DIR, target.ref)).href;
|
||||||
|
|
||||||
|
await page.goto(oursUrl, { waitUntil: "load" });
|
||||||
|
await page.evaluate(() => document.fonts.ready);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const ours = await snapshot(page, oursUrl);
|
||||||
|
|
||||||
|
await page.goto(refUrl, { waitUntil: "load" });
|
||||||
|
await page.evaluate(() => document.fonts.ready);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const ref = await snapshot(page, refUrl);
|
||||||
|
|
||||||
|
const diff = diffPair(ours, ref);
|
||||||
|
return { route: target.route, ref: target.ref, ...diff };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMarkdown(reports) {
|
||||||
|
const tokTable = (rows) =>
|
||||||
|
rows.length
|
||||||
|
? `| token | ours | ref |\n|---|---|---|\n` +
|
||||||
|
rows.map((r) => `| \`${r.key}\` | ${r.ours} | ${r.ref} |`).join("\n")
|
||||||
|
: "_все совпадают_";
|
||||||
|
const elBlock = (el) => {
|
||||||
|
const d = Object.entries(el.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 `### \`${el.path}\`${el.text ? ` — "${el.text}"` : ""}\n${d}`;
|
||||||
|
};
|
||||||
|
const summary =
|
||||||
|
`# Style audit (multi-route)\n\n_${new Date().toISOString()}_\n\n` +
|
||||||
|
`## Сводка\n\n| route | ref | tokL | tokD | els | onlyOurs | onlyRef |\n|---|---|---|---|---|---|---|\n` +
|
||||||
|
reports
|
||||||
|
.map(
|
||||||
|
(r) =>
|
||||||
|
`| \`${r.route}\` | ${r.ref} | ${r.counts.tokenLight} | ${r.counts.tokenDark} | ${r.counts.elements} | ${r.counts.onlyOurs} | ${r.counts.onlyRef} |`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
const sections = reports
|
||||||
|
.map((r) => {
|
||||||
|
const sorted = [...r.elementDiffs].sort(
|
||||||
|
(x, y) => Object.keys(y.deltas).length - Object.keys(x.deltas).length,
|
||||||
|
);
|
||||||
|
const els = sorted.length
|
||||||
|
? sorted.slice(0, 100).map(elBlock).join("\n\n")
|
||||||
|
: "_расхождений нет_";
|
||||||
|
return (
|
||||||
|
`\n\n## ${r.route} (vs ${r.ref})\n\n` +
|
||||||
|
`- Токены light: **${r.counts.tokenLight}**, dark: **${r.counts.tokenDark}**\n` +
|
||||||
|
`- Элементы: **${r.counts.elements}**, только у нас: ${r.counts.onlyOurs}, только в рефе: ${r.counts.onlyRef}\n\n` +
|
||||||
|
`### Токены — Light\n\n${tokTable(r.tokenDiffs.light)}\n\n` +
|
||||||
|
`### Токены — Dark\n\n${tokTable(r.tokenDiffs.dark)}\n\n` +
|
||||||
|
`### Расхождения элементов (топ ${Math.min(100, sorted.length)})\n\n${els}\n\n` +
|
||||||
|
`### Только у нас\n\n` +
|
||||||
|
(r.onlyOurs.length ? r.onlyOurs.map((p) => `- \`${p}\``).join("\n") : "_—_") +
|
||||||
|
`\n\n### Только в рефе\n\n` +
|
||||||
|
(r.onlyRef.length ? r.onlyRef.map((p) => `- \`${p}\``).join("\n") : "_—_")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
return summary + sections + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const targets = buildTargets();
|
||||||
|
if (!targets.length) {
|
||||||
|
console.error("no audit targets (refs-html/*.html missing?)");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`[audit] targets: ${targets.map((t) => t.route).join(", ")}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let server;
|
||||||
|
if (!args.noServe) {
|
||||||
|
const bin = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||||
|
server = spawn(bin, ["dev", "--port", String(PORT), "--strictPort"], {
|
||||||
|
cwd: WEB,
|
||||||
|
stdio: "ignore",
|
||||||
|
shell: true,
|
||||||
|
detached: process.platform !== "win32",
|
||||||
|
});
|
||||||
|
await waitFor(args.ours || `http://127.0.0.1:${PORT}${targets[0].route}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({
|
||||||
|
viewport: { width: 1440, height: 900 },
|
||||||
|
deviceScaleFactor: 1,
|
||||||
|
});
|
||||||
|
const reports = [];
|
||||||
|
try {
|
||||||
|
for (const target of targets) {
|
||||||
|
const rep = await auditOne(page, target);
|
||||||
|
reports.push(rep);
|
||||||
|
console.log(
|
||||||
|
`[audit] ${rep.route}: tokL=${rep.counts.tokenLight} tokD=${rep.counts.tokenDark} els=${rep.counts.elements} onlyOurs=${rep.counts.onlyOurs} onlyRef=${rep.counts.onlyRef}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
mkdirSync(resolve(WEB, "audit"), { recursive: true });
|
mkdirSync(resolve(WEB, "audit"), { recursive: true });
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
resolve(WEB, "audit/audit-report.json"),
|
resolve(WEB, "audit/audit-report.json"),
|
||||||
JSON.stringify(report, null, 2),
|
JSON.stringify({ generatedAt: new Date().toISOString(), reports }, 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}`,
|
|
||||||
);
|
);
|
||||||
|
writeFileSync(resolve(WEB, "audit/audit-report.md"), toMarkdown(reports));
|
||||||
console.log(`report: web/audit/audit-report.md`);
|
console.log(`report: web/audit/audit-report.md`);
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
@@ -264,51 +427,6 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMarkdown(report, ours, ref) {
|
|
||||||
const tokTable = (rows, a, b) =>
|
|
||||||
rows.length
|
|
||||||
? `| token | ours | ref |\n|---|---|---|\n` +
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
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");
|
|
||||||
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` +
|
|
||||||
`## Сводка\n\n- Токены light: **${report.counts.tokenLight}** расх.\n` +
|
|
||||||
`- Токены dark: **${report.counts.tokenDark}** расх.\n` +
|
|
||||||
`- Элементы (стиль/геометрия): **${report.counts.elements}** расх.\n` +
|
|
||||||
`- Только у нас: ${report.counts.onlyOurs}, только в рефе: ${report.counts.onlyRef}\n\n` +
|
|
||||||
`## Токены — Light\n\n${tokTable(report.tokenDiffs.light)}\n\n` +
|
|
||||||
`## Токены — 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")
|
|
||||||
: "_—_") +
|
|
||||||
`\n\n## Только в рефе (структурно)\n\n` +
|
|
||||||
(report.onlyRef.length
|
|
||||||
? report.onlyRef.map((p) => `- \`${p}\``).join("\n")
|
|
||||||
: "_—_") +
|
|
||||||
`\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((e) => {
|
main().catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user