Archive milestone artifacts: - milestones/v1.0-ROADMAP.md (full roadmap archive) - milestones/v1.0-REQUIREMENTS.md (26/26 requirements complete) - milestones/v1.0-phases/ (8 phase directories with plans, summaries, verifications) Updated: - PROJECT.md: full evolution review, all consolidation requirements validated - ROADMAP.md: collapsed to milestone summary with archive link - STATE.md: reset for next milestone - MILESTONES.md: created with stats and accomplishments - RETROSPECTIVE.md: created with lessons learned Deleted: - REQUIREMENTS.md (archived, fresh for next milestone) 8 phases, 17 plans, 34 tasks, 84 tests added, 6 days
11 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-frontend-performance-ux | 02 | execute | 1 |
|
true |
|
|
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.
<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>
@.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):
<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):
<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):
<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):
<lit-virtualizer
.items=${entries}
.renderItem=${(entry: ArtistEntry) => this.renderArtistCard(entry)}
></lit-virtualizer>
Key: entry.artist.ID (number)
genres-view.ts (1 virtualizer):
<lit-virtualizer
.items=${entries}
.renderItem=${(entry: GenreEntry) => this.renderGenreCard(entry)}
></lit-virtualizer>
Key: entry.genre.Name (string, genres identified by name)
Import needed:
import { repeat } from 'lit/directives/repeat.js';
track-list.ts:
- Add import:
import { repeat } from 'lit/directives/repeat.js'; - Find the
<lit-virtualizer>element (around line 1736-1741). Replace:With:<lit-virtualizer .items=${visibleTracks} .renderItem=${this.renderTrackRow} ></lit-virtualizer><lit-virtualizer .items=${visibleTracks} > ${repeat( visibleTracks, (track) => track.FilePath, (track, index) => this.renderTrackRow(track, index), )} </lit-virtualizer> - Remove the
.renderItemproperty but keep.items— lit-virtualizer still needs.itemsfor scroll sizing/virtualization calculations even when using repeat() for rendering. - Keep all other virtualizer properties unchanged (
.layout, event handlers, etc.).
queue-panel.ts:
- Add import:
import { repeat } from 'lit/directives/repeat.js'; - Find the
<lit-virtualizer>element (around line 1282-1288). Replace the same pattern:With:<lit-virtualizer .items=${tracks} .renderItem=${this.renderTrackItem} ></lit-virtualizer><lit-virtualizer .items=${tracks} > ${repeat( tracks, (track) => track.id, (track, index) => this.renderTrackItem(track, index), )} </lit-virtualizer> - Remove
.renderItemproperty, 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.
- Add import:
import { repeat } from 'lit/directives/repeat.js'; - 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:
<lit-virtualizer
.items=${items}
.renderItem=${this.renderGridEntry}
.keyFunction=${this.gridKeyFunction}
></lit-virtualizer>
With:
<lit-virtualizer
.items=${items}
>
${repeat(
items,
(entry) => entry.album.ID,
(entry, index) => this.renderGridEntry(entry, index),
)}
</lit-virtualizer>
- Remove both
.renderItemand.keyFunctionproperties from all three virtualizers. - The
gridKeyFunctionmethod 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. - Keep
.itemson all three for virtualization sizing. - Preserve all other properties (
.layout, CSS classes, event handlers).
artists-view.ts (1 virtualizer):
- Add import:
import { repeat } from 'lit/directives/repeat.js'; - Find the virtualizer (~line 1217-1227). Replace:
With:
<lit-virtualizer .items=${entries} .renderItem=${(entry: ArtistEntry) => this.renderArtistCard(entry)} ></lit-virtualizer><lit-virtualizer .items=${entries} > ${repeat( entries, (entry) => entry.artist.ID, (entry) => this.renderArtistCard(entry), )} </lit-virtualizer> - 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):
- Add import:
import { repeat } from 'lit/directives/repeat.js'; - Find the virtualizer (~line 1169-1177). Same pattern:
With:
<lit-virtualizer .items=${entries} .renderItem=${(entry: GenreEntry) => this.renderGenreCard(entry)} ></lit-virtualizer><lit-virtualizer .items=${entries} > ${repeat( entries, (entry) => entry.genre.Name, (entry) => this.renderGenreCard(entry), )} </lit-virtualizer> - 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.
<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>