Files
iu bf11cbada3 chore(openspec): archive material-icons-ui and sync ui-iconography spec
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.
2026-10-10 13:20:06 +03:00

86 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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; добавляем уведомление об
источнике рядом с реестром.