chore(openspec): sync lightroom-workspace-ui with implemented changes

Reflect post-planning work in proposal, design, specs (albums/photos/
ui-workspace) and tasks: photo rename, viewer delete/zoom/fullscreen,
resizable and collapsible panels with EXIF, upload size limit, album
counts, themes/scrollbars, back navigation.
This commit is contained in:
iu
2026-10-10 19:45:55 +03:00
parent d1e3aa85b4
commit dbf116999d
6 changed files with 199 additions and 21 deletions
@@ -93,6 +93,37 @@ filmstrip, панель метаданных/комментариев, тулб
Кнопка `+` с хинтом (в рейле и/или топбаре) открывает модалку с полем названия; `POST
/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
- [Много альбомов → много presign в списке] → presign это локальная крипто-операция без сети;
@@ -109,15 +140,24 @@ filmstrip, панель метаданных/комментариев, тулб
загрузки обложку не трогают, пользователь может задать явно.
- [Дополнительное состояние в маршруте просмотра (filmstrip, клавиатура)] → единый компонент
`Viewer` с props и общим источником фото.
- [Зум/панорамирование конфликтует с навигацией и прокруткой] → при увеличении отключаем
стрелки и свайп; колесо над областью фото перехватывается с `preventDefault`.
- [Глобальные хоткеи при вводе текста] → игнорируем `f`/`Space`/стрелки, когда фокус в
`input`/`textarea`/`contenteditable`.
- [Крупные файлы утяжеляют память] → лимит 100 МБ и чтение через `LimitReader`; превышение
даёт понятную 413.
- [Сохранённые ширина/состояние панелей] → значения ограничиваются допустимым диапазоном.
## Migration Plan
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);` затем
`ADD CONSTRAINT ... ON DELETE SET NULL`. Down — `DROP CONSTRAINT`.
2. Бэкенд: `models.Album` (+`CoverThumbURL`), `albums/store.go` (LEFT JOIN), `albums/service.go`
(enrich + presign), `photos/service.go` (SetCover только если NULL).
3. Фронтенд: каркас и маршруты, компоненты, токены, `types.ts`.
2. Бэкенд: `models.Album` (+`CoverThumbURL`, `PhotosCount`, `MembersCount`), `albums/store.go`
(LEFT JOIN + счётчики), `albums/service.go` (enrich + presign), `photos` (автообложка только
если NULL; `PATCH /photos/:id` для переименования; лимит 100 МБ и `413`).
3. Фронтенд: каркас и маршруты, компоненты (шелл с ресайзом/фуллскрином, рейл, панель с
разделами и EXIF, модалки, `Viewer` с зумом), стор альбомов, токены и скроллбары, `types.ts`.
4. Откат: снять constraint (down-миграция); фронтенд откатывается независимо, поле ответа
игнорируется старым клиентом.
@@ -27,9 +27,18 @@
- **Плотность сетки.** Слайдер размера миниатюр в тулбаре (состояние в localStorage).
Мультивыбор и массовые действия — вне объёма.
- **Загрузка.** Крупный dropzone заменяется кнопкой в тулбаре, полноэкранным
drag-overlay и dropzone-намёком только в пустом альбоме.
- **Тёмная тема.** Существующие 4 темы сохраняются; `dark` переодевается в графит
Lightroom и становится дефолтом; добавляются токены `--chrome` и `--canvas`.
drag-overlay и dropzone-намёком только в пустом альбоме. Лимит — 100 МБ с понятной
ошибкой при превышении.
- **Переименование фото.** `PATCH /photos/:id` меняет `original_filename`; инлайн-редактор
в правой панели, доступный автору фото и owner/admin.
- **Просмотр.** Полноэкранный режим (кнопка и хоткей `f`), зум колесом мыши, `Space` — 100%
с панорамированием; удаление текущего фото с модальным подтверждением; возврат к обзору.
- **Разделы и панели.** Правая панель — сворачиваемые разделы (состояние сохраняется), с
разделом «Информация» (EXIF/метаданные); левая и правая панели ресайзятся перетаскиванием.
- **Счётчики.** В ответе альбома — число фото и участников.
- **Темы и хром.** Существующие 4 темы сохраняются; `dark` — графит Lightroom и дефолт,
`neutral` — тёплый графит; переключатель упорядочен тёмная → нейтральная → светлая →
системная; добавляются токены `--chrome`/`--canvas`; скроллбары стилизованы в тон темы.
- **Адаптив.** На узких экранах рейл становится off-canvas drawer, правая панель —
bottom-sheet, filmstrip — горизонтальным скроллом.
@@ -42,22 +51,25 @@
### Modified Capabilities
- `albums`: отображение обложки в списке/карточках и явный выбор обложки; поведение
обложки при отсутствии выбора.
- `photos`: автоустановка обложки первым загруженным фото выполняется только если обложка
ещё не задана.
- `albums`: отображение обложки, счётчики фото/участников и явный выбор обложки.
- `photos`: автообложка только при отсутствии выбора; переименование фото; лимит размера
загрузки.
## Impact
- **Backend:** миграция FK `albums.cover_photo_id -> photos(id) ON DELETE SET NULL`;
`internal/albums/store.go` (LEFT JOIN `photos` → `cover_thumb_key` в `ListForUser` и
`GetByID`); `internal/albums/service.go` (presign → `CoverThumbURL`, обогащение
списка); `internal/models` (`Album.CoverThumbURL`); `internal/photos/service.go`
(`SetCover` только при `cover_photo_id IS NULL`).
- **Frontend:** реструктуризация `+layout.svelte` в группы `(app)`/`(public)`; новые
компоненты шелла, рейла, панели, модалки создания, пикера обложки, filmstrip и
просмотра; новый маршрут `albums/[id]/photos/[photoId]`; новые значения токенов темы;
`types.ts` (+`cover_thumb_url`); новые иконки в реестре.
`internal/albums/store.go` (LEFT JOIN `photos` → `cover_thumb_key` и счётчики
`photos_count`/`members_count` в `ListForUser`/`GetByID`); `internal/albums/service.go`
(presign → `CoverThumbURL`, обогащение списка); `internal/models` (`Album.CoverThumbURL`,
`PhotosCount`, `MembersCount`); `internal/photos` (автообложка только при
`cover_photo_id IS NULL`; `PATCH /photos/:id` для переименования; лимит загрузки 100 МБ с
`413 file_too_large`; событие `photo.updated`).
- **Frontend:** реструктуризация `+layout.svelte` в группы `(app)`/`(public)`, `AppShell`
с ресайзом панелей и кнопкой полного экрана; компоненты рейла, контекстной панели со
сворачиваемыми разделами и EXIF, модалки создания/пикера обложки/подтверждения,
`Viewer` с полноэкранным режимом и зумом, общий стор альбомов, контрол плотности; новый
маршрут `albums/[id]/photos/[photoId]`; новые значения токенов темы и стили скроллбаров;
`types.ts` (+`cover_thumb_url`, счётчики, `photos_count`); новые иконки в реестре.
- **Спеки:** `ui-workspace` (новая), `albums`, `photos` (дельты). `ui-iconography` не
затрагивается — иконки по-прежнему наследуют цвет темы.
- **Совместимость:** API-контракты сохраняются; добавляется поле ответа и один FK. Ломающих
@@ -29,3 +29,11 @@
#### Scenario: Нет прав на смену обложки
- **WHEN** участник без права редактирования пытается сменить обложку
- **THEN** система отклоняет запрос
### Requirement: Счётчики альбома
Данные альбома SHALL включать количество фото и участников, чтобы панель и карточки могли
показать их без отдельных запросов.
#### Scenario: Счётчики в ответе
- **WHEN** клиент запрашивает список альбомов или альбом по id
- **THEN** ответ содержит число фото и число участников альбома
@@ -13,3 +13,31 @@
#### Scenario: Обложка уже задана
- **WHEN** фото обработано, а обложка уже задана
- **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** пользователь открывает альбом без фото и имеет право загрузки
- **THEN** в центре показана зона-подсказка загрузки
### Requirement: Тёмная графитовая тема
### Requirement: Темы и графитовый хром
Тёмная тема SHALL использовать графитовый хром Lightroom и SHALL применяться по умолчанию;
остальные темы SHALL оставаться доступными.
нейтральная тема SHALL быть тёплым графитом; остальные темы SHALL оставаться доступными, а
скроллбары SHALL быть стилизованы в цвет темы.
#### Scenario: Первый вход
- **WHEN** пользователь впервые открывает приложение без сохранённой темы
@@ -101,7 +102,11 @@ SHALL отображаться вне каркаса, но в общей тем
#### Scenario: Смена темы
- **WHEN** пользователь выбирает другую тему
- **THEN** интерфейс и иконки переключаются согласованно и выбор сохраняется
- **THEN** интерфейс, иконки и скроллбары меняются согласованно, и выбор сохраняется
#### Scenario: Нейтральная тема
- **WHEN** пользователь выбирает нейтральную тему
- **THEN** применяется тёплый графитовый хром, отличный от холодной тёмной темы
### Requirement: Адаптивность рабочего стола
На узких экранах рейл SHALL становиться выдвижной панелью, контекстная панель —
@@ -119,3 +124,77 @@ SHALL отображаться вне каркаса, но в общей тем
#### Scenario: Клавиатурная навигация
- **WHEN** пользователь работает только с клавиатуры
- **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.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 Тема и хром: нейтральная тема — тёплый графит, порядок и значок переключателя, стилизация скроллбаров. Проверка: темы переключаются согласованно, скроллбары в тон темы.