chore: add audit script for compare with refs
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
# Demo fidelity: /preview/demo vs refs-html/demo.html
|
||||||
|
|
||||||
|
Метод: Playwright, viewport 1440×900, скриншоты обеих страниц + замер вычисленных
|
||||||
|
стилей ключевых элементов + пиксель-дифф (без чтения картинки моделью).
|
||||||
|
Пиксель-дифф: **24.9%** (наш скролл 1731px, реф 1579px). Зоны различий размазаны
|
||||||
|
по всей высоте → править надо глобально, не точечно.
|
||||||
|
|
||||||
|
Теперь процесс автоматизирован: `pnpm audit` (Playwright) пишет
|
||||||
|
`web/audit/audit-report.md` — light/dark токены + поэлементный дифф стилей по тексту.
|
||||||
|
Пиксель-дифф убран (неудобен для работы). Фиксы можно делать в любой момент и
|
||||||
|
перепроверять аудитом.
|
||||||
|
|
||||||
|
## Баги (объективно, правим в любом случае)
|
||||||
|
|
||||||
|
- **B1. Кнопки/инпуты рендерятся шрифтом Arial, а не IBM Plex Sans.**
|
||||||
|
Доказательство: пробы `Reset pipeline` → `fontFamily: Arial`. Причина: у `button/input`
|
||||||
|
не задан `font-family: inherit`. Фикс: добавить в reset `design2.css`
|
||||||
|
`button, input, select, textarea { font-family: inherit; }`. Влияет на все страницы. (P1)
|
||||||
|
- **B2. Двойной топбар.** Страница демо рендерит свой `.topbar` (brand / AUTO PIPELINE /
|
||||||
|
help / theme / lang), который перекрывается глобальным `fixed` Header из
|
||||||
|
`preview/+layout.svelte` (в нём только theme-toggle). В рефе — один топбар.
|
||||||
|
Контент демо-топбара (brand, статус, язык) визуально спрятан за фикс-хедером.
|
||||||
|
Фикс: убрать кастомный `.topbar` из демо и сделать хедер лейаута реф-стайл топбаром
|
||||||
|
(brand + version + status + help + theme + lang) — улучшит и C9/C10. (P1)
|
||||||
|
|
||||||
|
## Расхождения дизайна (нужно решение)
|
||||||
|
|
||||||
|
- **F1. Фон холста.** Реф: плоский серый `#EEF1F4`. Наш: белый + blueprint-сетка.
|
||||||
|
Решение: совпасть с рефом (убрать сетку на демо) или оставить сетку как сигнатуру нового дизайна.
|
||||||
|
- **F2. Бейдж типа шага** (BACKGROUND/TRANSFORM/STYLE). Реф: приглушённый серый `#6D787F`.
|
||||||
|
Наш (`StepCard` type → `Badge accent`): синий `#1769D2`. Решение: сделать приглушённым
|
||||||
|
(меняет `StepCard` глобально) или оставить синим.
|
||||||
|
- **F3. Подписи превью-тайлов** (SOURCE / STEP 01 / FINAL OUTPUT). Реф: синий `#1769D2`,
|
||||||
|
weight 400. Наш (`PreviewTile` label): серый, weight 500.
|
||||||
|
- **F4. Раскладка preview-stack.** Реф: одна колонка (5 строк, выше). Наш: 2 колонки (grid).
|
||||||
|
Решение: одна колонка (как реф) или оставить 2 колонки как выбор нового дизайна.
|
||||||
|
- **F5. Вес лейблов контролов.** Реф: 400. Наш (`MonoLabel`): 500. Мелко.
|
||||||
|
- **F6. Мета-подписи** (DIMENSIONS/FORMAT/SIZE): реф fs 9px, наш 10px. Мелко.
|
||||||
|
- **F7. Копирайт:** `Add tool` у нас `+ Add tool`; кнопка `Download result` — сверить стиль
|
||||||
|
(в рефе синяя с белым текстом 478×42). Пробы `Download result`/`Add tool`/`DIRECTION`
|
||||||
|
вернули NOT FOUND — скорее артефакт замера (вложенный текст/чип размера), проверить визуально.
|
||||||
|
- **F8. h1 line-height** чуть выше (наш 150px vs реф 115px) — мелкий зазор.
|
||||||
|
- **F9. Вертикальный ритм:** контент на ~152px выше, зазоры между секциями отличаются
|
||||||
|
(pipeline-head y 434 vs 400 и т.д.) — подровнять паддинги под реф.
|
||||||
|
- **F10. Внутренние отступы preview-панели** (Hide intermediate x 907 vs 1218) — паддинг панели.
|
||||||
|
|
||||||
|
## Темы (отложено — F1-F4)
|
||||||
|
|
||||||
|
Серый фон `#EEF1F4` — это **светлая** тема. Уточнение по F1: наш токен `--background`
|
||||||
|
уже равен `#EEF1F4` (совпадает с рефом); видимое отличие — только blueprint-сетка
|
||||||
|
поверх фона, а не «белый vs серый». То есть F1 = «сетка против плоского», не цвет фона.
|
||||||
|
|
||||||
|
Перед тем как сверять цвета бейджей/тайлов/раскладок (F2-F4), нужно сначала
|
||||||
|
зафиксировать палитры light/dark как единый источник токенов. Сейчас они есть в
|
||||||
|
`web/src/lib/styles/design2.css` (`:root` + `[data-theme="dark"]`), но не сверены с рефом.
|
||||||
|
|
||||||
|
**Как проверить темы в refs-html:**
|
||||||
|
|
||||||
|
- Открыть `refs-html/demo.html` (и соседние `gradient.html`, `background-remover.html`)
|
||||||
|
через Playwright/браузер.
|
||||||
|
- Нажать theme-toggle в топбаре рефа (иконка луны/солнца) — переключает тему
|
||||||
|
(скорее всего меняет атрибут/класс на `<html>` или `.page` и переопределяет CSS-переменные).
|
||||||
|
- Снять скриншоты светлой и тёмной темы, замерить computed-стиль `:root` переменных
|
||||||
|
рефа (через `getComputedStyle(document.documentElement).getPropertyValue('--...')`)
|
||||||
|
и сопоставить с нашими токенами в `design2.css`.
|
||||||
|
- Рефы, похоже, не доделаны по тёмной теме — зафиксировать, какие именно токены
|
||||||
|
меняются и где расхождения. На основе этого выровнять наши токены, затем вернуться
|
||||||
|
к F2 (бейдж типа), F3 (тайлы), F4 (раскладка preview-stack).
|
||||||
|
|
||||||
|
Решение по F1-F4 принимается только после сверки токенов светлой/тёмной темы с рефом.
|
||||||
|
|
||||||
|
**Состояние тем (уточнение):** наша dark-тема реализована в `design2.css`
|
||||||
|
(`[data-theme="dark"]`), но пока подключена только на preview-маршрутах (старые
|
||||||
|
страницы грузят `app.css`). Этого достаточно для сверки: токены, отличные от light,
|
||||||
|
соберём по всем `refs-html/*.html` (не только `demo.html`), остальные токены
|
||||||
|
не меняются. Light-токены уже совпадают с рефом (`--background`, `--panel`,
|
||||||
|
`--foreground`, `--muted`, `--line`, `--blue` равны).
|
||||||
|
|
||||||
|
## План действий (предлагаю)
|
||||||
|
|
||||||
|
1. Сразу поправить B1 + B2 (чёткие баги, улучшают все страницы).
|
||||||
|
2. По F1–F4 — решение за тобой (совпадать с рефом дословно ИЛИ оставить как сигнатуру нового дизайна).
|
||||||
|
3. F5–F10 — мелочи, подчистить в том же проходе, если решаем «совпадать с рефом».
|
||||||
|
4. Переснять скриншоты и перепроверить пиксель-дифф.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,377 @@
|
|||||||
|
# Style audit: http://127.0.0.1:5179/preview/demo
|
||||||
|
|
||||||
|
vs C:\+XBOCTuK\+life_projects\easy-png-tools\refs-html\demo.html
|
||||||
|
|
||||||
|
_2026-08-27T18:45:14.461Z_
|
||||||
|
|
||||||
|
## Сводка
|
||||||
|
|
||||||
|
- Токены light: **12** расх.
|
||||||
|
- Токены dark: **18** расх.
|
||||||
|
- Элементы (стиль/геометрия): **48** расх.
|
||||||
|
- Только у нас: 14, только в рефе: 7
|
||||||
|
|
||||||
|
## Токены — Light
|
||||||
|
|
||||||
|
| token | ours | ref |
|
||||||
|
|---|---|---|
|
||||||
|
| `--danger` | #e5484d | — |
|
||||||
|
| `--success` | #25a96a | — |
|
||||||
|
| `--font-sans` | "IBM Plex Sans", system-ui, -apple-system, sans-serif | "IBM Plex Sans", sans-serif |
|
||||||
|
| `--font-mono` | "IBM Plex Mono", ui-monospace, "SFMono-Regular", monospace | "IBM Plex Mono", monospace |
|
||||||
|
| `--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 |
|
||||||
|
| `--font-sans` | "IBM Plex Sans", system-ui, -apple-system, sans-serif | "IBM Plex Sans", sans-serif |
|
||||||
|
| `--font-mono` | "IBM Plex Mono", ui-monospace, "SFMono-Regular", monospace | "IBM Plex Mono", monospace |
|
||||||
|
| `--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 |
|
||||||
|
|
||||||
|
## Расхождения элементов (топ 48)
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(1)>span:nth-child(3)` — "BACKGROUND"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **color**: `rgb(23, 105, 210)` → `rgb(109, 120, 131)`
|
||||||
|
- **padding**: `2.4px 7.2px` → `0px`
|
||||||
|
- **letterSpacing**: `0.8px` → `1.2px`
|
||||||
|
- **borderRadius**: `4px` → `0px`
|
||||||
|
- **display**: `flex` → `inline`
|
||||||
|
- **gap**: `4.8px` → `normal`
|
||||||
|
- **rect**: `116,507 84x20` → `143,489 67x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(2)>div:nth-child(1)>span:nth-child(3)` — "TRANSFORM"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **color**: `rgb(23, 105, 210)` → `rgb(109, 120, 131)`
|
||||||
|
- **padding**: `2.4px 7.2px` → `0px`
|
||||||
|
- **letterSpacing**: `0.8px` → `1.2px`
|
||||||
|
- **borderRadius**: `4px` → `0px`
|
||||||
|
- **display**: `flex` → `inline`
|
||||||
|
- **gap**: `4.8px` → `normal`
|
||||||
|
- **rect**: `116,670 78x20` → `143,651 60x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(3)>div:nth-child(1)>span:nth-child(3)` — "STYLE"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **color**: `rgb(23, 105, 210)` → `rgb(109, 120, 131)`
|
||||||
|
- **padding**: `2.4px 7.2px` → `0px`
|
||||||
|
- **letterSpacing**: `0.8px` → `1.2px`
|
||||||
|
- **borderRadius**: `4px` → `0px`
|
||||||
|
- **display**: `flex` → `inline`
|
||||||
|
- **gap**: `4.8px` → `normal`
|
||||||
|
- **rect**: `116,785 50x20` → `143,799 34x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(2)>div:nth-child(4)>button:nth-child(1)` — "RU"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `11px` → `10px`
|
||||||
|
- **color**: `rgb(255, 255, 255)` → `rgb(238, 241, 244)`
|
||||||
|
- **backgroundColor**: `rgb(23, 105, 210)` → `rgb(23, 33, 43)`
|
||||||
|
- **padding**: `4.8px 11.2px` → `6px 9px`
|
||||||
|
- **letterSpacing**: `0.66px` → `normal`
|
||||||
|
- **rect**: `1307,20 37x24` → `1323,20 29x24`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>span:nth-child(1)` — "COLOR"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontWeight**: `500` → `400`
|
||||||
|
- **margin**: `0px` → `0px 0px 8px`
|
||||||
|
- **letterSpacing**: `1px` → `0.8px`
|
||||||
|
- **lineHeight**: `12px` → `normal`
|
||||||
|
- **display**: `block` → `flex`
|
||||||
|
- **rect**: `71,576 203x12` → `143,546 189x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(1)>span:nth-child(1)` — "SOURCE"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `10px` → `9px`
|
||||||
|
- **fontWeight**: `500` → `400`
|
||||||
|
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
|
||||||
|
- **letterSpacing**: `1px` → `normal`
|
||||||
|
- **lineHeight**: `12px` → `normal`
|
||||||
|
- **rect**: `806,320 262x12` → `808,663 30x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(2)>span:nth-child(1)` — "STEP 01"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `10px` → `9px`
|
||||||
|
- **fontWeight**: `500` → `400`
|
||||||
|
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
|
||||||
|
- **letterSpacing**: `1px` → `normal`
|
||||||
|
- **lineHeight**: `12px` → `normal`
|
||||||
|
- **rect**: `1094,320 262x12` → `1096,663 35x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(3)>span:nth-child(1)` — "STEP 02"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `10px` → `9px`
|
||||||
|
- **fontWeight**: `500` → `400`
|
||||||
|
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
|
||||||
|
- **letterSpacing**: `1px` → `normal`
|
||||||
|
- **lineHeight**: `12px` → `normal`
|
||||||
|
- **rect**: `806,752 262x12` → `808,1021 35x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(4)>span:nth-child(1)` — "STEP 03"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `10px` → `9px`
|
||||||
|
- **fontWeight**: `500` → `400`
|
||||||
|
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
|
||||||
|
- **letterSpacing**: `1px` → `normal`
|
||||||
|
- **lineHeight**: `12px` → `normal`
|
||||||
|
- **rect**: `1094,752 262x12` → `1096,1021 35x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(5)>span:nth-child(1)` — "FINAL OUTPUT"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `10px` → `9px`
|
||||||
|
- **fontWeight**: `500` → `400`
|
||||||
|
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
|
||||||
|
- **letterSpacing**: `1px` → `normal`
|
||||||
|
- **lineHeight**: `12px` → `normal`
|
||||||
|
- **rect**: `806,1183 262x12` → `808,1378 59x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(2)>button:nth-child(2)>span:nth-child(2)>span:nth-child(1)` — "1.2 MB"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `10px` → `11px`
|
||||||
|
- **color**: `rgb(255, 255, 255)` → `rgb(23, 33, 43)`
|
||||||
|
- **margin**: `0px` → `5px 0px 0px`
|
||||||
|
- **display**: `inline` → `block`
|
||||||
|
- **rect**: `1090,240 36x13` → `1246,291 79x13`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(2)>div:nth-child(4)>button:nth-child(2)` — "EN"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `11px` → `10px`
|
||||||
|
- **padding**: `4.8px 11.2px` → `6px 9px`
|
||||||
|
- **letterSpacing**: `0.66px` → `normal`
|
||||||
|
- **rect**: `1344,20 38x24` → `1352,20 29x24`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>button:nth-child(1)` — "100"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `11px` → `10px`
|
||||||
|
- **padding**: `4.8px 11.2px` → `0px`
|
||||||
|
- **letterSpacing**: `0.66px` → `normal`
|
||||||
|
- **rect**: `491,597 44x24` → `520,567 46x30`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>button:nth-child(2)` — "75"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `11px` → `10px`
|
||||||
|
- **padding**: `4.8px 11.2px` → `0px`
|
||||||
|
- **letterSpacing**: `0.66px` → `normal`
|
||||||
|
- **rect**: `535,597 38x24` → `566,567 46x30`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>button:nth-child(3)` — "50"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `11px` → `10px`
|
||||||
|
- **padding**: `4.8px 11.2px` → `0px`
|
||||||
|
- **letterSpacing**: `0.66px` → `normal`
|
||||||
|
- **rect**: `573,597 38x24` → `612,567 46x30`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>button:nth-child(4)` — "25"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `11px` → `10px`
|
||||||
|
- **padding**: `4.8px 11.2px` → `0px`
|
||||||
|
- **letterSpacing**: `0.66px` → `normal`
|
||||||
|
- **rect**: `611,597 38x24` → `658,567 46x30`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(4)>div:nth-child(2)>div:nth-child(2)>label:nth-child(2)>span:nth-child(1)` — "Preserve aspect ratio"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif` → `"IBM Plex Sans", sans-serif`
|
||||||
|
- **fontSize**: `13.6px` → `16px`
|
||||||
|
- **lineHeight**: `normal` → `24px`
|
||||||
|
- **display**: `block` → `inline`
|
||||||
|
- **rect**: `399,1036 129x18` → `432,1021 156x17`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(1)>span:nth-child(2)` — "01"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
|
||||||
|
- **padding**: `0px` → `17px 0px 0px 15px`
|
||||||
|
- **rect**: `95,510 13x14` → `59,466 46x150`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(1)>span:nth-child(4)` — "Gradient background"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `15px`
|
||||||
|
- **margin**: `0px` → `5px 0px 18px`
|
||||||
|
- **rect**: `208,508 467x19` → `143,510 157x18`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(2)>div:nth-child(1)>span:nth-child(2)` — "02"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
|
||||||
|
- **padding**: `0px` → `17px 0px 0px 15px`
|
||||||
|
- **rect**: `95,673 13x14` → `59,628 46x136`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(2)>div:nth-child(1)>span:nth-child(4)` — "Remove background"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `15px`
|
||||||
|
- **margin**: `0px` → `5px 0px 18px`
|
||||||
|
- **rect**: `201,671 473x19` → `143,672 140x18`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(3)>div:nth-child(1)>span:nth-child(2)` — "03"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
|
||||||
|
- **padding**: `0px` → `17px 0px 0px 15px`
|
||||||
|
- **rect**: `95,788 13x14` → `59,776 46x150`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(3)>div:nth-child(1)>span:nth-child(4)` — "Add outline"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `15px`
|
||||||
|
- **margin**: `0px` → `5px 0px 18px`
|
||||||
|
- **rect**: `174,785 501x19` → `143,820 91x18`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(4)>div:nth-child(1)>span:nth-child(2)` — "04"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
|
||||||
|
- **padding**: `0px` → `17px 0px 0px 15px`
|
||||||
|
- **rect**: `95,951 13x14` → `59,938 46x142`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(4)>div:nth-child(1)>span:nth-child(4)` — "Round corners"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `14.4px` → `15px`
|
||||||
|
- **margin**: `0px` → `5px 0px 18px`
|
||||||
|
- **rect**: `174,948 501x19` → `143,982 107x18`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(3)>div:nth-child(1)>span:nth-child(1)` — "DIMENSIONS"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `10px` → `9px`
|
||||||
|
- **letterSpacing**: `1px` → `normal`
|
||||||
|
- **rect**: `1150,215 70x13` → `1073,271 79x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>span:nth-child(1)` — "FORMAT"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `10px` → `9px`
|
||||||
|
- **letterSpacing**: `1px` → `normal`
|
||||||
|
- **rect**: `1150,239 42x13` → `1159,271 79x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(3)>div:nth-child(3)>span:nth-child(1)` — "SIZE"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **fontSize**: `10px` → `9px`
|
||||||
|
- **letterSpacing**: `1px` → `normal`
|
||||||
|
- **rect**: `1150,264 28x13` → `1246,271 79x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(1)>span:nth-child(2)` — "easy-png-tools"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **color**: `rgb(0, 0, 0)` → `rgb(23, 33, 43)`
|
||||||
|
- **rect**: `98,23 118x18` → `98,23 108x17`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "/"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **color**: `rgb(23, 105, 210)` → `rgb(109, 120, 131)`
|
||||||
|
- **rect**: `173,124 7x13` → `165,124 13x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(2)>div:nth-child(1)>h1:nth-child(1)` — "Build your image pipeline."
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif` → `"IBM Plex Sans", sans-serif`
|
||||||
|
- **lineHeight**: `normal` → `57.6px`
|
||||||
|
- **rect**: `58,155 474x150` → `58,154 482x115`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(3)>div:nth-child(1)>span:nth-child(2)` — "1200 × 800 px"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **margin**: `0px` → `5px 0px 0px`
|
||||||
|
- **rect**: `1236,214 86x14` → `1073,291 79x13`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>span:nth-child(2)` — "PNG-24"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **margin**: `0px` → `5px 0px 0px`
|
||||||
|
- **rect**: `1282,238 40x14` → `1159,291 79x13`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(2)>div:nth-child(1)>p:nth-child(2)` — "Chain simple tools together. Every chang"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif` → `"IBM Plex Sans", sans-serif`
|
||||||
|
- **rect**: `58,321 474x51` → `58,285 482x51`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(2)>div:nth-child(2)>span:nth-child(2)` — "source.png"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **rect**: `597,347 66x14` → `606,311 60x13`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(2)>div:nth-child(2)>b:nth-child(3)` — "1.8 MB"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **rect**: `671,347 40x14` → `674,311 36x13`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(3)>div:nth-child(1)>span:nth-child(1)` — "PROCESSING PIPELINE"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **rect**: `58,434 137x13` → `58,400 127x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(3)>div:nth-child(1)>strong:nth-child(2)>em:nth-child(1)` — "• LIVE"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **rect**: `184,457 36x13` → `173,425 33x12`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "PIPELINE OUTPUTS"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **rect**: `799,177 58x33` → `799,188 54x36`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(1)>strong:nth-child(2)` — "Visual history"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **rect**: `799,216 93x36` → `799,233 87x34`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "PNG"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **rect**: `923,498 28x19` → `923,478 27x18`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>span:nth-child(2)` — "original.png · 1200 × 800"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **rect**: `925,706 135x11` → `942,663 124x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(2)>div:nth-child(2)>div:nth-child(2)>span:nth-child(2)` — "gradient applied"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **rect**: `1262,706 86x11` → `1275,663 79x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>div:nth-child(2)>span:nth-child(2)` — "background removed"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **rect**: `962,1138 97x11` → `977,1021 89x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(4)>div:nth-child(2)>div:nth-child(2)>span:nth-child(2)` — "outline added"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **rect**: `1278,1138 70x11` → `1290,1021 64x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(5)>div:nth-child(2)>div:nth-child(2)>span:nth-child(2)` — "ready · PNG-24"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
- **rect**: `984,1569 76x11` → `997,1378 69x10`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>p:nth-child(3)` — "Output is generated in-browser. Your fil"
|
||||||
|
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif` → `"IBM Plex Sans", sans-serif`
|
||||||
|
- **rect**: `799,1624 565x17` → `799,1425 565x17`
|
||||||
|
|
||||||
|
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "EP"
|
||||||
|
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace` → `"IBM Plex Mono", monospace`
|
||||||
|
|
||||||
|
## Только у нас (структурно)
|
||||||
|
|
||||||
|
- `+ Add tool`
|
||||||
|
- `AUTO`
|
||||||
|
- `Gradient color`
|
||||||
|
- `DIRECTION`
|
||||||
|
- `135°`
|
||||||
|
- `OPACITY`
|
||||||
|
- `Automatic subject detection enabled`
|
||||||
|
- `WIDTH`
|
||||||
|
- `Outline width`
|
||||||
|
- `2px`
|
||||||
|
- `Outline color`
|
||||||
|
- `RADIUS`
|
||||||
|
- `Corner radius`
|
||||||
|
- `18px`
|
||||||
|
|
||||||
|
## Только в рефе (структурно)
|
||||||
|
|
||||||
|
- `/ DEMO`
|
||||||
|
- `135
|
||||||
|
°`
|
||||||
|
- `100%`
|
||||||
|
- `2
|
||||||
|
px`
|
||||||
|
- `18
|
||||||
|
px`
|
||||||
|
- `v2.4.0`
|
||||||
|
- `© 2024`
|
||||||
+3
-1
@@ -12,7 +12,8 @@
|
|||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "eslint ."
|
"lint": "eslint .",
|
||||||
|
"audit": "node scripts/audit.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-svelte": "^3.23.0",
|
"eslint-plugin-svelte": "^3.23.0",
|
||||||
"globals": "^17.11.0",
|
"globals": "^17.11.0",
|
||||||
|
"playwright": "^1.62.1",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"prettier-plugin-svelte": "^4.1.1",
|
"prettier-plugin-svelte": "^4.1.1",
|
||||||
"svelte": "^5.56.1",
|
"svelte": "^5.56.1",
|
||||||
|
|||||||
Generated
+29
@@ -42,6 +42,9 @@ importers:
|
|||||||
globals:
|
globals:
|
||||||
specifier: ^17.11.0
|
specifier: ^17.11.0
|
||||||
version: 17.11.0
|
version: 17.11.0
|
||||||
|
playwright:
|
||||||
|
specifier: ^1.62.1
|
||||||
|
version: 1.62.1
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.9.6
|
specifier: ^3.9.6
|
||||||
version: 3.9.6
|
version: 3.9.6
|
||||||
@@ -618,6 +621,11 @@ packages:
|
|||||||
flatted@3.4.4:
|
flatted@3.4.4:
|
||||||
resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
|
resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -832,6 +840,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
|
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
playwright-core@1.62.1:
|
||||||
|
resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
playwright@1.62.1:
|
||||||
|
resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
postcss-load-config@3.1.4:
|
postcss-load-config@3.1.4:
|
||||||
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
|
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -1632,6 +1650,9 @@ snapshots:
|
|||||||
|
|
||||||
flatted@3.4.4: {}
|
flatted@3.4.4: {}
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1788,6 +1809,14 @@ snapshots:
|
|||||||
|
|
||||||
picomatch@4.0.5: {}
|
picomatch@4.0.5: {}
|
||||||
|
|
||||||
|
playwright-core@1.62.1: {}
|
||||||
|
|
||||||
|
playwright@1.62.1:
|
||||||
|
dependencies:
|
||||||
|
playwright-core: 1.62.1
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.2
|
||||||
|
|
||||||
postcss-load-config@3.1.4(postcss@8.5.26):
|
postcss-load-config@3.1.4(postcss@8.5.26):
|
||||||
dependencies:
|
dependencies:
|
||||||
lilconfig: 2.1.0
|
lilconfig: 2.1.0
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
// Стиль-аудит: снимает design-токены (light/dark) и дерево вычисленных стилей
|
||||||
|
// нашей preview-страницы и refs-html страницы, сравнивает и пишет отчёт.
|
||||||
|
// Запуск: pnpm audit (опции: --ours <url> --ref <file> --no-serve --route <path>)
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
const args = (() => {
|
||||||
|
const a = {};
|
||||||
|
for (let i = 0; i < process.argv.length; i++) {
|
||||||
|
const v = process.argv[i];
|
||||||
|
if (v === "--no-serve") a.noServe = true;
|
||||||
|
else if (v === "--ours") a.ours = process.argv[++i];
|
||||||
|
else if (v === "--ref") a.ref = process.argv[++i];
|
||||||
|
else if (v === "--route") a.route = process.argv[++i];
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const WEB = process.cwd();
|
||||||
|
const PORT = 5179;
|
||||||
|
const ROUTE = args.route || "/preview/demo";
|
||||||
|
const OURS = args.ours || `http://127.0.0.1:${PORT}${ROUTE}`;
|
||||||
|
const REF = args.ref || resolve(WEB, "../refs-html/demo.html");
|
||||||
|
|
||||||
|
const FIELDS = [
|
||||||
|
"fontFamily",
|
||||||
|
"fontSize",
|
||||||
|
"fontWeight",
|
||||||
|
"color",
|
||||||
|
"backgroundColor",
|
||||||
|
"border",
|
||||||
|
"padding",
|
||||||
|
"margin",
|
||||||
|
"letterSpacing",
|
||||||
|
"lineHeight",
|
||||||
|
"borderRadius",
|
||||||
|
"display",
|
||||||
|
"gap"
|
||||||
|
];
|
||||||
|
|
||||||
|
const waitFor = async (url, ms = 60000) => {
|
||||||
|
const t = Date.now();
|
||||||
|
while (Date.now() - t < ms) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(url);
|
||||||
|
if (r.ok) return;
|
||||||
|
} catch {}
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
throw new Error("dev server not up: " + url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const snapshot = (page, url) =>
|
||||||
|
page.evaluate((url) => {
|
||||||
|
const readTokens = () => {
|
||||||
|
const s = getComputedStyle(document.documentElement);
|
||||||
|
const out = {};
|
||||||
|
for (const k of s) if (k.startsWith("--")) out[k] = s.getPropertyValue(k).trim();
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
const tokensLight = readTokens();
|
||||||
|
const prev = document.documentElement.getAttribute("data-theme");
|
||||||
|
document.documentElement.setAttribute("data-theme", "dark");
|
||||||
|
const tokensDark = readTokens();
|
||||||
|
if (prev) document.documentElement.setAttribute("data-theme", prev);
|
||||||
|
else document.documentElement.removeAttribute("data-theme");
|
||||||
|
|
||||||
|
const sig = (el) => {
|
||||||
|
const s = getComputedStyle(el);
|
||||||
|
const o = {};
|
||||||
|
for (const f of [
|
||||||
|
"fontFamily",
|
||||||
|
"fontSize",
|
||||||
|
"fontWeight",
|
||||||
|
"color",
|
||||||
|
"backgroundColor",
|
||||||
|
"borderTopWidth",
|
||||||
|
"borderTopColor",
|
||||||
|
"padding",
|
||||||
|
"margin",
|
||||||
|
"letterSpacing",
|
||||||
|
"lineHeight",
|
||||||
|
"borderRadius",
|
||||||
|
"display",
|
||||||
|
"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) };
|
||||||
|
};
|
||||||
|
const walk = (el, path) => {
|
||||||
|
if (["SCRIPT", "STYLE", "NOSCRIPT"].includes(el.tagName)) return [];
|
||||||
|
const out = [];
|
||||||
|
const kids = [...el.children];
|
||||||
|
out.push({
|
||||||
|
path,
|
||||||
|
tag: el.tagName.toLowerCase(),
|
||||||
|
text: el.children.length === 0 ? el.textContent.trim().slice(0, 40) : "",
|
||||||
|
rect: rect(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})`));
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
return { tokensLight, tokensDark, tree: walk(document.body, "body") };
|
||||||
|
}, 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 tokDiff = (a, b) => {
|
||||||
|
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
||||||
|
const rows = [];
|
||||||
|
for (const k of keys) {
|
||||||
|
if (TOK_NOISE.test(k)) continue;
|
||||||
|
const av = a[k] ?? "—";
|
||||||
|
const bv = b[k] ?? "—";
|
||||||
|
if (av !== bv) rows.push({ key: k, ours: av, ref: bv });
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
const tokenDiffs = {
|
||||||
|
light: tokDiff(ours.tokensLight, ref.tokensLight),
|
||||||
|
dark: tokDiff(ours.tokensDark, ref.tokensDark)
|
||||||
|
};
|
||||||
|
|
||||||
|
// element diff: совпадающие по тексту элементы (структуры разные,
|
||||||
|
// поэтому nth-child-пути не совпадают — матчим по видимому тексту)
|
||||||
|
const byText = (tree) => {
|
||||||
|
const m = new Map();
|
||||||
|
for (const e of tree) if (e.text) if (!m.has(e.text)) m.set(e.text, e);
|
||||||
|
return m;
|
||||||
|
};
|
||||||
|
const txtO = byText(ours.tree);
|
||||||
|
const txtR = byText(ref.tree);
|
||||||
|
const onlyOurs = [];
|
||||||
|
const onlyRef = [];
|
||||||
|
const elementDiffs = [];
|
||||||
|
for (const [k, o] of txtO) {
|
||||||
|
const r = txtR.get(k);
|
||||||
|
if (!r) {
|
||||||
|
onlyOurs.push(k);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const deltas = {};
|
||||||
|
for (const f of FIELDS) {
|
||||||
|
const ov = (o.style[f] ?? "").toString();
|
||||||
|
const rv = (r.style[f] ?? "").toString();
|
||||||
|
if (ov !== rv) deltas[f] = { ours: ov, ref: rv };
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
deltas.rect = { ours: dr, ref: rr };
|
||||||
|
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);
|
||||||
|
|
||||||
|
const report = {
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
ours: OURS,
|
||||||
|
ref: REF,
|
||||||
|
tokenDiffs,
|
||||||
|
elementDiffs,
|
||||||
|
onlyOurs: onlyOurs.slice(0, 60),
|
||||||
|
onlyRef: onlyRef.slice(0, 60),
|
||||||
|
counts: {
|
||||||
|
tokenLight: tokenDiffs.light.length,
|
||||||
|
tokenDark: tokenDiffs.dark.length,
|
||||||
|
elements: elementDiffs.length,
|
||||||
|
onlyOurs: onlyOurs.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));
|
||||||
|
console.log(
|
||||||
|
`audit done: tokens light/dark=${tokenDiffs.light.length}/${tokenDiffs.dark.length}, ` +
|
||||||
|
`elements=${elementDiffs.length}, onlyOurs=${onlyOurs.length}, onlyRef=${onlyRef.length}`
|
||||||
|
);
|
||||||
|
console.log(`report: web/audit/audit-report.md`);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
if (server) {
|
||||||
|
try {
|
||||||
|
if (process.platform === "win32")
|
||||||
|
spawn("taskkill", ["/pid", String(server.pid), "/f", "/t"], { stdio: "ignore" });
|
||||||
|
else server.kill("SIGTERM");
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user