Skip to content

Commit 4fee125

Browse files
mpstatonclaude
andcommitted
feat(gallery, docs-portal, corpora-curator): every member can publish its own component library, aggregated without being owned
The federated design system has two layers. The federal one — the token vocabulary — got its surface when the swatch page shipped. The local layer never did: seventeen members own their own components, composition patterns and interaction idioms, and no page anywhere rendered a single one of them. The number that started this whole effort — 158 button rule-sets, 13 card recipes, 34 badge treatments, 6 spinners — had only ever been counted, never looked at. Each member now publishes a component library from its own bundle, exposed over Module Federation as `./gallery` alongside the `./mount` it already had. Two contracts: `./mount` is what the member does, `./gallery` is what it is made of. Same bundle and same stylesheet, so the specimens are the real components rather than a copy that drifted. The aggregation rule is the architectural point. `apps/docs-portal` becomes a federation HOST and indexes the libraries, but it imports no member's components — it loads a library the member built and shipped, from that member's own bundle. A central library that imported from members would have re-created exactly the single queue federation exists to avoid. The index is central; the libraries are not. `packages/gallery` is one runtime for all seventeen catalogs. It owns how a library is browsed, isolated, deep-linked and audited, and knows nothing about any member — prefix, root class, origin and fixtures all arrive as data. Class recipes are first-class alongside components, because a gallery that catalogued only `.svelte` files would have shown none of the 158 buttons: not one of them is a component. Three modes render side by side from the real cascade. `theme.css` scopes its mode blocks to `[data-mode='…']` rather than `:root[data-mode='…']`, so an ordinary `<div>` re-points the whole token vocabulary for its subtree — dark, light and vibrant at once, on a surface selector, because a component that vanishes on `--color-surface-raised` is the swatch page's P2/P3 failure one level down. Every specimen carries a contract audit measured at render time from the CSSOM rules that actually matched it: F1a (Tier-1 token read directly), F4 (bare z-index), F8 (colour literal), F2/F3 (unprefixed class), plus contrast off every painted text node against its COMPOSITED background — the house `color-mix(…, transparent)` idiom makes a naive background transparent and a naive checker useless. `pnpm design:drift` sweeps files and reports per member; this reports per component, in the mode you are looking at. It still reports the same 99 findings it did before this work: the gallery adds no drift of its own. The route that matters for review is a single specimen at its own bare URL — `http://<lan-ip>:3017/#/gallery/source-row/active?iso=1` — no chrome, openable on a phone by someone who has neither this repo nor the shell running. Standalone the member owns its hash so the gallery routes on it; federated the hash belongs to the shell and the gallery keeps its route in state instead. The catalog is imported dynamically so every fixture and pattern file stays out of the product bundle. `corpora-curator` is the pilot, and building its library surfaced five things no static sweep had — including that its components are not prop-driven (all four read the `curation` runes singleton, so every fixture stages it), that `SourceList` filtered-to-nothing renders identically to empty, and that three badge treatments coexist that look fine apart and obviously redundant in one frame. Files changed: - packages/gallery/ — the runtime: Gallery, SpecimenFrame, ControlsPanel, AuditPanel, router, audit, types, README with the adoption recipe - apps/corpora-curator/src/gallery/ — the pilot catalog, fixtures, patterns, federated mount; plus the `./gallery` expose and the standalone hash branch - apps/docs-portal/ — MemberLibraries, members.ts, the tokens/components view split, and the portal becoming a federation host - shell/src/App.svelte, shell/src/DevelopersMenu.svelte — the Component libraries entry, handed over in sessionStorage so the remote's mount(target) contract stays as narrow as it was - context-v/specs/Federated-Component-Libraries.md - changelog/2026-08-08_03_Every-Microfrontend-Can-Now-Publish-What-It-Is-Made-Of.md Also included: - apps/docs-portal/src/members.ts and the pilot catalog's comment still carried the pre-rename `sc` prefix. The members index doubles as the prefix table, so that one was rendering the wrong value; both now read `cc`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PZJvZWco3z7SR2dEhFqEjA
1 parent 01e0f3a commit 4fee125

30 files changed

Lines changed: 3854 additions & 13 deletions

apps/corpora-curator/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
},
1313
"dependencies": {
1414
"@augment-it/federation": "workspace:*",
15+
"@augment-it/gallery": "workspace:*",
1516
"@augment-it/theme": "workspace:*",
1617
"@augment-it/workspace": "workspace:*",
1718
"svelte": "^5.56.4"

apps/corpora-curator/rsbuild.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export default defineConfig({
2727
filename: 'remoteEntry.js',
2828
exposes: {
2929
'./mount': './src/mount.ts',
30+
// The member's component library. A second contract alongside the
31+
// product surface: `./mount` is what this member does, `./gallery` is
32+
// what it is made of. Same bundle and same stylesheet, so the specimens
33+
// are the real components rather than a copy that drifted.
34+
// See context-v/specs/Federated-Component-Libraries.md.
35+
'./gallery': './src/gallery/mount.ts',
3036
},
3137
dts: false,
3238
}),

apps/corpora-curator/src/gallery/catalog.ts

Lines changed: 454 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Fixture data for the corpora-curator gallery.
2+
//
3+
// WHY THIS FILE HAS A `seed()` AT ALL — and what it is telling you.
4+
//
5+
// Every component in this member reads the `curation` runes singleton rather
6+
// than taking props. That is the house convention (Per-App-Workspace-Conventions:
7+
// single source of truth, components read derived getters and call actions), and
8+
// it is a perfectly good convention for an app. It is a bad one for a component
9+
// LIBRARY: a component whose inputs are ambient cannot be rendered in a state
10+
// its author did not anticipate, which means it cannot be reviewed in one.
11+
//
12+
// Rather than refactor five components to make the gallery look tidy, the
13+
// gallery seeds the singleton and says so — every fixture that needs `seed()`
14+
// shows an "Ambient dependency" note on its Usage tab. That is the honest
15+
// reading: these components ARE isolatable, but only by staging the world
16+
// around them, and the cost of each new state is a hand-written world.
17+
//
18+
// The prop-driven entries in this catalog (the pattern recipes) need none of
19+
// this. That contrast is the argument for pushing presentational leaves —
20+
// rows, chips, fields — down to props, and it is visible here rather than
21+
// asserted in a document.
22+
23+
import { curation } from '../curation.svelte';
24+
import type { Source, Strategy } from '../types';
25+
26+
export const TAG_VOCAB = [
27+
'Workforce-Development',
28+
'Work-Based-Learning',
29+
'Employer-Partnerships',
30+
'Apprenticeship',
31+
'Credential-Attainment',
32+
'Rural-Access',
33+
'HNWI',
34+
];
35+
36+
export const STRATEGIES: Strategy[] = [
37+
{
38+
slug: 'turning-jobs-into-degrees',
39+
type: 'strategy',
40+
client_slugs: ['reach-edu'],
41+
title: 'Turning Jobs Into Degrees',
42+
tags: ['Work-Based-Learning', 'Credential-Attainment'],
43+
},
44+
{
45+
slug: 'rural-income-mobility',
46+
type: 'strategy',
47+
client_slugs: ['reach-edu'],
48+
title: 'Rural Income Mobility',
49+
tags: ['Rural-Access'],
50+
},
51+
{
52+
slug: 'employer-of-record-models',
53+
type: 'strategy',
54+
client_slugs: ['reach-edu'],
55+
title: 'Employer-of-Record Models',
56+
tags: [],
57+
},
58+
];
59+
60+
export const SOURCES: Source[] = [
61+
{
62+
source_uuid: 'src-0001',
63+
url: 'https://www.brookings.edu/articles/the-degree-is-not-the-job/',
64+
normalized_url: 'brookings.edu/articles/the-degree-is-not-the-job',
65+
title: 'The degree is not the job',
66+
authors: ['Anthony P. Carnevale', 'Nicole Smith'],
67+
publisher: 'Brookings',
68+
published_date: '2025-11-04',
69+
tags: ['Work-Based-Learning', 'Credential-Attainment'],
70+
status: 'fetched',
71+
content_pulled: true,
72+
source_slug: 'the-degree-is-not-the-job',
73+
},
74+
{
75+
source_uuid: 'src-0002',
76+
url: 'https://example.org/reports/apprenticeship-at-scale-2026.pdf',
77+
title: 'Apprenticeship at Scale 2026',
78+
publisher: 'National Skills Coalition',
79+
published_date: '2026-02-18',
80+
tags: ['Apprenticeship', 'Employer-Partnerships'],
81+
status: 'fetched',
82+
content_pulled: true,
83+
source_slug: 'apprenticeship-at-scale-2026',
84+
binary_filename: 'apprenticeship-at-scale-2026.pdf',
85+
binary_bytes: 4_182_301,
86+
},
87+
{
88+
// The row that exists to be the ugly one: no title, no publisher, a URL
89+
// long enough to test truncation, and a failed fetch. A gallery whose
90+
// fixtures are all well-formed is a gallery that has never seen the
91+
// product.
92+
source_uuid: 'src-0003',
93+
url: 'https://www.dol.gov/agencies/eta/apprenticeship/policy/registered-apprenticeship-national-guidelines-standards-of-apprenticeship-2026-revision',
94+
tags: [],
95+
status: 'metadata-only',
96+
verdict_error: true,
97+
},
98+
{
99+
source_uuid: 'src-0004',
100+
url: 'https://www.rand.org/pubs/research_reports/RRA2214-1.html',
101+
title: 'Rural Postsecondary Access and the Income Ladder',
102+
publisher: 'RAND',
103+
published_date: '2026-01-09',
104+
tags: ['Rural-Access'],
105+
status: 'metadata-only',
106+
},
107+
];
108+
109+
type CurationPatch = Partial<{
110+
connection: 'idle' | 'connecting' | 'open' | 'closed' | 'error' | 'auth_required';
111+
lastError: string | null;
112+
clientSlug: string | null;
113+
domainType: string;
114+
strategies: Strategy[];
115+
activeSlug: string | null;
116+
sources: Source[];
117+
focusIdx: number;
118+
listFilter: string;
119+
tagVocab: string[];
120+
saveStatus: string;
121+
}>;
122+
123+
/**
124+
* Stage the world, then apply the fixture's deltas.
125+
*
126+
* Always writes EVERY field, never just the ones a fixture cares about —
127+
* otherwise the previous fixture's state leaks into the next one and a
128+
* specimen quietly renders something nobody declared.
129+
*/
130+
export function seed(patch: CurationPatch = {}): void {
131+
const base: Required<CurationPatch> = {
132+
connection: 'open',
133+
lastError: null,
134+
clientSlug: 'reach-edu',
135+
domainType: 'strategy',
136+
strategies: STRATEGIES,
137+
activeSlug: 'turning-jobs-into-degrees',
138+
sources: SOURCES,
139+
focusIdx: 0,
140+
listFilter: '',
141+
tagVocab: TAG_VOCAB,
142+
saveStatus: '',
143+
};
144+
Object.assign(curation, base, patch);
145+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Federation-exposed gallery mount — the second thing this member exposes.
2+
//
3+
// A member now publishes two contracts: `./mount` is the product surface, and
4+
// `./gallery` is what it is made of. Same remote, same bundle, same stylesheet,
5+
// so the specimens are the real components and not a copy that drifted.
6+
//
7+
// IMPORT ORDER IS LOAD-BEARING and identical to ./src/mount.ts's: the gallery
8+
// package pulls in token-baseline.css (the @property floor that keeps a member
9+
// legible when it outruns the deployed shell — see packages/federation), so it
10+
// must evaluate BEFORE '../app.css', whose rules read those tokens. ES module
11+
// imports evaluate in declaration order. Swap these two lines and the specimens
12+
// render unstyled.
13+
14+
import { makeGalleryMount } from '@augment-it/gallery';
15+
import '../app.css';
16+
import catalog from './catalog';
17+
18+
export const mountStrategyCuratorGallery = makeGalleryMount(catalog);
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<script module lang="ts">
2+
// The un-componentised half of this member's library.
3+
//
4+
// corpora-curator has four .svelte files and roughly forty class recipes.
5+
// The recipes are where the design lives — `.cc-card`, `.cc-row`, the four
6+
// button variants, the chips — and they are exactly the things that drift,
7+
// because nothing stops a sixth button variant from being added to app.css.
8+
// (The federation-wide measurement that started all this counted 158 button
9+
// rule-sets and 34 badge treatments; none of them were components.)
10+
//
11+
// So they get catalogued as first-class entries, as markup rather than as
12+
// components. Each snippet takes the resolved props object, so the gallery's
13+
// controls drive them the same way they drive a real component.
14+
//
15+
// Exported from `<script module>`: legal because none of these reference
16+
// instance state — they read only their own parameter.
17+
export {
18+
buttons,
19+
card,
20+
fields,
21+
chips,
22+
tags,
23+
sourceRow,
24+
headerBar,
25+
banner,
26+
attachedFile,
27+
emptyState,
28+
};
29+
</script>
30+
31+
{#snippet buttons(p: Record<string, unknown>)}
32+
<div class="cc-actions">
33+
<button disabled={Boolean(p.disabled)}>{String(p.label ?? 'Fetch metadata')}</button>
34+
<button class="cc-primary" disabled={Boolean(p.disabled)}>+ Add</button>
35+
<button class="cc-link">‹ corpora</button>
36+
<button class="cc-danger" disabled={Boolean(p.disabled)}>Remove source</button>
37+
</div>
38+
{/snippet}
39+
40+
{#snippet card(p: Record<string, unknown>)}
41+
<section class="cc-card">
42+
<h3>{String(p.heading ?? 'Source 1 of 4')}</h3>
43+
<div class="cc-field">
44+
<span class="cc-label">Publisher</span>
45+
<span class="cc-value">{String(p.body ?? 'Brookings')}</span>
46+
</div>
47+
<div class="cc-field">
48+
<span class="cc-label">Published</span>
49+
<span class="cc-value cc-mono">2025-11-04</span>
50+
</div>
51+
</section>
52+
{/snippet}
53+
54+
{#snippet fields(p: Record<string, unknown>)}
55+
<div>
56+
<div class="cc-field">
57+
<span class="cc-label">Title <span class="cc-muted cc-mini">— editable</span></span>
58+
<!-- svelte-ignore a11y_autofocus -->
59+
<input class:cc-saved={Boolean(p.saved)} value={String(p.value ?? 'The degree is not the job')} />
60+
</div>
61+
<div class="cc-field">
62+
<span class="cc-label">Slug <span class="cc-muted cc-mini">— lowercase-kebab</span></span>
63+
<input class="cc-mono" value="the-degree-is-not-the-job" />
64+
</div>
65+
<div class="cc-field">
66+
<span class="cc-label">Extract</span>
67+
<textarea placeholder="paste a quote…"></textarea>
68+
</div>
69+
<div class="cc-field">
70+
<span class="cc-label">Kind</span>
71+
<select>
72+
<option>Quotes</option>
73+
<option>Stats</option>
74+
<option>References</option>
75+
<option>Mentions</option>
76+
</select>
77+
</div>
78+
</div>
79+
{/snippet}
80+
81+
{#snippet chips(p: Record<string, unknown>)}
82+
<div class="cc-actions">
83+
<span class="cc-pill">reach-edu</span>
84+
<span class="cc-pill">strategy</span>
85+
<span class="cc-pill">{String(p.count ?? 4)} sources</span>
86+
<span class="cc-status-chip">metadata-only</span>
87+
<span class="cc-status-chip">fetched</span>
88+
<span class="cc-conn status-open">open</span>
89+
<span class="cc-conn status-error">error</span>
90+
<span class="cc-conn status-closed">closed</span>
91+
<span class="cc-conn">connecting</span>
92+
</div>
93+
{/snippet}
94+
95+
{#snippet tags(p: Record<string, unknown>)}
96+
<div class="cc-field">
97+
<span class="cc-label">Tags <span class="cc-muted cc-mini">— Train-Case, workspace vocabulary</span></span>
98+
<div class="cc-tags">
99+
<span class="cc-tag">Work-Based-Learning<button class="cc-tag-x" aria-label="remove tag">×</button></span>
100+
<span class="cc-tag">Credential-Attainment<button class="cc-tag-x" aria-label="remove tag">×</button></span>
101+
<span class="cc-tag-mini">Rural-Access</span>
102+
</div>
103+
{#if p.suggesting}
104+
<div class="cc-tag-input">
105+
<input value="Emp" />
106+
<div class="cc-tag-suggest">
107+
<button>Employer-Partnerships</button>
108+
<button>Employment-Outcomes</button>
109+
</div>
110+
</div>
111+
{:else}
112+
<div class="cc-tag-input"><input placeholder="add a tag…" /></div>
113+
{/if}
114+
</div>
115+
{/snippet}
116+
117+
{#snippet sourceRow(p: Record<string, unknown>)}
118+
<div class="cc-list">
119+
<button class="cc-row" class:active={Boolean(p.active)}>
120+
<span class="cc-dot"></span>
121+
<span class="cc-row-body">
122+
<span class="cc-row-title">{String(p.title ?? 'The degree is not the job')}</span>
123+
<span class="cc-row-meta">
124+
<span>Brookings</span>
125+
<span class="cc-status-chip">fetched</span>
126+
<span class="cc-tag-mini">Work-Based-Learning</span>
127+
</span>
128+
</span>
129+
</button>
130+
<button class="cc-row">
131+
<span class="cc-dot err"></span>
132+
<span class="cc-row-body">
133+
<span class="cc-row-title"
134+
>https://www.dol.gov/agencies/eta/apprenticeship/policy/registered-apprenticeship-national-guidelines</span
135+
>
136+
<span class="cc-row-meta"><span class="cc-status-chip">metadata-only</span></span>
137+
</span>
138+
</button>
139+
</div>
140+
{/snippet}
141+
142+
{#snippet headerBar(p: Record<string, unknown>)}
143+
<header class="cc-header">
144+
<span class="cc-brand">Corpora Curator</span>
145+
<span class="cc-pill">reach-edu</span>
146+
<span class="cc-pill">strategy</span>
147+
<button class="cc-back">‹ All corpora</button>
148+
<span class="cc-strategy">{String(p.strategy ?? 'Turning Jobs Into Degrees')}</span>
149+
<span class="cc-pill">4 sources</span>
150+
<span class="cc-spacer"></span>
151+
<span class="cc-conn status-{String(p.status ?? 'open')}">{String(p.status ?? 'open')}</span>
152+
</header>
153+
{/snippet}
154+
155+
{#snippet banner(p: Record<string, unknown>)}
156+
<div class="cc-banner">
157+
{String(p.message ?? 'source.add failed — capability not registered on this workspace')}
158+
</div>
159+
{/snippet}
160+
161+
{#snippet attachedFile(p: Record<string, unknown>)}
162+
<div>
163+
<div class="cc-attached">
164+
<span class="cc-attached-dot">●</span>
165+
<span class="cc-attached-name">{String(p.filename ?? 'apprenticeship-at-scale-2026.pdf')}</span>
166+
<span class="cc-muted cc-mini">4.0 MB</span>
167+
</div>
168+
<a class="cc-urllink" href="https://example.org/reports/apprenticeship-at-scale-2026.pdf"
169+
>https://example.org/reports/apprenticeship-at-scale-2026.pdf</a
170+
>
171+
</div>
172+
{/snippet}
173+
174+
{#snippet emptyState(p: Record<string, unknown>)}
175+
<p class="cc-muted cc-pad cc-mini">{String(p.message ?? 'No sources yet. Paste a URL to add one.')}</p>
176+
{/snippet}

0 commit comments

Comments
 (0)