diff --git a/app/Enum/AlbumLayoutType.php b/app/Enum/AlbumLayoutType.php new file mode 100644 index 00000000000..c9c92b379ff --- /dev/null +++ b/app/Enum/AlbumLayoutType.php @@ -0,0 +1,20 @@ +number_albums_per_row_mobile = request()->configs()->getValueAsInt('number_albums_per_row_mobile'); $this->photo_thumb_info = request()->configs()->getValueAsEnum('photo_thumb_info', PhotoThumbInfoType::class); $this->is_photo_thumb_tags_enabled = request()->configs()->getValueAsBool('photo_thumb_tags_enabled'); + $this->album_layout = request()->configs()->getValueAsEnum('album_layout', AlbumLayoutType::class); // Download configuration $this->is_thumb_download_enabled = request()->configs()->getValueAsBool('disable_thumb_download') === false; diff --git a/app/Http/Resources/Models/Utils/TimelineData.php b/app/Http/Resources/Models/Utils/TimelineData.php index d29eaede6dc..34bedc2ee83 100644 --- a/app/Http/Resources/Models/Utils/TimelineData.php +++ b/app/Http/Resources/Models/Utils/TimelineData.php @@ -100,7 +100,7 @@ private static function fromAlbum(ThumbAlbumResource $album, ColumnSortingType $ ColumnSortingType::MAX_TAKEN_AT => $album->max_taken_at_carbon(), ColumnSortingType::MIN_TAKEN_AT => $album->min_taken_at_carbon(), // Parse the title as date (e.g. "2020 something" or "2020-03 something" or "2020-03-25 something") - ColumnSortingType::TITLE => self::parseDateFromTitle($album->title), + ColumnSortingType::TITLE => self::parseDateFromTitle(trim($album->title)), default => null, }; diff --git a/app/Metadata/Versions/Remote/GitCommits.php b/app/Metadata/Versions/Remote/GitCommits.php index fb8e6b6faf6..c4b38b53b7a 100644 --- a/app/Metadata/Versions/Remote/GitCommits.php +++ b/app/Metadata/Versions/Remote/GitCommits.php @@ -35,7 +35,7 @@ * ... * },] */ -final class GitCommits extends AbstractGitRemote implements GitRemote +class GitCommits extends AbstractGitRemote implements GitRemote { use Trimable; diff --git a/app/Metadata/Versions/Remote/GitTags.php b/app/Metadata/Versions/Remote/GitTags.php index a05725e1b7c..05c8472e707 100644 --- a/app/Metadata/Versions/Remote/GitTags.php +++ b/app/Metadata/Versions/Remote/GitTags.php @@ -28,7 +28,7 @@ * "node_id": "MDM6UmVmMTQzOTc1ODA0OnJlZnMvdGFncy92NC42LjMtUkMx" * },] */ -final class GitTags extends AbstractGitRemote implements GitRemote +class GitTags extends AbstractGitRemote implements GitRemote { use Trimable; diff --git a/database/migrations/2026_01_04_000000_add_album_layout_config.php b/database/migrations/2026_01_04_000000_add_album_layout_config.php new file mode 100644 index 00000000000..fd193c52f34 --- /dev/null +++ b/database/migrations/2026_01_04_000000_add_album_layout_config.php @@ -0,0 +1,35 @@ + + */ + public function getConfigs(): array + { + return [ + [ + 'key' => 'album_layout', + 'value' => 'grid', + 'cat' => self::MOD_GALLERY, + 'type_range' => 'grid|list', + 'description' => 'Default album view layout.', + 'details' => 'Choose between grid (thumbnail cards) or list (detailed rows) view for albums. Users can toggle between views client-side, but preference does not persist across page reloads.', + 'is_secret' => false, + 'is_expert' => false, + 'order' => 50, + 'not_on_docker' => false, + 'level' => 0, + ], + ]; + } +}; 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 index 1bbaaeea5d9..8460bd3c82d 100644 --- a/docs/specs/4-architecture/features/005-album-list-view/plan.md +++ b/docs/specs/4-architecture/features/005-album-list-view/plan.md @@ -1,72 +1,93 @@ # 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 +_Status:_ Ready for Implementation +_Last updated:_ 2026-01-04 > 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. +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 > 0, sub-album count > 0) is visible at a glance on the same line (wide screens). Admin configures the default view; users can toggle client-side but preference does not persist across reloads. **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 +- Toggle controls are discoverable and functional in AlbumHero.vue and AlbumsHeader.vue +- List view displays all required information (thumbnail, full name, counts if > 0) in horizontal rows +- Counts displayed inline with title on wide screens (≥md), stacked on narrow screens +- Zero counts are hidden (no "0 photos" or "0 sub-albums") +- Default view mode is admin-configurable via Configs UI - No performance degradation when rendering 100+ albums in list view -- Responsive layout adapts gracefully on mobile devices +- RTL mode properly aligns thumbnails and text +- Albums are selectable in list view (Ctrl/Cmd/Shift modifiers) +- Drag-select overlay works in list view **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) +- View mode loads synchronously from InitConfig 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) +- Backend: `BaseConfigMigration` for `album_layout` config (`grid|list`) +- Backend: InitConfig.php modification to expose `album_layout` +- Frontend: LycheeState.ts modification to add `album_view_mode` state (initialized from InitConfig) +- Frontend: AlbumListView.vue component for rendering albums in horizontal list rows +- Frontend: AlbumListItem.vue component for individual list row rendering +- Frontend: AlbumHero.vue modifications to add grid/list toggle buttons (`pi-th-large`, `pi-list`) +- Frontend: AlbumsHeader.vue modifications to add grid/list toggle buttons +- Frontend: AlbumThumbPanel.vue modifications to conditionally render grid or list view +- Wide screen inline layout (title + counts on same line) +- Zero count hiding (only show if > 0) +- RTL support (right-aligned thumbnails, right-to-left text flow) +- Selection support (Ctrl/Cmd/Shift modifiers) +- Drag-select overlay compatibility +- Responsive design for mobile breakpoints (smaller thumbnails, stacked counts) - Keyboard accessibility for toggle controls -- Visual regression tests and component unit tests +- Component unit tests **Out of scope:** -- Backend API changes or database schema modifications +- User preference persistence (reloads reset to admin default) +- localStorage usage (removed from original plan) +- Backend API endpoints for user settings - 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 +**Backend Dependencies:** +- Laravel migration system (`BaseConfigMigration`) +- Existing Configs table and management UI +- InitConfig.php resource + **Frontend Dependencies:** - Vue 3 (Composition API) - TypeScript -- Tailwind CSS for styling -- PrimeVue for icons and accessibility utilities +- Tailwind CSS for styling (including RTL support with `ltr:` and `rtl:` prefixes) +- PrimeVue for icons (`pi-th-large`, `pi-list`) and accessibility utilities - LycheeState.ts store (state management) -- AlbumState.ts store (album data) +- AlbumsState.ts store (album data) - Existing Album model types (AlbumResource) **Components:** - AlbumHero.vue (existing - will be modified) +- AlbumsHeader.vue (existing - will be modified) - AlbumThumbPanel.vue (existing - will be modified) -- AlbumThumbPanelList.vue (existing - for comparison/reference) -- AlbumThumb.vue (existing - for comparison/reference) +- AlbumThumbPanelList.vue (existing - for reference) +- AlbumThumb.vue (existing - for reference) +- SelectDrag.vue (existing - must work with list view) **Interfaces:** -- Album data structure from AlbumState.ts (id, title, thumb, num_photos, num_children, badges) +- Album data structure from AlbumsState.ts (id, title, thumb, num_photos, num_children, policy/badges) - Router navigation (existing) +- InitConfig type from TypeScript transformer **Testing Infrastructure:** +- PHPUnit (backend config tests) - Vitest (component tests) - Vue Test Utils - Visual regression testing setup (if available) @@ -74,20 +95,23 @@ Users with many albums or albums with long names can now switch to a list view t ## 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 +- Album data structure includes `num_photos` and `num_children` fields ✅ confirmed +- PrimeVue icons `pi-th-large` and `pi-list` are available ✅ confirmed +- BaseConfigMigration pattern automatically creates admin UI ✅ confirmed via example migration +- TypeScript types auto-generated from InitConfig.php ✅ confirmed via Spatie transformer +- Existing album rendering infrastructure supports custom layouts ✅ confirmed +- RTL support available via Tailwind `ltr:` and `rtl:` prefixes ✅ confirmed **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 | +| Performance degradation with 1000+ albums | Medium | Test with large datasets, measure rendering time | +| Mobile layout complexity | Low | Use responsive Tailwind breakpoints, test on actual devices | +| Toggle button placement conflicts | Low | Follow existing AlbumHero/AlbumsHeader icon patterns | +| TypeScript type mismatches | Low | Follow existing patterns, verify types with `npm run check` | +| Selection behavior breaks in list view | Medium | Ensure AlbumListItem emits proper click events, test with modifiers | +| SelectDrag overlay positioning | Medium | Verify overlay calculates bounding rects correctly for list rows | ## Implementation Drift Gate @@ -97,177 +121,425 @@ Users with many albums or albums with long names can now switch to a list view t - 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) +- Backend: `php artisan test` passes +- Frontend: `npm run check` passes +- Visual screenshots of grid vs list views (desktop + mobile + RTL) - 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 +- Backend: `php artisan test`, `make phpstan` +- Frontend: `npm run format`, `npm run check` +- Development: `npm run dev` (local development server for manual testing) ## Increment Map -### I1 – LycheeState Store Modifications (View Mode State) +### I1 – Backend Config Migration (Admin Default Setting) -**Goal:** Add album view mode state to LycheeState.ts with localStorage persistence +**Goal:** Create database migration for `album_layout` config using BaseConfigMigration **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 +1. Create `database/migrations/2026_01_04_000000_add_album_layout_config.php` +2. Extend `BaseConfigMigration` (not standard Migration) +3. Define config in `getConfigs()` array: + - `key` = `'album_layout'` + - `value` = `'grid'` (default) + - `cat` = `'Gallery'` + - `type_range` = `'grid|list'` (creates dropdown in admin UI) + - `description` = `'Default album view layout.'` + - `details` = Explanation of grid vs list with note about no user persistence + - `is_expert` = `false` + - `order` = `50` +4. Run migration: `php artisan migrate` +5. Verify config appears in admin Configs UI under Gallery section +6. Test dropdown functionality (select grid/list) + +**Commands:** +- `php artisan migrate` +- `php artisan migrate:rollback` (to test down migration) +- Manual: Check admin UI → Settings → Gallery → album_layout + +**Exit:** Migration runs successfully, config appears in admin UI with dropdown, default is 'grid' + +**Implements:** FR-005-04 (Backend config for default album layout) + +**Code Reference:** See [IMPLEMENTATION-SNIPPETS.md](IMPLEMENTATION-SNIPPETS.md#1-database-migration) + +--- + +### I2 – InitConfig.php Modification (Expose Default to Frontend) + +**Goal:** Add `album_layout` property to InitConfig.php so frontend receives default value + +**Preconditions:** I1 complete (config migration ran) + +**Steps:** +1. Open `app/Http/Resources/GalleryConfigs/InitConfig.php` +2. Add property (line ~48, after album decoration settings): + ```php + // Album view mode + public string $album_layout; + ``` +3. Add initialization in constructor (line ~154, after `is_photo_thumb_tags_enabled`): + ```php + $this->album_layout = request()->configs()->getValueAsString('album_layout'); + ``` +4. Save file +5. Verify TypeScript type is auto-generated: + - Check that `App.Http.Resources.GalleryConfigs.InitConfig` includes `album_layout: string` + - If not, trigger TypeScript transformer rebuild + +**Commands:** +- Manual: Edit InitConfig.php +- Verify: Check GET `/api/Gallery::Init` response includes `album_layout` + +**Exit:** InitConfig.php includes `album_layout` property, API returns it in response + +**Implements:** FR-005-04 (Backend config exposed to frontend) + +**Code Reference:** See [IMPLEMENTATION-SNIPPETS.md](IMPLEMENTATION-SNIPPETS.md#2-initconfigphp) + +--- + +### I3 – LycheeState Store Modifications (View Mode State) + +**Goal:** Add album view mode state to LycheeState.ts, initialized from InitConfig + +**Preconditions:** I2 complete (InitConfig exposes album_layout) + +**Steps:** +1. Open `resources/js/stores/LycheeState.ts` +2. Add state property (line ~47, after album decoration settings): + ```typescript + // album stuff + album_view_mode: "grid" as "grid" | "list", + ``` +3. Add initialization in `load()` action (line ~167, after `is_photo_thumb_tags_enabled`): + ```typescript + this.album_view_mode = data.album_layout; + ``` +4. Save file +5. Run `npm run check` to verify TypeScript types +6. No toggle function needed - components will update state directly **Commands:** -- `npm run check` (verify TypeScript types and tests) +- `npm run check` (verify TypeScript types) +- `npm run format` (code formatting) + +**Exit:** LycheeState has `album_view_mode` state, initialized from InitConfig on app load -**Exit:** LycheeState has album_view_mode state, localStorage persistence works, tests pass +**Implements:** FR-005-03 (Client-side state management), NFR-005-01 (Load without blocking) -**Implements:** FR-005-04, NFR-005-01, S-005-03, S-005-04 +**Code Reference:** See [IMPLEMENTATION-SNIPPETS.md](IMPLEMENTATION-SNIPPETS.md#3-lycheestatets) --- -### I2 – AlbumListItem Component (Individual Row) +### I4 – AlbumListItem Component (Individual Row) -**Goal:** Create reusable component for single album list row +**Goal:** Create reusable component for single album list row with zero-hiding and inline layout -**Preconditions:** I1 complete (state management ready) +**Preconditions:** I3 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 +2. Define props interface: + ```typescript + defineProps<{ + album: App.Http.Resources.Models.AlbumResource; + isSelected: boolean; + }>(); + ``` +3. Define emits: + ```typescript + defineEmits<{ + clicked: [event: MouseEvent, album: AlbumResource]; + contexted: [event: MouseEvent, album: AlbumResource]; + }>(); + ``` +4. Implement template structure: + - Root div with flex layout, click/contextmenu handlers + - Router-link thumbnail (64px, 48px on mobile) + - Content div with title + counts + - Wide screens (≥md): Title and counts on same line (flexbox row) + - Narrow screens ((); + ``` +3. Define emits: + ```typescript + defineEmits<{ + "album-clicked": [event: MouseEvent, album: AlbumResource]; + "album-contexted": [event: MouseEvent, album: AlbumResource]; + }>(); + ``` +4. Implement template: + - Wrapper div: `flex flex-col gap-0` - 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 + - Render AlbumListItem for each album with `:is-selected="selectedIds.includes(album.id)"` + - Emit click/context events from AlbumListItem +5. Handle empty state (no albums) - can be minimal/inherit from parent +6. 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 +**Exit:** AlbumListView renders array of albums as list, emits events, tests pass + +**Implements:** FR-005-01 (List format), FR-005-08 (Drag-select compatible via event emission) -**Implements:** FR-005-01, S-005-05, S-005-06 +**Code Reference:** See [IMPLEMENTATION-SNIPPETS.md](IMPLEMENTATION-SNIPPETS.md#8-albumlistviewvue---new-component) --- -### I4 – AlbumThumbPanel Modifications (Conditional Rendering) +### I6 – 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) +**Preconditions:** I3, I5 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) +3. Import LycheeState store: + ```typescript + import { useLycheeStateStore } from "@/stores/LycheeState"; + const lycheeStore = useLycheeStateStore(); + ``` +4. Update template to conditionally render: + ```vue + + + ``` +5. Ensure both views receive same props and emit same events +6. Manually test toggle behavior (switch between views) +7. Verify SelectDrag component still works (test drag-select in both views) + +**Commands:** +- `npm run check` +- `npm run format` +- `npm run dev` (manual testing) + +**Exit:** AlbumThumbPanel correctly switches between grid and list views, no regression, drag-select works + +**Implements:** S-005-01, S-005-02, S-005-04, FR-005-08 (Drag-select) + +**Code Reference:** See [IMPLEMENTATION-SNIPPETS.md](IMPLEMENTATION-SNIPPETS.md#7-albumthumbpanelvue---conditional-rendering) + +--- + +### I7 – AlbumHero Toggle Buttons (UI Controls - Album Detail) + +**Goal:** Add grid/list toggle buttons to AlbumHero.vue icon row using `pi-th-large` and `pi-list` + +**Preconditions:** I3, I6 complete (state management + conditional rendering ready) + +**Steps:** +1. Read existing AlbumHero.vue to understand icon row structure (line ~33) +2. Import LycheeState store: + ```typescript + import { useLycheeStateStore } from "@/stores/LycheeState"; + const lycheeStore = useLycheeStateStore(); + ``` +3. Add toggle function: + ```typescript + function toggleAlbumView(mode: "grid" | "list") { + lycheeStore.album_view_mode = mode; + } + ``` +4. Add two PrimeVue Button components in the icon row: + - Grid button: `icon="pi pi-th-large"` + - List button: `icon="pi pi-list"` +5. Apply styling: + - `severity`: `primary` when active, `secondary` when inactive + - `text` attribute for flat buttons + - `class="border-none"` +6. Add aria attributes: + - `aria-label`: "Grid view" / "List view" + - `aria-pressed`: `true` when active, `false` when inactive +7. Add click handlers: `@click="toggleAlbumView('grid')"` / `@click="toggleAlbumView('list')"` +8. Test keyboard navigation (Tab to focus, Enter to activate) **Commands:** - `npm run check` - `npm run format` - `npm run dev` (manual testing) -**Exit:** AlbumThumbPanel correctly switches between grid and list views, no regression +**Exit:** Toggle buttons visible in AlbumHero, clickable, toggle view mode, keyboard accessible, aria-labels present + +**Implements:** FR-005-03 (Toggle controls), NFR-005-03 (Keyboard accessible), S-005-01, S-005-02 -**Implements:** S-005-01, S-005-02, S-005-04 +**Code Reference:** See [IMPLEMENTATION-SNIPPETS.md](IMPLEMENTATION-SNIPPETS.md#5-albumherovue---toggle-buttons) --- -### I5 – AlbumHero Toggle Buttons (UI Controls) +### I8 – AlbumsHeader Toggle Buttons (UI Controls - Albums Page) -**Goal:** Add grid/list toggle buttons to AlbumHero.vue icon row +**Goal:** Add grid/list toggle buttons to AlbumsHeader.vue menu -**Preconditions:** I1, I4 complete (state management + conditional rendering ready) +**Preconditions:** I7 complete (pattern established in AlbumHero) **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) +1. Open `resources/js/components/headers/AlbumsHeader.vue` +2. Import LycheeState store: + ```typescript + import { useLycheeStateStore } from "@/stores/LycheeState"; + const lycheeStore = useLycheeStateStore(); + ``` +3. Add toggle functions: + ```typescript + function toggleToGrid() { + lycheeStore.album_view_mode = "grid"; + } + function toggleToList() { + lycheeStore.album_view_mode = "list"; + } + ``` +4. Add two menu items to `menu` computed property (before search button): + ```typescript + { + icon: "pi pi-th-large", + type: "fn" as const, + callback: toggleToGrid, + severity: lycheeStore.album_view_mode === "grid" ? "primary" : "secondary", + if: true, + key: "view_grid", + }, + { + icon: "pi pi-list", + type: "fn" as const, + callback: toggleToList, + severity: lycheeStore.album_view_mode === "list" ? "primary" : "secondary", + if: true, + key: "view_list", + }, + ``` +5. Test that menu items appear in both desktop menu and mobile SpeedDial +6. Verify toggle state syncs between AlbumHero and AlbumsHeader **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 +**Exit:** Toggle buttons visible in AlbumsHeader, clickable, sync with AlbumHero toggles -**Implements:** FR-005-03, NFR-005-03, S-005-01, S-005-02, UI-005-03 +**Implements:** FR-005-03 (Toggle in AlbumsHeader), S-005-01, S-005-02 + +**Code Reference:** See [IMPLEMENTATION-SNIPPETS.md](IMPLEMENTATION-SNIPPETS.md#6-albumsheadervue---toggle-buttons) --- -### I6 – Responsive Mobile Layout Testing +### I9 – RTL Layout Testing + +**Goal:** Verify RTL mode properly aligns thumbnails and text + +**Preconditions:** I4, I5, I6 complete (list view components implemented) + +**Steps:** +1. Set browser/OS to RTL language (Arabic, Hebrew) +2. Load album page in list view +3. Verify thumbnails appear on right side +4. Verify text flows right-to-left +5. Verify counts appear right-to-left (sub-albums, then photos, then title) +6. Verify selection styling works in RTL +7. Take screenshots for documentation +8. If issues found, adjust Tailwind classes: + - Ensure `ltr:flex-row rtl:flex-row-reverse` is applied + - Ensure `ltr:text-left rtl:text-right` is applied + +**Commands:** +- `npm run dev` (test in browser with RTL language setting) + +**Exit:** List view renders correctly in RTL mode, thumbnails right-aligned, text flows right-to-left + +**Implements:** FR-005-01 (RTL support), S-005-11 + +--- + +### I10 – Selection & Drag-Select Testing + +**Goal:** Verify albums are selectable in list view and drag-select works + +**Preconditions:** I4, I5, I6 complete (list view components with event emission) + +**Steps:** +1. Load album page in list view +2. Click album without modifiers → navigates to album detail (not selected) +3. Ctrl+Click (Windows/Linux) or Cmd+Click (macOS) album → album selected, stays on page +4. Shift+Click album → range selection works +5. Verify selected albums have proper styling (blue background, ring) +6. Test drag-select: + - Click and drag across multiple albums + - Verify SelectDrag overlay appears + - Verify albums within overlay are selected +7. Switch from list to grid view → verify selection persists +8. Switch back to list → verify selection persists + +**Commands:** +- `npm run dev` (manual testing) + +**Exit:** Selection works identically in list view as grid view, drag-select works, selection persists across view changes + +**Implements:** FR-005-07 (Selectable), FR-005-08 (Drag-select), S-005-12, S-005-13, S-005-14 + +--- + +### I11 – Responsive Mobile Layout Testing **Goal:** Verify and refine mobile responsive layout for list view -**Preconditions:** I2, I3, I4, I5 complete (all components implemented) +**Preconditions:** I4, I5, I6, I7, I8 complete (all components implemented) **Steps:** 1. Test on various mobile viewport sizes: @@ -276,41 +548,96 @@ Users with many albums or albums with long names can now switch to a list view t - 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 +4. Verify counts stack below title on narrow screens (` elements in the flex-row-reverse icon container (line 33) with grid/list icons (PrimeVue pi-th, pi-list). +- [x] T-005-28 – Add setup logic to AlbumHero.vue (FR-005-03). + _Intent:_ Add `const lycheeStore = useLycheeStateStore();` and `toggleAlbumView(mode: "grid" | "list")` function that sets `lycheeStore.album_view_mode = mode;`. + _Verification commands:_ + - `npm run check` + _Notes:_ Reference IMPLEMENTATION-SNIPPETS.md for exact code. + +- [x] T-005-29 – Add grid and list toggle button elements to AlbumHero.vue (FR-005-03, S-005-01, S-005-02). + _Intent:_ Add two `