Skip to content

Commit 1fcc02f

Browse files
committed
feat(lab-card): group columns by kind, not per-link
Rework the column layout so each column represents a distinct link kind (Demo, Repository, Case study), with same-kind links stacking vertically within the column in document order. This makes the Forms Lab card read cleanly — one "Demo" column with Forms Platform above Forms Lab (experiment), one "Repository" column with flexion/forms above flexion/forms-lab. Matching list positions across columns refer to the same project. Headings no longer repeat, so they carry real categorical weight. Other cards adjust naturally: Messaging has one Repository column, Document Extractor has Repository and Case study columns. - `LabCard`: groups links by kind, preserves per-kind document order, emits one column per present kind in fixed order (Demo → Repository → Case study). - Styles: columns container auto-fits at 14rem minimum above 32rem; stacks below. Inner link list is a vertical flex stack with the icon-prefixed anchors. - Update home and component view tests to match; update the home test fixture to include the required `kind` field. - Update docs/featured-content.md to describe the group-by-kind layout.
1 parent a275301 commit 1fcc02f

6 files changed

Lines changed: 93 additions & 43 deletions

File tree

docs/featured-content.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ links:
3737

3838
## Rendering
3939

40-
The home page renders one `<LabCard />` per lab. Each card places the title and tagline on top, with a horizontal row of columns below it — one column per link. Each column has a small uppercase kind heading ("Demo", "Repository", or "Case study") and the link itself, prefixed with an icon (globe for `demo`, GitHub mark for `repo`, document for `case-study`). Columns flow from 1 → 2 → 4 based on card width via container queries at 32rem and 56rem. The home page's featured list is constrained to `72rem` to keep cards at a readable width on wide displays.
40+
The home page renders one `<LabCard />` per lab. Each card places the title and tagline on top, with a horizontal row of columns below it — **one column per distinct link kind**, in fixed order (Demo → Repository → Case study). Links sharing a kind stack vertically within their column in document order; matching positions across columns (first demo ↔ first repo) refer to the same project, so a lab with multiple projects should list them in the same order under each kind.
41+
42+
Each link is prefixed with an icon (globe for `demo`, GitHub mark for `repo`, document for `case-study`). The columns container collapses to a single column below 32rem; above it, columns auto-fit at a minimum of 14rem wide. The home page's featured list is constrained to `72rem` to keep cards at a readable width on wide displays.
4143

4244
## Adding a featured lab
4345

src/design/components/lab-card/examples.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ export function LabCardExamples() {
3737
<section id="lab-card">
3838
<h2>Lab card</h2>
3939
<p>
40-
Featured-lab card on the home page. Title and tagline sit on top; each link becomes
41-
its own column below, titled with the link kind (Demo, Repository, or Case study).
42-
Columns flow from 1 → 2 → 4 based on card width.
40+
Featured-lab card on the home page. Title and tagline sit on top; links group into one
41+
column per distinct kind (Demo, Repository, or Case study) below. Columns always appear
42+
in kind order (Demo → Repository → Case study), and multiple links of the same kind
43+
stack vertically within their column in document order. Links with matching list
44+
positions across columns (first Demo ↔ first Repository) refer to the same project.
4345
</p>
4446
<div class="l-stack" data-space="md">
4547
<LabCard lab={multiProject} />

src/design/components/lab-card/index.tsx

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,55 @@
1-
import type { FeaturedLab, FeaturedLinkKind } from '../../../build/featured'
1+
import type { FeaturedLab, FeaturedLink, FeaturedLinkKind } from '../../../build/featured'
22

33
const KIND_HEADING: Record<FeaturedLinkKind, string> = {
44
demo: 'Demo',
55
repo: 'Repository',
66
'case-study': 'Case study',
77
}
88

9+
// Column order whenever these kinds are present.
10+
const KIND_ORDER: readonly FeaturedLinkKind[] = ['demo', 'repo', 'case-study']
11+
12+
type Column = { kind: FeaturedLinkKind; links: FeaturedLink[] }
13+
14+
function groupByKind(links: readonly FeaturedLink[]): Column[] {
15+
const byKind = new Map<FeaturedLinkKind, FeaturedLink[]>()
16+
for (const link of links) {
17+
const bucket = byKind.get(link.kind) ?? []
18+
bucket.push(link)
19+
byKind.set(link.kind, bucket)
20+
}
21+
return KIND_ORDER.filter((k) => byKind.has(k)).map((kind) => ({
22+
kind,
23+
links: byKind.get(kind)!,
24+
}))
25+
}
26+
927
export function LabCard({ lab }: { lab: FeaturedLab }) {
28+
const columns = groupByKind(lab.links)
1029
return (
1130
<article class="lab-card">
1231
<div class="lab-card__intro">
1332
<h3 class="lab-card__title">{lab.title}</h3>
1433
<p class="lab-card__tagline">{lab.tagline}</p>
1534
</div>
1635
<ul class="lab-card__columns">
17-
{lab.links.map((link) => (
36+
{columns.map((column) => (
1837
<li class="lab-card__column">
19-
<p class="lab-card__column-heading">{KIND_HEADING[link.kind]}</p>
20-
<a
21-
class="lab-card__column-link"
22-
href={link.url}
23-
rel="noopener external"
24-
>
25-
<LinkIcon kind={link.kind} />
26-
<span>{link.label}</span>
27-
</a>
38+
<p class="lab-card__column-heading">{KIND_HEADING[column.kind]}</p>
39+
<ul class="lab-card__column-links">
40+
{column.links.map((link) => (
41+
<li>
42+
<a
43+
class="lab-card__column-link"
44+
href={link.url}
45+
rel="noopener external"
46+
>
47+
<LinkIcon kind={column.kind} />
48+
<span>{link.label}</span>
49+
</a>
50+
</li>
51+
))}
52+
</ul>
2853
</li>
2954
))}
3055
</ul>

src/design/components/lab-card/styles.css

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
.lab-card__column {
4646
display: flex;
4747
flex-direction: column;
48-
gap: var(--space-2);
48+
gap: var(--space-3);
4949
min-inline-size: 0;
5050
}
5151

@@ -58,6 +58,15 @@
5858
margin: 0;
5959
}
6060

61+
.lab-card__column-links {
62+
list-style: none;
63+
padding: 0;
64+
margin: 0;
65+
display: flex;
66+
flex-direction: column;
67+
gap: var(--space-2);
68+
}
69+
6170
.lab-card__column-link {
6271
display: inline-flex;
6372
align-items: center;
@@ -86,17 +95,10 @@
8695
color: var(--color-link-hover);
8796
}
8897

89-
/* Flow to 2 columns once the card is wide enough to host them cleanly. */
98+
/* Wide enough for a multi-column layout — one column per link kind. */
9099
@container (min-width: 32rem) {
91100
.lab-card__columns {
92-
grid-template-columns: repeat(2, minmax(0, 1fr));
93-
}
94-
}
95-
96-
/* And to a row of 4 on wide cards, so Forms Lab's four links sit side-by-side. */
97-
@container (min-width: 56rem) {
98-
.lab-card__columns {
99-
grid-template-columns: repeat(4, minmax(0, 1fr));
101+
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
100102
}
101103
}
102104
}

tests/views/components.test.tsx

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -100,35 +100,54 @@ describe('LabCard', () => {
100100
expect(html).toContain('Digitize forms to create modern, accessible experiences')
101101
})
102102

103-
test('renders one column per link with kind heading, icon, and external anchor', async () => {
103+
test('groups links by kind — one column per distinct kind', async () => {
104104
const html = await renderToHtml(<LabCard lab={multiProject} />)
105-
expect(html.match(/class="lab-card__column"/g)?.length).toBe(4)
106-
expect(html.match(/class="lab-card__column-heading"/g)?.length).toBe(4)
105+
// Two distinct kinds (demo, repo) → two columns
106+
expect(html.match(/class="lab-card__column"/g)?.length).toBe(2)
107+
expect(html.match(/class="lab-card__column-heading"/g)?.length).toBe(2)
108+
109+
// Four links total, each with its own anchor + icon + external rel
107110
expect(html.match(/class="lab-card__column-link"/g)?.length).toBe(4)
108111
expect(html.match(/class="lab-card__icon"/g)?.length).toBe(4)
109112
expect(html.match(/rel="noopener external"/g)?.length).toBe(4)
113+
})
110114

111-
// Kind headings match each link's kind in order: demo, repo, demo, repo
112-
expect(html).toContain('Demo')
113-
expect(html).toContain('Repository')
115+
test('column order is Demo, Repository, Case study (only kinds present)', async () => {
116+
const html = await renderToHtml(<LabCard lab={multiProject} />)
117+
const demoIdx = html.indexOf('>Demo<')
118+
const repoIdx = html.indexOf('>Repository<')
119+
expect(demoIdx).toBeGreaterThan(-1)
120+
expect(repoIdx).toBeGreaterThan(-1)
121+
expect(demoIdx).toBeLessThan(repoIdx)
122+
// No case-study heading in this lab
123+
expect(html).not.toContain('>Case study<')
124+
})
114125

115-
// Labels come through as the visible link text
116-
expect(html).toContain('>Forms Platform<')
117-
expect(html).toContain('>flexion/forms<')
118-
expect(html).toContain('>Forms Lab (experiment)<')
119-
expect(html).toContain('>flexion/forms-lab<')
126+
test('same-kind links stack in document order inside their column', async () => {
127+
const html = await renderToHtml(<LabCard lab={multiProject} />)
128+
// In the Demo column: "Forms Platform" appears before "Forms Lab (experiment)"
129+
expect(html.indexOf('>Forms Platform<')).toBeLessThan(
130+
html.indexOf('>Forms Lab (experiment)<'),
131+
)
132+
// In the Repository column: "flexion/forms" appears before "flexion/forms-lab"
133+
expect(html.indexOf('>flexion/forms<')).toBeLessThan(
134+
html.indexOf('>flexion/forms-lab<'),
135+
)
120136
})
121137

122138
test('a single-link lab renders a single column', async () => {
123139
const html = await renderToHtml(<LabCard lab={singleLink} />)
124140
expect(html.match(/class="lab-card__column"/g)?.length).toBe(1)
125-
expect(html).toContain('Repository')
141+
expect(html).toContain('>Repository<')
126142
expect(html).toContain('href="https://github.com/flexion/flexion-notify"')
127143
})
128144

129-
test('case-study kind renders its own heading', async () => {
145+
test('case-study kind renders its own column and heading', async () => {
130146
const html = await renderToHtml(<LabCard lab={caseStudy} />)
131-
expect(html).toContain('Case study')
147+
// Repository + Case study → two columns in that order
148+
expect(html.match(/class="lab-card__column"/g)?.length).toBe(2)
149+
expect(html).toContain('>Case study<')
150+
expect(html.indexOf('>Repository<')).toBeLessThan(html.indexOf('>Case study<'))
132151
expect(html).toContain('href="https://flexion.us/case-study/"')
133152
})
134153
})

tests/views/home.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,24 @@ const featured: FeaturedLab[] = [
2121
tagline: 'Digitize forms to create modern, accessible experiences for public outreach.',
2222
order: 1,
2323
links: [
24-
{ label: 'Demo (Forms Platform)', url: 'https://pp4cc7kwbf.us-east-1.awsapprunner.com/' },
25-
{ label: 'GitHub repository — Forms Platform', url: 'https://github.com/flexion/forms' },
24+
{ label: 'Forms Platform', url: 'https://pp4cc7kwbf.us-east-1.awsapprunner.com/', kind: 'demo' },
25+
{ label: 'flexion/forms', url: 'https://github.com/flexion/forms', kind: 'repo' },
2626
],
2727
},
2828
{
2929
title: 'Messaging Lab',
3030
tagline: 'Text messaging services to deliver critical updates to the people you serve.',
3131
order: 2,
3232
links: [
33-
{ label: 'GitHub repository', url: 'https://github.com/flexion/flexion-notify' },
33+
{ label: 'flexion/flexion-notify', url: 'https://github.com/flexion/flexion-notify', kind: 'repo' },
3434
],
3535
},
3636
{
3737
title: 'Document Extractor Lab',
3838
tagline: 'Accurately extract data from PDFs and images for faster application processing.',
3939
order: 3,
4040
links: [
41-
{ label: 'GitHub repository', url: 'https://github.com/flexion/document-extractor' },
41+
{ label: 'flexion/document-extractor', url: 'https://github.com/flexion/document-extractor', kind: 'repo' },
4242
],
4343
},
4444
]

0 commit comments

Comments
 (0)