feat(frontend): replace emoji with inline Material Symbols icon system
Add a self-contained icon system: vendored Material Symbols registry, an <Icon> component that references a single <symbol> sprite via <use>, plus IconButton/IconSprite. Replace every emoji/Unicode glyph (logo, theme switcher, cover placeholder, like/comment counts, lightbox controls, back links) and add leading icons to key controls. Decorative icons are aria-hidden; icon-only buttons get labels; theme toggles expose aria-pressed; counters get accessible names. Recorded in openspec change material-icons-ui (all tasks complete).
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-10
|
||||
@@ -0,0 +1,85 @@
|
||||
# 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; добавляем уведомление об
|
||||
источнике рядом с реестром.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Сейчас все «иконки» в интерфейсе — это эмодзи и юникод-глифы, которые рисует системный
|
||||
шрифт. Они по-разному выглядят в разных ОС и браузерах (проект self-hosted — парк
|
||||
устройств непредсказуем), плохо наследуют цвет темы и семантически неточны: 🌾 для
|
||||
«нейтральной темы», 💻 для «системной», ♥/💬 для метрик. Нужен единый, самодостаточный
|
||||
набор векторных иконок без внешних запросов, согласованный с тремя темами и доступностью.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Ввести inline-SVG иконочную систему: спрайт `<symbol>` + `<use>`, реестр иконок
|
||||
(`registry.ts`), компоненты `Icon`, `IconSprite`, `IconButton`.
|
||||
- Заменить все эмодзи/юникод-глифы на SVG-иконки: логотип (`📷`), переключатель тем
|
||||
(`☀️ 🌙 🌾 💻`), плейсхолдер обложки (`🖼️`), лайк/комментарий (`♥ 💬`), закрытие
|
||||
и стрелки лайтбокса (`✕ ‹ ›`), «назад» (`←`).
|
||||
- Добавить ведущие иконки ключевым контролам: создать, загрузить, скачать, сохранить,
|
||||
отправить, удалить, выйти, участники, настройки, share-ссылки, принять инвайт.
|
||||
- Источник данных — Google Material Symbols (стиль outlined) плюс `*_fill`-варианты для
|
||||
состояний (например, лайк нажат/не нажат). Данные иконок вендорятся в репозиторий,
|
||||
новые зависимости не добавляются.
|
||||
- Проход по доступности: декоративные иконки скрыты от скринридеров (`aria-hidden`),
|
||||
смысловые/одиночные получают `aria-label`/`role="img"`, тумблеры тем — `aria-pressed`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `ui-iconography`: единая inline-SVG система иконок фронтенда, её доступность и
|
||||
наследование цветовых токенов тем.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_Нет — существующие спеки описывают доменную логику (albums, auth, photos, …); иконочная
|
||||
система их требований не меняет._
|
||||
|
||||
## Impact
|
||||
|
||||
- **Код:** `frontend/src/lib/icons/*`, `frontend/src/lib/components/Icon.svelte`,
|
||||
`IconSprite.svelte`, `IconButton.svelte`, правки в `+layout.svelte` и во всех
|
||||
экранах/компонентах (`ThemeSwitcher`, `PhotoGrid`, `Lightbox`, `Uploader`, `Comments`,
|
||||
список альбомов, страница альбома, участники, настройки, инвайт, публичный share).
|
||||
- **Зависимости:** без новых — данные иконок вендорятся.
|
||||
- **Совместимость:** backend, API и БД не затрагиваются; меняется только фронтенд-разметка
|
||||
и доступность.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Единая система иконок веб-интерфейса: самодостаточные векторные иконки, которые наследуют
|
||||
цвет темы и корректно ведут себя для вспомогательных технологий.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Самодостаточные векторные иконки
|
||||
Интерфейс SHALL отображать иконки как векторную графику, встроенную в документ, без запросов
|
||||
к внешним источникам за иконками.
|
||||
|
||||
#### Scenario: Отображение иконки
|
||||
- **WHEN** интерфейс показывает иконку (например, лайк, тему, закрытие лайтбокса)
|
||||
- **THEN** она отрисована встроенной векторной графикой, а не эмодзи или символом системного шрифта
|
||||
|
||||
#### Scenario: Автономная загрузка
|
||||
- **WHEN** страница загружается без доступа к внешней сети, кроме собственного сервера
|
||||
- **THEN** все иконки отображаются, так как не требуют внешних ресурсов
|
||||
|
||||
### Requirement: Наследование цвета темы
|
||||
Иконка SHALL наследовать цвет текущей темы и изменять его при переключении темы.
|
||||
|
||||
#### Scenario: Переключение темы
|
||||
- **WHEN** пользователь переключает тему (light/dark/neutral)
|
||||
- **THEN** цвет иконок меняется вместе с темой без дополнительных действий
|
||||
|
||||
### Requirement: Доступность иконок
|
||||
Система SHALL скрывать декоративные иконки от вспомогательных технологий и давать доступное
|
||||
имя смысловым иконочным элементам управления.
|
||||
|
||||
#### Scenario: Декоративная иконка
|
||||
- **WHEN** иконка сопровождает видимый текст или находится внутри кнопки с текстом
|
||||
- **THEN** она скрыта от скринридеров
|
||||
|
||||
#### Scenario: Иконочная кнопка без текста
|
||||
- **WHEN** кнопка содержит только иконку (например, выбор темы)
|
||||
- **THEN** кнопка имеет доступное имя
|
||||
|
||||
#### Scenario: Тумблер темы
|
||||
- **WHEN** кнопка выбора темы активна или неактивна
|
||||
- **THEN** её состояние передаётся вспомогательным технологиям как «нажата/не нажата»
|
||||
@@ -0,0 +1,35 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Ядро иконочной системы
|
||||
|
||||
- [x] 1.1 Создать `frontend/src/lib/icons/registry.ts`: записи `имя -> { path, filled?, label? }` с вендоренной inner-разметкой Material Symbols; проверить, что реестр импортируется без ошибок и содержит все иконки из инвентаря
|
||||
- [x] 1.2 Реализовать `Icon.svelte` (пропсы `name`, `label`; `aria-hidden`/`focusable`, `role=img` при `label`, классы размера, `fill=currentColor`); проверить рендер известного и неизвестного имени (dev-warn)
|
||||
- [x] 1.3 Реализовать `IconSprite.svelte` (один скрытый `<svg>` со всеми `<symbol id="i-...">` из реестра) и смонтировать его в `+layout.svelte`; проверить, что `<use>` в дочерних компонентах разрешается
|
||||
- [x] 1.4 Реализовать `IconButton.svelte` с обязательным `label` (-> `aria-label` на `<button>`); проверить доступное имя в дереве доступности
|
||||
|
||||
## 2. Замена эмодзи и юникод-глифов
|
||||
|
||||
- [x] 2.1 `ThemeSwitcher`: заменить `☀️ 🌙 🌾 💻` на `light_mode`/`dark_mode`/`palette`/`brightness_auto`; проверить переключение тем и `aria-pressed`
|
||||
- [x] 2.2 Логотип в `+layout.svelte`: заменить `📷` на `photo_camera` рядом с текстом «Galo»; проверить отображение в трёх темах
|
||||
- [x] 2.3 Список альбомов: заменить `🖼️` на `image` (крупный плейсхолдер); проверить вёрстку карточки
|
||||
- [x] 2.4 `PhotoGrid`: заменить `♥`/`💬` на `favorite`/`favorite_fill` и `mode_comment`; проверить счётчики и состояние «лайкнут»
|
||||
- [x] 2.5 `Lightbox`: заменить `✕ ‹ ›` на `close`/`chevron_left`/`chevron_right`; проверить навигацию и закрытие
|
||||
- [x] 2.6 Ссылки «назад к альбому» (участники, настройки): заменить `←` на `arrow_back`; проверить навигацию
|
||||
|
||||
## 3. Ведущие иконки контролов
|
||||
|
||||
- [x] 3.1 Список альбомов и страница альбома: добавить `add`/`photo_library` (создать, пустой список); проверить создание альбома
|
||||
- [x] 3.2 `Uploader`: `cloud_upload`/`add_photo_alternate`; проверить загрузку фото
|
||||
- [x] 3.3 `Lightbox`/share: `download`; проверить скачивание
|
||||
- [x] 3.4 Участники/настройки: `group`, `settings`, `add_link`, `link_off`, `save`, `delete_forever`; проверить роли, share-ссылки, сохранение и удаление
|
||||
- [x] 3.5 Навигация и вход: `login`, `logout`, `check`, `send`, `delete`, `content_copy`, `expand_more`; проверить login/logout, инвайт и комментарии
|
||||
|
||||
## 4. Доступность
|
||||
|
||||
- [x] 4.1 Проставить `aria-hidden="true" focusable="false"` декоративным иконкам и `aria-label` иконочным кнопкам; проверить через дерево доступности браузера
|
||||
- [x] 4.2 Дать счётчикам лайков/комментариев доступные имена (`aria-label="N лайков"`); проверить, что число читается скринридером
|
||||
|
||||
## 5. Проверка
|
||||
|
||||
- [x] 5.1 Собрать фронтенд (`make frontend`) и прогнать `svelte-check`; проверить отсутствие ошибок типов и сборки
|
||||
- [x] 5.2 Playwright-проверка: иконки отрисованы, эмодзи/юникод-глифы в разметке отсутствуют; проверить на трёх темах
|
||||
Reference in New Issue
Block a user