Compare commits

...
3 Commits
25 changed files with 264 additions and 79 deletions
+122
View File
@@ -0,0 +1,122 @@
# План C17: перенос preview → корень + (old) → /v1/
> Статус: план к выполнению. Предпосылки: tech debt закрыт, backlog №14 (старые
> файлы → lib/old/) выполнен. Домен НЕ блокер (см. plan-redesign.md §10,
> plan-seo.md §6).
## Цель
Новый дизайн становится основным на корневых маршрутах (`/`, `/tools/[id]`,
`/list-tools`). Старый дизайн переезжает на `/v1/*` как архив для справки.
Удаление старого кода (C19) откладывается до полной приёмки нового дизайна.
## Структура маршрутов (до → после)
**До:** `(old)/` даёт URLs `/`, `/demo`, `/list-tools`, `/tools/[id]`.
`preview/` даёт `/preview`, `/preview/list-tools`, `/preview/tools/[id]`,
`/preview/kit`.
**После:** `v1/` даёт `/v1/`, `/v1/demo`, `/v1/list-tools`, `/v1/tools/[id]`.
Корень даёт `/`, `/list-tools`, `/tools/[id]`. Витрина kit — на реальном
маршруте `/kit` (дизайн в стадии тестирования).
## Решения
1. **`v1/` — реальная папка, НЕ route group** — group `(v1)/` не меняет URL, а
нам нужно чтобы старые страницы жили по `/v1/*`.
2. **Корневой layout: shell нового дизайна** — TopBar, Footer, тема, хлебные
крошки, импорт `app.css`, `@layer app, design;`.
3. **`v1/+layout.svelte` — только импорт `app_v1.css`** — без header/footer/shell.
4. **Тема:** использовать `theme.svelte.ts` (не дублировать локальный `$state`).
5. **`@layer app, design;`** остаётся — design выигрывает у app везде.
6. **`noindex` остаётся** на `tools/[id]/+page.svelte` до покупки домена.
7. **CSS-файлы переименовываются:** `preview.css``app.css`, `old.css`
`app_v1.css`. Все ссылки обновляются.
8. **`kit/` витрина → `/kit`** — переносится на реальный маршрут вместе с
остальными preview-страницами; отдельный `preview/+layout` удаляется
(`/kit` наследует корневой shell).
## Что делать (шаги)
### 1. Rename `(old)/` → `v1/`
Переименовать папку. Обновить `v1/+layout.svelte`:
убрать «Preview v2» ссылку, обновить пути навигации, поправить импорт CSS.
### 2. Новый корневой `+layout.svelte` + rename CSS
Перезаписать текущий нейтральный layout содержимым из `preview/+layout.svelte`
(с правками: тема через `theme.svelte.ts`, путь к CSS). Переименовать
`src/preview.css``src/app.css`, `src/old.css``src/app_v1.css`.
Обновить все ссылки на эти файлы в проекте ( layout, лайнтеры, eslint-плагины,
скрипты, тесты).
### 3. Move preview pages → корень
Переместить страницы из `preview/` в корень `routes/`: `+page``/`,
`list-tools``/`, `tools/[id]``/`, витрину `kit``/kit`. Удалить
`preview/+layout.svelte` (корневой shell заменяет его).
### 4. Flatten `kit/` → `components/`
Перенести компоненты из `lib/components/kit/` в `lib/components/`, обновить
все импорты, удалить пустую `kit/`.
### 5. Update hardcoded `/preview/` URLs
В компонентах с захардкоженными путями `/preview/` заменить на корневые.
### 6. E2E тесты + playwright config
Заменить `/preview/...` на `/...` во всех e2e файлах и конфиге playwright.
### 7. ESLint config + AGENTS.md
Обновить glob-паттерны: `routes/preview/**``routes/**`,
`components/kit/**``components/**`. Обновить `AGENTS.md`.
### 8. Isolation plugin
Обновить `DEFAULT_OLD`: `routes/(old)/**``routes/v1/**`.
Убрать `routes/preview/**` из `DEFAULT_NEW`.
### 9. Archive audit scripts
Перенести скрипты аудита в `scripts/archive/`, убрать вызовы из манифестов.
## Порядок коммитов
| # | Описание | Зависит от |
| --- | ----------------------------------------------- | ---------- |
| 1 | Rename `(old)/``v1/`, правки v1/layout | — |
| 2 | Новый корневой layout + rename CSS | — |
| 3 | Move preview pages → корень (+ `/preview/kit` → | 2 |
| | `/kit`, удалить `preview/+layout`) | |
| 4 | Flatten kit/ → components/, обновить импорты | — |
| 5 | TopNav + ToolCard: убрать `/preview/` из URL | 3 |
| 6 | E2E тесты + playwright config | 3 |
| 7 | ESLint config + AGENTS.md | 3, 4 |
| 8 | Isolation plugin | 1 |
| 9 | Archive audit scripts + убрать из манифестов | — |
> **3+5 делаются вместе (один коммит):** после переноса страниц в корень
> `ToolCard`/`TopNav` продолжают линковать `/preview/...`, prerender падает на
> 404 — между шагами 3 и 5 билд не зелёный.
После каждого коммита: `pnpm --dir web lint` + `pnpm --dir web build` (+
`pnpm --dir web test` если затронуты e2e).
## Что НЕ меняется
- `$lib/preview/`, `$lib/registry-new/` — без изменений
- `$lib/old/components/` — без изменений (каждая сторона хранит свою копию)
- `routes/+layout.ts` — prerender = true остаётся
- Слаги инструментов (`*-png`) — без изменений
- `/kit` — витрина дизайна остаётся (в стадии тестирования)
## Риски
- **v1/ без shell** — старый дизайн без header/footer.
- **`@layer` взаимодействие** — проверить что design-токены перекрывают
app-токены на обоих маршрутах.
- **Isolation: shared-классификация** — маршруты нового дизайна shared до C19.
+20 -12
View File
@@ -3,8 +3,10 @@
> Статус: план к выполнению. Шаги 5–6 (C17–C21) остаются **следующим этапом**. > Статус: план к выполнению. Шаги 5–6 (C17–C21) остаются **следующим этапом**.
> Они больше не привязаны к «верности против рефа»: > Они больше не привязаны к «верности против рефа»:
> `docs/archive/plan-design-fix.md` закрыт, аудит против рефа прекращён. > `docs/archive/plan-design-fix.md` закрыт, аудит против рефа прекращён.
> Tech debt закрыт (`docs/archive/plan-tech-debt.md`). Перед C17: backlog №14 > Tech debt закрыт (`docs/archive/plan-tech-debt.md`). Backlog №14 (переезд
> (переезд старых файлов в `old/`) и покупка домена (`plan-domain.local.md`). > старых файлов в `old/`) — **выполнено**. Покупка домена НЕ блокер C17:
> домен нужен для S1 (`plan-seo.md`) — canonical, sitemap, снятие `noindex`.
> C17 можно запускать без домена (см. §10).
> >
> Источники: `refs/` — Next/React-референс (правда по пикселям и интерактиву), > Источники: `refs/` — Next/React-референс (правда по пикселям и интерактиву),
> `refs-html/` — статические HTML-снимки для быстрого просмотра в браузере. > `refs-html/` — статические HTML-снимки для быстрого просмотра в браузере.
@@ -358,18 +360,23 @@ src/routes/preview/tools/[id]/+page.svelte
### Шаг 5. Сделать новый дизайн основным — СЛЕДУЮЩИЙ ЭТАП ### Шаг 5. Сделать новый дизайн основным — СЛЕДУЮЩИЙ ЭТАП
> Tech debt закрыт. Перед стартом C17: покупка домена (`plan-domain.local.md`). > Tech debt закрыт. Backlog №14 (переезд старых файлов в `lib/old/`) — **выполнено**.
> Переезд старых файлов в `lib/old/` (backlog №14) — **выполнено**. > Покупка домена — НЕ блокер C17. C17 переносит маршруты и делает новый дизайн
> основным; `noindex` остаётся до покупки домена и запуска S1 (`plan-seo.md`).
> Детальный план см. в `docs/plan-c17.md` (старый `(old)/` → `/v1/`, удаление
> отложено до C19).
- [ ] **[C17]** Перенести `preview/*` → реальные маршруты (move), удалить - [ ] **[C17]** Перенести `preview/*` → реальные маршруты (move), удалить
`(old)/`, корневой `+layout.svelte` импортирует `design2.css` + новый `(old)/`, корневой `+layout.svelte` импортирует `design2.css` + новый
shell. Убрать флаг/кнопку. **И обновить `newCode` в shell. Убрать флаг/кнопку. **Снять `noindex, nofollow`** со страницы
инструмента (`preview/tools/[id]/+page.svelte`) — страницы становятся
боевыми лендингами ровно в момент переезда (SEO: `plan-seo.md`, S1a).
**Временное решение:** если домен ещё не куплен, `noindex` можно оставить
до S1 — тогда C17 делается без снятия `noindex`, а снятие происходит
вместе с подключением canonical в S1. **И обновить `newCode` в
`web/eslint.config.js`:** заменить `src/routes/preview/**` на `web/eslint.config.js`:** заменить `src/routes/preview/**` на
`src/routes/**` (после переезда все маршруты — новый дизайн), компонентный `src/routes/**` (после переезда все маршруты — новый дизайн), компонентный
glob `src/lib/components/kit/**` пока оставить. **И снять glob `src/lib/components/kit/**` пока оставить.
`noindex, nofollow`** со страницы инструмента
(`preview/tools/[id]/+page.svelte`) — страницы становятся боевыми
лендингами ровно в момент переезда (SEO: `plan-seo.md`, S1a).
- [ ] **[C18]** Правки импортов/редиректов после переноса, проверка билда. - [ ] **[C18]** Правки импортов/редиректов после переноса, проверка билда.
### Шаг 6. Удаление старого ### Шаг 6. Удаление старого
@@ -401,9 +408,10 @@ src/routes/preview/tools/[id]/+page.svelte
> существовать в том же коммите. > существовать в том же коммите.
> - Коммиты **C3→C16 не трогают старый сайт** `(old)/` — они только добавляют > - Коммиты **C3→C16 не трогают старый сайт** `(old)/` — они только добавляют
> новые файлы/маршруты, сломать старый сайт на них нельзя. > новые файлы/маршруты, сломать старый сайт на них нельзя.
> - **C17 — единственный атомарный переключатель**: перенос `preview/*` на > - **C17 — атомарный переключатель**: перенос `preview/*` на реальные пути +
> реальные пути + удаление `(old)/` + смена root-layout должны быть в ОДНОМ > удаление `(old)/` + смена root-layout — в ОДНОМ коммите. Снятие `noindex`
> коммите (иначе конфликт маршрутов или пропажа страниц). Не дробить. > также в C17, **если домен уже куплен**; иначе — отложить на S1. Не дробить
> сам C17.
> - **C19 должен идти строго после C17** (старый `app.css`/`ui/` удаляем только > - **C19 должен идти строго после C17** (старый `app.css`/`ui/` удаляем только
> когда old-сайт уже выключен). > когда old-сайт уже выключен).
> - Очерёдность зависимостей: `design2.css` (C3) и kit-компоненты (C5–C7) должны > - Очерёдность зависимостей: `design2.css` (C3) и kit-компоненты (C5–C7) должны
+40 -22
View File
@@ -1,8 +1,11 @@
# План: SEO + GEO — индексация и конкуренция в image tools # План: SEO + GEO — индексация и конкуренция в image tools
> **Статус (2026-09-09):** план к выполнению. Старт — после C17 > **Статус (2026-09-10):** план к выполнению. Старт — после C17
> (`plan-redesign.md`): до переезда `preview/*` на реальные маршруты страницы > (`plan-redesign.md`): до переезда `preview/*` на реальные маршруты страницы
> инструментов в `noindex`, применять техминимум некуда. > инструментов в `noindex`, применять техминимум некуда.
> **Уточнено:** домен НЕ блокер C17 (см. §6). Задачи S1 разбиты на
> domain-free (можно делать параллельно с покупкой домена) и domain-required
> (после покупки и настройки домена на Vercel).
> >
> **Цель:** каждая страница инструмента — индексируемый лендинг под конкретный > **Цель:** каждая страница инструмента — индексируемый лендинг под конкретный
> интент («crop png», «make png transparent», «jpg to png»…), конкуренция с > интент («crop png», «make png transparent», «jpg to png»…), конкуренция с
@@ -133,22 +136,16 @@
### S1. Технический минимум (сразу после C17) ### S1. Технический минимум (сразу после C17)
- [ ] **[S1a]** Снять `noindex, nofollow` со страницы инструмента Разбито на две группы: **domain-free** (делаем параллельно с покупкой домена)
(`preview/tools/[id]/+page.svelte`) — встроено в C17 как атомарный шаг и **domain-required** (после покупки и DNS-настройки).
(см. `plan-redesign.md`).
- [ ] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs`: генерирует `sitemap.xml` в #### S1-free: без домена (параллельно с C18–C19)
`build/` из `TOOLS` реестра (`registry-new`) + статических страниц. Вызов
из `pnpm build` после `vite build` (adapter-static пишет в `build/`,
дописываем файл там же). Origin — из env (`PUBLIC_SITE_ORIGIN`), учёт
опционального `BASE_PATH`.
- [ ] **[S1c]** `robots.txt`: `Sitemap:` + закрыть служебные маршруты
(`/preview/kit` до C17; после — вернуть минимальный allow-all).
- [ ] **[S1d]** Компонент `Seo.svelte` (kit): canonical (абсолютный URL по
актуальному маршруту), OG-теги (title/description/type/url/image),
twitter:card. Подключить в root-layout.
- [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на - [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на
существующем ядре (мы сами умеем рисовать PNG) — имя инструмента + бренд, существующем ядре (мы сами умеем рисовать PNG) — имя инструмента + бренд,
файл `static/og/[id].png`, ссылка из `Seo.svelte`. файл `static/og/[id].png`, ссылка из `Seo.svelte`. Относительные пути
(`/og/crop-png.png`) работают без домена; при деплое на Vercel с
собственным доменом OG-картинки будут корректными.
- [ ] **[S1f]** Download-кнопка с селектором формата PNG/JPG/WebP/BMP (+quality - [ ] **[S1f]** Download-кнопка с селектором формата PNG/JPG/WebP/BMP (+quality
для lossy) для всех инструментов — механика `ToolEntry.output` уже есть для lossy) для всех инструментов — механика `ToolEntry.output` уже есть
(backlog №7, райз «выбор формата/качества»). Продуктовая основа «image (backlog №7, райз «выбор формата/качества»). Продуктовая основа «image
@@ -159,8 +156,27 @@
png. Существующие страницы не трогать: `jpg-to-png`, `png-to-jpg`, png. Существующие страницы не трогать: `jpg-to-png`, `png-to-jpg`,
`png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами со своим `png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами со своим
контентом (§5). контентом (§5).
- Гейты: build зелёный; в `build/` лежит корректный `sitemap.xml` со всеми
инструментами; canonical/OG присутствуют на всех страницах; e2e зелёные. #### S1-domain: с доменом (после покупки + DNS на Vercel)
- [ ] **[S1a]** Снять `noindex, no_follow` со страницы инструмента
(`tools/[id]/+page.svelte`) — делается одновременно с S1d (canonical)
чтобы Google сразу проиндексировал с правильным доменом.
- [ ] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs`: генерирует `sitemap.xml` в
`build/` из `TOOLS` реестра (`registry-new`) + статических страниц. Вызов
из `pnpm build` после `vite build` (adapter-static пишет в `build/`,
дописываем файл там же). Origin — из env (`PUBLIC_SITE_ORIGIN`), учёт
опционального `BASE_PATH`.
- [ ] **[S1c]** `robots.txt`: `Sitemap:` + закрыть служебные маршруты
(`/kit` до C19; после — вернуть минимальный allow-all).
- [ ] **[S1d]** Компонент `Seo.svelte` (kit): canonical (абсолютный URL по
актуальному маршруту), OG-теги (title/description/type/url/image),
twitter:card. Подключить в root-layout. **Требует `PUBLIC_SITE_ORIGIN`**
для абсолютных URL в canonical и OG:url.
> **Порядок S1:** S1e/S1f/S1g → покупка домена → S1a+S1b+S1c+S1d.
> Гейты: build зелёный; в `build/` лежит корректный `sitemap.xml` со всеми
> инструментами; canonical/OG присутствуют на всех страницах; e2e зелёные.
### S2. Контентный слой (параллельно C18–C19, волнами по категориям) ### S2. Контентный слой (параллельно C18–C19, волнами по категориям)
@@ -297,7 +313,7 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
интенты, которые подтверждены вордстатом, и сразу с контентом S2 (страница без интенты, которые подтверждены вордстатом, и сразу с контентом S2 (страница без
текста — потраченный URL). текста — потраченный URL).
## 6. Переименование бренда и домен (сделать до S1, дёшево сейчас) ## 6. Переименование бренда и домен (сделать до S1-domain, дёшево сейчас)
Новое имя и конкретный домен **не записаны в этом репозитории** — детальный Новое имя и конкретный домен **не записаны в этом репозитории** — детальный
чек-лист в `docs/plan-domain.local.md` (исключён из git через `.gitignore`), чек-лист в `docs/plan-domain.local.md` (исключён из git через `.gitignore`),
@@ -311,10 +327,12 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
ключи localStorage (`easy-png-tools:*` — или мигрировать отдельной ключи localStorage (`easy-png-tools:*` — или мигрировать отдельной
мелочью), историю docs/archive. мелочью), историю docs/archive.
- [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и - [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и
настроить деплой без `BASE_PATH` (§0). Домен нужен до S1: canonical и настроить деплой на Vercel без `BASE_PATH` (§0). Домен нужен до S1-domain:
sitemap генерятся от `PUBLIC_SITE_ORIGIN`. canonical и sitemap генерятся от `PUBLIC_SITE_ORIGIN`.
- Порядок: R1+R2 в любой момент до C17; после индексации переименование = - **Порядок:** R1+R2 можно делать параллельно с C17/C18/C19 (не блокируют).
переезд с 301, потеря позиций и ссылок — не делать. R2 (домен) — блокер только для S1-domain (canonical + sitemap + снятие
`noindex`). После индексации переименование = переезд с 301, потеря позиций
и ссылок — не делать. Поэтому R1/R2 — до S1-domain, но не обязательно до C17.
## 7. Что осознанно НЕ делаем ## 7. Что осознанно НЕ делаем
+3 -1
View File
@@ -3,7 +3,9 @@
> **Статус:** Фаза 1 (полноценный TS-сайт) в основном выполнена — > **Статус:** Фаза 1 (полноценный TS-сайт) в основном выполнена —
> сайт живёт, каталог переведён в типизированный `registry-new` (121/125), идёт > сайт живёт, каталог переведён в типизированный `registry-new` (121/125), идёт
> редизайн на `preview/*` по `plan-redesign.md` (параллельная ветка, старый UI > редизайн на `preview/*` по `plan-redesign.md` (параллельная ветка, старый UI
> на `(old)/`). Tech debt закрыт. Фазы 2–8 — будущие. Фазы 9–10 > на `(old)/`). Tech debt закрыт. Переезд старых файлов в `old/` выполнен.
> Следующий этап: C17 (перенос preview → корневые маршруты). Домен НЕ блокер
> C17, нужен для SEO (`plan-seo.md`, S1-domain). Фазы 2–8 — будущие. Фазы 9–10
> спланированы в `docs/plan-platform.md`. > спланированы в `docs/plan-platform.md`.
## 0. Решения ## 0. Решения
@@ -37,9 +37,9 @@ export default {
hardcodedZIndex: hardcodedZIndex:
"Hardcoded z-index '{{value}}'. Use a var(--z-...) token.", "Hardcoded z-index '{{value}}'. Use a var(--z-...) token.",
hardcodedBreakpoint: hardcodedBreakpoint:
"Hardcoded breakpoint '{{value}}' in @media. Use the @custom-media name instead: declare `@custom-media --bp-* (...)` in preview.css and write `@media (--bp-*)`.", "Hardcoded breakpoint '{{value}}' in @media. Use the @custom-media name instead: declare `@custom-media --bp-* (...)` in app.css and write `@media (--bp-*)`.",
varInMedia: varInMedia:
"var() inside @media '{{value}}'. Custom properties do not resolve in media queries — declare `@custom-media --bp-* (...)` in preview.css and use `@media (--bp-*)`.", "var() inside @media '{{value}}'. Custom properties do not resolve in media queries — declare `@custom-media --bp-* (...)` in app.css and use `@media (--bp-*)`.",
colorMix: colorMix:
"color-mix() in component ({{value}}). Tokenize the result in the design CSS file.", "color-mix() in component ({{value}}). Tokenize the result in the design CSS file.",
}, },
@@ -2,7 +2,7 @@
// A custom property may be DEFINED inside a component's <style> block, but its // A custom property may be DEFINED inside a component's <style> block, but its
// value must not introduce a design primitive: a raw color literal (hex/rgb/ // value must not introduce a design primitive: a raw color literal (hex/rgb/
// oklch/named) or an absolute size (px/rem/em). This keeps the design surface // oklch/named) or an absolute size (px/rem/em). This keeps the design surface
// (colors/sizes) a single source of truth in preview.css, while still allowing // (colors/sizes) a single source of truth in app.css, while still allowing
// local DERIVED variables built from tokens: var(--...), calc(), unitless // local DERIVED variables built from tokens: var(--...), calc(), unitless
// ratios (--ratio: 1.5) — those are legitimate component-local state. // ratios (--ratio: 1.5) — those are legitimate component-local state.
@@ -24,7 +24,7 @@ export default {
}, },
messages: { messages: {
tokenPrimitive: tokenPrimitive:
"Custom property '{{prop}}' defines a primitive '{{value}}' in a component. Move it to preview.css or derive it from tokens via var()/calc().", "Custom property '{{prop}}' defines a primitive '{{value}}' in a component. Move it to app.css or derive it from tokens via var()/calc().",
}, },
schema: [], schema: [],
}, },
@@ -1,6 +1,6 @@
// Rule: NO UNDEFINED CSS-TOKEN USAGE IN COMPONENTS. // Rule: NO UNDEFINED CSS-TOKEN USAGE IN COMPONENTS.
// A var(--...) referenced in a Svelte <style> block must exist in the design // A var(--...) referenced in a Svelte <style> block must exist in the design
// token file (src/preview.css) or be a local override defined in the same // token file (src/app.css) or be a local override defined in the same
// component. Catches typos and drop-in tokens that were never added to the // component. Catches typos and drop-in tokens that were never added to the
// "single source of truth". // "single source of truth".
@@ -16,7 +16,7 @@ const VAR_REF_RE = /var\(\s*(--[\w-]+)/g;
const definedCache = new Map(); const definedCache = new Map();
function getDefinedTokens(cwd) { function getDefinedTokens(cwd) {
const file = resolve(cwd, "src/preview.css"); const file = resolve(cwd, "src/app.css");
if (!existsSync(file)) return null; if (!existsSync(file)) return null;
if (definedCache.has(file)) return definedCache.get(file); if (definedCache.has(file)) return definedCache.get(file);
@@ -34,13 +34,13 @@ export default {
type: "problem", type: "problem",
docs: { docs: {
description: description:
"Disallow referencing a CSS variable that is not defined in preview.css (design token file) and not defined locally in the component.", "Disallow referencing a CSS variable that is not defined in app.css (design token file) and not defined locally in the component.",
category: "Design tokens", category: "Design tokens",
recommended: true, recommended: true,
}, },
messages: { messages: {
undefinedToken: undefinedToken:
"CSS variable '{{token}}' is not defined in preview.css and not locally in this component.", "CSS variable '{{token}}' is not defined in app.css and not locally in this component.",
}, },
schema: [], schema: [],
}, },
+1 -1
View File
@@ -3,7 +3,7 @@
* *
* Goal: a single source of truth for design. In Svelte components, hardcoded * Goal: a single source of truth for design. In Svelte components, hardcoded
* colors and sizes are banned; everything must come from CSS variables (tokens * colors and sizes are banned; everything must come from CSS variables (tokens
* in preview.css). Rules inspect the postcss AST of Svelte <style> blocks * in app.css). Rules inspect the postcss AST of Svelte <style> blocks
* exposed by svelte-eslint-parser. * exposed by svelte-eslint-parser.
*/ */
import noCategoryMismatch from "./design-tokens/no-category-mismatch.js"; import noCategoryMismatch from "./design-tokens/no-category-mismatch.js";
+2 -2
View File
@@ -4,10 +4,10 @@ import postcssCustomMedia from "postcss-custom-media";
export default { export default {
plugins: [ plugins: [
// Breakpoints live only in preview.css (next to the --bp-* tokens); // Breakpoints live only in app.css (next to the --bp-* tokens);
// global-data injects them so @media (--bp-*) expands in every file. // global-data injects them so @media (--bp-*) expands in every file.
postcssGlobalData({ postcssGlobalData({
files: ["src/preview.css"], files: ["src/app.css"],
}), }),
postcssCustomMedia({ postcssCustomMedia({
preserve: false, preserve: false,
+3 -3
View File
@@ -1,5 +1,5 @@
// Design-token audit, one command: theme parity, hct-only color authorship and // Design-token audit, one command: theme parity, hct-only color authorship and
// unused-token warnings over src/preview.css. // unused-token warnings over src/app.css.
// //
// Usage: pnpm --dir web exec node scripts/check-tokens.mjs // Usage: pnpm --dir web exec node scripts/check-tokens.mjs
// Exit code 1 when a failing check (parity or color authorship) reports. // Exit code 1 when a failing check (parity or color authorship) reports.
@@ -36,10 +36,10 @@ async function main() {
const unused = checkUnused(root); const unused = checkUnused(root);
if (unused.length > 0) { if (unused.length > 0) {
console.log("\nUnused tokens (defined in preview.css, never used):"); console.log("\nUnused tokens (defined in app.css, never used):");
for (const token of unused) console.log(` ${token}`); for (const token of unused) console.log(` ${token}`);
} else { } else {
console.log("All preview.css tokens are used somewhere."); console.log("All app.css tokens are used somewhere.");
} }
if (failed) process.exitCode = 1; if (failed) process.exitCode = 1;
+1 -1
View File
@@ -1,4 +1,4 @@
// Color authorship: every color value in preview.css must be authored as hct() // Color authorship: every color value in app.css must be authored as hct()
// — literals AND derived forms (hct(from var(...) h c t) with channel math) — // — literals AND derived forms (hct(from var(...) h c t) with channel math) —
// so the whole palette is computed through HCT channels and output as sRGB by // so the whole palette is computed through HCT channels and output as sRGB by
// the postcss-hct plugin. Exceptions: the seed tokens --brand-main/--brand-alt // the postcss-hct plugin. Exceptions: the seed tokens --brand-main/--brand-alt
+3 -3
View File
@@ -1,4 +1,4 @@
// Shared plumbing for the design-token audits: reading preview.css, scanning // Shared plumbing for the design-token audits: reading app.css, scanning
// for var() usages across src, and the color/form predicates the checks use. // for var() usages across src, and the color/form predicates the checks use.
import { readdirSync, readFileSync, statSync } from "node:fs"; import { readdirSync, readFileSync, statSync } from "node:fs";
@@ -7,7 +7,7 @@ import { join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import postcss from "postcss"; import postcss from "postcss";
export const FILE = new URL("../../src/preview.css", import.meta.url); export const FILE = new URL("../../src/app.css", import.meta.url);
const SRC_DIR = fileURLToPath(new URL("../../src/", import.meta.url)); const SRC_DIR = fileURLToPath(new URL("../../src/", import.meta.url));
// A var(--x) REFERENCE anywhere in the app (primary argument only). // A var(--x) REFERENCE anywhere in the app (primary argument only).
@@ -65,7 +65,7 @@ export function collectTokens(root, selector) {
return map; return map;
} }
// Parse preview.css once and hand out the postcss root plus both theme maps. // Parse app.css once and hand out the postcss root plus both theme maps.
export async function parsePreview() { export async function parsePreview() {
const css = await readFile(FILE, "utf8"); const css = await readFile(FILE, "utf8");
const root = postcss.parse(css); const root = postcss.parse(css);
+1 -1
View File
@@ -1,4 +1,4 @@
// Unused tokens: defined in preview.css but never referenced via var() // Unused tokens: defined in app.css but never referenced via var()
// anywhere in src. A dead token is not the single source of truth — it's dust. // anywhere in src. A dead token is not the single source of truth — it's dust.
// Reported as a warning; it does not fail the run. // Reported as a warning; it does not fail the run.
+1 -1
View File
@@ -5,7 +5,7 @@ import { Color } from "@panmdaa/colors";
declare const process: { cwd(): string }; declare const process: { cwd(): string };
const css = readFileSync("src/preview.css", "utf8"); const css = readFileSync("src/app.css", "utf8");
type Hct = { h: number; c: number; t: number }; type Hct = { h: number; c: number; t: number };
+51 -9
View File
@@ -1,28 +1,70 @@
<script lang="ts"> <script lang="ts">
import { page } from "$app/state";
import favicon from "$lib/assets/favicon.svg"; import favicon from "$lib/assets/favicon.svg";
import { onMount } from "svelte"; import Footer from "$lib/components/kit/layout/Footer.svelte";
import TopBar from "$lib/components/kit/layout/TopBar.svelte";
import { initLocale } from "$lib/i18n/locale.svelte"; import { initLocale } from "$lib/i18n/locale.svelte";
import { initTheme } from "$lib/theme.svelte"; import { getTheme, initTheme, setTheme } from "$lib/theme.svelte";
import type { Snippet } from "svelte";
import { onMount } from "svelte";
import "../app.css";
let { children } = $props(); interface Props {
children: Snippet;
}
let { children }: Props = $props();
let theme = $derived(getTheme());
function toggle() {
setTheme(theme === "light" ? "dark" : "light");
}
onMount(() => { onMount(() => {
initLocale(); initLocale();
initTheme(); initTheme();
}); });
let crumb = $derived(toCrumb(page.url.pathname));
const CRUMB: Record<string, string> = {
"/list-tools": "CATALOG",
"/tools/linear-gradient-png": "GRADIENT",
"/tools/remove-background-png": "BACKGROUND REMOVER",
};
function toCrumb(path: string): string {
return (
"/ " +
(CRUMB[path] ??
path.split("/").filter(Boolean).pop()?.toUpperCase() ??
"HOME")
);
}
</script> </script>
<svelte:head> <svelte:head>
<link rel="icon" href={favicon} /> <link rel="icon" href={favicon} />
</svelte:head> </svelte:head>
{@render children()} <main class="preview-root" data-theme={theme}>
<TopBar {theme} {crumb} ontoggle={toggle} />
{@render children()}
<Footer />
</main>
<style> <style>
/* Порядок cascade-слоёв для всего документа (грузится на всех маршрутах): .preview-root {
app = старый дизайн ((old)), design = новый (preview/*). design выигрывает background-color: var(--color-background);
у app на совпадающих правилах независимо от порядка <link> / истории background-image:
SPA-навигации. Содержимое слоёв — в app.css (@layer app) и design2.css linear-gradient(var(--color-background-muted) 1px, transparent 1px),
(@layer design). */ linear-gradient(90deg, var(--color-background-muted) 1px, transparent 1px);
background-size: var(--space-xxxl) var(--space-xxxl);
min-height: 100vh;
display: flex;
flex-direction: column;
}
@layer app, design; @layer app, design;
</style> </style>
+1 -1
View File
@@ -3,7 +3,7 @@
import Footer from "$lib/components/kit/layout/Footer.svelte"; import Footer from "$lib/components/kit/layout/Footer.svelte";
import TopBar from "$lib/components/kit/layout/TopBar.svelte"; import TopBar from "$lib/components/kit/layout/TopBar.svelte";
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import "../../preview.css"; import "../../app.css";
interface Props { interface Props {
children: Snippet; children: Snippet;
@@ -6,7 +6,7 @@
import { t } from "$lib/i18n/t"; import { t } from "$lib/i18n/t";
import { getTheme, initTheme, setTheme } from "$lib/theme.svelte"; import { getTheme, initTheme, setTheme } from "$lib/theme.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import "../../old.css"; import "../../app_v1.css";
let { children } = $props(); let { children } = $props();
@@ -24,20 +24,13 @@
<div class="app"> <div class="app">
<header> <header>
<a href={resolve("/")} class="brand">easy-png-tools</a> <a href={resolve("/v1/")} class="brand">easy-png-tools</a>
<nav aria-label={t("header.sectionsAria")}> <nav aria-label={t("header.sectionsAria")}>
<a class="nav-link workspace-link" href={resolve("/")} <a class="nav-link workspace-link" href={resolve("/v1/")}
>{t("header.workspace")}</a >{t("header.workspace")}</a
> >
<a class="nav-link" href={resolve("/list-tools")}>{t("header.catalog")}</a <a class="nav-link" href={resolve("/v1/list-tools")}>{t("header.catalog")}</a
> >
<a
class="nav-link preview-v2"
href={resolve("/preview")}
onclick={() => localStorage.setItem("easy-png-tools:preview-v2", "1")}
>
Preview v2
</a>
<div class="lang-switch" role="group" aria-label="Language / Язык"> <div class="lang-switch" role="group" aria-label="Language / Язык">
{#each LOCALES as l (l)} {#each LOCALES as l (l)}
<button <button
+3 -3
View File
@@ -1,12 +1,12 @@
// Stylelint config — design-token enforcement for easy-png-tools. // Stylelint config — design-token enforcement for easy-png-tools.
// FWHM: keeps the design system a single source of truth. Colors and sizes must // FWHM: keeps the design system a single source of truth. Colors and sizes must
// come from prefixed tokens; direct color values are allowed only for the two // come from prefixed tokens; direct color values are allowed only for the two
// brand tokens. old.css is the legacy design and is ignored (removed later). // brand tokens. app_v1.css is the legacy design and is ignored (removed later).
export default { export default {
extends: ["stylelint-config-standard"], extends: ["stylelint-config-standard"],
ignoreFiles: [ ignoreFiles: [
"src/old.css", "src/app_v1.css",
"**/node_modules/**", "**/node_modules/**",
"**/build/**", "**/build/**",
"**/.svelte-kit/**", "**/.svelte-kit/**",
@@ -52,7 +52,7 @@ export default {
}, },
overrides: [ overrides: [
{ {
// Applies to every CSS file EXCEPT old.css (already in ignoreFiles) — // Applies to every CSS file EXCEPT app_v1.css (already in ignoreFiles) —
// the legacy design is exempt and will be removed later. // the legacy design is exempt and will be removed later.
files: ["src/**/*.css"], files: ["src/**/*.css"],
rules: { rules: {