diff --git a/web/src/lib/components/AlphabeticalGrid.svelte b/web/src/lib/components/AlphabeticalGrid.svelte index 4a735a67..d430f69a 100644 --- a/web/src/lib/components/AlphabeticalGrid.svelte +++ b/web/src/lib/components/AlphabeticalGrid.svelte @@ -11,62 +11,117 @@ item: Snippet<[T]>; } = $props(); - // Compute the divider letter for each item; if it differs from the - // previous item's letter, that index gets a divider above it. + // Tag the first item of each letter so the rail can scrollIntoView + // it. Continuous flow — no per-letter row breaks — so packed + // alphabets don't waste a row for single-item letters. const segments = $derived.by(() => { - const out: { divider: string | null; item: T }[] = []; + const out: { letter: string | null; item: T }[] = []; let last: string | null = null; for (const it of items) { const k = (getKey(it) || '').charAt(0).toUpperCase(); if (k !== last) { - out.push({ divider: k, item: it }); + out.push({ letter: k, item: it }); last = k; } else { - out.push({ divider: null, item: it }); + out.push({ letter: null, item: it }); } } return out; }); + + // Distinct first-letters in the order they appear (preserves the + // upstream sort: 2 → 8 → A → B → … instead of pure alpha order + // because the source is already pre-sorted). + const letters = $derived.by(() => { + const seen: string[] = []; + const seenSet = new Set(); + for (const it of items) { + const k = (getKey(it) || '').charAt(0).toUpperCase(); + if (k && !seenSet.has(k)) { + seenSet.add(k); + seen.push(k); + } + } + return seen; + }); + + function jumpTo(letter: string) { + const el = document.getElementById(`alpha-${letter}`); + el?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } -
- {#each segments as seg, i (i)} - {#if seg.divider} -
-

{seg.divider}

- +
+
+ {#each segments as seg, i (i)} +
+ {@render item(seg.item)}
- {/if} -
- {@render item(seg.item)} -
- {/each} + {/each} +
+ + {#if letters.length > 1} + + + {/if}
diff --git a/web/src/lib/components/AlphabeticalGrid.test.ts b/web/src/lib/components/AlphabeticalGrid.test.ts index c2822b8a..0ad79688 100644 --- a/web/src/lib/components/AlphabeticalGrid.test.ts +++ b/web/src/lib/components/AlphabeticalGrid.test.ts @@ -23,7 +23,7 @@ const itemSnippet = createRawSnippet<[Item]>((getIt) => ({ type AnyProps = any; describe('AlphabeticalGrid', () => { - test('inserts a divider for each new key letter', () => { + test('emits a jump button for each distinct first-letter', () => { render(AlphabeticalGrid, { props: { items, @@ -31,14 +31,16 @@ describe('AlphabeticalGrid', () => { item: itemSnippet } as AnyProps }); - // Three dividers (A, B, D); C is absent because no items use C. - expect(screen.getByText('A')).toBeInTheDocument(); - expect(screen.getByText('B')).toBeInTheDocument(); - expect(screen.getByText('D')).toBeInTheDocument(); - expect(screen.queryByText('C')).not.toBeInTheDocument(); + // Rail has one button per distinct first-letter (A, B, D); + // C is absent because no items use C. Letters render as + //