diff --git a/docs/specs/4-architecture/features/005-album-list-view/plan.md b/docs/specs/4-architecture/features/005-album-list-view/plan.md new file mode 100644 index 00000000000..1bbaaeea5d9 --- /dev/null +++ b/docs/specs/4-architecture/features/005-album-list-view/plan.md @@ -0,0 +1,459 @@ +# Feature Plan 005 – Album List View Toggle + +_Linked specification:_ `docs/specs/4-architecture/features/005-album-list-view/spec.md` +_Status:_ Draft +_Last updated:_ 2026-01-03 + +> Guardrail: Keep this plan traceable back to the governing spec. Reference FR/NFR/Scenario IDs from `spec.md` where relevant, log any new high- or medium-impact questions in [docs/specs/4-architecture/open-questions.md](docs/specs/4-architecture/open-questions.md), and assume clarifications are resolved only when the spec's normative sections (requirements/NFR/behaviour/telemetry) and, where applicable, ADRs under `docs/specs/5-decisions/` have been updated. + +## Vision & Success Criteria + +**User Value:** +Users with many albums or albums with long names can now switch to a list view that prioritizes information density and scannability. Full album names are displayed without truncation, and metadata (photo count, sub-album count) is visible at a glance without requiring hover or navigation. + +**Success Signals:** +- Toggle control is discoverable and functional in AlbumHero.vue icon row +- List view displays all required information (thumbnail, full name, counts) in horizontal rows +- View preference persists across page reloads via localStorage +- No performance degradation when rendering 100+ albums in list view +- Responsive layout adapts gracefully on mobile devices + +**Quality Bars:** +- Code follows Vue 3 Composition API and TypeScript conventions (NFR-005-04) +- Toggle control is keyboard-accessible with proper aria-labels (NFR-005-03) +- View mode loads synchronously from localStorage without blocking album data fetch (NFR-005-01) +- List view rendering completes within 300ms for 100 albums (NFR-005-02) + +## Scope Alignment + +**In scope:** +- New AlbumListView.vue component for rendering albums in horizontal list rows +- New AlbumListItem.vue component for individual list row rendering +- Modifications to AlbumHero.vue to add grid/list toggle buttons +- Modifications to AlbumThumbPanel.vue to conditionally render grid or list view +- LycheeState.ts modifications to add album_view_mode state with localStorage persistence +- Responsive design for mobile breakpoints (smaller thumbnails, compact layout) +- Keyboard accessibility for toggle controls +- Visual regression tests and component unit tests + +**Out of scope:** +- Backend API changes or database schema modifications +- Per-album view preferences (global preference only) +- Sorting or filtering capabilities specific to list view +- Customizable column layout or field selection +- Photo-level list view (feature is album-only) +- Multi-device sync of view preference (localStorage only, not synced to user settings) +- Advanced list features (drag-and-drop reordering, column resizing, etc.) + +## Dependencies & Interfaces + +**Frontend Dependencies:** +- Vue 3 (Composition API) +- TypeScript +- Tailwind CSS for styling +- PrimeVue for icons and accessibility utilities +- LycheeState.ts store (state management) +- AlbumState.ts store (album data) +- Existing Album model types (AlbumResource) + +**Components:** +- AlbumHero.vue (existing - will be modified) +- AlbumThumbPanel.vue (existing - will be modified) +- AlbumThumbPanelList.vue (existing - for comparison/reference) +- AlbumThumb.vue (existing - for comparison/reference) + +**Interfaces:** +- Album data structure from AlbumState.ts (id, title, thumb, num_photos, num_children, badges) +- Router navigation (existing) + +**Testing Infrastructure:** +- Vitest (component tests) +- Vue Test Utils +- Visual regression testing setup (if available) + +## Assumptions & Risks + +**Assumptions:** +- Album data structure includes `num_photos` and `num_children` fields (confirmed via exploration) +- PrimeVue icons (`pi-th`, `pi-list`) are available for toggle buttons +- localStorage is available in all supported browsers (graceful degradation if unavailable) +- Existing album rendering infrastructure supports custom layouts + +**Risks / Mitigations:** + +| Risk | Impact | Mitigation | +|------|--------|-----------| +| LocalStorage unavailable in private browsing mode | Medium - view preference won't persist | Default to grid view, feature still functional | +| Performance degradation with 1000+ albums | Medium - slow rendering | Test with large datasets, consider virtualization if needed (defer to follow-up) | +| Mobile layout complexity | Low - UI crowding on small screens | Use responsive Tailwind breakpoints, test on actual devices | +| Toggle button placement conflicts with existing icons | Low - UI crowding | Verify visual spacing, consider icon-only on mobile | +| TypeScript type mismatches in new components | Low - compile errors | Follow existing patterns from AlbumThumb.vue and AlbumThumbPanelList.vue | + +## Implementation Drift Gate + +**Drift Detection Strategy:** +- Before each increment, verify the specification FR/NFR requirements still match the planned work +- After each increment, confirm deliverables align with success criteria +- Record any deviations or clarifications in this plan's appendix + +**Evidence Collection:** +- Component tests pass (`npm run check`) +- Visual screenshots of grid vs list views (desktop + mobile) +- Performance measurements (rendering time for 100 albums) +- Accessibility audit results (keyboard navigation, aria-labels) + +**Commands to Rerun:** +- `npm run format` - Frontend formatting +- `npm run check` - Frontend tests and type checking +- `npm run dev` - Local development server for manual testing + +## Increment Map + +### I1 – LycheeState Store Modifications (View Mode State) + +**Goal:** Add album view mode state to LycheeState.ts with localStorage persistence + +**Preconditions:** None (foundational increment) + +**Steps:** +1. Read existing LycheeState.ts to understand structure +2. Add `album_view_mode: "grid" | "list"` property (default: "grid") +3. Add computed getter `albumViewMode` +4. Add action `setAlbumViewMode(mode: "grid" | "list")` that: + - Updates state + - Writes to localStorage key `album_view_mode` +5. Add initialization logic in store setup to read from localStorage on mount +6. Write unit test for localStorage read/write behavior + +**Commands:** +- `npm run check` (verify TypeScript types and tests) + +**Exit:** LycheeState has album_view_mode state, localStorage persistence works, tests pass + +**Implements:** FR-005-04, NFR-005-01, S-005-03, S-005-04 + +--- + +### I2 – AlbumListItem Component (Individual Row) + +**Goal:** Create reusable component for single album list row + +**Preconditions:** I1 complete (state management ready) + +**Steps:** +1. Create `resources/js/components/gallery/albumModule/AlbumListItem.vue` +2. Define props interface (album: AlbumResource, aspectRatio for thumb) +3. Implement template structure: + - Router-link wrapper for navigation (FR-005-02) + - 64px square thumbnail (left) - use existing AlbumThumbImage component + - Album title (full, untruncated, text-wrap allowed) + - Photo count display (icon + text or text only) + - Sub-album count display (icon + text or text only) + - Badge display (NSFW, password, etc.) - reuse existing badge logic +4. Apply Tailwind styling: + - Flex row layout: `flex items-center gap-4` + - Hover state: `hover:bg-gray-100 dark:hover:bg-gray-800` + - Border separator: `border-b border-gray-200 dark:border-gray-700` +5. Add responsive mobile styles: + - Smaller thumbnail on mobile: `md:w-16 md:h-16 w-12 h-12` + - Compact count layout +6. Write component unit test with sample album data + +**Commands:** +- `npm run check` (tests + types) +- `npm run format` (code formatting) + +**Exit:** AlbumListItem renders correctly with all required information, responsive, tests pass + +**Implements:** FR-005-01, FR-005-02, FR-005-05, FR-005-06, S-005-05, S-005-06, S-005-07, S-005-08, S-005-09 + +--- + +### I3 – AlbumListView Component (List Container) + +**Goal:** Create container component that renders albums as list using AlbumListItem + +**Preconditions:** I2 complete (AlbumListItem ready) + +**Steps:** +1. Create `resources/js/components/gallery/albumModule/AlbumListView.vue` +2. Define props interface (albums: AlbumResource[], aspectRatio: AspectRatioCSSType) +3. Implement template: + - Wrapper div with appropriate classes + - v-for loop over albums array + - Render AlbumListItem for each album + - Handle empty state (no albums) +4. Apply styling for list container: + - Flex column layout: `flex flex-col w-full` + - Spacing between rows handled by AlbumListItem borders +5. Handle click events (delegate to AlbumListItem router-link) +6. Handle context menu events (if needed - match grid behavior) +7. Write component test with multiple albums + +**Commands:** +- `npm run check` +- `npm run format` + +**Exit:** AlbumListView renders array of albums as list, navigation works, tests pass + +**Implements:** FR-005-01, S-005-05, S-005-06 + +--- + +### I4 – AlbumThumbPanel Modifications (Conditional Rendering) + +**Goal:** Update AlbumThumbPanel.vue to conditionally render grid or list based on view mode + +**Preconditions:** I1, I3 complete (state management + list view ready) + +**Steps:** +1. Read existing AlbumThumbPanel.vue to understand structure +2. Import AlbumListView component +3. Import LycheeState store to access albumViewMode +4. Add computed property to read current view mode from store +5. Update template to conditionally render: + - AlbumThumbPanelList (existing) when mode === "grid" + - AlbumListView (new) when mode === "list" +6. Ensure both views receive same props (albums, aspectRatio, etc.) +7. Manually test toggle behavior (switch between views) + +**Commands:** +- `npm run check` +- `npm run format` +- `npm run dev` (manual testing) + +**Exit:** AlbumThumbPanel correctly switches between grid and list views, no regression + +**Implements:** S-005-01, S-005-02, S-005-04 + +--- + +### I5 – AlbumHero Toggle Buttons (UI Controls) + +**Goal:** Add grid/list toggle buttons to AlbumHero.vue icon row + +**Preconditions:** I1, I4 complete (state management + conditional rendering ready) + +**Steps:** +1. Read existing AlbumHero.vue to understand icon row structure (line 33) +2. Import LycheeState store +3. Add computed property to read current view mode +4. Add two new `` elements in the flex-row-reverse container: + - Grid icon button (`pi-th` or similar) + - List icon button (`pi-list` or similar) +5. Apply existing icon styling pattern: + - Base: `shrink-0 px-3 cursor-pointer text-muted-color inline-block transform duration-300 hover:scale-150 hover:text-color` + - Active state: Different color or styling when selected +6. Add click handlers that call `lycheeStore.setAlbumViewMode('grid' | 'list')` +7. Add aria-labels for accessibility: + - Grid: `aria-label="Switch to grid view"` + - List: `aria-label="Switch to list view"` +8. Add aria-pressed attribute based on active state +9. Add tooltips (v-tooltip) similar to other icons +10. Test keyboard navigation (Tab to focus, Enter to activate) + +**Commands:** +- `npm run check` +- `npm run format` +- `npm run dev` (manual testing) + +**Exit:** Toggle buttons visible, clickable, toggle view mode, keyboard accessible, aria-labels present + +**Implements:** FR-005-03, NFR-005-03, S-005-01, S-005-02, UI-005-03 + +--- + +### I6 – Responsive Mobile Layout Testing + +**Goal:** Verify and refine mobile responsive layout for list view + +**Preconditions:** I2, I3, I4, I5 complete (all components implemented) + +**Steps:** +1. Test on various mobile viewport sizes: + - 320px (very narrow) + - 375px (iPhone SE) + - 768px (tablet) +2. Verify thumbnail sizes adjust (48px on mobile) +3. Verify album names wrap appropriately +4. Verify counts display compactly (may stack or inline) +5. Verify toggle buttons are usable on mobile +6. Make CSS adjustments if needed (use md: breakpoints) +7. Take screenshots for documentation + +**Commands:** +- `npm run dev` (test in browser DevTools responsive mode) + +**Exit:** List view renders correctly on all mobile breakpoints, no layout overflow + +**Implements:** FR-005-06, S-005-09, UI-005-05 + +--- + +### I7 – Component Unit Tests + +**Goal:** Add comprehensive unit tests for new components + +**Preconditions:** I2, I3 complete (components implemented) + +**Steps:** +1. Create test file for AlbumListItem: + - Test rendering with sample album data + - Test click navigation behavior + - Test badge display + - Test long album name wrapping + - Test 0 photos / 0 sub-albums edge cases +2. Create test file for AlbumListView: + - Test rendering multiple albums + - Test empty state + - Test props passing to AlbumListItem +3. Create test file for LycheeState view mode: + - Test default value (grid) + - Test setAlbumViewMode updates state + - Test localStorage read/write (mock localStorage) +4. Create fixture file `albums-list-view.json` with sample data + +**Commands:** +- `npm run check` (run all tests) + +**Exit:** All unit tests pass, coverage for new components + +**Implements:** Test strategy from spec, S-005-07, S-005-08 + +--- + +### I8 – Integration Testing & Visual Regression + +**Goal:** Test end-to-end toggle behavior and capture visual baselines + +**Preconditions:** I5, I6 complete (full feature implemented) + +**Steps:** +1. Manual integration testing: + - Load album page in grid view + - Click list toggle → verify switch + - Click grid toggle → verify switch back + - Reload page → verify preference persisted + - Test in private browsing mode → verify defaults to grid +2. Visual regression testing (if tooling available): + - Capture screenshot of grid view (desktop) + - Capture screenshot of list view (desktop) + - Capture screenshot of list view (mobile) + - Store as baseline images +3. Accessibility testing: + - Keyboard navigation through toggle buttons + - Screen reader testing (if available) + - Verify aria-labels and aria-pressed + +**Commands:** +- `npm run dev` (manual testing) +- Visual regression tool commands (if available) + +**Exit:** Toggle behavior works end-to-end, visual baselines captured, accessibility verified + +**Implements:** S-005-01, S-005-02, S-005-03, S-005-04, S-005-10, NFR-005-03 + +--- + +### I9 – Documentation Updates + +**Goal:** Update knowledge map and spec documentation + +**Preconditions:** I8 complete (feature fully implemented and tested) + +**Steps:** +1. Update [docs/specs/4-architecture/knowledge-map.md](docs/specs/4-architecture/knowledge-map.md): + - Add AlbumListView.vue component entry + - Add AlbumListItem.vue component entry + - Note AlbumHero.vue modifications + - Note LycheeState.ts modifications +2. Update spec.md status to "Implemented" +3. Archive resolved open questions (already done) +4. Create PR description with: + - Feature summary + - Screenshots (grid vs list views) + - Testing notes + +**Commands:** +- None (documentation updates) + +**Exit:** Knowledge map updated, documentation current + +**Implements:** Documentation deliverables from spec + +--- + +## Scenario Tracking + +| Scenario ID | Increment / Task reference | Notes | +|-------------|---------------------------|-------| +| S-005-01 | I5 | User clicks list toggle → view switches, localStorage updated | +| S-005-02 | I5 | User clicks grid toggle → view switches, localStorage updated | +| S-005-03 | I1 | User loads page, no localStorage → defaults to grid | +| S-005-04 | I1, I4 | User loads page with localStorage "list" → list view displayed | +| S-005-05 | I2, I3 | User clicks list row → navigates to album detail | +| S-005-06 | I2, I7 | Long album name (50+ chars) → full name displayed with wrapping | +| S-005-07 | I2, I7 | Album with 0 photos → shows "0 photos" | +| S-005-08 | I2, I7 | Album with badges → badges visible in row | +| S-005-09 | I6 | Mobile toggle → responsive layout with smaller thumbnails | +| S-005-10 | I8 | Private browsing → toggle works, resets to grid on reload | + +## Analysis Gate + +**Status:** Not yet executed (spec just created) + +**Checklist to complete before implementation:** +- [ ] Review spec.md for completeness (all FR/NFR defined) +- [ ] Verify all open questions resolved (Q-005-01, Q-005-02, Q-005-03 - ✅ DONE) +- [ ] Confirm existing components can be extended without breaking changes +- [ ] Verify TypeScript type definitions are sufficient +- [ ] Check for potential conflicts with other active features (Feature 004) +- [ ] Review mobile responsive requirements against existing breakpoint strategy + +**Findings:** (To be filled after analysis gate execution) + +## Exit Criteria + +Before declaring Feature 005 complete, the following must pass: + +- [ ] All increments (I1-I9) completed successfully +- [ ] `npm run format` passes (frontend code formatting) +- [ ] `npm run check` passes (frontend tests and TypeScript type checking) +- [ ] Manual testing confirms: + - [ ] Toggle buttons visible and functional in AlbumHero.vue + - [ ] Grid view displays albums in card layout (existing behavior) + - [ ] List view displays albums in horizontal rows with all required info + - [ ] View preference persists across page reloads + - [ ] Keyboard navigation works (Tab to toggle, Enter to activate) + - [ ] Mobile responsive layout works on narrow screens +- [ ] Visual regression baselines captured (if tooling available) +- [ ] Accessibility audit passes (aria-labels, keyboard navigation) +- [ ] Documentation updated (knowledge map, spec status) +- [ ] No performance regression (rendering 100 albums < 300ms) + +## Follow-ups / Backlog + +**Potential enhancements (defer to future features):** +- Virtualization for 1000+ albums to improve performance +- Sortable columns in list view (click column header to sort) +- Customizable column layout (show/hide fields) +- Backend persistence of view preference (sync across devices) +- Per-album view preference (remember different views for different albums) +- Drag-and-drop album reordering in list view +- Bulk selection checkboxes in list view +- Column resizing in list view +- Export list view data to CSV + +**Monitoring & Metrics:** +- Track localStorage usage/failures (if telemetry added in future) +- Monitor view mode distribution (how many users prefer list vs grid) +- Performance metrics for large album counts + +**Known Limitations:** +- View preference not synced across devices (localStorage only) +- Private browsing mode loses preference on reload (acceptable trade-off) +- No column customization in initial version (fixed layout) + +--- + +_Last updated: 2026-01-03_ diff --git a/docs/specs/4-architecture/features/005-album-list-view/spec.md b/docs/specs/4-architecture/features/005-album-list-view/spec.md new file mode 100644 index 00000000000..877f4130f4b --- /dev/null +++ b/docs/specs/4-architecture/features/005-album-list-view/spec.md @@ -0,0 +1,341 @@ +# Feature 005 – Album List View Toggle + +| Field | Value | +|-------|-------| +| Status | Draft | +| Last updated | 2026-01-03 | +| Owners | Agent | +| Linked plan | `docs/specs/4-architecture/features/005-album-list-view/plan.md` | +| Linked tasks | `docs/specs/4-architecture/features/005-album-list-view/tasks.md` | +| Roadmap entry | #005 | + +> Guardrail: This specification is the single normative source of truth for the feature. Track high- and medium-impact questions in [docs/specs/4-architecture/open-questions.md](docs/specs/4-architecture/open-questions.md), encode resolved answers directly in the Requirements/NFR/Behaviour/UI/Telemetry sections below (no per-feature `## Clarifications` sections), and use ADRs under `docs/specs/5-decisions/` for architecturally significant clarifications (referencing their IDs from the relevant spec sections). + +## Overview +Add a view toggle to the album display that allows users to switch between the current grid/card layout and a new list view. The list view displays albums in a horizontal row format (similar to Windows Explorer details view) with a thumbnail on the left, full untruncated album name, photo count, and sub-album count. This feature affects the UI layer only, requires no backend changes, and stores user preference in browser localStorage. + +## Goals +- Provide an alternative list view for albums that prioritizes information density and scannability over visual thumbnails +- Display full, untruncated album names to improve discoverability for albums with long titles +- Show album metadata (photo count, sub-album count) inline for quick scanning +- Persist user view preference across sessions using localStorage +- Maintain existing grid view functionality with seamless toggle capability + +## Non-Goals +- Backend API changes or database schema modifications +- Per-album view preferences (global preference only) +- Sorting or filtering capabilities in list view (use existing sort mechanisms) +- Customizable column layout or field selection +- Photo-level list view (this feature is album-only) +- Multi-device sync of view preference (localStorage only, not user settings) + +## Functional Requirements + +| ID | Requirement | Success path | Validation path | Failure path | Telemetry & traces | Source | +|----|-------------|--------------|-----------------|--------------|--------------------|--------| +| FR-005-01 | Display albums in list view format when list mode is active | Each album renders as a horizontal row with: 64px square thumbnail (left), full album name (untruncated, text-wrapped if needed), photo count (format: "X photos" or icon + number), sub-album count (format: "Y albums" or icon + number) | N/A (UI layout) | N/A | None | User requirement Q-005-01 | +| FR-005-02 | List view rows must be clickable and navigate to album detail | Clicking anywhere on the row navigates to the album (same behavior as grid card click) | N/A | N/A | None | Consistency with grid view | +| FR-005-03 | Provide toggle control to switch between grid and list views | Two icon buttons in AlbumHero.vue icon row (line 33): grid icon and list icon. Active view is visually indicated. Clicking toggles between views and updates localStorage | N/A | N/A | None | User requirement Q-005-02 | +| FR-005-04 | Persist view preference across sessions | On toggle, save preference to localStorage key `album_view_mode` with value `"grid"` or `"list"`. On page load, read from localStorage and apply saved view mode (default: grid) | N/A | If localStorage unavailable (private browsing), default to grid view without error | None | User requirement Q-005-03 | +| FR-005-05 | Display album badges in list view | Show existing badges (NSFW, password, public, etc.) in list view rows, positioned adjacent to thumbnail or album name | N/A | N/A | None | Feature parity with grid view | +| FR-005-06 | List view must be responsive on mobile | On mobile breakpoints (below md:), list view adapts: smaller thumbnails (48px), stacked or compact layout for counts, maintain full album name visibility | N/A | N/A | None | Responsive design requirement | + +## Non-Functional Requirements + +| ID | Requirement | Driver | Measurement | Dependencies | Source | +|----|-------------|--------|-------------|--------------|--------| +| NFR-005-01 | View preference must load without blocking album data fetch | User experience - instant view mode application | View mode applied synchronously from localStorage before first render, no API call | localStorage API | Performance requirement | +| NFR-005-02 | List view rendering must not degrade performance for large album lists | Performance - handle 100+ albums smoothly | Vue reactivity and DOM rendering should complete within 300ms for 100 albums | Vue 3, Tailwind CSS | User experience | +| NFR-005-03 | Toggle control must be accessible via keyboard | Accessibility - keyboard navigation support | Tab to focus toggle buttons, Enter/Space to activate, aria-labels present | PrimeVue accessibility features | WCAG 2.1 AA | +| NFR-005-04 | Component code must follow Vue 3 Composition API and TypeScript conventions | Code quality and maintainability | Follows existing patterns in AlbumThumbPanel.vue and AlbumThumb.vue, TypeScript types for props/emits | Vue 3, TypeScript, existing codebase patterns | [docs/specs/3-reference/coding-conventions.md](docs/specs/3-reference/coding-conventions.md) | + +## UI / Interaction Mock-ups + +### Grid View (Current - Default) +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Album: Vacation 2024 │ +│ ┌─────────┐ [Download] [Share] [Stats] [Grid*] [List] ... │ +│ └─────────┘ │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ +│ │ Album│ │ Album│ │ Album│ │ Album│ │ +│ │ #1 │ │ #2 │ │ #3 │ │ #4 │ │ +│ │ Name │ │ Name │ │ Name │ │ Name │ │ +│ └──────┘ └──────┘ └──────┘ └──────┘ │ +│ │ +│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ +│ │ Album│ │ Album│ │ Album│ │ Album│ │ +│ │ #5 │ │ #6 │ │ #7 │ │ #8 │ │ +│ │ Name │ │ Name │ │ Name │ │ Name │ │ +│ └──────┘ └──────┘ └──────┘ └──────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### List View (New - When Toggle Active) +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Album: Vacation 2024 │ +│ ┌─────────┐ [Download] [Share] [Stats] [Grid] [List*] ... │ +│ └─────────┘ │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────┐ Summer Vacation 2024 - California Road Trip │ +│ │ T1 │ 📷 145 photos 📁 3 sub-albums │ +│ └────┘ ──────────────────────────────────────────────────── │ +│ │ +│ ┌────┐ Winter Sports - Skiing and Snowboarding Adventures │ +│ │ T2 │ 📷 87 photos 📁 0 sub-albums │ +│ └────┘ ──────────────────────────────────────────────────── │ +│ │ +│ ┌────┐ Family Gathering │ +│ │ T3 │ 📷 23 photos 📁 2 sub-albums │ +│ └────┘ ──────────────────────────────────────────────────── │ +│ │ +│ ┌────┐ Work Conference - Tech Summit 2024 with Long Name... │ +│ │ T4 │ 📷 56 photos 📁 1 sub-album │ +│ └────┘ ──────────────────────────────────────────────────── │ +│ │ +└─────────────────────────────────────────────────────────────────┘ + +Legend: + T1-T4 = 64px square thumbnails + [Grid] [List*] = Toggle buttons (asterisk indicates active) + 📷 = Photo count icon (or text: "145 photos") + 📁 = Sub-album count icon (or text: "3 albums") + Full album names displayed without truncation + Horizontal separator lines between rows +``` + +### Mobile List View (Responsive) +``` +┌────────────────────────────┐ +│ Album: Vacation 2024 │ +│ [≡] [↓] [📊] [⊞] [☰*] ... │ +├────────────────────────────┤ +│ │ +│ ┌──┐ Summer Vacation 2024 │ +│ │T1│ California Road Trip │ +│ └──┘ 📷 145 📁 3 │ +│ ─────────────────────── │ +│ │ +│ ┌──┐ Winter Sports - │ +│ │T2│ Skiing Adventures │ +│ └──┘ 📷 87 📁 0 │ +│ ─────────────────────── │ +│ │ +│ ┌──┐ Family Gathering │ +│ │T3│ 📷 23 📁 2 │ +│ └──┘ ─────────────────── │ +│ │ +└────────────────────────────┘ + +Legend: + T1-T3 = 48px thumbnails (mobile) + Text may wrap on narrow screens + Counts inline/compact format +``` + +## Branch & Scenario Matrix + +| Scenario ID | Description / Expected outcome | +|-------------|--------------------------------| +| S-005-01 | User clicks list view toggle in AlbumHero → albums switch from grid cards to horizontal list rows, toggle button shows active state, localStorage updated | +| S-005-02 | User clicks grid view toggle in AlbumHero → albums switch from list rows back to grid cards, toggle button shows active state, localStorage updated | +| S-005-03 | User loads album page with no localStorage preference → default grid view displayed | +| S-005-04 | User loads album page with localStorage preference "list" → list view displayed automatically | +| S-005-05 | User clicks album row in list view → navigates to album detail page (same as grid card behavior) | +| S-005-06 | List view displays album with long name (50+ characters) → full name displayed with text wrapping, no truncation | +| S-005-07 | List view displays album with 0 photos → shows "0 photos" (or equivalent empty state) | +| S-005-08 | List view displays album with badges (NSFW, password, etc.) → badges visible in row | +| S-005-09 | User toggles view on mobile device → responsive list layout with smaller thumbnails and compact counts | +| S-005-10 | User with localStorage unavailable (private mode) toggles view → toggle works, but resets to grid on reload | + +## Test Strategy +- **Core:** N/A (no backend changes) +- **Application:** N/A (no backend changes) +- **REST:** N/A (no API changes) +- **CLI:** N/A (no CLI changes) +- **UI (JS/Selenium):** + - Unit tests for AlbumListView component (if created as separate component) + - Unit tests for localStorage read/write in LycheeState store + - Component tests for toggle button behavior (click → view change → localStorage update) + - Integration tests for view rendering (grid cards vs list rows) + - Visual regression tests for list view layout on desktop and mobile + - Accessibility tests for keyboard navigation and aria-labels +- **Docs/Contracts:** N/A (no API contracts) + +## Interface & Contract Catalogue + +### Domain Objects +| ID | Description | Modules | +|----|-------------|---------| +| DO-005-01 | Album display data (id, title, thumbnail, num_photos, num_children) - uses existing Album model | UI | + +### API Routes / Services +N/A - No API changes required + +### CLI Commands / Flags +N/A - No CLI changes required + +### Telemetry Events +N/A - No telemetry events (per project scope) + +### Fixtures & Sample Data +| ID | Path | Purpose | +|----|------|---------| +| FX-005-01 | resources/js/components/gallery/albumModule/__tests__/fixtures/albums-list-view.json | Sample album data for list view component testing | + +### UI States +| ID | State | Trigger / Expected outcome | +|----|-------|---------------------------| +| UI-005-01 | Grid view active | Default state or user clicks grid toggle. AlbumThumbPanelList renders grid cards. Grid toggle button has active styling. | +| UI-005-02 | List view active | User clicks list toggle. New AlbumListView component renders horizontal rows. List toggle button has active styling. | +| UI-005-03 | Toggle button focused | User tabs to toggle button. Visual focus outline visible for accessibility. | +| UI-005-04 | List row hover | User hovers over list row. Hover state styling applied (similar to grid card hover). | +| UI-005-05 | Mobile list view | Viewport width < md: breakpoint. List rows adapt to compact layout with smaller thumbnails. | + +## Telemetry & Observability +No telemetry events are defined for this feature per project scope. + +## Documentation Deliverables +- Update [docs/specs/4-architecture/roadmap.md](docs/specs/4-architecture/roadmap.md) with Feature 005 entry +- Update [docs/specs/4-architecture/knowledge-map.md](docs/specs/4-architecture/knowledge-map.md) with new components: + - AlbumListView.vue (if created) + - AlbumHero.vue modifications (toggle buttons) + - LycheeState.ts modifications (view mode state) + +## Fixtures & Sample Data +Create fixture file `resources/js/components/gallery/albumModule/__tests__/fixtures/albums-list-view.json` with sample album data including: +- Albums with long names (50+ characters) +- Albums with 0 photos and 0 sub-albums +- Albums with various badge combinations (NSFW, password, public) +- Albums with high photo/sub-album counts (1000+ photos, 50+ sub-albums) + +## Spec DSL + +```yaml +domain_objects: + - id: DO-005-01 + name: Album (existing) + fields: + - name: id + type: string + - name: title + type: string + - name: thumb + type: object (Photo) + - name: num_photos + type: integer + - name: num_children + type: integer + - name: badges + type: array + +routes: [] + +cli_commands: [] + +telemetry_events: [] + +fixtures: + - id: FX-005-01 + path: resources/js/components/gallery/albumModule/__tests__/fixtures/albums-list-view.json + purpose: Sample album data for list view component testing + +ui_states: + - id: UI-005-01 + description: Grid view active (default) + - id: UI-005-02 + description: List view active + - id: UI-005-03 + description: Toggle button focused (keyboard navigation) + - id: UI-005-04 + description: List row hover state + - id: UI-005-05 + description: Mobile list view (responsive) + +ui_components: + - id: UC-005-01 + name: AlbumListView.vue (new component) + location: resources/js/components/gallery/albumModule/AlbumListView.vue + purpose: Renders albums in horizontal list row format + - id: UC-005-02 + name: AlbumListItem.vue (new component) + location: resources/js/components/gallery/albumModule/AlbumListItem.vue + purpose: Individual list row item component + - id: UC-005-03 + name: AlbumHero.vue (modified) + location: resources/js/components/gallery/albumModule/AlbumHero.vue + modifications: Add grid/list toggle buttons to icon row (line 33) + - id: UC-005-04 + name: LycheeState.ts (modified) + location: resources/js/stores/LycheeState.ts + modifications: Add album_view_mode state property with localStorage persistence +``` + +## Appendix + +### Resolved Open Questions +All open questions (Q-005-01, Q-005-02, Q-005-03) have been resolved and incorporated into the spec: + +- **Q-005-01:** List view uses Windows Details View Pattern (horizontal rows, 64px thumbnails, full names, counts) +- **Q-005-02:** Toggle controls placed in AlbumHero.vue icon row (same line as statistics/download buttons) +- **Q-005-03:** View preference stored in localStorage only (no backend, session-scoped to device/browser) + +### Implementation Notes + +1. **Component Architecture:** + - Create new `AlbumListView.vue` component parallel to `AlbumThumbPanelList.vue` + - Create new `AlbumListItem.vue` component parallel to `AlbumThumb.vue` + - Modify `AlbumThumbPanel.vue` to conditionally render grid or list based on view mode + - Modify `AlbumHero.vue` to add toggle buttons to existing icon row + +2. **State Management:** + - Add `album_view_mode: "grid" | "list"` to LycheeState store + - Implement localStorage read on app mount + - Implement localStorage write on toggle click + - Default to "grid" if localStorage not available or no preference saved + +3. **Styling Considerations:** + - List rows should use Tailwind flexbox: `flex flex-row items-center gap-4` + - Thumbnails: `w-16 h-16` (64px) on desktop, `w-12 h-12` (48px) on mobile + - Album name: `flex-1 text-base font-medium` (allows text wrapping) + - Counts: `text-sm text-muted-color flex items-center gap-1` + - Hover state: `hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer` + - Row separator: `border-b border-gray-200 dark:border-gray-700` + +4. **Accessibility:** + - Toggle buttons must have `aria-label` attributes + - Active toggle should have `aria-pressed="true"` + - List rows should have `role="button"` or remain as router-links + - Ensure keyboard navigation works (Tab to toggle, Enter to activate) + +5. **Mobile Responsiveness:** + - Use Tailwind breakpoints: `md:` for desktop-specific styles + - Stack counts vertically on very narrow screens (<320px) if needed + - Ensure full album names remain visible (wrapping allowed) + - Toggle buttons may need tooltip labels on mobile due to space constraints + +### Data Flow + +``` +User clicks toggle button + → AlbumHero.vue emits viewModeChanged event + → LycheeState.album_view_mode updated + → localStorage.setItem('album_view_mode', newMode) + → AlbumThumbPanel.vue computed property reacts + → Conditionally renders AlbumListView or AlbumThumbPanelList +``` + +### LocalStorage Schema + +```typescript +// Key: 'album_view_mode' +// Value: 'grid' | 'list' +// Example: +localStorage.setItem('album_view_mode', 'list'); +const viewMode = localStorage.getItem('album_view_mode') || 'grid'; +``` diff --git a/docs/specs/4-architecture/features/005-album-list-view/tasks.md b/docs/specs/4-architecture/features/005-album-list-view/tasks.md new file mode 100644 index 00000000000..9159b93cdc1 --- /dev/null +++ b/docs/specs/4-architecture/features/005-album-list-view/tasks.md @@ -0,0 +1,304 @@ +# Feature 005 Tasks – Album List View Toggle + +_Status: Draft_ +_Last updated: 2026-01-03_ + +> Keep this checklist aligned with the feature plan increments. Stage tests before implementation, record verification commands beside each task, and prefer bite-sized entries (≤90 minutes). +> **Mark tasks `[x]` immediately** after each one passes verification—do not batch completions. Update the roadmap status when all tasks are done. +> When referencing requirements, keep feature IDs (`FR-`), non-goal IDs (`N-`), and scenario IDs (`S-`) inside the same parentheses immediately after the task title (omit categories that do not apply). +> When new high- or medium-impact questions arise during execution, add them to [docs/specs/4-architecture/open-questions.md](docs/specs/4-architecture/open-questions.md) instead of informal notes, and treat a task as fully resolved only once the governing spec sections (requirements/NFR/behaviour/telemetry) and, when required, ADRs under `docs/specs/5-decisions/` reflect the clarified behaviour. + +## Checklist + +### Increment I1 – LycheeState Store Modifications + +- [ ] T-005-01 – Add album_view_mode state property to LycheeState.ts (FR-005-04, NFR-005-01, S-005-03, S-005-04). + _Intent:_ Add `album_view_mode: "grid" | "list"` property with default value "grid" to LycheeState.ts store. + _Verification commands:_ + - `npm run check` (TypeScript compilation) + _Notes:_ Property should be reactive, accessible via computed getter. + +- [ ] T-005-02 – Add setAlbumViewMode action to LycheeState.ts (FR-005-04, S-005-01, S-005-02). + _Intent:_ Implement action `setAlbumViewMode(mode: "grid" | "list")` that updates state and writes to localStorage. + _Verification commands:_ + - `npm run check` + _Notes:_ Use localStorage key `album_view_mode`, handle localStorage unavailability gracefully. + +- [ ] T-005-03 – Add localStorage initialization logic to LycheeState.ts (FR-005-04, NFR-005-01, S-005-04). + _Intent:_ On store setup/mount, read from localStorage and initialize album_view_mode state. + _Verification commands:_ + - `npm run check` + - Manual test: Set localStorage, reload page, verify state initialized correctly + _Notes:_ Default to "grid" if localStorage unavailable or key not found. + +- [ ] T-005-04 – Write unit tests for LycheeState view mode state and localStorage persistence (S-005-03, S-005-04, S-005-10). + _Intent:_ Test default value, setAlbumViewMode updates, localStorage read/write behavior (mock localStorage). + _Verification commands:_ + - `npm run check` (includes unit tests) + _Notes:_ Mock localStorage API for tests, test both success and unavailable scenarios. + +--- + +### Increment I2 – AlbumListItem Component + +- [ ] T-005-05 – Create AlbumListItem.vue component skeleton (FR-005-01, FR-005-02). + _Intent:_ Create new file `resources/js/components/gallery/albumModule/AlbumListItem.vue` with basic Vue 3 Composition API structure, props interface (album: AlbumResource). + _Verification commands:_ + - `npm run check` (TypeScript compilation) + _Notes:_ Import AlbumResource type from existing types. + +- [ ] T-005-06 – Implement AlbumListItem template structure (FR-005-01, FR-005-02, S-005-05). + _Intent:_ Add router-link wrapper, thumbnail slot (use AlbumThumbImage component), album title, photo count, sub-album count sections. + _Verification commands:_ + - `npm run check` + - `npm run dev` (visual inspection) + _Notes:_ Use Tailwind classes for layout: `flex items-center gap-4`. + +- [ ] T-005-07 – Add styling to AlbumListItem (FR-005-01, FR-005-06). + _Intent:_ Apply Tailwind CSS: hover state, border separator, responsive thumbnail sizes (64px desktop, 48px mobile). + _Verification commands:_ + - `npm run format` + - `npm run dev` (visual inspection) + _Notes:_ Classes: `hover:bg-gray-100 dark:hover:bg-gray-800`, `border-b border-gray-200 dark:border-gray-700`, `md:w-16 md:h-16 w-12 h-12` for thumbnail. + +- [ ] T-005-08 – Add badge display to AlbumListItem (FR-005-05, S-005-08). + _Intent:_ Reuse existing badge logic from AlbumThumb.vue to display NSFW, password, public badges in list row. + _Verification commands:_ + - `npm run check` + - `npm run dev` (test with albums that have badges) + _Notes:_ Position badges adjacent to thumbnail or album name. + +- [ ] T-005-09 – Write unit tests for AlbumListItem (S-005-06, S-005-07, S-005-08). + _Intent:_ Test rendering with sample album data, long album names, 0 photos/sub-albums, badge display. + _Verification commands:_ + - `npm run check` + _Notes:_ Create test fixture with edge cases (long names, 0 counts, multiple badges). + +--- + +### Increment I3 – AlbumListView Component + +- [ ] T-005-10 – Create AlbumListView.vue component skeleton (FR-005-01). + _Intent:_ Create new file `resources/js/components/gallery/albumModule/AlbumListView.vue` with props interface (albums: AlbumResource[], aspectRatio: AspectRatioCSSType). + _Verification commands:_ + - `npm run check` + _Notes:_ Import AlbumListItem component. + +- [ ] T-005-11 – Implement AlbumListView template with v-for loop (FR-005-01, S-005-05). + _Intent:_ Render AlbumListItem for each album in albums array, handle empty state. + _Verification commands:_ + - `npm run check` + - `npm run dev` (test with sample albums) + _Notes:_ Use flex column layout: `flex flex-col w-full`. + +- [ ] T-005-12 – Write unit tests for AlbumListView (S-005-06). + _Intent:_ Test rendering multiple albums, empty state, props passing to AlbumListItem. + _Verification commands:_ + - `npm run check` + _Notes:_ Use fixture data from FX-005-01. + +--- + +### Increment I4 – AlbumThumbPanel Modifications + +- [ ] T-005-13 – Import AlbumListView and LycheeState in AlbumThumbPanel.vue (S-005-01, S-005-02). + _Intent:_ Add necessary imports to conditionally render grid or list view. + _Verification commands:_ + - `npm run check` + _Notes:_ Read existing AlbumThumbPanel.vue to understand structure before modifying. + +- [ ] T-005-14 – Add computed property for view mode in AlbumThumbPanel.vue (S-005-04). + _Intent:_ Read album_view_mode from LycheeState store via computed property. + _Verification commands:_ + - `npm run check` + _Notes:_ Use `const lycheeStore = useLycheeStateStore()`. + +- [ ] T-005-15 – Update AlbumThumbPanel template to conditionally render grid or list (S-005-01, S-005-02). + _Intent:_ Use v-if to render AlbumThumbPanelList when mode === "grid", AlbumListView when mode === "list". + _Verification commands:_ + - `npm run check` + - `npm run dev` (manually toggle view mode in localStorage, verify switch) + _Notes:_ Ensure both components receive same props (albums array, etc.). + +--- + +### Increment I5 – AlbumHero Toggle Buttons + +- [ ] T-005-16 – Add grid and list toggle button elements to AlbumHero.vue (FR-005-03, S-005-01, S-005-02). + _Intent:_ Add two `` elements in the flex-row-reverse icon container (line 33) with grid/list icons (PrimeVue pi-th, pi-list). + _Verification commands:_ + - `npm run check` + - `npm run dev` (visual inspection - buttons appear in icon row) + _Notes:_ Follow existing icon pattern: `shrink-0 px-3 cursor-pointer text-muted-color inline-block transform duration-300 hover:scale-150 hover:text-color`. + +- [ ] T-005-17 – Add click handlers to toggle buttons in AlbumHero.vue (FR-005-03, S-005-01, S-005-02). + _Intent:_ Implement @click handlers that call `lycheeStore.setAlbumViewMode('grid' | 'list')`. + _Verification commands:_ + - `npm run check` + - `npm run dev` (click buttons, verify view switches, localStorage updated) + _Notes:_ Import LycheeState store at top of component. + +- [ ] T-005-18 – Add active state styling to toggle buttons in AlbumHero.vue (FR-005-03, UI-005-01, UI-005-02). + _Intent:_ Apply different styling when button is active (current view mode), e.g., different color or text-primary-emphasis. + _Verification commands:_ + - `npm run dev` (visual inspection - active button highlighted) + _Notes:_ Use computed property or v-bind:class based on current view mode. + +- [ ] T-005-19 – Add aria-labels and aria-pressed to toggle buttons (NFR-005-03, UI-005-03). + _Intent:_ Add accessibility attributes: aria-label="Switch to grid view" / "Switch to list view", aria-pressed based on active state. + _Verification commands:_ + - `npm run check` + - Manual accessibility audit (keyboard navigation, screen reader) + _Notes:_ Ensure buttons are keyboard-navigable (Tab to focus, Enter to activate). + +- [ ] T-005-20 – Add tooltips to toggle buttons in AlbumHero.vue (FR-005-03). + _Intent:_ Add v-tooltip.bottom directives similar to other icons in AlbumHero.vue. + _Verification commands:_ + - `npm run dev` (hover over buttons, verify tooltips appear) + _Notes:_ Tooltip text: "Grid view" / "List view". + +--- + +### Increment I6 – Responsive Mobile Layout Testing + +- [ ] T-005-21 – Test list view on 320px viewport (FR-005-06, S-005-09). + _Intent:_ Verify layout doesn't overflow, thumbnails scale to 48px, album names wrap, counts display compactly. + _Verification commands:_ + - `npm run dev` (browser DevTools responsive mode, set to 320px width) + _Notes:_ Make CSS adjustments if needed, use Tailwind md: breakpoints. + +- [ ] T-005-22 – Test list view on 375px and 768px viewports (FR-005-06, S-005-09, UI-005-05). + _Intent:_ Verify responsive behavior at common mobile breakpoints. + _Verification commands:_ + - `npm run dev` (test multiple viewport sizes) + _Notes:_ Capture screenshots for documentation. + +- [ ] T-005-23 – Test toggle buttons on mobile viewports (FR-005-03, S-005-09). + _Intent:_ Verify toggle buttons remain usable and don't crowd header on mobile. + _Verification commands:_ + - `npm run dev` (mobile testing) + _Notes:_ Consider icon-only display on very narrow screens if needed. + +--- + +### Increment I7 – Component Unit Tests + +- [ ] T-005-24 – Create fixture file albums-list-view.json (FX-005-01). + _Intent:_ Create `resources/js/components/gallery/albumModule/__tests__/fixtures/albums-list-view.json` with sample album data (long names, 0 counts, badges, high counts). + _Verification commands:_ + - File exists and is valid JSON + _Notes:_ Include edge cases mentioned in spec. + +- [ ] T-005-25 – Write unit tests for AlbumListItem component (S-005-05, S-005-06, S-005-07, S-005-08). + _Intent:_ Test rendering with various album data scenarios, navigation behavior, badge display. + _Verification commands:_ + - `npm run check` + _Notes:_ Use Vue Test Utils, test props passing and rendering output. + +- [ ] T-005-26 – Write unit tests for AlbumListView component (S-005-06). + _Intent:_ Test rendering multiple albums, empty state, props passing. + _Verification commands:_ + - `npm run check` + _Notes:_ Verify correct number of AlbumListItem components rendered. + +- [ ] T-005-27 – Write integration tests for view mode toggle (S-005-01, S-005-02, S-005-03, S-005-04). + _Intent:_ Test end-to-end toggle behavior, localStorage persistence, default value. + _Verification commands:_ + - `npm run check` + _Notes:_ Mock localStorage for tests, test both available and unavailable scenarios. + +--- + +### Increment I8 – Integration Testing & Visual Regression + +- [ ] T-005-28 – Manual integration testing: toggle between views (S-005-01, S-005-02). + _Intent:_ Load album page, click list toggle, verify switch, click grid toggle, verify switch back. + _Verification commands:_ + - `npm run dev` (manual testing) + _Notes:_ Test with real album data, various album counts. + +- [ ] T-005-29 – Manual integration testing: localStorage persistence (S-005-04, S-005-10). + _Intent:_ Set view to list, reload page, verify still in list view. Test private browsing mode (defaults to grid on reload). + _Verification commands:_ + - `npm run dev` (manual testing with page reloads) + _Notes:_ Clear localStorage between tests to verify default behavior. + +- [ ] T-005-30 – Keyboard accessibility testing (NFR-005-03, UI-005-03). + _Intent:_ Tab to toggle buttons, verify focus outline, press Enter to activate, verify view switches. + _Verification commands:_ + - Manual keyboard navigation testing + _Notes:_ Test with screen reader if available, verify aria-labels announced. + +- [ ] T-005-31 – Visual regression baseline capture (optional, if tooling available). + _Intent:_ Capture screenshots of grid view (desktop), list view (desktop), list view (mobile) as baseline images. + _Verification commands:_ + - Visual regression tool commands + _Notes:_ Store baselines for future regression testing. + +- [ ] T-005-32 – Performance testing with 100 albums (NFR-005-02). + _Intent:_ Load album with 100+ albums, measure rendering time, verify < 300ms for list view. + _Verification commands:_ + - Browser DevTools Performance tab + _Notes:_ Compare grid vs list rendering performance. + +--- + +### Increment I9 – Documentation Updates + +- [ ] T-005-33 – Update knowledge-map.md with new components. + _Intent:_ Add entries for AlbumListView.vue, AlbumListItem.vue, note modifications to AlbumHero.vue and LycheeState.ts. + _Verification commands:_ + - File updated and readable + _Notes:_ Follow existing knowledge map format. + +- [ ] T-005-34 – Update spec.md status to Implemented. + _Intent:_ Change status field in spec.md from "Draft" to "Implemented", update last updated date. + _Verification commands:_ + - File updated + _Notes:_ Update after all other tasks complete. + +- [ ] T-005-35 – Update roadmap.md feature status. + _Intent:_ Change Feature 005 status from "Planning" to "In Progress" when implementation starts, "Complete" when done. + _Verification commands:_ + - File updated + _Notes:_ Update incrementally as progress is made. + +- [ ] T-005-36 – Create PR description with screenshots. + _Intent:_ Document feature summary, add screenshots showing grid vs list views, testing notes. + _Verification commands:_ + - PR description complete + _Notes:_ Include before/after screenshots (desktop and mobile). + +--- + +## Notes / TODOs + +**Environment setup:** +- Ensure Node.js and npm are up to date +- Run `npm install` to install dependencies before starting + +**Testing strategy:** +- Prefer unit tests for components (fast feedback) +- Manual integration testing for user flows +- Visual regression testing optional (if tooling exists) + +**Deferred items (out of scope for Feature 005):** +- Virtualization for 1000+ albums (performance optimization) +- Backend persistence of view preference (multi-device sync) +- Sortable columns in list view +- Customizable column layout +- Per-album view preferences + +**Common commands:** +- `npm run format` - Format frontend code (Prettier) +- `npm run check` - Run frontend tests and TypeScript type checking +- `npm run dev` - Start local development server + +**Potential blockers:** +- If PrimeVue icons (pi-th, pi-list) are not available, choose alternative icons +- If localStorage is restricted (CSP, privacy settings), feature will default to grid view (acceptable) +- If performance with 100+ albums is poor, may need virtualization (defer to follow-up) + +--- + +_Last updated: 2026-01-03_ diff --git a/docs/specs/4-architecture/features/006-photo-rating-filter/plan.md b/docs/specs/4-architecture/features/006-photo-rating-filter/plan.md new file mode 100644 index 00000000000..00abddca70b --- /dev/null +++ b/docs/specs/4-architecture/features/006-photo-rating-filter/plan.md @@ -0,0 +1,577 @@ +# Feature Plan 006 – Photo Star Rating Filter + +_Linked specification:_ `docs/specs/4-architecture/features/006-photo-rating-filter/spec.md` +_Status:_ Draft +_Last updated:_ 2026-01-03 + +> Guardrail: Keep this plan traceable back to the governing spec. Reference FR/NFR/Scenario IDs from `spec.md` where relevant, log any new high- or medium-impact questions in [docs/specs/4-architecture/open-questions.md](docs/specs/4-architecture/open-questions.md), and assume clarifications are resolved only when the spec's normative sections (requirements/NFR/behaviour/telemetry) and, where applicable, ADRs under `docs/specs/5-decisions/` have been updated. + +## Vision & Success Criteria + +**User Value:** +Users can quickly filter photos in an album by minimum star rating threshold using an intuitive visual interface (5 clickable stars). This makes it easy to find highly-rated photos or explore photos by rating quality without complex UI. The filter only appears when relevant (at least one rated photo exists), keeping the interface clean. + +**Success Signals:** +- Star filter control visible when album has ≥1 rated photo, hidden otherwise +- Clicking star N filters photos to show rating ≥ N (minimum threshold) +- Clicking same star again clears filter (toggle behavior) +- Filter state persists in Pinia store during session +- Filtering is instant (client-side, no API calls) +- Responsive keyboard navigation works (Tab, Arrow keys, Enter) + +**Quality Bars:** +- Code follows Vue 3 Composition API and TypeScript conventions (NFR-006-03) +- Keyboard accessible with proper aria-labels (NFR-006-02) +- Filtering performance handles 1000+ photos smoothly (<100ms) (NFR-006-04) +- No API calls, fully client-side filtering (NFR-006-01) + +## Scope Alignment + +**In scope:** +- Star filter control in PhotoThumbPanelControl.vue (5 clickable stars) +- Conditional rendering (show only when rated photos exist) +- Minimum threshold filtering logic (≥ N stars) +- Toggle behavior (click same star to clear filter) +- Filter state in PhotosState store (session-only persistence) +- Client-side filtering computed property +- Keyboard accessibility (Tab, Arrow keys, Enter) +- Visual feedback (filled/empty stars, hover states) +- Responsive design for mobile + +**Out of scope:** +- Backend API filtering or query parameters +- Exact rating match filtering (show only N-star photos) +- Filtering for unrated photos explicitly +- Multiple rating selection (checkboxes, multi-select) +- LocalStorage or URL query parameter persistence +- Filter controls for albums (feature is photo-only) +- Range sliders or complex UI controls +- Filtering by aggregate rating (average rating from all users) + +## Dependencies & Interfaces + +**Frontend Dependencies:** +- Vue 3 (Composition API) +- TypeScript +- Tailwind CSS for styling +- PrimeVue for star icons (`pi-star`, `pi-star-fill`) +- PhotosState.ts store (state management) +- Existing Photo model types (PhotoResource with user_rating field) + +**Feature Dependencies:** +- **Feature 001 (Photo Star Rating):** This feature depends on Feature 001 being implemented. Photos must have `user_rating` field populated. + +**Components:** +- PhotoThumbPanelControl.vue (existing - will be modified) +- PhotosState.ts (existing - will be modified) +- PhotoThumbPanel.vue or parent component (may need modification for filtered list) + +**Interfaces:** +- Photo data structure from PhotosState.ts (id, user_rating: null | 0-5) + +**Testing Infrastructure:** +- Vitest (component tests) +- Vue Test Utils +- Performance testing utilities (for 1000+ photo filtering) + +## Assumptions & Risks + +**Assumptions:** +- Feature 001 (Photo Star Rating) is complete and user_rating field is available +- PhotosState store exists and can be extended with filter state +- Photo grid components accept filtered photo array via props or computed property +- PrimeVue star icons (`pi-star`, `pi-star-fill`) are available +- User ratings are stored as integers 1-5 (or null for unrated) + +**Risks / Mitigations:** + +| Risk | Impact | Mitigation | +|------|--------|-----------| +| Feature 001 not complete | High - blocking dependency | Verify Feature 001 status before starting. If incomplete, defer Feature 006 or implement mock data for testing | +| Performance issues with 1000+ photos | Medium - slow filtering | Use Vue computed properties (cached), test with large datasets, consider virtualization if needed (defer to follow-up) | +| Star icon visual clarity | Low - UX concern | Use standard PrimeVue icons, test with users, adjust colors/size if needed | +| Mobile touch targets too small | Low - accessibility | Ensure stars are ≥44px touch targets, test on real devices | +| Filter state conflicts with other features | Low - state management | Use unique state property name, follow existing patterns (e.g., NSFW visibility) | + +## Implementation Drift Gate + +**Drift Detection Strategy:** +- Before each increment, verify the specification FR/NFR requirements still match the planned work +- After each increment, confirm deliverables align with success criteria +- Record any deviations or clarifications in this plan's appendix + +**Evidence Collection:** +- Component tests pass (`npm run check`) +- Visual screenshots of star filter (empty, partial filled, hover states) +- Performance measurements (filtering time for 1000 photos) +- Accessibility audit results (keyboard navigation, aria-labels) + +**Commands to Rerun:** +- `npm run format` - Frontend formatting +- `npm run check` - Frontend tests and TypeScript type checking +- `npm run dev` - Local development server for manual testing + +## Increment Map + +### I1 – PhotosState Store Modifications (Filter State) + +**Goal:** Add photo rating filter state to PhotosState.ts + +**Preconditions:** PhotosState.ts store exists (verify location) + +**Steps:** +1. Read existing PhotosState.ts to understand structure +2. Add `photo_rating_filter: null | 1 | 2 | 3 | 4 | 5` property (default: null) +3. Add computed getter `photoRatingFilter` +4. Add action `setPhotoRatingFilter(rating: null | 1 | 2 | 3 | 4 | 5)` that updates state +5. Write unit test for filter state behavior + +**Commands:** +- `npm run check` (verify TypeScript types and tests) + +**Exit:** PhotosState has photo_rating_filter state, getter, and action. Tests pass. + +**Implements:** FR-006-04, S-006-08 + +--- + +### I2 – Filtering Logic Computed Property + +**Goal:** Create computed property that filters photos by minimum rating threshold + +**Preconditions:** I1 complete (filter state ready), Feature 001 complete (user_rating field exists) + +**Steps:** +1. Identify where photo list is rendered (likely PhotoThumbPanel.vue or parent) +2. Add computed property `filteredPhotos`: + ```typescript + const filteredPhotos = computed(() => { + const filter = photosStore.photoRatingFilter; + const hasRated = photos.value.some(p => p.user_rating && p.user_rating > 0); + + if (filter === null || !hasRated) { + return photos.value; + } + + return photos.value.filter(p => + p.user_rating !== null && + p.user_rating >= filter + ); + }); + ``` +3. Update photo grid rendering to use `filteredPhotos` instead of `photos` +4. Write unit tests for filtering logic: + - Test filter === null → all photos + - Test filter === 3 → only photos with rating ≥ 3 + - Test filter === 5 → only 5-star photos + - Test no rated photos → all photos shown + - Test unrated photos excluded when filter active + +**Commands:** +- `npm run check` (tests + types) +- `npm run dev` (manual testing with mock photo data) + +**Exit:** Filtered photo list works correctly based on filter state, tests pass + +**Implements:** FR-006-02, FR-006-05, NFR-006-01, NFR-006-04, S-006-03, S-006-04, S-006-05, S-006-07 + +--- + +### I3 – Star Filter Control Component Structure + +**Goal:** Add star filter UI control to PhotoThumbPanelControl.vue + +**Preconditions:** I1, I2 complete (filter state and logic ready) + +**Steps:** +1. Read existing PhotoThumbPanelControl.vue to understand layout +2. Add computed property `hasRatedPhotos`: + ```typescript + const hasRatedPhotos = computed(() => + photos.value.some(p => p.user_rating && p.user_rating > 0) + ); + ``` +3. Add template section for star filter (before layout buttons): + ```vue +
+ +
+ ``` +4. Add methods: + - `handleStarClick(star: number)`: toggle logic + - `starIconClass(star: number)`: filled vs empty icon +5. Import PhotosState store to access filter state + +**Commands:** +- `npm run check` +- `npm run dev` (visual inspection) + +**Exit:** Star filter control renders when hasRatedPhotos === true, hidden otherwise + +**Implements:** FR-006-01, FR-006-07, S-006-01, S-006-02 + +--- + +### I4 – Star Click Interaction (Toggle Behavior) + +**Goal:** Implement click handling for star filter with toggle logic + +**Preconditions:** I3 complete (star UI rendered) + +**Steps:** +1. Implement `handleStarClick(star: number)` method: + ```typescript + const handleStarClick = (star: number) => { + const current = photosStore.photoRatingFilter; + if (current === star) { + // Click same star → clear filter + photosStore.setPhotoRatingFilter(null); + } else { + // Click different star → set filter + photosStore.setPhotoRatingFilter(star); + } + }; + ``` +2. Implement `starIconClass(star: number)` method: + ```typescript + const starIconClass = (star: number) => { + const filter = photosStore.photoRatingFilter; + const filled = filter !== null && star <= filter; + return filled + ? 'pi pi-star-fill text-yellow-500' + : 'pi pi-star text-gray-300 dark:text-gray-600'; + }; + ``` +3. Test click behavior manually: + - Click star 3 → stars 1-3 filled, photos filtered + - Click star 3 again → all stars empty, filter cleared + - Click star 5 → all stars filled, only 5-star photos shown + +**Commands:** +- `npm run check` +- `npm run dev` (manual testing) + +**Exit:** Star clicks toggle filter correctly, visual feedback works + +**Implements:** FR-006-02, FR-006-03, FR-006-06, S-006-03, S-006-06, S-006-07 + +--- + +### I5 – Hover and Visual Feedback + +**Goal:** Add hover states and visual polish to star filter + +**Preconditions:** I4 complete (click behavior works) + +**Steps:** +1. Add hover styling to star buttons: + - Hover effect: `hover:text-yellow-400` for empty stars + - Hover effect: `hover:scale-110 transition-transform duration-150` +2. Add focus styling for keyboard navigation: + - Focus outline: `focus:outline-none focus:ring-2 focus:ring-primary` +3. Test hover states: + - Hover over empty star → color preview + - Hover over filled star → maintain filled color +4. Ensure touch targets are ≥44px on mobile: + - Add padding if needed: `p-2` or `p-3` + +**Commands:** +- `npm run dev` (visual inspection, hover testing) + +**Exit:** Hover states work correctly, visual feedback is clear + +**Implements:** FR-006-06, UI-006-04 + +--- + +### I6 – Keyboard Accessibility + +**Goal:** Add keyboard navigation and ARIA attributes for accessibility + +**Preconditions:** I5 complete (visual feedback works) + +**Steps:** +1. Verify ARIA attributes are correct: + - Group: `role="group" aria-label="Filter by star rating"` + - Buttons: `aria-label="Filter by N stars or higher"` and `aria-pressed="true|false"` +2. Add keyboard event handlers: + - Arrow Left/Right to navigate between stars + - Enter/Space to activate star (same as click) + - Tab to focus into/out of star group +3. Implement keyboard navigation logic: + ```typescript + const handleKeyDown = (event: KeyboardEvent, star: number) => { + if (event.key === 'ArrowRight' && star < 5) { + // Focus next star + focusStar(star + 1); + } else if (event.key === 'ArrowLeft' && star > 1) { + // Focus previous star + focusStar(star - 1); + } else if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleStarClick(star); + } + }; + ``` +4. Test keyboard navigation: + - Tab to star filter + - Arrow keys to navigate + - Enter to select + - Tab out to layout buttons + +**Commands:** +- `npm run check` +- Manual keyboard testing + +**Exit:** Keyboard navigation works, ARIA attributes correct, screen reader friendly + +**Implements:** NFR-006-02, UI-006-05 + +--- + +### I7 – Responsive Mobile Layout + +**Goal:** Ensure star filter works on mobile devices + +**Preconditions:** I6 complete (full desktop functionality) + +**Steps:** +1. Test on mobile viewport sizes: + - 320px (very narrow) + - 375px (iPhone SE) + - 768px (tablet) +2. Verify touch targets are ≥44px: + - Add padding if needed: `p-2` on mobile (`md:p-1` on desktop) +3. Adjust spacing for mobile: + - Star gap: `gap-1` on mobile, `gap-2` on desktop + - Border separator may need adjustment +4. Test touch interaction: + - Tap star to filter + - Ensure no hover state interferes with touch +5. Consider icon-only on very narrow screens (optional) + +**Commands:** +- `npm run dev` (test in browser DevTools responsive mode) + +**Exit:** Star filter works on all mobile breakpoints, touch targets adequate + +**Implements:** Responsive design requirements + +--- + +### I8 – Component Unit Tests + +**Goal:** Add comprehensive unit tests for star filter functionality + +**Preconditions:** I4 complete (core functionality works) + +**Steps:** +1. Create test file for PhotoThumbPanelControl (if not exists) +2. Write tests for `hasRatedPhotos` computed property: + - No rated photos → false + - At least one rated photo → true +3. Write tests for star click behavior: + - Click star N → filter set to N + - Click star N when filter === N → filter cleared +4. Write tests for filtering logic (from I2): + - Filter null → all photos + - Filter 3 → only ≥3 star photos + - Filter 5 → only 5-star photos + - No rated photos → no filtering applied +5. Write tests for starIconClass: + - Filter null → all empty stars + - Filter 3 → stars 1-3 filled, 4-5 empty +6. Create fixture file `photos-rating-filter.json` with sample data + +**Commands:** +- `npm run check` (run all tests) + +**Exit:** All unit tests pass, coverage for filter functionality + +**Implements:** Test strategy from spec, S-006-01 through S-006-10 + +--- + +### I9 – Performance Testing + +**Goal:** Verify filtering performance with 1000+ photos + +**Preconditions:** I2 complete (filtering logic implemented) + +**Steps:** +1. Create test dataset with 1000 photos (various ratings) +2. Measure filtering performance: + - Use browser DevTools Performance tab + - Measure computed property recalculation time + - Target: <100ms for 1000 photos (NFR-006-04) +3. Test scenarios: + - Filter from null to 3 (large change) + - Filter from 3 to 4 (small change) + - Clear filter (back to all photos) +4. If performance is poor: + - Check for unnecessary re-renders + - Verify computed property is cached correctly + - Consider optimization (memoization, virtualization) + +**Commands:** +- `npm run dev` (manual performance testing) +- Browser DevTools Performance profiling + +**Exit:** Filtering completes within 100ms for 1000 photos + +**Implements:** NFR-006-04 + +--- + +### I10 – Integration Testing & Edge Cases + +**Goal:** Test end-to-end filter behavior and edge cases + +**Preconditions:** I8 complete (unit tests pass) + +**Steps:** +1. Manual integration testing: + - Load album with mixed rated/unrated photos + - Verify filter appears + - Click stars, verify filtering works + - Navigate away and back, verify filter state persists in session + - Reload page, verify filter resets to null +2. Test edge cases: + - Album with no rated photos → filter hidden + - Album with all same rating (e.g., all 3 stars) → filter ≥4 shows empty grid + - User rates photo while filter active → list updates reactively + - User changes photo rating → filtered list updates +3. Test with Feature 001 integration: + - Verify user_rating field is populated correctly + - Test rating a photo, then filtering by that rating + +**Commands:** +- `npm run dev` (manual testing) + +**Exit:** All integration tests pass, edge cases handled correctly + +**Implements:** S-006-08, S-006-09, S-006-10 + +--- + +### I11 – Documentation Updates + +**Goal:** Update knowledge map and spec documentation + +**Preconditions:** I10 complete (feature fully implemented and tested) + +**Steps:** +1. Update [docs/specs/4-architecture/knowledge-map.md](docs/specs/4-architecture/knowledge-map.md): + - Note PhotoThumbPanelControl.vue modifications (star filter) + - Note PhotosState.ts modifications (filter state) + - Document filtering logic location +2. Update spec.md status to "Implemented" +3. Update roadmap.md feature status to "Complete" +4. Create PR description with: + - Feature summary + - Screenshots (filter empty, filter active, hover states) + - Testing notes + - Dependency note (Feature 001 required) + +**Commands:** +- None (documentation updates) + +**Exit:** Knowledge map updated, documentation current + +**Implements:** Documentation deliverables from spec + +--- + +## Scenario Tracking + +| Scenario ID | Increment / Task reference | Notes | +|-------------|---------------------------|-------| +| S-006-01 | I3 | No rated photos → filter hidden | +| S-006-02 | I3 | ≥1 rated photo → filter visible | +| S-006-03 | I2, I4 | Click star 3 → filter ≥3, photos filtered | +| S-006-04 | I2, I4 | Click star 5 → filter ≥5, only 5-star photos | +| S-006-05 | I2 | Click star 1 → filter ≥1, all rated photos (excludes unrated) | +| S-006-06 | I4 | Click star when already selected → filter cleared | +| S-006-07 | I4 | Click star 4, then star 2 → filter changes to ≥2 | +| S-006-08 | I1, I10 | Navigate within album → filter state persists | +| S-006-09 | I10 | Reload page → filter resets | +| S-006-10 | I10 | Rate photo while filter active → list updates reactively | + +## Analysis Gate + +**Status:** Not yet executed (spec just created) + +**Checklist to complete before implementation:** +- [ ] Verify Feature 001 (Photo Star Rating) is complete and deployed +- [ ] Confirm PhotosState store exists and can be extended +- [ ] Check that PhotoResource includes user_rating field +- [ ] Verify PrimeVue star icons are available (`pi-star`, `pi-star-fill`) +- [ ] Review existing PhotoThumbPanelControl.vue structure +- [ ] Confirm no conflicts with other active features (Feature 005) + +**Findings:** (To be filled after analysis gate execution) + +## Exit Criteria + +Before declaring Feature 006 complete, the following must pass: + +- [ ] All increments (I1-I11) completed successfully +- [ ] `npm run format` passes (frontend code formatting) +- [ ] `npm run check` passes (frontend tests and TypeScript type checking) +- [ ] Manual testing confirms: + - [ ] Star filter hidden when no rated photos + - [ ] Star filter visible when ≥1 rated photo exists + - [ ] Click star N → photos filtered to rating ≥ N + - [ ] Click same star → filter cleared + - [ ] Hover states work correctly + - [ ] Keyboard navigation works (Tab, Arrow keys, Enter) + - [ ] Mobile responsive layout works + - [ ] Filter state persists during session + - [ ] Filter resets on page reload +- [ ] Performance test passes (1000 photos filtered in <100ms) +- [ ] Accessibility audit passes (aria-labels, keyboard navigation) +- [ ] Integration with Feature 001 works (user_rating field populated) +- [ ] Documentation updated (knowledge map, spec status) + +## Follow-ups / Backlog + +**Potential enhancements (defer to future features):** +- Exact rating match filter (show only 3-star photos, not ≥3) +- Explicit "Unrated" filter option (show only unrated photos) +- Multi-select rating filter (checkboxes for 4 AND 5 stars) +- Combined filters (rating + date range + tags + NSFW) +- Save filter presets (named filters) +- URL query parameter support (shareable filtered views) +- Backend filtering (API query parameter `?min_rating=3`) +- Filter by aggregate rating (average from all users, not just current user) +- Filter animation transitions (smooth photo grid updates) + +**Monitoring & Metrics:** +- Track filter usage (how often users use filter, which ratings are most filtered) +- Monitor performance with large albums (1000+ photos) +- Collect user feedback on filter UX + +**Known Limitations:** +- Filter state not synced across devices (session-only, Pinia store) +- Page reload clears filter (acceptable per requirements) +- Cannot filter for exact rating (only minimum threshold) +- Cannot filter explicitly for unrated photos (they're excluded from filtered results) +- Depends on Feature 001 (blocking dependency) + +--- + +_Last updated: 2026-01-03_ diff --git a/docs/specs/4-architecture/features/006-photo-rating-filter/spec.md b/docs/specs/4-architecture/features/006-photo-rating-filter/spec.md new file mode 100644 index 00000000000..d3c14a7391a --- /dev/null +++ b/docs/specs/4-architecture/features/006-photo-rating-filter/spec.md @@ -0,0 +1,367 @@ +# Feature 006 – Photo Star Rating Filter + +| Field | Value | +|-------|-------| +| Status | Draft | +| Last updated | 2026-01-03 | +| Owners | Agent | +| Linked plan | `docs/specs/4-architecture/features/006-photo-rating-filter/plan.md` | +| Linked tasks | `docs/specs/4-architecture/features/006-photo-rating-filter/tasks.md` | +| Roadmap entry | #006 | + +> Guardrail: This specification is the single normative source of truth for the feature. Track high- and medium-impact questions in [docs/specs/4-architecture/open-questions.md](docs/specs/4-architecture/open-questions.md), encode resolved answers directly in the Requirements/NFR/Behaviour/UI/Telemetry sections below (no per-feature `## Clarifications` sections), and use ADRs under `docs/specs/5-decisions/` for architecturally significant clarifications (referencing their IDs from the relevant spec sections). + +## Overview +Add a star rating filter control to the photo panel that allows users to quickly filter photos in the current album by minimum star rating threshold. The filter displays as 5 hoverable/clickable stars positioned to the left of the photo layout selection buttons. Clicking a star filters photos to show rating ≥ selected star (e.g., click 3rd star → show 3, 4, 5 star photos). Clicking the same star again removes the filter. The filter is frontend-only (no backend changes), uses client-side filtering on the already-loaded photo array, and only appears when at least one photo in the album has a rating. Filter state persists in the Pinia state store during the session. + +## Goals +- Provide quick visual filtering of photos by minimum star rating threshold +- Display filter control only when relevant (at least one rated photo exists) +- Support intuitive interaction: click star to set minimum threshold, click again to clear filter +- Maintain filter state in Pinia store (similar to NSFW visibility pattern) +- Implement fully client-side filtering (no API calls) +- Position filter control to the left of existing photo layout selection buttons + +## Non-Goals +- Backend API changes or query parameter filtering +- Exact rating matching (filter shows ≥ N stars, not == N stars) +- Filtering for unrated photos explicitly (unrated excluded from filtered results) +- Multiple rating selection (checkboxes, multi-select) +- Persistent filter state across page reloads (localStorage) +- Filter controls for albums (feature is photo-only) +- Range sliders or complex UI controls + +## Functional Requirements + +| ID | Requirement | Success path | Validation path | Failure path | Telemetry & traces | Source | +|----|-------------|--------------|-----------------|--------------|--------------------|--------| +| FR-006-01 | Display star filter control only when at least one photo has a rating | Component computes `hasRatedPhotos` from photo array. If true, render 5-star filter control. If false, hide filter control entirely. | N/A (UI conditional rendering) | N/A | None | User requirement (conditional display) | +| FR-006-02 | Filter photos by minimum star rating threshold (≥ N stars) | User clicks star N (1-5) → filter state updates to N → photo list filtered to show only photos with `user_rating >= N`. Unrated photos (null/0 rating) excluded from results. | N/A (client-side filtering) | N/A | None | User requirement Q-006-01, Q-006-04 | +| FR-006-03 | Toggle filter off by clicking same star | User clicks star N when filter already set to N → filter state resets to null → all photos shown (no filtering applied). | N/A | N/A | None | User requirement Q-006-01 | +| FR-006-04 | Persist filter state in Pinia store during session | Filter state stored in PhotosState store (similar to NSFW visibility). State persists during navigation within album but resets on page reload or closing tab. | N/A | N/A | None | User requirement Q-006-03 | +| FR-006-05 | Apply filtering only when filter is active and rated photos exist | If filter state is null OR no rated photos exist → display all photos unfiltered. If filter state is set AND rated photos exist → apply filter. | N/A | N/A | None | User requirement (conditional filtering) | +| FR-006-06 | Visual feedback: highlight selected star threshold | Selected star and all stars below it should be visually highlighted (filled) to indicate active filter. Empty stars indicate no filter active. | N/A | N/A | None | UX clarity | +| FR-006-07 | Position filter control to the left of photo layout selection | Filter control rendered in PhotoThumbPanelControl.vue, positioned before (to the left of) existing layout buttons (squares, justified, masonry, grid). | N/A | N/A | None | User requirement (placement) | + +## Non-Functional Requirements + +| ID | Requirement | Driver | Measurement | Dependencies | Source | +|----|-------------|--------|-------------|--------------|--------| +| NFR-006-01 | Filtering must be instant (no API call, client-side only) | Performance - immediate user feedback | Filter applied synchronously via computed property, no observable delay | Vue 3 reactivity, photo data already loaded | User requirement (frontend-only) | +| NFR-006-02 | Filter control must be accessible via keyboard | Accessibility - keyboard navigation support | Tab to focus stars, Enter/Space to select, arrow keys to navigate stars, aria-labels present | PrimeVue accessibility features | WCAG 2.1 AA | +| NFR-006-03 | Component code must follow Vue 3 Composition API and TypeScript conventions | Code quality and maintainability | Follows existing patterns in PhotoThumbPanelControl.vue, TypeScript types for props/state | Vue 3, TypeScript, existing codebase patterns | [docs/specs/3-reference/coding-conventions.md](docs/specs/3-reference/coding-conventions.md) | +| NFR-006-04 | Filtering performance must handle 1000+ photos smoothly | Performance - large album support | Computed property recalculation completes within 100ms for 1000 photos | Vue 3 computed property optimization | User experience | + +## UI / Interaction Mock-ups + +### PhotoThumbPanelControl with Star Filter (No Filter Active) +``` +┌────────────────────────────────────────────────────┐ +│ Photos Panel │ +├────────────────────────────────────────────────────┤ +│ │ +│ [☆][☆][☆][☆][☆] [⊞][≡][⊟][▦] │ +│ ^Star Filter^ ^Layout buttons^ │ +│ │ +│ Photo grid below... │ +└────────────────────────────────────────────────────┘ + +Legend: + [☆] = Empty star (no filter active) + Filter only visible if at least one photo has rating +``` + +### Star Filter Active (Minimum 3 Stars Selected) +``` +┌────────────────────────────────────────────────────┐ +│ Photos Panel │ +├────────────────────────────────────────────────────┤ +│ │ +│ [★][★][★][☆][☆] [⊞][≡][⊟][▦] │ +│ ^3+ stars^ ^Layout buttons^ │ +│ │ +│ Showing photos with rating ≥ 3 stars │ +│ (excludes 1-2 star and unrated photos) │ +└────────────────────────────────────────────────────┘ + +Legend: + [★] = Filled star (stars 1-3 filled → filter ≥ 3) + [☆] = Empty star (stars 4-5 not part of threshold) + Click on star 3 again → clear filter +``` + +### Star Filter Hover Interaction +``` +User hovers over star 4: +[★][★][★][★*][☆] + ^Hover highlight + +User clicks star 4: +[★][★][★][★][☆] → Filter set to ≥ 4 stars + Shows 4 and 5 star photos only + +User clicks star 4 again: +[☆][☆][☆][☆][☆] → Filter cleared + Shows all photos +``` + +### Filter Hidden (No Rated Photos) +``` +┌────────────────────────────────────────────────────┐ +│ Photos Panel │ +├────────────────────────────────────────────────────┤ +│ │ +│ [⊞][≡][⊟][▦] (Star filter hidden) │ +│ ^Layout buttons^ │ +│ │ +│ All photos shown (none have ratings) │ +└────────────────────────────────────────────────────┘ + +Legend: + Star filter not rendered when no photos have ratings +``` + +## Branch & Scenario Matrix + +| Scenario ID | Description / Expected outcome | +|-------------|--------------------------------| +| S-006-01 | Album has no rated photos → star filter hidden, all photos displayed | +| S-006-02 | Album has ≥1 rated photo → star filter visible (5 empty stars) | +| S-006-03 | User clicks star 3 (no filter active) → filter set to ≥3, stars 1-3 filled, photos filtered to show 3+ star ratings | +| S-006-04 | User clicks star 5 → filter set to ≥5, all 5 stars filled, only 5-star photos shown | +| S-006-05 | User clicks star 1 → filter set to ≥1, star 1 filled, all rated photos shown (excludes unrated) | +| S-006-06 | User clicks star 3 when filter already ≥3 → filter cleared, all stars empty, all photos shown | +| S-006-07 | User clicks star 4, then clicks star 2 → filter changes from ≥4 to ≥2, stars 1-2 filled, photos with 2+ stars shown | +| S-006-08 | User navigates within album with filter active → filter state persists | +| S-006-09 | User reloads page → filter state resets to no filter (all photos shown) | +| S-006-10 | User rates a photo while filter active → filtered list updates reactively if photo meets threshold | + +## Test Strategy +- **Core:** N/A (no backend changes) +- **Application:** N/A (no backend changes) +- **REST:** N/A (no API changes) +- **CLI:** N/A (no CLI changes) +- **UI (JS/Selenium):** + - Unit tests for filter computed property logic (≥ threshold filtering) + - Unit tests for hasRatedPhotos detection + - Unit tests for toggle behavior (click star → set filter, click again → clear) + - Component tests for PhotoThumbPanelControl with filter rendering + - Integration tests for filter state persistence in PhotosState store + - Visual regression tests for star filter UI (empty, partial filled, all filled) + - Accessibility tests for keyboard navigation (Tab, Enter, Arrow keys) + - Performance tests with 1000+ photos +- **Docs/Contracts:** N/A (no API contracts) + +## Interface & Contract Catalogue + +### Domain Objects +| ID | Description | Modules | +|----|-------------|---------| +| DO-006-01 | Photo data with user_rating field (existing PhotoResource) | UI | + +### API Routes / Services +N/A - No API changes required + +### CLI Commands / Flags +N/A - No CLI changes required + +### Telemetry Events +N/A - No telemetry events (per project scope) + +### Fixtures & Sample Data +| ID | Path | Purpose | +|----|------|---------| +| FX-006-01 | resources/js/components/gallery/photoModule/__tests__/fixtures/photos-rating-filter.json | Sample photo data with various ratings (0-5) for filter testing | + +### UI States +| ID | State | Trigger / Expected outcome | +|----|-------|---------------------------| +| UI-006-01 | No filter active (empty stars) | Default state or user clears filter. All photos displayed (if rated photos exist, filter is visible). | +| UI-006-02 | Filter active (≥N stars filled) | User clicks star N. Stars 1-N filled, stars N+1-5 empty. Photos with rating ≥ N shown. | +| UI-006-03 | Filter hidden (no rated photos) | Album has no photos with ratings. Star filter control not rendered. | +| UI-006-04 | Star hover state | User hovers over star N. Visual highlight on star N (preview state). | +| UI-006-05 | Star focused (keyboard nav) | User tabs to star filter. Visual focus outline on current star. | + +## Telemetry & Observability +No telemetry events are defined for this feature per project scope. + +## Documentation Deliverables +- Update [docs/specs/4-architecture/roadmap.md](docs/specs/4-architecture/roadmap.md) with Feature 006 entry +- Update [docs/specs/4-architecture/knowledge-map.md](docs/specs/4-architecture/knowledge-map.md) with: + - PhotoThumbPanelControl.vue modifications (star filter control) + - PhotosState.ts modifications (filter state property) + - Filtering logic documentation + +## Fixtures & Sample Data +Create fixture file `resources/js/components/gallery/photoModule/__tests__/fixtures/photos-rating-filter.json` with sample photo data including: +- Photos with ratings 1-5 (at least 2 photos per rating level) +- Photos with no rating (user_rating: null or 0) +- Album with no rated photos (all user_rating: null) +- Album with mixed rated/unrated photos + +## Spec DSL + +```yaml +domain_objects: + - id: DO-006-01 + name: Photo (existing PhotoResource) + fields: + - name: id + type: string + - name: user_rating + type: integer | null + constraints: "0-5 or null" + +routes: [] + +cli_commands: [] + +telemetry_events: [] + +fixtures: + - id: FX-006-01 + path: resources/js/components/gallery/photoModule/__tests__/fixtures/photos-rating-filter.json + purpose: Sample photo data for rating filter testing + +ui_states: + - id: UI-006-01 + description: No filter active (empty stars) + - id: UI-006-02 + description: Filter active (≥N stars filled) + - id: UI-006-03 + description: Filter hidden (no rated photos) + - id: UI-006-04 + description: Star hover state + - id: UI-006-05 + description: Star focused (keyboard navigation) + +ui_components: + - id: UC-006-01 + name: PhotoThumbPanelControl.vue (modified) + location: resources/js/components/gallery/photoModule/PhotoThumbPanelControl.vue + modifications: Add star filter control (5 clickable stars) before layout buttons + - id: UC-006-02 + name: PhotosState.ts (modified) + location: resources/js/stores/PhotosState.ts + modifications: Add photo_rating_filter property (null | 1-5) for filter state +``` + +## Appendix + +### Resolved Open Questions +All open questions (Q-006-01, Q-006-02, Q-006-03, Q-006-04) have been resolved and incorporated into the spec: + +- **Q-006-01:** Filter UI uses hoverable star list with minimum threshold filtering and toggle-off +- **Q-006-02:** Unrated photos excluded from filtered results (addressed by minimum threshold logic) +- **Q-006-03:** Filter state persisted in Pinia store (like NSFW visibility), not localStorage +- **Q-006-04:** Minimum threshold filtering (≥ N stars) rather than exact match or multi-select + +### Implementation Notes + +1. **Component Architecture:** + - Modify existing `PhotoThumbPanelControl.vue` to add star filter control + - Add computed property `hasRatedPhotos` to check if any photo has user_rating > 0 + - Render star filter conditionally: `v-if="hasRatedPhotos"` + - Add computed property `filteredPhotos` to PhotosState or parent component + +2. **State Management:** + - Add `photo_rating_filter: null | 1 | 2 | 3 | 4 | 5` to PhotosState store + - Default value: `null` (no filter active) + - Action: `setPhotoRatingFilter(rating: null | 1-5)` + - Getter: `photoRatingFilter` + +3. **Filtering Logic:** + ```typescript + const filteredPhotos = computed(() => { + const filter = photosStore.photoRatingFilter; + const hasRated = photos.value.some(p => p.user_rating > 0); + + // Only apply filter if active AND rated photos exist + if (filter === null || !hasRated) { + return photos.value; + } + + return photos.value.filter(p => + p.user_rating !== null && + p.user_rating >= filter + ); + }); + ``` + +4. **Star Control Component:** + - 5 clickable star icons (PrimeVue `pi-star` and `pi-star-fill`) + - Stars 1-N filled when filter = N + - Click star N: if filter !== N → set filter to N, else → set filter to null + - Hover effect on stars (preview state) + - Aria-labels: "Filter by N stars or higher" for each star + - Keyboard support: Tab to focus, Arrow keys to select star, Enter to activate + +5. **Styling Considerations:** + - Star filter inline with layout buttons: `flex flex-row items-center gap-2` + - Stars grouped with small gap: `inline-flex gap-1` + - Star size: match layout button icon size (e.g., `text-lg` or `w-5 h-5`) + - Filled stars: `text-yellow-500` or `text-primary` + - Empty stars: `text-gray-300 dark:text-gray-600` + - Hover: `hover:text-yellow-400 cursor-pointer` + - Separator between filter and layout buttons: border or margin + +6. **Accessibility:** + - Star filter wrapped in `
` + - Each star button: `