feat: Lightroom-style workspace UI #2
@@ -93,6 +93,37 @@ filmstrip, панель метаданных/комментариев, тулб
|
|||||||
Кнопка `+` с хинтом (в рейле и/или топбаре) открывает модалку с полем названия; `POST
|
Кнопка `+` с хинтом (в рейле и/или топбаре) открывает модалку с полем названия; `POST
|
||||||
/albums`, созданный альбом становится активным. Инлайн-форма удаляется.
|
/albums`, созданный альбом становится активным. Инлайн-форма удаляется.
|
||||||
|
|
||||||
|
### 12. Переименование фото: `PATCH /photos/:id`
|
||||||
|
Имя файла меняется через новый эндпоинт; права — как у удаления (`CanDeleteAnyPhoto` или
|
||||||
|
автор своего фото). Пустое имя отклоняется, длина ограничивается 255 символами, изменение
|
||||||
|
транслируется событием `photo.updated`. UI — инлайн-редактор в правой панели. Альтернатива —
|
||||||
|
редактировать `caption` вместо имени — отклонена: пользователь просил имя файла.
|
||||||
|
|
||||||
|
### 13. Просмотр: полный экран и зум
|
||||||
|
Полный экран — `requestFullscreen` на корне `Viewer` (кнопка + `f`), состояние — по
|
||||||
|
`fullscreenchange`. Зум — локальные `zoomed`/`scale`: колесо меняет масштаб с привязкой к
|
||||||
|
курсору; `Space` переключает 100% (1:1); при увеличении изображение панорамируется
|
||||||
|
перетаскиванием и скроллбарами. Хоткеи игнорируются при фокусе в поле ввода. Зум
|
||||||
|
сбрасывается при смене фото. Альтернатива — только `object-fit` без ручного масштаба —
|
||||||
|
не даёт 1:1 и панорамирования.
|
||||||
|
|
||||||
|
### 14. Панели: ресайз и сворачиваемые разделы
|
||||||
|
Ширина рейла и панели хранится в `localStorage`, меняется перетаскиванием разделителя
|
||||||
|
(и стрелками с клавиатуры), ширина подаётся CSS-переменной на десктопных брейкпоинтах.
|
||||||
|
Разделы панели — компонент `PanelSection` со сворачиванием и сохранением состояния.
|
||||||
|
Альтернатива — фиксированные ширины и всегда развёрнутые секции — отклонена: не отвечает
|
||||||
|
запросу.
|
||||||
|
|
||||||
|
### 15. Лимит загрузки
|
||||||
|
Общий лимит тела запроса поднят до 100 МБ; при `http.MaxBytesError` возвращается
|
||||||
|
`413 file_too_large` с понятным сообщением вместо маскировки под ошибку разбора multipart.
|
||||||
|
Клиент заранее проверяет размер каждого файла и суммарный.
|
||||||
|
|
||||||
|
### 16. Счётчики альбома и событие обновления фото
|
||||||
|
`photos_count`/`members_count` считаются подзапросами в тех же `ListForUser`/`GetByID`, чтобы
|
||||||
|
панель обходилась одним запросом. Переименование публикует `photo.updated`, страница альбома
|
||||||
|
обновляет запись в списке.
|
||||||
|
|
||||||
## Risks / Trade-offs
|
## Risks / Trade-offs
|
||||||
|
|
||||||
- [Много альбомов → много presign в списке] → presign это локальная крипто-операция без сети;
|
- [Много альбомов → много presign в списке] → presign это локальная крипто-операция без сети;
|
||||||
@@ -109,15 +140,24 @@ filmstrip, панель метаданных/комментариев, тулб
|
|||||||
загрузки обложку не трогают, пользователь может задать явно.
|
загрузки обложку не трогают, пользователь может задать явно.
|
||||||
- [Дополнительное состояние в маршруте просмотра (filmstrip, клавиатура)] → единый компонент
|
- [Дополнительное состояние в маршруте просмотра (filmstrip, клавиатура)] → единый компонент
|
||||||
`Viewer` с props и общим источником фото.
|
`Viewer` с props и общим источником фото.
|
||||||
|
- [Зум/панорамирование конфликтует с навигацией и прокруткой] → при увеличении отключаем
|
||||||
|
стрелки и свайп; колесо над областью фото перехватывается с `preventDefault`.
|
||||||
|
- [Глобальные хоткеи при вводе текста] → игнорируем `f`/`Space`/стрелки, когда фокус в
|
||||||
|
`input`/`textarea`/`contenteditable`.
|
||||||
|
- [Крупные файлы утяжеляют память] → лимит 100 МБ и чтение через `LimitReader`; превышение
|
||||||
|
даёт понятную 413.
|
||||||
|
- [Сохранённые ширина/состояние панелей] → значения ограничиваются допустимым диапазоном.
|
||||||
|
|
||||||
## Migration Plan
|
## Migration Plan
|
||||||
|
|
||||||
1. Миграция `000002_album_cover_fk`: `UPDATE albums SET cover_photo_id = NULL WHERE
|
1. Миграция `000002_album_cover_fk`: `UPDATE albums SET cover_photo_id = NULL WHERE
|
||||||
cover_photo_id IS NOT NULL AND cover_photo_id NOT IN (SELECT id FROM photos);` затем
|
cover_photo_id IS NOT NULL AND cover_photo_id NOT IN (SELECT id FROM photos);` затем
|
||||||
`ADD CONSTRAINT ... ON DELETE SET NULL`. Down — `DROP CONSTRAINT`.
|
`ADD CONSTRAINT ... ON DELETE SET NULL`. Down — `DROP CONSTRAINT`.
|
||||||
2. Бэкенд: `models.Album` (+`CoverThumbURL`), `albums/store.go` (LEFT JOIN), `albums/service.go`
|
2. Бэкенд: `models.Album` (+`CoverThumbURL`, `PhotosCount`, `MembersCount`), `albums/store.go`
|
||||||
(enrich + presign), `photos/service.go` (SetCover только если NULL).
|
(LEFT JOIN + счётчики), `albums/service.go` (enrich + presign), `photos` (автообложка только
|
||||||
3. Фронтенд: каркас и маршруты, компоненты, токены, `types.ts`.
|
если NULL; `PATCH /photos/:id` для переименования; лимит 100 МБ и `413`).
|
||||||
|
3. Фронтенд: каркас и маршруты, компоненты (шелл с ресайзом/фуллскрином, рейл, панель с
|
||||||
|
разделами и EXIF, модалки, `Viewer` с зумом), стор альбомов, токены и скроллбары, `types.ts`.
|
||||||
4. Откат: снять constraint (down-миграция); фронтенд откатывается независимо, поле ответа
|
4. Откат: снять constraint (down-миграция); фронтенд откатывается независимо, поле ответа
|
||||||
игнорируется старым клиентом.
|
игнорируется старым клиентом.
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,18 @@
|
|||||||
- **Плотность сетки.** Слайдер размера миниатюр в тулбаре (состояние в localStorage).
|
- **Плотность сетки.** Слайдер размера миниатюр в тулбаре (состояние в localStorage).
|
||||||
Мультивыбор и массовые действия — вне объёма.
|
Мультивыбор и массовые действия — вне объёма.
|
||||||
- **Загрузка.** Крупный dropzone заменяется кнопкой в тулбаре, полноэкранным
|
- **Загрузка.** Крупный dropzone заменяется кнопкой в тулбаре, полноэкранным
|
||||||
drag-overlay и dropzone-намёком только в пустом альбоме.
|
drag-overlay и dropzone-намёком только в пустом альбоме. Лимит — 100 МБ с понятной
|
||||||
- **Тёмная тема.** Существующие 4 темы сохраняются; `dark` переодевается в графит
|
ошибкой при превышении.
|
||||||
Lightroom и становится дефолтом; добавляются токены `--chrome` и `--canvas`.
|
- **Переименование фото.** `PATCH /photos/:id` меняет `original_filename`; инлайн-редактор
|
||||||
|
в правой панели, доступный автору фото и owner/admin.
|
||||||
|
- **Просмотр.** Полноэкранный режим (кнопка и хоткей `f`), зум колесом мыши, `Space` — 100%
|
||||||
|
с панорамированием; удаление текущего фото с модальным подтверждением; возврат к обзору.
|
||||||
|
- **Разделы и панели.** Правая панель — сворачиваемые разделы (состояние сохраняется), с
|
||||||
|
разделом «Информация» (EXIF/метаданные); левая и правая панели ресайзятся перетаскиванием.
|
||||||
|
- **Счётчики.** В ответе альбома — число фото и участников.
|
||||||
|
- **Темы и хром.** Существующие 4 темы сохраняются; `dark` — графит Lightroom и дефолт,
|
||||||
|
`neutral` — тёплый графит; переключатель упорядочен тёмная → нейтральная → светлая →
|
||||||
|
системная; добавляются токены `--chrome`/`--canvas`; скроллбары стилизованы в тон темы.
|
||||||
- **Адаптив.** На узких экранах рейл становится off-canvas drawer, правая панель —
|
- **Адаптив.** На узких экранах рейл становится off-canvas drawer, правая панель —
|
||||||
bottom-sheet, filmstrip — горизонтальным скроллом.
|
bottom-sheet, filmstrip — горизонтальным скроллом.
|
||||||
|
|
||||||
@@ -42,22 +51,25 @@
|
|||||||
|
|
||||||
### Modified Capabilities
|
### Modified Capabilities
|
||||||
|
|
||||||
- `albums`: отображение обложки в списке/карточках и явный выбор обложки; поведение
|
- `albums`: отображение обложки, счётчики фото/участников и явный выбор обложки.
|
||||||
обложки при отсутствии выбора.
|
- `photos`: автообложка только при отсутствии выбора; переименование фото; лимит размера
|
||||||
- `photos`: автоустановка обложки первым загруженным фото выполняется только если обложка
|
загрузки.
|
||||||
ещё не задана.
|
|
||||||
|
|
||||||
## Impact
|
## Impact
|
||||||
|
|
||||||
- **Backend:** миграция FK `albums.cover_photo_id -> photos(id) ON DELETE SET NULL`;
|
- **Backend:** миграция FK `albums.cover_photo_id -> photos(id) ON DELETE SET NULL`;
|
||||||
`internal/albums/store.go` (LEFT JOIN `photos` → `cover_thumb_key` в `ListForUser` и
|
`internal/albums/store.go` (LEFT JOIN `photos` → `cover_thumb_key` и счётчики
|
||||||
`GetByID`); `internal/albums/service.go` (presign → `CoverThumbURL`, обогащение
|
`photos_count`/`members_count` в `ListForUser`/`GetByID`); `internal/albums/service.go`
|
||||||
списка); `internal/models` (`Album.CoverThumbURL`); `internal/photos/service.go`
|
(presign → `CoverThumbURL`, обогащение списка); `internal/models` (`Album.CoverThumbURL`,
|
||||||
(`SetCover` только при `cover_photo_id IS NULL`).
|
`PhotosCount`, `MembersCount`); `internal/photos` (автообложка только при
|
||||||
- **Frontend:** реструктуризация `+layout.svelte` в группы `(app)`/`(public)`; новые
|
`cover_photo_id IS NULL`; `PATCH /photos/:id` для переименования; лимит загрузки 100 МБ с
|
||||||
компоненты шелла, рейла, панели, модалки создания, пикера обложки, filmstrip и
|
`413 file_too_large`; событие `photo.updated`).
|
||||||
просмотра; новый маршрут `albums/[id]/photos/[photoId]`; новые значения токенов темы;
|
- **Frontend:** реструктуризация `+layout.svelte` в группы `(app)`/`(public)`, `AppShell`
|
||||||
`types.ts` (+`cover_thumb_url`); новые иконки в реестре.
|
с ресайзом панелей и кнопкой полного экрана; компоненты рейла, контекстной панели со
|
||||||
|
сворачиваемыми разделами и EXIF, модалки создания/пикера обложки/подтверждения,
|
||||||
|
`Viewer` с полноэкранным режимом и зумом, общий стор альбомов, контрол плотности; новый
|
||||||
|
маршрут `albums/[id]/photos/[photoId]`; новые значения токенов темы и стили скроллбаров;
|
||||||
|
`types.ts` (+`cover_thumb_url`, счётчики, `photos_count`); новые иконки в реестре.
|
||||||
- **Спеки:** `ui-workspace` (новая), `albums`, `photos` (дельты). `ui-iconography` не
|
- **Спеки:** `ui-workspace` (новая), `albums`, `photos` (дельты). `ui-iconography` не
|
||||||
затрагивается — иконки по-прежнему наследуют цвет темы.
|
затрагивается — иконки по-прежнему наследуют цвет темы.
|
||||||
- **Совместимость:** API-контракты сохраняются; добавляется поле ответа и один FK. Ломающих
|
- **Совместимость:** API-контракты сохраняются; добавляется поле ответа и один FK. Ломающих
|
||||||
|
|||||||
@@ -29,3 +29,11 @@
|
|||||||
#### Scenario: Нет прав на смену обложки
|
#### Scenario: Нет прав на смену обложки
|
||||||
- **WHEN** участник без права редактирования пытается сменить обложку
|
- **WHEN** участник без права редактирования пытается сменить обложку
|
||||||
- **THEN** система отклоняет запрос
|
- **THEN** система отклоняет запрос
|
||||||
|
|
||||||
|
### Requirement: Счётчики альбома
|
||||||
|
Данные альбома SHALL включать количество фото и участников, чтобы панель и карточки могли
|
||||||
|
показать их без отдельных запросов.
|
||||||
|
|
||||||
|
#### Scenario: Счётчики в ответе
|
||||||
|
- **WHEN** клиент запрашивает список альбомов или альбом по id
|
||||||
|
- **THEN** ответ содержит число фото и число участников альбома
|
||||||
|
|||||||
@@ -13,3 +13,31 @@
|
|||||||
#### Scenario: Обложка уже задана
|
#### Scenario: Обложка уже задана
|
||||||
- **WHEN** фото обработано, а обложка уже задана
|
- **WHEN** фото обработано, а обложка уже задана
|
||||||
- **THEN** система не меняет обложку
|
- **THEN** система не меняет обложку
|
||||||
|
|
||||||
|
### Requirement: Переименование фото
|
||||||
|
Автор фото, владелец и админ альбома SHALL иметь возможность изменить имя файла фото;
|
||||||
|
остальные SHALL получать отказ, а пустое имя SHALL отклоняться.
|
||||||
|
|
||||||
|
#### Scenario: Переименование автором
|
||||||
|
- **WHEN** автор меняет имя файла своего фото на непустое
|
||||||
|
- **THEN** система сохраняет новое имя и транслирует изменение подписчикам альбома
|
||||||
|
|
||||||
|
#### Scenario: Пустое имя
|
||||||
|
- **WHEN** пользователь подтверждает пустое имя файла
|
||||||
|
- **THEN** система отклоняет запрос
|
||||||
|
|
||||||
|
#### Scenario: Нет прав
|
||||||
|
- **WHEN** участник без прав переименовывает чужое фото
|
||||||
|
- **THEN** система отклоняет запрос
|
||||||
|
|
||||||
|
### Requirement: Ограничение размера загрузки
|
||||||
|
Система SHALL ограничивать размер загружаемых файлов и SHALL отвечать понятной ошибкой при
|
||||||
|
превышении лимита, а не ошибкой разбора формы.
|
||||||
|
|
||||||
|
#### Scenario: Превышение лимита
|
||||||
|
- **WHEN** загружаемый файл превышает максимальный размер
|
||||||
|
- **THEN** система отклоняет загрузку с явной ошибкой о превышении размера
|
||||||
|
|
||||||
|
#### Scenario: Файл в пределах лимита
|
||||||
|
- **WHEN** файл не превышает лимит
|
||||||
|
- **THEN** система принимает multipart-загрузку и обрабатывает фото
|
||||||
|
|||||||
@@ -91,9 +91,10 @@ SHALL отображаться вне каркаса, но в общей тем
|
|||||||
- **WHEN** пользователь открывает альбом без фото и имеет право загрузки
|
- **WHEN** пользователь открывает альбом без фото и имеет право загрузки
|
||||||
- **THEN** в центре показана зона-подсказка загрузки
|
- **THEN** в центре показана зона-подсказка загрузки
|
||||||
|
|
||||||
### Requirement: Тёмная графитовая тема
|
### Requirement: Темы и графитовый хром
|
||||||
Тёмная тема SHALL использовать графитовый хром Lightroom и SHALL применяться по умолчанию;
|
Тёмная тема SHALL использовать графитовый хром Lightroom и SHALL применяться по умолчанию;
|
||||||
остальные темы SHALL оставаться доступными.
|
нейтральная тема SHALL быть тёплым графитом; остальные темы SHALL оставаться доступными, а
|
||||||
|
скроллбары SHALL быть стилизованы в цвет темы.
|
||||||
|
|
||||||
#### Scenario: Первый вход
|
#### Scenario: Первый вход
|
||||||
- **WHEN** пользователь впервые открывает приложение без сохранённой темы
|
- **WHEN** пользователь впервые открывает приложение без сохранённой темы
|
||||||
@@ -101,7 +102,11 @@ SHALL отображаться вне каркаса, но в общей тем
|
|||||||
|
|
||||||
#### Scenario: Смена темы
|
#### Scenario: Смена темы
|
||||||
- **WHEN** пользователь выбирает другую тему
|
- **WHEN** пользователь выбирает другую тему
|
||||||
- **THEN** интерфейс и иконки переключаются согласованно и выбор сохраняется
|
- **THEN** интерфейс, иконки и скроллбары меняются согласованно, и выбор сохраняется
|
||||||
|
|
||||||
|
#### Scenario: Нейтральная тема
|
||||||
|
- **WHEN** пользователь выбирает нейтральную тему
|
||||||
|
- **THEN** применяется тёплый графитовый хром, отличный от холодной тёмной темы
|
||||||
|
|
||||||
### Requirement: Адаптивность рабочего стола
|
### Requirement: Адаптивность рабочего стола
|
||||||
На узких экранах рейл SHALL становиться выдвижной панелью, контекстная панель —
|
На узких экранах рейл SHALL становиться выдвижной панелью, контекстная панель —
|
||||||
@@ -119,3 +124,77 @@ SHALL отображаться вне каркаса, но в общей тем
|
|||||||
#### Scenario: Клавиатурная навигация
|
#### Scenario: Клавиатурная навигация
|
||||||
- **WHEN** пользователь работает только с клавиатуры
|
- **WHEN** пользователь работает только с клавиатуры
|
||||||
- **THEN** органы управления каркаса достижимы, доступные имена присутствуют, фокус виден
|
- **THEN** органы управления каркаса достижимы, доступные имена присутствуют, фокус виден
|
||||||
|
|
||||||
|
### Requirement: Изменение ширины панелей
|
||||||
|
Пользователь SHALL иметь возможность менять ширину левой и правой панелей перетаскиванием, а
|
||||||
|
выбранная ширина SHALL сохраняться между сессиями.
|
||||||
|
|
||||||
|
#### Scenario: Перетаскивание разделителя
|
||||||
|
- **WHEN** пользователь тянет разделитель у панели на рабочем столе
|
||||||
|
- **THEN** панель меняет ширину в допустимых пределах
|
||||||
|
|
||||||
|
#### Scenario: Сохранение ширины
|
||||||
|
- **WHEN** пользователь перезагружает приложение
|
||||||
|
- **THEN** восстановленные панели имеют сохранённую ширину
|
||||||
|
|
||||||
|
### Requirement: Сворачиваемые разделы панели
|
||||||
|
Контекстная панель SHALL группировать содержимое в сворачиваемые разделы, а состояние
|
||||||
|
сворачивания SHALL сохраняться; раздел «Информация» SHALL показывать метаданные EXIF фото.
|
||||||
|
|
||||||
|
#### Scenario: Сворачивание раздела
|
||||||
|
- **WHEN** пользователь нажимает заголовок раздела
|
||||||
|
- **THEN** содержимое раздела скрывается или показывается, состояние сохраняется
|
||||||
|
|
||||||
|
#### Scenario: Метаданные фото
|
||||||
|
- **WHEN** открыт просмотр фото
|
||||||
|
- **THEN** раздел «Информация» показывает размер, тип, разрешение, дату съёмки и координаты
|
||||||
|
|
||||||
|
### Requirement: Полноэкранный просмотр фотографии
|
||||||
|
В режиме просмотра SHALL быть доступен переход в полноэкранный режим кнопкой и хоткеем `f`.
|
||||||
|
|
||||||
|
#### Scenario: Вход в полный экран
|
||||||
|
- **WHEN** пользователь нажимает кнопку полного экрана или клавишу `f`
|
||||||
|
- **THEN** просмотр разворачивается на весь экран
|
||||||
|
|
||||||
|
#### Scenario: Выход из полного экрана
|
||||||
|
- **WHEN** пользователь повторно нажимает кнопку или клавишу `f`
|
||||||
|
- **THEN** полноэкранный режим закрывается
|
||||||
|
|
||||||
|
### Requirement: Зум в просмотре
|
||||||
|
Просмотр SHALL поддерживать зум: колесо мыши меняет масштаб, `Space` переключает 100%, а при
|
||||||
|
увеличении фото SHALL панорамироваться. Зум SHALL работать и в полноэкранном режиме.
|
||||||
|
|
||||||
|
#### Scenario: Зум колесом
|
||||||
|
- **WHEN** пользователь вращает колесо мыши над фото
|
||||||
|
- **THEN** масштаб фото меняется относительно положения курсора
|
||||||
|
|
||||||
|
#### Scenario: 100% по пробелу
|
||||||
|
- **WHEN** пользователь нажимает `Space`
|
||||||
|
- **THEN** фото показывается в масштабе 100% (1:1), повторное нажатие возвращает вписанный вид
|
||||||
|
|
||||||
|
#### Scenario: Панорамирование
|
||||||
|
- **WHEN** фото увеличено
|
||||||
|
- **THEN** пользователь может перемещать изображение перетаскиванием и прокруткой
|
||||||
|
|
||||||
|
### Requirement: Удаление фото из просмотра
|
||||||
|
Автор фото, владелец и админ альбома SHALL иметь возможность удалить текущее фото из режима
|
||||||
|
просмотра; удаление SHALL подтверждаться диалогом.
|
||||||
|
|
||||||
|
#### Scenario: Подтверждение удаления
|
||||||
|
- **WHEN** пользователь с правом удаления нажимает «Удалить»
|
||||||
|
- **THEN** открывается диалог подтверждения, и после подтверждения фото удаляется
|
||||||
|
|
||||||
|
#### Scenario: Лента после удаления
|
||||||
|
- **WHEN** фото удалено
|
||||||
|
- **THEN** просмотр переходит к соседнему фото, а лента миниатюр обновляется без удалённого
|
||||||
|
|
||||||
|
### Requirement: Возврат к обзору
|
||||||
|
Из альбома и из режима просмотра SHALL быть доступен возврат к обзору альбомов.
|
||||||
|
|
||||||
|
#### Scenario: Возврат из альбома
|
||||||
|
- **WHEN** пользователь нажимает кнопку «назад» слева от названия альбома
|
||||||
|
- **THEN** открывается обзор альбомов
|
||||||
|
|
||||||
|
#### Scenario: Возврат из просмотра
|
||||||
|
- **WHEN** пользователь нажимает кнопку «назад» в режиме просмотра
|
||||||
|
- **THEN** открывается альбом с сеткой фото
|
||||||
|
|||||||
@@ -42,3 +42,14 @@
|
|||||||
|
|
||||||
- [x] 7.1 Пройти сценарий: создать альбом → загрузить фото → задать обложку → открыть просмотр → открыть share-ссылку; проверить поведение на десктопе и узком экране.
|
- [x] 7.1 Пройти сценарий: создать альбом → загрузить фото → задать обложку → открыть просмотр → открыть share-ссылку; проверить поведение на десктопе и узком экране.
|
||||||
- [x] 7.2 Проверить `make build` (сборка фронтенда в `backend/static`, статический Go-бинарник) и `openspec validate "lightroom-workspace-ui"`.
|
- [x] 7.2 Проверить `make build` (сборка фронтенда в `backend/static`, статический Go-бинарник) и `openspec validate "lightroom-workspace-ui"`.
|
||||||
|
|
||||||
|
## 8. Дополнительно реализовано (после планирования)
|
||||||
|
|
||||||
|
- [x] 8.1 Переименование фото: `PATCH /photos/:id` (права автора/owner/admin, пустое имя 400) и инлайн-редактор в правой панели; событие `photo.updated`. Проверка: переименование сохраняется, пустое имя отклоняется, чужое фото — отказ, событие приходит.
|
||||||
|
- [x] 8.2 Удаление фото из просмотра с модальным подтверждением; переход к соседнему и обновление ленты. Проверка: после удаления лента не содержит удалённое фото.
|
||||||
|
- [x] 8.3 Полноэкранный просмотр (кнопка и `f`) и зум (колесо, `Space` 100%, панорамирование) в обычном и полноэкранном режиме. Проверка: хоткеи и зум работают, при вводе текста не срабатывают.
|
||||||
|
- [x] 8.4 Сворачиваемые разделы правой панели с сохранением состояния и раздел «Информация» (EXIF). Проверка: состояние сохраняется между сессиями, метаданные отображаются.
|
||||||
|
- [x] 8.5 Ресайз левой и правой панелей перетаскиванием с сохранением ширины. Проверка: ширина восстанавливается после перезагрузки.
|
||||||
|
- [x] 8.6 Лимит загрузки 100 МБ и понятные ошибки (клиентская проверка и `413 file_too_large`). Проверка: 60 МБ проходит разбор, превышение лимита даёт явную ошибку размера.
|
||||||
|
- [x] 8.7 Счётчики `photos_count`/`members_count` в ответе альбома. Проверка: `GET /api/albums` и `GET /api/albums/:id` содержат счётчики.
|
||||||
|
- [x] 8.8 Тема и хром: нейтральная тема — тёплый графит, порядок и значок переключателя, стилизация скроллбаров. Проверка: темы переключаются согласованно, скроллбары в тон темы.
|
||||||
|
|||||||
Reference in New Issue
Block a user