Skip to content

Commit 158e9b6

Browse files
committed
feat(lab-card): horizontal band layout with typed, grouped links
Rework the featured-labs cards on the home page to address two UX issues: **Link differentiation.** Every link now carries a `kind` of `demo`, `repo`, or `case-study`, rendered with a recognizable inline icon (globe, GitHub mark, document). Labels shorten to "Live demo", "Repository", "Case study" — the icons do the category work. **Sub-project grouping.** Links accept an optional `group` name. When set, same-group links render under a small uppercase heading. Forms Lab now makes its two distinct projects (production Platform vs. the experiment) visually explicit. **Horizontal band layout.** The card becomes a two-column grid above 40rem (container query) — intro on the left, grouped link panel on the right. On narrow viewports it stacks. The home page's featured list is also capped at 72rem so desktop bands don't stretch edge-to-edge. Schema: `links: [{label, url, kind, group?}]`. Updated the three content files, loader, loader tests, LabCard component + styles + examples, LabCard view tests, and docs/featured-content.md.
1 parent 2700faf commit 158e9b6

11 files changed

Lines changed: 336 additions & 47 deletions

File tree

content/featured/document-extractor-lab.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ title: Document Extractor Lab
33
tagline: Accurately extract data from PDFs and images for faster application processing.
44
order: 3
55
links:
6-
- label: GitHub repository
6+
- label: Repository
77
url: https://github.com/flexion/document-extractor
8+
kind: repo
89
- label: Case study
910
url: https://flexion.us/case-study/document-extraction-for-faster-processing/
11+
kind: case-study
1012
---

content/featured/forms-lab.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@ title: Forms Lab
33
tagline: Digitize forms to create modern, accessible experiences for public outreach.
44
order: 1
55
links:
6-
- label: Demo (Forms Platform)
6+
- label: Live demo
77
url: https://pp4cc7kwbf.us-east-1.awsapprunner.com/
8-
- label: GitHub repository — Forms Platform
8+
kind: demo
9+
group: Forms Platform
10+
- label: Repository
911
url: https://github.com/flexion/forms
10-
- label: Demo (Forms Lab experiment)
12+
kind: repo
13+
group: Forms Platform
14+
- label: Live demo
1115
url: https://ec2-34-197-222-16.compute-1.amazonaws.com/
12-
- label: GitHub repository — Forms Lab (experiment)
16+
kind: demo
17+
group: Forms Lab (experiment)
18+
- label: Repository
1319
url: https://github.com/flexion/forms-lab
20+
kind: repo
21+
group: Forms Lab (experiment)
1422
---

content/featured/messaging-lab.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ title: Messaging Lab
33
tagline: Text messaging services to deliver critical updates to the people you serve.
44
order: 2
55
links:
6-
- label: GitHub repository
6+
- label: Repository
77
url: https://github.com/flexion/flexion-notify
8+
kind: repo
89
---

docs/featured-content.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ title: Forms Lab
1212
tagline: Digitize forms to create modern, accessible experiences for public outreach.
1313
order: 1
1414
links:
15-
- label: Demo (Forms Platform)
15+
- label: Live demo
1616
url: https://pp4cc7kwbf.us-east-1.awsapprunner.com/
17-
- label: GitHub repository — Forms Platform
17+
kind: demo
18+
group: Forms Platform
19+
- label: Repository
1820
url: https://github.com/flexion/forms
21+
kind: repo
22+
group: Forms Platform
1923
---
2024
```
2125

@@ -24,15 +28,19 @@ links:
2428
- `title` — card heading (string, required)
2529
- `tagline` — one-sentence summary (string, required)
2630
- `order` — display order ascending (integer, required)
27-
- `links` — list of `{ label, url }` pairs rendered as external links (array, required)
31+
- `links` — list of link objects (array, required). Each link has:
32+
- `label` — visible link text (string, required)
33+
- `url` — destination URL (string, required)
34+
- `kind` — one of `demo`, `repo`, or `case-study` (required). Drives the icon shown before the label.
35+
- `group` — optional sub-project name (string). When multiple links share a `group`, they render together under a small heading; ungrouped links render without one. Use this for a lab that contains more than one distinct project (e.g., a production and an experiment variant).
2836

2937
## Loader
3038

3139
`src/build/featured.ts` exports `loadFeatured(rootDir)` which reads every `.md` file in `content/featured/`, parses front-matter, validates the schema, and returns labs sorted by `order`.
3240

3341
## Rendering
3442

35-
The home page renders one `<LabCard />` per lab. The card shows the title (not linked), the tagline, and a vertical list of external links. Cards stack on narrow viewports and flow into a grid on wider viewports via the existing `.home-featured__list` composition.
43+
The home page renders one `<LabCard />` per lab. Each card is a horizontal band — title and tagline on the left, grouped link list on the right — that collapses to a stacked layout on narrow viewports via a `@container (min-width: 40rem)` rule. Links are prefixed with an icon (globe for `demo`, GitHub mark for `repo`, document for `case-study`) and grouped under a small heading when a `group` is set. The home page's featured list is constrained to `72rem` to keep the bands at a readable width on wide displays.
3644

3745
## Adding a featured lab
3846

src/build/featured.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@ import { parse as parseYaml } from 'yaml'
22
import { readdir } from 'node:fs/promises'
33
import { join } from 'node:path'
44

5+
export type FeaturedLinkKind = 'demo' | 'repo' | 'case-study'
6+
57
export type FeaturedLink = {
68
label: string
79
url: string
10+
kind: FeaturedLinkKind
11+
group?: string
812
}
913

14+
const LINK_KINDS: ReadonlySet<FeaturedLinkKind> = new Set([
15+
'demo',
16+
'repo',
17+
'case-study',
18+
])
19+
1020
export type FeaturedLab = {
1121
title: string
1222
tagline: string
@@ -55,10 +65,21 @@ function parseLinks(file: string, value: unknown): FeaturedLink[] {
5565
throw new Error(`content/featured/${file}: links[${i}] must be an object`)
5666
}
5767
const o = item as Record<string, unknown>
58-
return {
68+
const kindRaw = requireString(file, `links[${i}].kind`, o.kind)
69+
if (!LINK_KINDS.has(kindRaw as FeaturedLinkKind)) {
70+
throw new Error(
71+
`content/featured/${file}: links[${i}].kind must be one of ${[...LINK_KINDS].join(', ')}`,
72+
)
73+
}
74+
const link: FeaturedLink = {
5975
label: requireString(file, `links[${i}].label`, o.label),
6076
url: requireString(file, `links[${i}].url`, o.url),
77+
kind: kindRaw as FeaturedLinkKind,
78+
}
79+
if (typeof o.group === 'string' && o.group.length > 0) {
80+
link.group = o.group
6181
}
82+
return link
6283
})
6384
}
6485

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,52 @@
11
import { LabCard } from './index'
2+
import type { FeaturedLab } from '../../../build/featured'
23

3-
const example = {
4+
const multiProject: FeaturedLab = {
45
title: 'Forms Lab',
56
tagline: 'Digitize forms to create modern, accessible experiences for public outreach.',
67
order: 1,
78
links: [
8-
{ label: 'Demo (Forms Platform)', url: 'https://example.com/demo' },
9-
{ label: 'GitHub repository — Forms Platform', url: 'https://github.com/flexion/forms' },
10-
{ label: 'GitHub repository — Forms Lab (experiment)', url: 'https://github.com/flexion/forms-lab' },
9+
{ label: 'Live demo', url: 'https://example.com/demo', kind: 'demo', group: 'Forms Platform' },
10+
{ label: 'Repository', url: 'https://github.com/flexion/forms', kind: 'repo', group: 'Forms Platform' },
11+
{ label: 'Live demo', url: 'https://example.com/lab', kind: 'demo', group: 'Forms Lab (experiment)' },
12+
{ label: 'Repository', url: 'https://github.com/flexion/forms-lab', kind: 'repo', group: 'Forms Lab (experiment)' },
13+
],
14+
}
15+
16+
const singleLink: FeaturedLab = {
17+
title: 'Messaging Lab',
18+
tagline: 'Text messaging services to deliver critical updates to the people you serve.',
19+
order: 2,
20+
links: [
21+
{ label: 'Repository', url: 'https://github.com/flexion/flexion-notify', kind: 'repo' },
22+
],
23+
}
24+
25+
const withCaseStudy: FeaturedLab = {
26+
title: 'Document Extractor Lab',
27+
tagline: 'Accurately extract data from PDFs and images for faster application processing.',
28+
order: 3,
29+
links: [
30+
{ label: 'Repository', url: 'https://github.com/flexion/document-extractor', kind: 'repo' },
31+
{ label: 'Case study', url: 'https://flexion.us/case-study/document-extraction-for-faster-processing/', kind: 'case-study' },
1132
],
1233
}
1334

1435
export function LabCardExamples() {
1536
return (
1637
<section id="lab-card">
1738
<h2>Lab card</h2>
18-
<p>Featured-lab card on the home page. Title is not a link; each link inside the card is an external link.</p>
19-
<LabCard lab={example} />
39+
<p>
40+
Featured-lab card on the home page. Cards read as a horizontal band on wide containers
41+
and stack vertically on narrow ones. Links are grouped by sub-project when a card has
42+
more than one, and each link is prefixed with an icon that signals its type (demo,
43+
repository, or case study).
44+
</p>
45+
<div class="l-stack" data-space="md">
46+
<LabCard lab={multiProject} />
47+
<LabCard lab={singleLink} />
48+
<LabCard lab={withCaseStudy} />
49+
</div>
2050
</section>
2151
)
2252
}
Lines changed: 119 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,127 @@
1-
import { Link } from '../link'
2-
import type { FeaturedLab } from '../../../build/featured'
1+
import type { FeaturedLab, FeaturedLink, FeaturedLinkKind } from '../../../build/featured'
32

43
export function LabCard({ lab }: { lab: FeaturedLab }) {
4+
const groups = groupLinks(lab.links)
55
return (
66
<article class="lab-card">
7-
<h3 class="lab-card__title">{lab.title}</h3>
8-
<p class="lab-card__tagline">{lab.tagline}</p>
9-
<ul class="lab-card__links">
10-
{lab.links.map((link) => (
11-
<li class="lab-card__link">
12-
<Link href={link.url} external>{link.label}</Link>
13-
</li>
7+
<div class="lab-card__intro">
8+
<h3 class="lab-card__title">{lab.title}</h3>
9+
<p class="lab-card__tagline">{lab.tagline}</p>
10+
</div>
11+
<div class="lab-card__links">
12+
{groups.map((group) => (
13+
<div class="lab-card__group">
14+
{group.name ? (
15+
<p class="lab-card__group-name">{group.name}</p>
16+
) : null}
17+
<ul class="lab-card__link-list">
18+
{group.links.map((link) => (
19+
<li class="lab-card__link">
20+
<a
21+
class="lab-card__link-anchor"
22+
href={link.url}
23+
rel="noopener external"
24+
>
25+
<LinkIcon kind={link.kind} />
26+
<span>{link.label}</span>
27+
</a>
28+
</li>
29+
))}
30+
</ul>
31+
</div>
1432
))}
15-
</ul>
33+
</div>
1634
</article>
1735
)
1836
}
37+
38+
type LinkGroup = { name: string | null; links: FeaturedLink[] }
39+
40+
function groupLinks(links: FeaturedLink[]): LinkGroup[] {
41+
const groups: LinkGroup[] = []
42+
const byName = new Map<string | null, LinkGroup>()
43+
for (const link of links) {
44+
const name = link.group ?? null
45+
let group = byName.get(name)
46+
if (!group) {
47+
group = { name, links: [] }
48+
byName.set(name, group)
49+
groups.push(group)
50+
}
51+
group.links.push(link)
52+
}
53+
return groups
54+
}
55+
56+
function LinkIcon({ kind }: { kind: FeaturedLinkKind }) {
57+
switch (kind) {
58+
case 'demo':
59+
return <DemoIcon />
60+
case 'repo':
61+
return <RepoIcon />
62+
case 'case-study':
63+
return <CaseStudyIcon />
64+
}
65+
}
66+
67+
function DemoIcon() {
68+
// Globe / launch — signals "visit a running thing"
69+
return (
70+
<svg
71+
class="lab-card__icon"
72+
width="18"
73+
height="18"
74+
viewBox="0 0 24 24"
75+
fill="none"
76+
stroke="currentColor"
77+
stroke-width="1.75"
78+
stroke-linecap="round"
79+
stroke-linejoin="round"
80+
aria-hidden="true"
81+
>
82+
<circle cx="12" cy="12" r="9" />
83+
<path d="M3 12h18" />
84+
<path d="M12 3a14 14 0 0 1 0 18" />
85+
<path d="M12 3a14 14 0 0 0 0 18" />
86+
</svg>
87+
)
88+
}
89+
90+
function RepoIcon() {
91+
// GitHub mark
92+
return (
93+
<svg
94+
class="lab-card__icon"
95+
width="18"
96+
height="18"
97+
viewBox="0 0 24 24"
98+
fill="currentColor"
99+
aria-hidden="true"
100+
>
101+
<path d="M12 1.5a10.5 10.5 0 0 0-3.32 20.47c.53.1.72-.23.72-.51v-1.8c-2.93.64-3.55-1.41-3.55-1.41-.48-1.22-1.17-1.55-1.17-1.55-.96-.65.07-.64.07-.64 1.06.07 1.62 1.09 1.62 1.09.94 1.61 2.47 1.14 3.07.87.1-.68.37-1.14.67-1.4-2.34-.27-4.8-1.17-4.8-5.2 0-1.15.41-2.09 1.08-2.82-.11-.27-.47-1.35.1-2.81 0 0 .88-.28 2.89 1.07a10 10 0 0 1 5.26 0c2-1.35 2.88-1.07 2.88-1.07.58 1.46.22 2.54.11 2.81.67.73 1.08 1.67 1.08 2.82 0 4.04-2.47 4.93-4.82 5.19.38.33.72.97.72 1.96v2.91c0 .28.19.62.73.51A10.5 10.5 0 0 0 12 1.5z" />
102+
</svg>
103+
)
104+
}
105+
106+
function CaseStudyIcon() {
107+
// Document with horizontal lines
108+
return (
109+
<svg
110+
class="lab-card__icon"
111+
width="18"
112+
height="18"
113+
viewBox="0 0 24 24"
114+
fill="none"
115+
stroke="currentColor"
116+
stroke-width="1.75"
117+
stroke-linecap="round"
118+
stroke-linejoin="round"
119+
aria-hidden="true"
120+
>
121+
<path d="M7 3h7l5 5v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z" />
122+
<path d="M14 3v5h5" />
123+
<path d="M9 13h6" />
124+
<path d="M9 17h6" />
125+
</svg>
126+
)
127+
}

0 commit comments

Comments
 (0)