Files
pipu-music/docs/design.md
T
mrleo1nidandClaude Opus 5.5 de4e59d9f8
ci / mobile / mobile (Metro-бандл Android) (push) Canceled after 31s
ci / ts / ts (packages + web) (push) Canceled after 0s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 44s
fix(web): accessibility pass to WCAG 2.1 AA (M7)
An axe-core audit of every screen in both themes found low contrast on white
text over the accent and on caption text, track rows using row roles without
cells, and unlabelled progress bars. Accent fills now use a darker token,
caption text meets 4.5:1, track lists are lists and progress bars are labelled.
The queue drawer slides in from the right as designed, takes focus, closes on
Escape and notes that the wave never ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:15:03 +03:00

167 lines
16 KiB
Markdown
Raw 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.
# Дизайн и дизайн-система
Источник — кликабельные прототипы в [`Разработка дизайна приложения/`](Разработка%20дизайна%20приложения/):
| Файл | Что внутри |
|---|---|
| `Pipu Web.dc.html` | Веб: вход, setup-экраны, главная, поиск, исполнитель, альбом, коллекция, профиль, вся админка, диалоги, очередь плеера |
| `Pipu Android.dc.html` | Android: подключение к серверу, вход, главная, поиск, коллекция (со «Скачанным»), альбом, исполнитель, профиль, полноэкранный плеер, листы (качество, пароль, очередь, обновление), офлайн-режим |
| `Wave Options.dc.html` | Три варианта кнопки «Моя волна» |
| `wave.js` | Реализация кнопки волны (Canvas 2D, React) — **референс для продакшен-компонента** |
| `pipu-data.js` | Демо-данные прототипа |
| `support.js`, `android-frame.jsx` | Рантайм прототипа и рамка устройства — в продукт не переносятся |
Просмотр прототипов: прототипам нужен HTTP-сервер (из `file://` скрипты не работают).
В репозитории есть конфиг `.claude/launch.json` (`python -m http.server 8765 --directory docs`),
вручную — та же команда, затем `http://localhost:8765/`.
## 1. Принятые решения по вариантам прототипа
| Развилка | Решение |
|---|---|
| Кнопка «Моя волна» | **Сфера с шумом (`orb`)** и **Жидкая форма (`liquid`)** — пользователь выбирает в профиле, по умолчанию `orb`. Вариант «Кольца» не реализуем. |
| Каркас веба | **Classic**: сайдбар 240 px, топбар 60 px, разделители-линии, без «парящих» панелей |
| Плеер веба | **Оба**: `bar` (полоса 88 px внизу, по умолчанию) и `island` (плавающий остров) — выбор в профиле |
| Главная | **Center**: волна по центру, под ней чипы «Занятие» и «Настроение» |
| Очередь на добавление (админ) | `grouped` — группировка по альбомам (вариант прототипа по умолчанию) |
| Полноэкранный плеер Android | `cover` (крупная обложка) по умолчанию, `wave` (визуализация волны) — переключатель в профиле |
| Тема | Тёмная (по умолчанию) и светлая, переключатель в профиле; `system` — следовать ОС |
| Ambient-фон | Включён: размытые дрейфующие пятна, третье пятно окрашено в цвет обложки текущего трека; отключается в профиле и при `prefers-reduced-motion` |
Пользовательские настройки внешнего вида хранятся в `users.ui_prefs` и синхронизируются между устройствами
(см. [data-model.md](data-model.md)).
## 2. Токены
Источник истины — `packages/design-tokens/tokens.json`; из него генерируются CSS-переменные (веб) и TS-объект (React Native).
Значения взяты из прототипа без изменений.
### Цвета
| Токен | Тёмная | Светлая | Назначение |
|---|---|---|---|
| `bg` | `#05070D` | `#F3F5FA` | Фон приложения |
| `bg2` | `#080B14` | `#EBEEF5` | Вторичный фон |
| `surface` | `#0B0F1A` | `#FFFFFF` | Карточки, панели |
| `surface2` | `#121827` | `#F0F3F9` | Поля ввода, вложенные блоки |
| `surface3` | `#1A2236` | `#E4E9F3` | Чипы, кнопки второго уровня |
| `elev` | `#111726` | `#FFFFFF` | Поповеры, меню |
| `hover` | `rgba(120,150,255,.07)` | `rgba(30,60,140,.06)` | Подсветка при наведении |
| `line` / `line2` | `rgba(130,160,255,.10)` / `.18` | `rgba(20,35,80,.10)` / `.17` | Разделители / рамки |
| `text` / `text2` / `text3` | `#EDF1FF` / `#A0ABC6` / `#7F8BA8` | `#0A0F1E` / `#4B5675` / `#5C6785` | Текст: основной / вторичный / подписи |
| `accent` / `accent2` | `#2F80FF` / `#5E9BFF` | `#1F6FF2` / `#1F6FF2` | Основной акцент, ссылки |
| `accent-fill` / `accent-fill2` | `#1A66E6` / `#1F6FF2` | `#1A63DB` / `#165ED6` | Заливка под белым текстом (кнопки, выбранные сегменты) и её hover — контраст ≥ 4.5:1 |
| `accent-soft` | `rgba(47,128,255,.15)` | `rgba(31,111,242,.10)` | Активный пункт меню, фокус |
| `violet` / `violet-soft` | `#9B7CFF` / `rgba(124,92,255,.16)` | `#6A47F0` / `.10` | Второй акцент (волна, бейджи) |
| `ok` / `ok-soft` | `#2FD39A` | `#0E9465` | Успех |
| `warn` / `warn-soft` | `#FFB547` | `#A86200` | Предупреждение |
| `danger` / `danger-soft` | `#FF5C70` | `#D32F47` | Ошибка, дизлайк, отклонение |
| `overlay` | `rgba(3,5,10,.66)` | `rgba(20,30,60,.32)` | Подложка диалогов |
| `glass` | `rgba(12,16,28,.78)` | `rgba(255,255,255,.84)` | Стеклянные карточки (вход, диалоги) + `backdrop-filter: blur(28px) saturate(1.3)` |
Палитра волны по умолчанию: `#2F80FF`, `#7C5CFF`, `#35D3FF`. Логотип — радиальный градиент `#9CC6FF → #2F80FF → #3A1FA8`.
### Типографика
| Роль | Шрифт | Где |
|---|---|---|
| Заголовки, логотип, надпись на волне | **Unbounded** 500–700, `letter-spacing: -0.01em` | Заголовки страниц 24–28 px, «Моя волна» |
| Основной текст | **Onest** 400–700 | Всё остальное; базовый размер 15 px, подписи 12–13 px |
| Моноширинный | **JetBrains Mono** 400/600 | Пути файлов, регэкспы, шаблоны, временные пароли, хэши |
| Иконки | **Material Symbols Rounded** (вариативный, `FILL 0..1`) | Залитые иконки — активное состояние |
Все шрифты — OFL / Apache 2.0. **Встраиваем в сборку** (`@fontsource/*`, сабсеты latin + cyrillic) — self-hosted
сервис не должен зависеть от Google Fonts. Иконки — сабсет Material Symbols только с используемыми глифами
(генерируется скриптом при сборке).
### Формы и тени
| Токен | Значение |
|---|---|
| Радиусы | 8 (мелкие кнопки), 10–12 (поля, кнопки), 16 (карточки), 24 (крупные карточки, диалоги), 999 (чипы, бейджи) |
| Высота поля / основной кнопки | 48 px |
| `shadow` | `0 24px 64px rgba(0,0,0,.55)` (светлая: `rgba(20,30,60,.18)`) |
| `glow` | `0 8px 28px rgba(47,128,255,.38)` — основная кнопка |
| Фокус | `border-color: accent` + `box-shadow: 0 0 0 3px accent-soft` |
### Анимация
| Что | Как |
|---|---|
| Появление контента | `pipuIn`: opacity + translateY(6px), 200 мс |
| Эквалайзер текущего трека | `pipuEq`: scaleY 0.25 → 1 |
| Ambient-фон | 3 пятна 55–70vmax, blur 30 px, дрейф 16 / 21 / 26 с, `hue-rotate` ±35° за 30 с, прозрачность `amb-op` (0.8 тёмная / 0.45 светлая) |
| Пружины (волна, нажатия) | Демпфированная пружина как в `wave.js` (`v += (target − x)·0.35; v *= 0.62`) |
| `prefers-reduced-motion` | Ambient статичен, волна замедлена ×0.15 без пульса |
## 3. Кнопка «Моя волна»
Продакшен-компонент — порт `wave.js` на TypeScript, **Canvas 2D** (прототип показал, что этого достаточно; WebGL не нужен).
```ts
interface WaveButtonProps {
variant: 'orb' | 'liquid';
playing: boolean;
bpm?: number; // из TrackDto.features, по умолчанию 100
energy?: number; // 0..1, по умолчанию 0.6
colors?: [string, string, string]; // палитра обложки текущего трека, иначе палитра волны
theme: 'dark' | 'light';
reduced?: boolean; // prefers-reduced-motion
interactive?: boolean; // false — декоративный фон экрана входа
size: number; // 260–360 px на главной, 620 px на фоне входа
onClick?(): void;
}
```
Поведение (как в прототипе):
- **Пульс в такт**: фаза растёт со скоростью `bpm/60`, импульс `(1 − phase)^4 · (0.35 + 0.65·energy)`; в покое — медленное «дыхание».
- **Hover**: скорость анимации ×(1 + 1.4·hover), формы смещаются к курсору; **press**: сжатие до 0.93 с отскоком;
клик / Enter / Space — запуск / пауза.
- Внутри — иконка `play_arrow` / `pause` (44 px) и надпись «Моя волна» (Unbounded 18 px), подпись контекста («Под ваш вкус», «Бегаю · Энергичное»).
- Канвас масштабируется по `devicePixelRatio`; анимация останавливается, когда вкладка скрыта.
- Android: тот же алгоритм на `@shopify/react-native-skia` (API Skia Canvas близок к Canvas 2D), общие формулы —
в `packages/core/wave-anim`.
## 4. Каркас и экраны
### Веб (Classic)
- Сетка: `grid-template-rows: 60px 1fr 88px` (плеер `bar`) или `60px 1fr` + остров снизу (`island`, отступ контента 132 px).
- Топбар: логотип, поиск по центру (выпадающий список с группами «Исполнители / Альбомы / Треки» и подсказкой
исправленной раскладки), аватар с меню профиля (тема, язык, выход).
- Сайдбар: Главная · Коллекция (История, Лайки + счётчик, Дизлайки + счётчик) · Админка (Пользователи, Хранилища,
Библиотека, Предложить треки, Очередь + бейдж, Настройки) — пункты по ролям.
- Плеер: обложка, название / исполнитель, лайк / дизлайк, назад / play (круглая белая кнопка) / вперёд, прогресс со временем,
очередь, громкость. Очередь — выезжающая справа панель; в режиме волны внизу пометка «Волна бесконечна…».
- Диалоги: стеклянные карточки по центру поверх `overlay`. Тосты — внизу над плеером.
Экраны, которые есть в прототипе, и документы, где описана логика:
| Экран | Логика |
|---|---|
| Вход, смена учётных данных админа, обязательная смена пароля | [auth-and-roles.md](auth-and-roles.md) |
| Главная, поиск, исполнитель, альбом | [web.md](web.md), [library-and-storage.md](library-and-storage.md#поиск) |
| Коллекция: История (по дням) / Лайки / Дизлайки | [api.md](api.md) |
| Профиль: пароль, язык, тема, кроссфейд, выравнивание громкости (EBU R128), вид волны, вид плеера, выход | [playback.md](playback.md) |
| Пользователи: таблица, создание (пароль генерируется автоматически), временный пароль один раз, меню действий | [auth-and-roles.md](auth-and-roles.md) |
| Хранилища: NFS / local, опции, «Принимает новые треки», регэкспы с перетаскиванием, предпросмотр, шаблон раскладки с автоподбором и проверкой | [library-and-storage.md](library-and-storage.md) |
| Библиотека: состояние, скан, аудиоанализ, качество волны за 7 дней, ошибки | [recommendations.md](recommendations.md#1-цели-и-как-измеряем-успех) |
| Предложить треки, Очередь на добавление, Настройки очереди | [uploads.md](uploads.md) |
### Android
- Нижняя навигация: Главная · Поиск · Коллекция · Профиль; мини-плеер над ней; полноэкранный плеер (`cover` / `wave`).
- Подключение: адрес сервера → проверка (`/server-info`: версия, HTTPS, совместимость) → список сохранённых серверов.
- Поиск: недавние запросы (локально), подсказка про опечатки и раскладку.
- Коллекция → Скачанное: занято / свободно, «Скачивать новые лайки», «Скачать все лайки», очередь загрузок «по Wi-Fi».
- Профиль: пароль, язык, качество звука (лист), только Wi-Fi, кэш недавних треков (очистить), проверка обновлений
(точка-индикатор), сервер, «Админка — откроется в браузере» (для персонала), выход.
- Лист обновления: «Что нового» → разрешение на установку (один раз) → загрузка → проверка SHA-256 → «Установить».
- Офлайн-баннер при отсутствии сети.
## 5. Пробелы дизайна (дорабатываем при реализации)
- **Адаптивность веба**: прототип рассчитан на десктоп (≥ 1200 px). Для < 1024 px сайдбар сворачивается в иконки,
для < 768 px — нижняя навигация как в Android. Макетов нет — делаем по образцу Android-версии.
- **Пустые состояния и ошибки** есть не везде (например, хранилище недоступно, нет сети в вебе) — по образцу существующих
(иконка + короткий текст + действие).
- **Онбординг вкуса** (выбор жанров / исполнителей при первом входе) в прототипе отсутствует — макет нужен до M6.
- **Английская локализация**: в прототипе только русские строки; при вёрстке проверять длинные строки на обоих языках.