Files
yellowjacket/.planning/milestones/v1.0-phases/08-frontend-performance-ux/08-02-PLAN.md
T
yonlu 6ce0661fca chore: complete v1.0 Consolidation milestone
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
2026-03-05 09:34:43 -05:00

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
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
true
PERF-05
UX-02
truths artifacts key_links
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()
path provides contains
frontend/src/components/track-list/track-list.ts repeat() with FilePath key for track virtualizer repeat(
path provides contains
frontend/src/components/queue-panel/queue-panel.ts repeat() with QueueTrack.id key for queue virtualizer repeat(
path provides contains
frontend/src/components/cover-grid/cover-grid.ts repeat() with album.ID key for all 3 cover grid virtualizers repeat(
path provides contains
frontend/src/components/artists-view/artists-view.ts repeat() with artist entry key repeat(
path provides contains
frontend/src/components/genres-view/genres-view.ts repeat() with genre entry key repeat(
from to via pattern
track-list.ts lit-virtualizer repeat() directive as child of lit-virtualizer repeat(.*FilePath
from to via pattern
cover-grid.ts lit-virtualizer repeat() directive replacing .items/.renderItem/.keyFunction 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.

<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';
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 <lit-virtualizer> element (around line 1736-1741). Replace:
    <lit-virtualizer
        .items=${visibleTracks}
        .renderItem=${this.renderTrackRow}
    ></lit-virtualizer>
    
    With:
    <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:
    <lit-virtualizer
        .items=${tracks}
        .renderItem=${this.renderTrackItem}
    ></lit-virtualizer>
    
    With:
    <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. 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 <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>
  1. Remove both .renderItem and .keyFunction properties from all three virtualizers.
  2. 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.
  3. Keep .items on all three for virtualization sizing.
  4. 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:
    <lit-virtualizer
        .items=${entries}
        .renderItem=${(entry: ArtistEntry) => this.renderArtistCard(entry)}
    ></lit-virtualizer>
    
    With:
    <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:
    <lit-virtualizer
        .items=${entries}
        .renderItem=${(entry: GenreEntry) => this.renderGenreCard(entry)}
    ></lit-virtualizer>
    
    With:
    <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. 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)

<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>
After completion, create `.planning/phases/08-frontend-performance-ux/08-02-SUMMARY.md`