--- phase: 08-frontend-performance-ux plan: 02 type: execute wave: 1 depends_on: [] files_modified: - frontend/src/components/track-list/track-list.ts - frontend/src/components/queue-panel/queue-panel.ts - frontend/src/components/cover-grid/cover-grid.ts - frontend/src/components/artists-view/artists-view.ts - frontend/src/components/genres-view/genres-view.ts autonomous: true requirements: - PERF-05 - UX-02 must_haves: truths: - "All virtualizer components use repeat() directive with stable keys instead of .items/.renderItem" - "Track list uses FilePath as key, cover grid uses album.ID, queue panel uses QueueTrack.id" - "Artists and genres views use their entity ID as repeat() key" - "Scrolling through 10k+ tracks reuses DOM nodes efficiently via keyed repeat()" artifacts: - path: "frontend/src/components/track-list/track-list.ts" provides: "repeat() with FilePath key for track virtualizer" contains: "repeat(" - path: "frontend/src/components/queue-panel/queue-panel.ts" provides: "repeat() with QueueTrack.id key for queue virtualizer" contains: "repeat(" - path: "frontend/src/components/cover-grid/cover-grid.ts" provides: "repeat() with album.ID key for all 3 cover grid virtualizers" contains: "repeat(" - path: "frontend/src/components/artists-view/artists-view.ts" provides: "repeat() with artist entry key" contains: "repeat(" - path: "frontend/src/components/genres-view/genres-view.ts" provides: "repeat() with genre entry key" contains: "repeat(" key_links: - from: "track-list.ts" to: "lit-virtualizer" via: "repeat() directive as child of lit-virtualizer" pattern: "repeat\\(.*FilePath" - from: "cover-grid.ts" to: "lit-virtualizer" via: "repeat() directive replacing .items/.renderItem/.keyFunction" pattern: "repeat\\(.*album\\.ID" --- Migrate all virtualizer components from the `.items/.renderItem` property pattern to Lit's `repeat()` directive with stable keys for efficient DOM reuse during scrolling and filtering. Purpose: The repeat() directive with stable keys enables Lit's DOM recycling — when items are reordered, added, or removed, Lit moves existing DOM nodes instead of destroying and recreating them. This eliminates jank during scrolling and filtering in large libraries. Output: All 5 virtualizer components use repeat() with appropriate stable keys. @/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/opencode/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/08-frontend-performance-ux/08-CONTEXT.md @frontend/src/components/track-list/track-list.ts @frontend/src/components/queue-panel/queue-panel.ts @frontend/src/components/cover-grid/cover-grid.ts @frontend/src/components/artists-view/artists-view.ts @frontend/src/components/genres-view/genres-view.ts track-list.ts (1 virtualizer): ```html ``` Key: track.FilePath (unique per track, string) renderTrackRow signature: (track: library.Track, index: number) => TemplateResult cover-grid.ts (3 virtualizers — main grid, before-split, after-split): ```html ``` Current gridKeyFunction: `(entry: GridEntry) => \`a-${entry.album.ID}\`` Key: entry.album.ID (number, use as string in repeat key) renderGridEntry signature: (entry: GridEntry, index: number) => TemplateResult queue-panel.ts (1 virtualizer): ```html ``` Key: QueueTrack.id (string field, unique per queue entry even for duplicate tracks) renderTrackItem signature: (track: QueueTrack, index: number) => TemplateResult artists-view.ts (1 virtualizer): ```html this.renderArtistCard(entry)} > ``` Key: entry.artist.ID (number) genres-view.ts (1 virtualizer): ```html this.renderGenreCard(entry)} > ``` Key: entry.genre.Name (string, genres identified by name) Import needed: ```typescript import { repeat } from 'lit/directives/repeat.js'; ``` Task 1: Migrate track-list and queue-panel virtualizers to repeat() directive frontend/src/components/track-list/track-list.ts, frontend/src/components/queue-panel/queue-panel.ts Both components use flow layout virtualizers with `.items` + `.renderItem`. Convert to repeat() directive. **track-list.ts:** 1. Add import: `import { repeat } from 'lit/directives/repeat.js';` 2. Find the `` element (around line 1736-1741). Replace: ```html ``` With: ```html ${repeat( visibleTracks, (track) => track.FilePath, (track, index) => this.renderTrackRow(track, index), )} ``` 3. Remove the `.renderItem` property but keep `.items` — lit-virtualizer still needs `.items` for scroll sizing/virtualization calculations even when using repeat() for rendering. 4. Keep all other virtualizer properties unchanged (`.layout`, event handlers, etc.). **queue-panel.ts:** 1. Add import: `import { repeat } from 'lit/directives/repeat.js';` 2. Find the `` element (around line 1282-1288). Replace the same pattern: ```html ``` With: ```html ${repeat( tracks, (track) => track.id, (track, index) => this.renderTrackItem(track, index), )} ``` 3. Remove `.renderItem` property, keep `.items`. **Important:** The `renderTrackRow` and `renderTrackItem` methods stay as-is. The repeat() directive wraps them — it provides the key function, while the existing render methods provide the template. Do NOT change render method signatures. cd frontend && npx tsc --noEmit 2>&1 | head -30 track-list.ts uses repeat() with FilePath key. queue-panel.ts uses repeat() with QueueTrack.id key. Both keep .items for virtualization sizing. TypeScript compiles. Task 2: Migrate cover-grid, artists-view, and genres-view virtualizers to repeat() directive frontend/src/components/cover-grid/cover-grid.ts, frontend/src/components/artists-view/artists-view.ts, frontend/src/components/genres-view/genres-view.ts **cover-grid.ts (3 virtualizers):** 1. Add import: `import { repeat } from 'lit/directives/repeat.js';` 2. Cover-grid has THREE `` instances (main grid ~line 1853, before-split ~line 1880, after-split ~line 1909). ALL three currently use `.items`, `.renderItem`, and `.keyFunction`. Convert ALL three. For each virtualizer, replace: ```html ``` With: ```html ${repeat( items, (entry) => entry.album.ID, (entry, index) => this.renderGridEntry(entry, index), )} ``` 3. Remove both `.renderItem` and `.keyFunction` properties from all three virtualizers. 4. The `gridKeyFunction` method can be removed since its logic is now inline in the repeat() calls. Alternatively, keep it as a private method and reference it: `(entry) => this.gridKeyFunction(entry)` — either approach is fine, but inline is cleaner. 5. Keep `.items` on all three for virtualization sizing. 6. Preserve all other properties (`.layout`, CSS classes, event handlers). **artists-view.ts (1 virtualizer):** 1. Add import: `import { repeat } from 'lit/directives/repeat.js';` 2. Find the virtualizer (~line 1217-1227). Replace: ```html this.renderArtistCard(entry)} > ``` With: ```html ${repeat( entries, (entry) => entry.artist.ID, (entry) => this.renderArtistCard(entry), )} ``` 3. Determine the correct key — look at the ArtistEntry type to find the artist ID field. Use the artist's unique identifier. **genres-view.ts (1 virtualizer):** 1. Add import: `import { repeat } from 'lit/directives/repeat.js';` 2. Find the virtualizer (~line 1169-1177). Same pattern: ```html this.renderGenreCard(entry)} > ``` With: ```html ${repeat( entries, (entry) => entry.genre.Name, (entry) => this.renderGenreCard(entry), )} ``` 3. Determine the correct key — genres are identified by name (string). Use the genre name as key. **Important for all:** Keep `.items` property on virtualizers. The virtualizer needs the items array for scroll height calculation and viewport management. The repeat() directive handles the rendering and keying. cd frontend && npx tsc --noEmit 2>&1 | head -30 All three cover-grid virtualizers use repeat() with album.ID key. artists-view uses repeat() with artist ID key. genres-view uses repeat() with genre name key. .keyFunction and .renderItem properties removed. TypeScript compiles. 1. `cd frontend && npx tsc --noEmit` compiles without errors 2. All 7 virtualizer instances across 5 files use repeat() directive 3. No .renderItem properties remain on any lit-virtualizer element 4. No .keyFunction properties remain on any lit-virtualizer element 5. All virtualizers retain .items property for scroll sizing 6. Stable keys: FilePath (tracks), album.ID (covers), QueueTrack.id (queue), artist.ID (artists), genre.Name (genres) - Every lit-virtualizer in the codebase uses repeat() directive with stable keys - .items is preserved on all virtualizers for virtualization sizing - .renderItem and .keyFunction properties are removed - TypeScript compiles without errors After completion, create `.planning/phases/08-frontend-performance-ux/08-02-SUMMARY.md`