chore(openspec): archive grid-keyboard-navigation and sync main specs

This commit is contained in:
iu
2026-10-11 14:51:44 +03:00
parent 7359495a73
commit 3311de57c5
6 changed files with 120 additions and 0 deletions
@@ -0,0 +1,35 @@
# Tasks
## 1. PhotoGrid: API курсора и визуализация
- [x] 1.1 Добавить в `lib/components/PhotoGrid.svelte` проп `cursorIndex` и колбэки `onCursor`, `onToggle`, `onExtend`, `onOpen`, `onClear`; прокинуть их из `routes/(app)/albums/[id]/+page.svelte`. Проверка: `cd frontend && npm run check` проходит, сетка рендерится без изменений поведения мыши.
- [x] 1.2 Реализовать roving tabindex: у ячейки под курсором `tabindex="0"`, у остальных `-1`; при пустом курсоре `0` у первой ячейки. Проверка: Tab вводит фокус в сетку один раз, стрелки не покидают сетку.
- [x] 1.3 Добавить визуальный контур курсора (`data-cursor`, внешний `outline-2 outline-offset-2` в токене `text`) с показом только при `:focus-within`. Проверка: в светлой, тёмной и нейтральной темах контур отличим от `ring-primary` и читается, когда ячейка одновременно выделена и под курсором.
## 2. Клавиши сетки
- [x] 2.1 Реализовать геометрию перемещения по плоскому индексу: `←`/`→` — `index ± 1` (естественный переход на соседнюю строку), `↑`/`↓` — `index ± density.cols` с зажимом в `[0, n-1]`. Проверка: ручной прогон — `→` в конце строки уходит на следующую строку, `↑`/`↓` не переносят через верх/низ.
- [x] 2.2 Повесить `keydown` на контейнер сетки (не на `window`); игнорировать события вне фокуса сетки; `preventDefault` только для обрабатываемых клавиш. Проверка: при фокусе в поле ввода панели или в диалоге стрелки и `Space` не двигают курсор.
- [x] 2.3 Обработать клавиши-действия: `Space` → `onToggle`, `Shift`+стрелка → `onExtend`, `Enter` → `onOpen`, `Esc` → `onClear`. Проверка: `Space` переключает выделение и обновляет правую панель, `Shift`+стрелка расширяет диапазон от якоря, `Enter` открывает просмотр, `Esc` снимает выделение и оставляет курсор.
## 3. Снятие выделения кликом
- [x] 3.1 Добавить обработчик клика на контейнер сетки вместе с отступами: пропускать цель внутри `[data-photo-cell]`, иначе `selection.clear()`; контейнеру дать `tabindex="-1"` и фокусировать его после снятия. Проверка: клик по пустому месту возвращает в правой панели свойства альбома, клик по фото/кнопке тулбара/элементу панели выделение не снимает, после снятия стрелки сразу двигают курсор.
## 4. Интеграция страницы альбома
- [x] 4.1 Ввести `cursorIndex` в `+page.svelte`; синхронизировать с мышью (клик, `ctrl`/`cmd`-клик, `shift`-клик, контекстное меню ставят курсор); сбрасывать в `null` при смене альбома в том же `$effect`, где `selection.clear()`.
- [x] 4.2 При перемещении курсора вызывать `scrollIntoView({ block: 'nearest' })` для ячейки. Проверка: движение к краю прокручивает сетку, ячейка под курсором видна.
- [x] 4.3 При движении вперёд за последнее загруженное фото и наличии `nextCursor` вызывать `loadMore()`, затем ставить курсор на новое фото; без `nextCursor` — зажим. Проверка: `→` на последнем загруженном фото догружает следующую порцию и перемещает курсор.
- [x] 4.4 Реализовать `onExtend`: если якорь не задан, взять текущий курсор; выставлять выделение диапазоном от якоря до курсора через `selection.set`. Проверка: `Shift`+стрелка добавляет соседние фото в выделение, правая панель показывает количество.
## 5. Интеграционная проверка
- [x] 5.1 Прогнать `cd frontend && npm run check` без ошибок типов и предупреждений.
- [x] 5.2 Вручную пройти сценарии дельты `ui-workspace` в светлой, тёмной и нейтральной темах: движение курсора без смены панели, `Space`+панель, `Shift`-диапазон, `Enter`, `Esc`, клик по пустому месту, догрузка; убедиться, что клик/дабл-клик/контекстное меню/drag работают как прежде.
## 6. Возврат фокуса из просмотра
- [x] 6.1 Добавить модульный стор `lib/stores/viewer-return.ts` (однослотовая память `{albumId, photoId}` с `remember`/`consume`) и вызывать `remember(albumId, photoId)` в `routes/(app)/albums/[id]/photos/[photoId]/+page.svelte` через `$effect` на `photoId`. Проверка: `npm run check` проходит; при открытии просмотра и листании слот хранит текущий `photoId`.
- [x] 6.2 В `routes/(app)/albums/[id]/+page.svelte` в `$effect` на `albumId` вызывать `viewerReturn.consume(albumId)`, а после `loading = false` и `tick()` — восстанавливать фокус: `selection.set(albumId, [photo])`, `anchorId`, `cursorIndex`, `.focus({preventScroll:true})` и `scrollIntoView({block:'center'})`; если фото нет среди загруженных — догружать `loadMore()` в цикле. Проверка: возврат из просмотра выделяет и фокусирует просмотренное фото, стрелки работают сразу; фото со второй страницы догружается.
- [x] 6.3 Расширить QA-скрипт сценариями возврата (закрытие просмотра, листание и возврат, фото вне первой страницы, возврат в другой альбом) и прогнать его. Проверка: все проверки PASS, `npm run check` без ошибок и предупреждений.