Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 39 additions & 9 deletions src/design/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,29 +68,59 @@
}
.home-featured__grid,
.home-stats__grid,
.work-index__list {
.work-index__featured-grid {
display: grid;
gap: var(--space-5);
container-type: inline-size;
}
@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);
Expand Down
141 changes: 83 additions & 58 deletions src/pages/work/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
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'

declare module 'hono/jsx' {
namespace JSX {
interface IntrinsicElements {
'catalog-filter': { children?: any }
'side-nav': { children?: any }
}
}
}

const CATEGORY_LABEL: Record<CatalogEntry['category'], string> = {
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,
Expand All @@ -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 (
<Layout title="Work" config={config}>
<h1>Our work</h1>
<p class="work-index__intro">
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{' '}
<a href={url('/work/health/', config.basePath)}>stewardship scorecard</a> for
how each repo measures up.
</p>
<catalog-filter>
<form class="catalog-filter">
<fieldset>
<legend>Filter</legend>
<Select name="tier" label="Tier">
<option value="">All</option>
<option value="active">Active</option>
<option value="unreviewed">Unreviewed</option>
<option value="as-is">As-is</option>
<option value="archived">Archived</option>
</Select>
<Select name="category" label="Category">
<option value="">All</option>
<option value="product">Product</option>
<option value="tool">Tool</option>
<option value="workshop">Workshop</option>
<option value="prototype">Prototype</option>
<option value="fork">Fork</option>
<option value="uncategorized">Uncategorized</option>
</Select>
</fieldset>
</form>
<ul class="work-index__list">
{sorted.map((entry, i) => (
<>
{entry.featured && i === 0 ? (
<li class="work-index__section-heading" data-featured="true" role="presentation" aria-hidden="true">
<h2>Featured</h2>
</li>
) : null}
{!entry.featured && (i === 0 || sorted[i - 1].featured) ? (
<li class="work-index__section-heading" role="presentation" aria-hidden="true">
<h2>All projects</h2>

<div class="l-sidebar">
<side-nav>
<nav class="side-nav" aria-label="Work sections">
<ul class="side-nav__list">
{navItems.map(({ href, label }) => (
<li class="side-nav__item">
<a class="side-nav__link" href={href}>{label}</a>
</li>
) : null}
<li data-tier={entry.tier} data-category={entry.category} data-featured={entry.featured ? 'true' : undefined}>
<RepoCard entry={entry} basePath={config.basePath} />
</li>
</>
))}
</ul>
</nav>
</side-nav>

<div class="l-stack" data-space="xl">
{sections.map((section) => (
<section id={section.id} aria-labelledby={`${section.id}-heading`}>
<h2 id={`${section.id}-heading`}>{section.label}</h2>
{section.featured ? (
<div class="work-index__featured-grid">
{section.entries.map((entry) => (
<FeaturedCard entry={entry} basePath={config.basePath} />
))}
</div>
) : (
<ul class="work-list">
{section.entries.map((entry) => (
<li class="work-list__item">
<div class="work-list__header">
<a class="work-list__name" href={url(`/work/${entry.name}/`, config.basePath)}>
{entry.name}
</a>
<Tag variant={`category-${entry.category}`}>
{CATEGORY_LABEL[entry.category]}
</Tag>
</div>
{entry.overlay?.summary || entry.description ? (
<p class="work-list__desc">
{entry.overlay?.summary ?? entry.description}
</p>
) : null}
</li>
))}
</ul>
)}
</section>
))}
</ul>
</catalog-filter>
</div>
</div>
</Layout>
)
}

function defaultSort(a: CatalogEntry, b: CatalogEntry): number {
if (a.featured !== b.featured) return a.featured ? -1 : 1
const tierRank: Record<CatalogEntry['tier'], number> = {
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)
}
38 changes: 23 additions & 15 deletions tests/views/work-index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<WorkIndex catalog={fixtureCatalog} config={config} />,
)
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 () => {
Expand All @@ -22,26 +24,32 @@ describe('WorkIndex', () => {
const html = await renderToHtml(
<WorkIndex catalog={catalog} config={config} />,
)
expect(html).not.toContain('messaging')
expect(html).not.toContain('href="/work/messaging/"')
})

test('wraps the list in a <catalog-filter> web component', async () => {
test('renders a side-nav with section links', async () => {
const html = await renderToHtml(
<WorkIndex catalog={fixtureCatalog} config={config} />,
)
expect(html).toContain('<catalog-filter')
expect(html).toContain('<side-nav')
expect(html).toContain('class="side-nav"')
})

test('applies the default sort: featured first, then active, then by pushedAt desc', async () => {
test('groups non-featured repos into tier sections', async () => {
const html = await renderToHtml(
<WorkIndex catalog={fixtureCatalog} config={config} />,
)
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(
<WorkIndex catalog={fixtureCatalog} config={config} />,
)
expect(html).toContain('class="work-list__name"')
expect(html).toContain('class="work-list__desc"')
})
})
Loading