Files
yellowjacket/.planning/phases/08-frontend-performance-ux/08-02-PLAN.md
T

312 lines
11 KiB
Markdown

---
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"
---
<objective>
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.
</objective>
<execution_context>
@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
<!-- Current virtualizer patterns to replace -->
track-list.ts (1 virtualizer):
```html
<lit-virtualizer
.items=${visibleTracks}
.renderItem=${this.renderTrackRow}
></lit-virtualizer>
```
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
<lit-virtualizer
.items=${this.buildGridEntries()}
.renderItem=${this.renderGridEntry}
.keyFunction=${this.gridKeyFunction}
></lit-virtualizer>
```
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
<lit-virtualizer
.items=${tracks}
.renderItem=${this.renderTrackItem}
></lit-virtualizer>
```
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
<lit-virtualizer
.items=${entries}
.renderItem=${(entry: ArtistEntry) => this.renderArtistCard(entry)}
></lit-virtualizer>
```
Key: entry.artist.ID (number)
genres-view.ts (1 virtualizer):
```html
<lit-virtualizer
.items=${entries}
.renderItem=${(entry: GenreEntry) => this.renderGenreCard(entry)}
></lit-virtualizer>
```
Key: entry.genre.Name (string, genres identified by name)
Import needed:
```typescript
import { repeat } from 'lit/directives/repeat.js';
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Migrate track-list and queue-panel virtualizers to repeat() directive</name>
<files>frontend/src/components/track-list/track-list.ts, frontend/src/components/queue-panel/queue-panel.ts</files>
<action>
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 `<lit-virtualizer>` element (around line 1736-1741). Replace:
```html
<lit-virtualizer
.items=${visibleTracks}
.renderItem=${this.renderTrackRow}
></lit-virtualizer>
```
With:
```html
<lit-virtualizer
.items=${visibleTracks}
>
${repeat(
visibleTracks,
(track) => track.FilePath,
(track, index) => this.renderTrackRow(track, index),
)}
</lit-virtualizer>
```
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 `<lit-virtualizer>` element (around line 1282-1288). Replace the same pattern:
```html
<lit-virtualizer
.items=${tracks}
.renderItem=${this.renderTrackItem}
></lit-virtualizer>
```
With:
```html
<lit-virtualizer
.items=${tracks}
>
${repeat(
tracks,
(track) => track.id,
(track, index) => this.renderTrackItem(track, index),
)}
</lit-virtualizer>
```
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.
</action>
<verify>
<automated>cd frontend && npx tsc --noEmit 2>&1 | head -30</automated>
</verify>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Migrate cover-grid, artists-view, and genres-view virtualizers to repeat() directive</name>
<files>frontend/src/components/cover-grid/cover-grid.ts, frontend/src/components/artists-view/artists-view.ts, frontend/src/components/genres-view/genres-view.ts</files>
<action>
**cover-grid.ts (3 virtualizers):**
1. Add import: `import { repeat } from 'lit/directives/repeat.js';`
2. Cover-grid has THREE `<lit-virtualizer>` 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
<lit-virtualizer
.items=${items}
.renderItem=${this.renderGridEntry}
.keyFunction=${this.gridKeyFunction}
></lit-virtualizer>
```
With:
```html
<lit-virtualizer
.items=${items}
>
${repeat(
items,
(entry) => entry.album.ID,
(entry, index) => this.renderGridEntry(entry, index),
)}
</lit-virtualizer>
```
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
<lit-virtualizer
.items=${entries}
.renderItem=${(entry: ArtistEntry) => this.renderArtistCard(entry)}
></lit-virtualizer>
```
With:
```html
<lit-virtualizer
.items=${entries}
>
${repeat(
entries,
(entry) => entry.artist.ID,
(entry) => this.renderArtistCard(entry),
)}
</lit-virtualizer>
```
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
<lit-virtualizer
.items=${entries}
.renderItem=${(entry: GenreEntry) => this.renderGenreCard(entry)}
></lit-virtualizer>
```
With:
```html
<lit-virtualizer
.items=${entries}
>
${repeat(
entries,
(entry) => entry.genre.Name,
(entry) => this.renderGenreCard(entry),
)}
</lit-virtualizer>
```
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.
</action>
<verify>
<automated>cd frontend && npx tsc --noEmit 2>&1 | head -30</automated>
</verify>
<done>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.</done>
</task>
</tasks>
<verification>
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)
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/08-frontend-performance-ux/08-02-SUMMARY.md`
</output>