Files
ds-galo/openspec/changes/archive/2026-10-10-material-icons-ui/design.md
T
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

7.3 KiB
Raw Blame History

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; добавляем уведомление об источнике рядом с реестром.