Skip to content

Commit 70c1945

Browse files
committed
Add shared and TDA component gallery
1 parent a9f136b commit 70c1945

10 files changed

Lines changed: 940 additions & 9 deletions

File tree

site/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,17 @@ Astro collection exports and schema assembly remain explicit here on purpose. Ke
5050
collection entries spelled out preserves Astro's discriminated content types as more TDA kinds are
5151
added. Kind fields, registries, metadata furniture, route policy, identity, theme, and prose styling
5252
remain local because they express this foundry's domain model and policy.
53+
54+
## Shared specimens and the local gallery
55+
56+
`src/pages/gallery/` renders every case exported by `@galaxy-foundry/site-kit/specimens` through
57+
this site's own theme. Groups marked `isolated` or `document` receive generated standalone routes
58+
and are framed by the index; the package's `surface` declaration decides which cases can safely
59+
share a page.
60+
61+
The same gallery adds instance-owned specimen groups for the filtration hero, persistence divider,
62+
point-cloud fingerprint, topology breadcrumb, and every reference kind declared by this Foundry.
63+
Those groups use the shared `SpecimenGroup` shape but remain local because their components,
64+
vocabulary, and visual meaning are TDA-specific. Built-site tests assert that every shared and local
65+
case is rendered, every required standalone route exists, and the design-record index links the
66+
gallery.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
---
2+
import { specimenPath, type Specimen, type SpecimenGroup } from '@galaxy-foundry/site-kit/specimens';
3+
4+
interface Props {
5+
group: SpecimenGroup;
6+
specimen: Specimen;
7+
origin: 'shared' | 'tda';
8+
}
9+
10+
const { group, specimen, origin } = Astro.props;
11+
const anchor = specimenPath(group, specimen).replace('/', '--');
12+
---
13+
14+
<article
15+
id={anchor}
16+
class="gallery-specimen"
17+
data-gallery-component={group.component}
18+
data-gallery-origin={origin}
19+
>
20+
<header class="gallery-specimen-copy">
21+
<span>{origin === 'shared' ? 'Shared case' : 'TDA case'}</span>
22+
<h3><a href={`#${anchor}`}>{specimen.name}</a></h3>
23+
<p>{specimen.why}</p>
24+
<code>{specimenPath(group, specimen)}</code>
25+
</header>
26+
<div class="gallery-specimen-stage bg-grid">
27+
<slot />
28+
</div>
29+
</article>
30+
31+
<style>
32+
.gallery-specimen {
33+
display: grid;
34+
grid-template-columns: minmax(12rem, 0.34fr) minmax(0, 1fr);
35+
gap: clamp(1.25rem, 4vw, 3.5rem);
36+
padding: clamp(1.5rem, 4vw, 3rem) 0;
37+
border-top: 1px solid var(--color-border-subtle);
38+
scroll-margin-top: 6rem;
39+
}
40+
41+
.gallery-specimen-copy {
42+
align-self: start;
43+
position: sticky;
44+
top: 5.5rem;
45+
}
46+
47+
.gallery-specimen-copy > span {
48+
color: var(--color-brand);
49+
font-family: var(--font-mono);
50+
font-size: 0.66rem;
51+
font-weight: 600;
52+
letter-spacing: 0.12em;
53+
text-transform: uppercase;
54+
}
55+
56+
.gallery-specimen-copy h3 {
57+
margin: 0.45rem 0 0.7rem;
58+
font-family: var(--font-serif);
59+
font-size: clamp(1.35rem, 2.5vw, 1.9rem);
60+
font-variation-settings: 'opsz' 28;
61+
font-weight: 580;
62+
letter-spacing: -0.035em;
63+
line-height: 1.08;
64+
}
65+
66+
.gallery-specimen-copy h3 a {
67+
color: var(--color-text-primary);
68+
text-decoration: none;
69+
}
70+
71+
.gallery-specimen-copy h3 a:hover {
72+
color: var(--color-link);
73+
}
74+
75+
.gallery-specimen-copy p {
76+
margin: 0;
77+
color: var(--color-text-secondary);
78+
font-size: 0.9rem;
79+
line-height: 1.6;
80+
text-wrap: pretty;
81+
}
82+
83+
.gallery-specimen-copy code {
84+
display: inline-block;
85+
margin-top: 1rem;
86+
color: var(--color-text-muted);
87+
font-family: var(--font-mono);
88+
font-size: 0.67rem;
89+
}
90+
91+
.gallery-specimen-stage {
92+
min-width: 0;
93+
overflow: hidden;
94+
border: 1px solid var(--color-border-subtle);
95+
border-radius: 0.8rem 0.8rem 2rem 0.8rem;
96+
background-color: var(--color-surface-raised);
97+
box-shadow: 0 1.2rem 3.5rem color-mix(in srgb, var(--color-chrome) 8%, transparent);
98+
padding: clamp(1rem, 2.5vw, 1.75rem);
99+
}
100+
101+
[data-gallery-component='SiteFooter'] .gallery-specimen-stage,
102+
[data-gallery-component='SiteHeader'] .gallery-specimen-stage,
103+
[data-gallery-component='SiteShell'] .gallery-specimen-stage,
104+
[data-gallery-component='FiltrationHero'] .gallery-specimen-stage {
105+
padding: 0;
106+
}
107+
108+
[data-gallery-component='PersistenceDivider'] .gallery-specimen-stage,
109+
[data-gallery-component='PointCloudFingerprint'] .gallery-specimen-stage,
110+
[data-gallery-component='TopologyBreadcrumb'] .gallery-specimen-stage {
111+
display: grid;
112+
min-height: 9rem;
113+
align-items: center;
114+
}
115+
116+
@media (max-width: 760px) {
117+
.gallery-specimen {
118+
grid-template-columns: 1fr;
119+
gap: 1rem;
120+
}
121+
122+
.gallery-specimen-copy {
123+
position: static;
124+
}
125+
}
126+
</style>

site/src/layouts/Base.astro

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
---
2-
import '@fontsource-variable/ibm-plex-sans/wght.css';
3-
import '@fontsource-variable/ibm-plex-sans/wght-italic.css';
4-
import '@fontsource-variable/source-serif-4/opsz.css';
5-
import '@fontsource-variable/source-serif-4/opsz-italic.css';
6-
import '@fontsource/ibm-plex-mono/latin-400.css';
7-
import '@fontsource/ibm-plex-mono/latin-500.css';
8-
import '@fontsource/ibm-plex-mono/latin-600.css';
9-
import '@fontsource/ibm-plex-mono/latin-700.css';
10-
import '../styles/global.css';
2+
import '../styles/site-theme';
113
import SiteShell from '@galaxy-foundry/site-kit/SiteShell.astro';
124
135
import { base } from '../lib/site-base';

site/src/lib/gallery-renderers.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import ContentNote from '@galaxy-foundry/site-kit/ContentNote.astro';
2+
import LicenseBadge from '@galaxy-foundry/site-kit/LicenseBadge.astro';
3+
import LicenseFileBody from '@galaxy-foundry/site-kit/LicenseFileBody.astro';
4+
import ReferenceContract from '@galaxy-foundry/site-kit/ReferenceContract.astro';
5+
import SiteFooter from '@galaxy-foundry/site-kit/SiteFooter.astro';
6+
import SiteHeader from '@galaxy-foundry/site-kit/SiteHeader.astro';
7+
import SiteShell from '@galaxy-foundry/site-kit/SiteShell.astro';
8+
import TagChips from '@galaxy-foundry/site-kit/TagChips.astro';
9+
import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
10+
11+
import FiltrationHero from '../components/FiltrationHero.astro';
12+
import PersistenceDivider from '../components/PersistenceDivider.astro';
13+
import PointCloudFingerprint from '../components/PointCloudFingerprint.astro';
14+
import TopologyBreadcrumb from '../components/TopologyBreadcrumb.astro';
15+
import { ALL_SPECIMENS } from './gallery';
16+
17+
export const GALLERY_RENDERERS: Record<string, AstroComponentFactory> = {
18+
ContentNote,
19+
LicenseBadge,
20+
LicenseFileBody,
21+
ReferenceContract,
22+
SiteFooter,
23+
SiteHeader,
24+
SiteShell,
25+
TagChips,
26+
FiltrationHero,
27+
PersistenceDivider,
28+
PointCloudFingerprint,
29+
TopologyBreadcrumb,
30+
};
31+
32+
const unrenderable = ALL_SPECIMENS.filter((group) => !GALLERY_RENDERERS[group.component]);
33+
if (unrenderable.length > 0) {
34+
throw new Error(
35+
`gallery has no renderer for: ${unrenderable.map((group) => group.component).join(', ')}`,
36+
);
37+
}

site/src/lib/gallery.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { contractKeys } from '@galaxy-foundry/reference-contract';
2+
import type { ReferenceContractProps } from '@galaxy-foundry/site-kit';
3+
import {
4+
SPECIMENS,
5+
type Specimen,
6+
type SpecimenGroup,
7+
} from '@galaxy-foundry/site-kit/specimens';
8+
9+
import { contentReader } from './content-reader';
10+
import { referenceContract as loadReferenceContract } from './reference-contract';
11+
import { base } from './site-base';
12+
13+
const referenceContract = loadReferenceContract();
14+
15+
const kindSpecimen = (kind: string): Specimen<ReferenceContractProps> => {
16+
const term = referenceContract.kinds[kind];
17+
return {
18+
id: kind,
19+
name: term?.label ?? kind,
20+
why:
21+
term?.description ??
22+
`The ${kind} reference kind under this Foundry's palette and fallback accent.`,
23+
props: {
24+
contract: referenceContract,
25+
references: [
26+
{
27+
kind,
28+
ref: `[[gallery-${kind}]]`,
29+
used_at: 'runtime',
30+
load: 'upfront',
31+
mode: 'verbatim',
32+
evidence: 'corpus-observed',
33+
},
34+
],
35+
},
36+
};
37+
};
38+
39+
export const TDA_KIND_SPECIMENS: SpecimenGroup<ReferenceContractProps> = {
40+
id: 'tda-reference-kinds',
41+
component: 'ReferenceContract',
42+
importPath: '@galaxy-foundry/site-kit/ReferenceContract.astro',
43+
summary:
44+
'Every reference kind this Foundry declares, derived from its contract so a new kind cannot arrive unseen.',
45+
surface: 'inline',
46+
specimens: contractKeys(referenceContract, 'kinds').map(kindSpecimen),
47+
};
48+
49+
export const FILTRATION_HERO_SPECIMENS: SpecimenGroup<{ noteCount: number }> = {
50+
id: 'tda-filtration-hero',
51+
component: 'FiltrationHero',
52+
importPath: '../components/FiltrationHero.astro',
53+
summary:
54+
'The interactive frontispiece: a point cloud becomes a complex and leaves one persistent loop.',
55+
surface: 'isolated',
56+
specimens: [
57+
{
58+
id: 'stable-loop',
59+
name: 'Structure across scale',
60+
why:
61+
'The range control makes filtration tangible while the copy connects multiscale structure to the Foundry workflow.',
62+
props: { noteCount: contentReader.noteTargets().length },
63+
},
64+
],
65+
};
66+
67+
export const PERSISTENCE_DIVIDER_SPECIMENS: SpecimenGroup<{ seed: string }> = {
68+
id: 'tda-persistence-divider',
69+
component: 'PersistenceDivider',
70+
importPath: '../components/PersistenceDivider.astro',
71+
summary:
72+
'A deterministic barcode used as punctuation between conceptual sections.',
73+
surface: 'inline',
74+
specimens: [
75+
{
76+
id: 'section-punctuation',
77+
name: 'Persistent section break',
78+
why:
79+
'One interval receives the persistent accent, turning a divider into domain-specific hierarchy without adding prose.',
80+
props: { seed: 'gallery:persistence-divider' },
81+
},
82+
],
83+
};
84+
85+
export const POINT_CLOUD_SPECIMENS: SpecimenGroup<{ seed: string }> = {
86+
id: 'tda-point-cloud-fingerprint',
87+
component: 'PointCloudFingerprint',
88+
importPath: '../components/PointCloudFingerprint.astro',
89+
summary:
90+
'Seeded geometric marginalia that gives each surface a stable visual fingerprint.',
91+
surface: 'inline',
92+
specimens: [
93+
{
94+
id: 'stable-fingerprint',
95+
name: 'Seeded point cloud',
96+
why:
97+
'The same semantic seed always yields the same points, neighborhood edges, and highlighted loop.',
98+
props: { seed: 'gallery:point-cloud-fingerprint' },
99+
},
100+
],
101+
};
102+
103+
export const TOPOLOGY_BREADCRUMB_SPECIMENS: SpecimenGroup<{
104+
href: string;
105+
label: string;
106+
current: string;
107+
seed: string;
108+
}> = {
109+
id: 'tda-topology-breadcrumb',
110+
component: 'TopologyBreadcrumb',
111+
importPath: '../components/TopologyBreadcrumb.astro',
112+
summary:
113+
'Back navigation drawn as connected nodes, paired with a deterministic point-cloud signature.',
114+
surface: 'inline',
115+
specimens: [
116+
{
117+
id: 'method-detail',
118+
name: 'Connected reading path',
119+
why:
120+
'The ordinary collection-to-note relationship is expressed as a tiny graph without weakening breadcrumb semantics.',
121+
props: {
122+
href: `${base}/methods/`,
123+
label: 'Methods',
124+
current: 'Persistent homology',
125+
seed: 'gallery:topology-breadcrumb',
126+
},
127+
},
128+
],
129+
};
130+
131+
export const TDA_SPECIMENS: readonly SpecimenGroup[] = [
132+
TDA_KIND_SPECIMENS,
133+
FILTRATION_HERO_SPECIMENS,
134+
PERSISTENCE_DIVIDER_SPECIMENS,
135+
POINT_CLOUD_SPECIMENS,
136+
TOPOLOGY_BREADCRUMB_SPECIMENS,
137+
];
138+
139+
export const ALL_SPECIMENS: readonly SpecimenGroup[] = [...SPECIMENS, ...TDA_SPECIMENS];
140+
141+
const groupIds = ALL_SPECIMENS.map((group) => group.id);
142+
if (new Set(groupIds).size !== groupIds.length) {
143+
throw new Error('gallery specimen group ids must be unique across shared and TDA groups');
144+
}
145+
146+
export const specimenOrigin = (group: SpecimenGroup): 'shared' | 'tda' =>
147+
TDA_SPECIMENS.some((candidate) => candidate.id === group.id) ? 'tda' : 'shared';

0 commit comments

Comments
 (0)