312 lines
11 KiB
Markdown
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>
|