diff --git a/openspec/changes/lightroom-workspace-ui/design.md b/openspec/changes/lightroom-workspace-ui/design.md index 4f203d4..bcd5540 100644 --- a/openspec/changes/lightroom-workspace-ui/design.md +++ b/openspec/changes/lightroom-workspace-ui/design.md @@ -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-миграция); фронтенд откатывается независимо, поле ответа игнорируется старым клиентом. diff --git a/openspec/changes/lightroom-workspace-ui/proposal.md b/openspec/changes/lightroom-workspace-ui/proposal.md index c4e6a36..9295f84 100644 --- a/openspec/changes/lightroom-workspace-ui/proposal.md +++ b/openspec/changes/lightroom-workspace-ui/proposal.md @@ -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. Ломающих diff --git a/openspec/changes/lightroom-workspace-ui/specs/albums/spec.md b/openspec/changes/lightroom-workspace-ui/specs/albums/spec.md index 43a7d00..90093a9 100644 --- a/openspec/changes/lightroom-workspace-ui/specs/albums/spec.md +++ b/openspec/changes/lightroom-workspace-ui/specs/albums/spec.md @@ -29,3 +29,11 @@ #### Scenario: Нет прав на смену обложки - **WHEN** участник без права редактирования пытается сменить обложку - **THEN** система отклоняет запрос + +### Requirement: Счётчики альбома +Данные альбома SHALL включать количество фото и участников, чтобы панель и карточки могли +показать их без отдельных запросов. + +#### Scenario: Счётчики в ответе +- **WHEN** клиент запрашивает список альбомов или альбом по id +- **THEN** ответ содержит число фото и число участников альбома diff --git a/openspec/changes/lightroom-workspace-ui/specs/photos/spec.md b/openspec/changes/lightroom-workspace-ui/specs/photos/spec.md index b31b173..ab25c72 100644 --- a/openspec/changes/lightroom-workspace-ui/specs/photos/spec.md +++ b/openspec/changes/lightroom-workspace-ui/specs/photos/spec.md @@ -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-загрузку и обрабатывает фото diff --git a/openspec/changes/lightroom-workspace-ui/specs/ui-workspace/spec.md b/openspec/changes/lightroom-workspace-ui/specs/ui-workspace/spec.md index 2de1845..4d00240 100644 --- a/openspec/changes/lightroom-workspace-ui/specs/ui-workspace/spec.md +++ b/openspec/changes/lightroom-workspace-ui/specs/ui-workspace/spec.md @@ -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** открывается альбом с сеткой фото diff --git a/openspec/changes/lightroom-workspace-ui/tasks.md b/openspec/changes/lightroom-workspace-ui/tasks.md index 286df5d..fa2dcf3 100644 --- a/openspec/changes/lightroom-workspace-ui/tasks.md +++ b/openspec/changes/lightroom-workspace-ui/tasks.md @@ -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 Тема и хром: нейтральная тема — тёплый графит, порядок и значок переключателя, стилизация скроллбаров. Проверка: темы переключаются согласованно, скроллбары в тон темы.