diff --git a/src/components/changelog.test.tsx b/src/components/changelog.test.tsx index 516ba972..01fac082 100644 --- a/src/components/changelog.test.tsx +++ b/src/components/changelog.test.tsx @@ -79,7 +79,10 @@ describe('the changelog on the server', () => { expect(renderToStaticMarkup()).toBe(''); }); - it('offers a filter for each part, as toggle buttons that are all off', () => { + it('labels the filter group and marks All parts as the default selection', () => { + expect(html).toContain('id="marfa-cl-filter-label" class="marfa-cl-filter-label">Filter by part'); + expect(html).toContain('role="group" aria-labelledby="marfa-cl-filter-label"'); + expect(html).toMatch(/]*aria-pressed="true"[^>]*>All parts<\/button>/); expect(html.match(/]*aria-pressed="false"[^>]*>(Server|CLI)<\/button>/g)).toHaveLength(2); }); }); @@ -103,18 +106,26 @@ describe('the part filters', () => { const button = (name: string) => [...container.querySelectorAll('button')].find((b) => b.textContent === name)!; const hiddenParts = () => [...container.querySelectorAll('.marfa-cl-part')].filter((p) => p.hidden).map((p) => p.getAttribute('aria-label')); - it('shows only the selected parts, and every part again when none is selected', () => { + it('shows only selected parts, and restores All parts when the filter is cleared', () => { + expect(button('All parts').getAttribute('aria-pressed')).toBe('true'); act(() => button('Server').click()); expect(button('Server').getAttribute('aria-pressed')).toBe('true'); + expect(button('All parts').getAttribute('aria-pressed')).toBe('false'); expect(hiddenParts()).toEqual(['CLI', 'CLI']); - act(() => button('CLI').click()); + act(() => button('All parts').click()); + expect(button('All parts').getAttribute('aria-pressed')).toBe('true'); + expect(button('Server').getAttribute('aria-pressed')).toBe('false'); expect(hiddenParts()).toEqual([]); + }); + it('returns to All parts when every part is selected', () => { act(() => button('Server').click()); act(() => button('CLI').click()); + expect(button('All parts').getAttribute('aria-pressed')).toBe('true'); + expect(button('Server').getAttribute('aria-pressed')).toBe('false'); + expect(button('CLI').getAttribute('aria-pressed')).toBe('false'); expect(hiddenParts()).toEqual([]); - expect(container.querySelectorAll('[aria-pressed="true"]')).toHaveLength(0); }); it('keeps every release heading while a filter hides its parts, and says when nothing is left', () => { diff --git a/src/components/changelog.tsx b/src/components/changelog.tsx index 504b067c..cbc0cb69 100644 --- a/src/components/changelog.tsx +++ b/src/components/changelog.tsx @@ -106,18 +106,35 @@ export function ChangelogView({ releases }: { releases: Release[] }) { setSelected((current) => { const next = new Set(current); if (!next.delete(name)) next.add(name); - return next; + return next.size === parts.length ? new Set() : next; }); return (
{parts.length > 1 ? ( -
- {parts.map((name) => ( - - ))} + {parts.map((name) => ( + + ))} +
) : null} {releases.map((release, index) => ( diff --git a/src/styles/app.css b/src/styles/app.css index ebd80649..94478747 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -517,15 +517,50 @@ html > body[data-scroll-locked] { line-height: 1.6; } -/* The changelog: a row of part filters, then one entry for each release. The - filters are the buttons of an interactive figure (marfa-ix-button). A part - is hidden, never removed, so a release keeps its heading and summary. */ +/* The changelog filters sit inline with a visible label, like a filter rather + than page navigation. A part is hidden, never removed, so a release keeps + its heading and summary. */ .marfa-cl-filters { display: flex; flex-wrap: wrap; - gap: 0.25rem; + align-items: center; + gap: 0.5rem 1rem; margin: 1.5rem 0 0.5rem; } +.marfa-cl-filter-label { + color: var(--color-fd-foreground); + font-size: 0.9375rem; + font-weight: 600; + letter-spacing: -0.01em; +} +.marfa-cl-filter-options { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.375rem; +} +.marfa-cl-filter-button { + min-height: 2.25rem; + padding: 0.3125rem 0.875rem; + border: 1px solid var(--color-fd-border); + border-radius: 9999px; + background: transparent; + color: var(--color-fd-muted-foreground); + font-size: 0.875rem; + font-weight: 550; + line-height: 1.25rem; + white-space: nowrap; + transition: background-color 0.15s, border-color 0.15s, color 0.15s; +} +.marfa-cl-filter-button:hover { + border-color: color-mix(in oklab, var(--color-fd-foreground) 35%, var(--color-fd-border)); + color: var(--color-fd-foreground); +} +.marfa-cl-filter-button[aria-pressed='true'] { + border-color: var(--color-fd-foreground); + background: var(--color-fd-foreground); + color: var(--color-fd-background); +} .marfa-cl-release { padding-bottom: 1rem; border-bottom: 1px solid var(--color-fd-border);