From a909dfb8aabd02d7d9881eecabb51efa19000b69 Mon Sep 17 00:00:00 2001 From: Daniel Naab Date: Wed, 29 Apr 2026 04:26:01 +0000 Subject: [PATCH] feat(work): side-nav layout with compact list sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the card grid + dropdown filters with a side-nav + sectioned compact list approach: - Sticky side-nav with section links (Featured, Active, Available, Archived) - Featured section uses FeaturedCards inside the right panel (not above it) - Non-featured repos in compact list: name, category tag, description - List items have hover accent, padding, and clear visual separation - Tags sit inline with flex-shrink: 0 to prevent compression - Drops catalog-filter entirely — spatial navigation replaces it - IntersectionObserver highlights current section as you scroll --- src/design/layout.css | 48 +++++++++-- src/pages/work/index.tsx | 141 +++++++++++++++++++------------- tests/views/work-index.test.tsx | 38 +++++---- 3 files changed, 145 insertions(+), 82 deletions(-) diff --git a/src/design/layout.css b/src/design/layout.css index bc8db33..66e1a5b 100644 --- a/src/design/layout.css +++ b/src/design/layout.css @@ -68,7 +68,7 @@ } .home-featured__grid, .home-stats__grid, - .work-index__list { + .work-index__featured-grid { display: grid; gap: var(--space-5); container-type: inline-size; @@ -76,21 +76,51 @@ @container (min-width: 48rem) { .home-featured__grid { grid-template-columns: repeat(2, 1fr); } .home-stats__grid { grid-template-columns: repeat(3, 1fr); } - .work-index__list { grid-template-columns: repeat(2, 1fr); } + .work-index__featured-grid { grid-template-columns: repeat(2, 1fr); } } @container (min-width: 72rem) { .home-featured__grid { grid-template-columns: repeat(4, 1fr); } - .work-index__list { grid-template-columns: repeat(3, 1fr); } } - .work-index__section-heading { - grid-column: 1 / -1; + .work-list { list-style: none; + padding: 0; + display: flex; + flex-direction: column; + gap: 0; } - .work-index__section-heading h2 { - font-size: var(--step-0); - color: var(--color-ink-subtle); + .work-list__item { + padding: var(--space-4) var(--space-4); border-block-end: 1px solid var(--color-surface-alt); - padding-block-end: var(--space-2); + border-inline-start: 3px solid transparent; + } + .work-list__item:last-child { + border-block-end: none; + } + .work-list__item:hover { + border-inline-start-color: var(--color-accent); + background: var(--color-surface-highlight); + } + .work-list__header { + display: flex; + align-items: center; + gap: var(--space-3); + } + .work-list__header .tag { + flex-shrink: 0; + } + .work-list__name { + font-weight: 600; + text-decoration: none; + color: var(--color-ink); + } + .work-list__name:hover { + color: var(--color-link-hover); + text-decoration: underline; + } + .work-list__desc { + color: var(--color-ink-subtle); + font-size: var(--step--1); + margin-block-start: var(--space-2); } .work-detail__related { margin-block-start: var(--space-6); diff --git a/src/pages/work/index.tsx b/src/pages/work/index.tsx index 6a583b9..8fdd879 100644 --- a/src/pages/work/index.tsx +++ b/src/pages/work/index.tsx @@ -1,6 +1,6 @@ import { Layout } from '../../design/common/layout' -import { RepoCard } from '../../design/components/repo-card' -import { Select } from '../../design/components/select' +import { FeaturedCard } from '../../design/components/featured-card' +import { Tag } from '../../design/components/tag' import type { Catalog, CatalogEntry } from '../../catalog/types' import { url } from '../../build/config' import type { SiteConfig } from '../../build/config' @@ -8,11 +8,22 @@ import type { SiteConfig } from '../../build/config' declare module 'hono/jsx' { namespace JSX { interface IntrinsicElements { - 'catalog-filter': { children?: any } + 'side-nav': { children?: any } } } } +const CATEGORY_LABEL: Record = { + product: 'Product', + tool: 'Tool', + workshop: 'Workshop', + prototype: 'Prototype', + fork: 'Fork', + uncategorized: 'Uncategorized', +} + +type Section = { id: string; label: string; entries: CatalogEntry[]; featured?: boolean } + export function WorkIndex({ catalog, config, @@ -21,72 +32,86 @@ export function WorkIndex({ config: SiteConfig }) { const visible = catalog.filter((e) => !e.hidden) - const sorted = [...visible].sort(defaultSort) + const featured = visible.filter((e) => e.featured) + const rest = visible.filter((e) => !e.featured) + + const sections: Section[] = [ + ...(featured.length > 0 ? [{ id: 'featured', label: 'Featured', entries: featured, featured: true }] : []), + { id: 'active', label: 'Active', entries: rest.filter((e) => e.tier === 'active') }, + { id: 'available', label: 'Available', entries: rest.filter((e) => e.tier === 'unreviewed' || e.tier === 'as-is') }, + { id: 'archived', label: 'Archived', entries: rest.filter((e) => e.tier === 'archived') }, + ].filter((s) => s.entries.length > 0) + + // Sort non-featured entries within each section by pushedAt descending + for (const section of sections) { + if (!section.featured) { + section.entries.sort((a, b) => b.pushedAt.localeCompare(a.pushedAt)) + } + } + + const navItems = sections.map((s) => ({ + href: `#${s.id}`, + label: s.featured ? s.label : `${s.label} (${s.entries.length})`, + })) return (

Our work

Flexion's public portfolio — tools we've built, prototypes we've shipped, and - open-source projects we actively contribute to. Use the filters to explore by - tier or category. See our{' '} + open-source projects we actively contribute to. See our{' '} stewardship scorecard for how each repo measures up.

- -
-
- Filter - - -
-
-
    - {sorted.map((entry, i) => ( - <> - {entry.featured && i === 0 ? ( - - ) : null} - {!entry.featured && (i === 0 || sorted[i - 1].featured) ? ( -
-
+ +
) } - -function defaultSort(a: CatalogEntry, b: CatalogEntry): number { - if (a.featured !== b.featured) return a.featured ? -1 : 1 - const tierRank: Record = { - active: 0, - unreviewed: 1, - 'as-is': 2, - archived: 3, - } - if (tierRank[a.tier] !== tierRank[b.tier]) return tierRank[a.tier] - tierRank[b.tier] - return b.pushedAt.localeCompare(a.pushedAt) -} diff --git a/tests/views/work-index.test.tsx b/tests/views/work-index.test.tsx index d2392ab..cfc7a0d 100644 --- a/tests/views/work-index.test.tsx +++ b/tests/views/work-index.test.tsx @@ -6,13 +6,15 @@ import { fixtureCatalog } from '../fixtures/catalog' const config = { basePath: '/', buildTime: '2026-04-27T12:00:00Z' } describe('WorkIndex', () => { - test('renders a row for every non-hidden repo', async () => { + test('renders featured repos in a featured section', async () => { const html = await renderToHtml( , ) - for (const entry of fixtureCatalog) { - expect(html).toContain(entry.name) - } + expect(html).toContain('id="featured"') + expect(html).toContain('featured-card') + expect(html).toContain('messaging') + expect(html).toContain('forms') + expect(html).toContain('document-extractor') }) test('omits hidden repos', async () => { @@ -22,26 +24,32 @@ describe('WorkIndex', () => { const html = await renderToHtml( , ) - expect(html).not.toContain('messaging') + expect(html).not.toContain('href="/work/messaging/"') }) - test('wraps the list in a web component', async () => { + test('renders a side-nav with section links', async () => { const html = await renderToHtml( , ) - expect(html).toContain(' { + test('groups non-featured repos into tier sections', async () => { const html = await renderToHtml( , ) - const order = ['messaging', 'forms', 'document-extractor'] - let last = -1 - for (const name of order) { - const idx = html.indexOf(`href="/work/${name}/"`) - expect(idx).toBeGreaterThan(last) - last = idx - } + expect(html).toContain('id="available"') + expect(html).toContain('id="archived"') + expect(html).toContain('fork-of-thing') + expect(html).toContain('archived-thing') + }) + + test('renders compact list items with name and category', async () => { + const html = await renderToHtml( + , + ) + expect(html).toContain('class="work-list__name"') + expect(html).toContain('class="work-list__desc"') }) })