feat: context menus, unified photo panel, tags, album trash and counters

- backend: deleting an album moves its photos to the trash without losing files (photos.album_id nullable, FK ON DELETE SET NULL, migration 000006; dropped S3 prefix delete); restore accepts an album target (album_required when the album is gone); user-scoped tags (000007) with GET /tags, POST/DELETE /photos/:id/tags, tags included in photo responses; GET /trash/count
- frontend: right-click context menus for photos (grid + viewer) and albums (rail + overview); single shared PhotoPanel reused in viewer and gallery selection (not a copy); viewer download/delete moved to the panel 'Действия'; transparent compact viewer top bar; trash entry as last rail item with per-album photo counts and trash count; album rename/delete dialogs
- openspec: sync lightroom-workspace-ui artifacts (specs photos/ui-workspace, design #24-30, tasks 9.17-9.24)
This commit is contained in:
iu
2026-10-10 22:45:41 +03:00
parent 2d6a3fea1a
commit 61b45b2742
37 changed files with 1639 additions and 328 deletions
@@ -205,9 +205,60 @@ owner/admin) и загрузка в цель (member+).
### 24. Компактный верхний хром
Верхняя панель приложения (`AppShell`) и верхняя панель режима просмотра (`Viewer`) сведены к
минимуму: узкие вертикальные отступы (`py-0.5`/`py-1`), иконочные органы управления без рамок,
иконки `size-4`, компактный переключатель темы. Функциональность (меню, панель, темы, полный
иконки `size-4`, компактный переключатель темы. Верхняя панель просмотра — прозрачный оверлей
(`absolute`, без фона и рамки, `pointer-events-none` с активными кнопками), поэтому не занимает
место в раскладке и увеличивает область фото. Функциональность (меню, панель, темы, полный
экран, навигация, скачивание, удаление) сохраняется.
### 25. Действия в режиме просмотра — в правой панели
Скачивание и удаление убраны из верхней панели просмотра и перенесены в правую панель, в раздел
«Действия», вместе с «Переместить» и «Копировать». Скачивание — ссылка на presigned оригинал;
перенос/копирование — через `transfer`/`TransferDialog`; удаление инициируется стором
`viewerActions`, а обрабатывает его страница просмотра (та же логика перехода к соседнему фото
после удаления).
### 26. Единая панель фото
Содержимое правой панели для одного фото (имя файла, «Действия», «Информация», «Комментарии»)
вынесено в общий компонент `PhotoPanel`, который переиспользуется и в режиме просмотра
(`viewer=true`), и при одиночном выборе фото в сетке — это одна и та же панель, а не копия.
Отличие только в наборе действий: в просмотре нет «Открыть», а удаление идёт через стор
`viewerActions` (обрабатывает страница просмотра с переходом к соседнему фото); при выборе в
сетке удаление выполняет сам компонент и очищает выбор. Мультивыбор (>1) показывает только
раздел «Действия».
### 27. Контекстное меню фото
Правый клик по фото в сетке и по изображению в просмотре открывает контекстное меню с теми же
пунктами, что и раздел «Действия». Меню — общий стор `contextMenu` (`x`, `y`, items) и глобальный
компонент `ContextMenu` (портал), закрывается по клику вне, `Escape`, скроллу и ресайзу.
Пункты формируются на местах: в сетке — «Открыть (просмотр)», «Скачать», «Переместить»,
«Копировать», «Удалить» (для мультивыбора без «Скачать»); в просмотре — без «Открыть».
### 28. Удаление альбома и восстановление фото
Удаление альбома не должно терять файлы: перед удалением альбома его фото мягко удаляются
(`deleted_at`), а FK `photos.album_id` меняется на `ON DELETE SET NULL` (колонка nullable,
миграция `000006`) — после удаления альбома фото остаются строками с `album_id = NULL` в
корзине. Объекты S3 не удаляются (каскадный `DeletePrefix` убран: он ломал бы копии в других
альбомах). `GET /trash` включает такие «без альбома» фото загрузчика. Восстановление с
указанием целевого альбома (`POST /photos/:id/restore {album_id}`) переносит фото туда; без
цели и без альбома возвращается ошибка `album_required`, и клиент предлагает выбрать альбом или
создать новый.
### 29. Контекстное меню альбома и счётчики в рейле
Правый клик по альбому (в рейле и на обзоре) открывает меню «Переименовать»/«Удалить» через
общий стор `contextMenu`; диалоги переименования и подтверждения удаления — глобальный
`AlbumActionsDialogs` (стор `albumActions`). В рейле у каждого альбома показано число фото
(`album.photos_count`), у корзины — число удалённых фото (лёгкий `GET /trash/count`, стор
`trash`).
### 30. Метки фото (область — пользователь)
Метки принадлежат пользователю: таблицы `tags` (`user_id`, уникальность по `lower(name)`) и
`photo_tags` (миграция `000007`). Фото отдаёт метки текущего пользователя (подзапрос
`json_agg` в `photoColumns` по `$2`-viewer). Эндпоинты: `GET /tags?q=` (поиск по меткам
пользователя), `POST /photos/:id/tags {name}` (создаёт метку при необходимости и привязывает,
возвращает обновлённое фото), `DELETE /photos/:id/tags/:tag_id`. UI — раздел «Метки» в
`PhotoPanel`: чипы с удалением и поле с автодополнением (поиск + «Создать «…»»). Права: метку
можно ставить на фото альбома, доступного пользователю.
## Risks / Trade-offs
- [Много альбомов → много presign в списке] → presign это локальная крипто-операция без сети;