Move the completed material-icons-ui change to openspec/changes/archive/2026-10-10-material-icons-ui/ and create the ui-iconography main spec from its ADDED requirements.
86 lines
7.3 KiB
Markdown
86 lines
7.3 KiB
Markdown
# Design
|
||
|
||
## Context
|
||
|
||
Фронтенд — SvelteKit 2 + Svelte 5, `adapter-static` в режиме SPA (`ssr=false`),
|
||
собирается в статику и вшивается в Go-бинарник через `go:embed`. Стили — Tailwind v4,
|
||
семантические токены через CSS-переменные и `data-theme` на `<html>` (темы light/dark/neutral
|
||
+ system). Внешние CDN-запросы нежелательны: приложение self-hosted и должно работать
|
||
полностью автономно. Сейчас все «иконки» — эмодзи и юникод-глифы, рендерящиеся системным
|
||
шрифтом (мотивация — в proposal.md).
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- Единый набор векторных иконок, самодостаточный (без внешних запросов и без иконочных шрифтов).
|
||
- Иконки наследуют цвет текущей темы и не влияют на размеры/раскладку при переключении тем.
|
||
- Переиспользуемый компонентный API, спрайт для дедупликации повторяющихся иконок.
|
||
- Доступность: корректное поведение для декоративных и смысловых иконок и для тумблеров.
|
||
|
||
**Non-Goals:**
|
||
- Не менять доменную логику, API, БД и backend.
|
||
- Не вводить анимации/переходы иконок.
|
||
- Не добавлять внешние зависимости (иконочные пакеты, Vite-плагины).
|
||
- Не делать полный редизайн макета — только замена иконок и ведущие иконки контролов.
|
||
|
||
## Decisions
|
||
|
||
### 1. Inline SVG через спрайт `<symbol>` + `<use>`
|
||
В `+layout.svelte` монтируется один скрытый `<svg>` со всеми `<symbol id="i-...">`;
|
||
места использования рисуют `<svg><use href="#i-..."/></svg>`.
|
||
- **Почему:** нет внешних запросов, работает внутри статичного бандла, `currentColor`
|
||
наследуется через `<use>`, а данные пути хранятся один раз в DOM (важно для сеток, где
|
||
`favorite`/`mode_comment` повторяются на каждой плитке).
|
||
- **Альтернативы (отклонены):** иконочный шрифт через CDN — внешний запрос, FOUT, ломает
|
||
автономность; внешний файл-спрайт `<use href="/icons.svg#id">` — межфайловые `<use>` имеют
|
||
ограничения в ряде браузеров; «иконка = компонент с полным inline-путём» — дублирование
|
||
path-данных в DOM при массовом повторении.
|
||
|
||
### 2. Вендорим данные иконок в `registry.ts`, без зависимостей
|
||
Реестр — объект `имя -> { path }`, где `path` — сырая inner-разметка (`<path d="..." .../>`,
|
||
иногда с `fill-rule`/`clip-rule`). Источник — Google Material Symbols (через Iconify,
|
||
prefix `material-symbols`); базовые имена дают стиль **filled**, поэтому набор вендорится в
|
||
filled-стиле, а для состояний-пар берутся отдельные ключи (`favorite` = filled, лайкнут;
|
||
`favorite-outline` = контурный, не лайкнут). Примечание: контурного варианта для всех
|
||
иконок в источнике нет, поэтому единый filled-набор согласованнее, чем смесь стилей.
|
||
- **Почему:** ноль зависимостей и supply-chain, малый бандл, полный контроль, согласуется с
|
||
философией «один статичный бинарник».
|
||
- **Альтернативы (отклонены):** `@mdi/js` — готовые path-строки, но новая зависимость и другой
|
||
визуальный набор; Iconify + `unplugin-icons` — мощный DX, но тяжёлый тулчейн ради ~25 иконок.
|
||
- **Задел:** при росте набора переход на `@mdi/js` не меняет компонентный API — только источник
|
||
данных реестра.
|
||
|
||
### 3. Компонентный API
|
||
- `Icon.svelte`: пропсы `name`, необязательный `label`; размер и цвет задаются классами
|
||
(`size-4/5/6/16`, `text-*`). По умолчанию `aria-hidden="true" focusable="false"`; при
|
||
заданном `label` — `role="img"` + `aria-label` и `aria-hidden` снимается.
|
||
- `IconSprite.svelte`: рендерит все `<symbol>` из реестра один раз, монтируется в `+layout.svelte`.
|
||
- `IconButton.svelte`: иконочная кнопка с обязательным `label` (попадает в `aria-label`).
|
||
- **Неизвестное имя:** в dev — `console.warn` и пустой бокс (не падать); в prod — пусто.
|
||
|
||
### 4. Стиль и метрики
|
||
- `fill="currentColor"`, шкала размеров: `size-4` (в тексте/счётчиках), `size-5`
|
||
(кнопки/навигация), `size-6` (панель лайтбокса), `size-16` (плейсхолдеры).
|
||
- Пара состояний лайка: `favorite-outline` (не лайкнут) / `favorite` (залитая, лайкнут).
|
||
|
||
### 5. Маппинг иконок тем
|
||
`light_mode`, `dark_mode`, `palette` (нейтральная), `brightness_auto` (системная).
|
||
|
||
### 6. Доступность
|
||
- Декоративные иконки рядом с текстом/внутри кнопки с текстом — `aria-hidden`.
|
||
- Одиночные иконочные кнопки (тумблеры тем) — `aria-label` на `<button>` + `aria-pressed`.
|
||
- Счётчики лайков/комментариев: иконка декоративная, число несёт доступное имя
|
||
(`aria-label="12 лайков"`).
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **[Спрайт должен оставаться в DOM]** → `<use>` ссылается на `#id` в том же документе;
|
||
`IconSprite` живёт в `+layout.svelte` и не размонтируется.
|
||
- **[`{@html}` в `<symbol>`]** → данные статичны и вендорятся на этапе сборки, не приходят от
|
||
пользователя; риск XSS отсутствует.
|
||
- **[Рост DOM при больших сетках]** → митигируется спрайтом (одна копия пути).
|
||
- **[Визуальная регрессия/раскладка]** → размеры задаём классами, иконки в квадратном
|
||
`viewBox 0 0 24 24`; проверяем на трёх темах.
|
||
- **[Лицензия/атрибуция]** → Material Symbols под Apache 2.0; добавляем уведомление об
|
||
источнике рядом с реестром.
|