@bitbaum/sitekit 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mao Nakamoto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @bitbaum/sitekit
2
+
3
+ A website as data.
4
+
5
+ A site is pages; a page is sections; sections come from a **closed** union of
6
+ shapes validated by Zod at runtime. One set of React renderers draws every
7
+ site. A provenance sidecar records where every generated field came from — so
8
+ "we fabricated nothing" is an assertion, not a promise.
9
+
10
+ Extracted from [substrata](https://github.com/bitbaum/substrata)'s hosted-site
11
+ model (2026-08). Decided in orangecat's ADR-0003 (the Site Factory).
12
+
13
+ ## Why a closed union
14
+
15
+ A generator emitting **data against this schema** is a task with a
16
+ machine-checkable result; a generator emitting components is not. Per-site
17
+ quality becomes a property of the renderers — fixing the `cards` renderer
18
+ improves every site ever generated. And the schema is where review happens:
19
+ `validateSite()` returns `path: message` errors a model can act on directly.
20
+
21
+ The current kinds: `hero` (with optional CTAs), `prose`, `stats`, `meter`,
22
+ `cards`, `definitions`, `feature` (a figure beside a story), `contact`
23
+ (locations, address, hours — the fields provenance exists to protect), `faq`,
24
+ `index`, `table`. Most kinds take an optional `anchor`, and the schema rejects
25
+ any index entry or hero action that points at a fragment nothing carries. Extending the union is a deliberate act in THIS repo — a new
26
+ kind lands with its schema, its renderer, and its tests in one commit, and
27
+ every consumer gets it on the next version bump.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pnpm add @bitbaum/sitekit
33
+ ```
34
+
35
+ ESM-only. `react >= 18` is a peer dependency of `@bitbaum/sitekit/react`; the root
36
+ export is framework-free (a Node generator or CI check can use it without React).
37
+
38
+ ## Use
39
+
40
+ ```ts
41
+ import { validateSite, siteNavItems, sitePageAt, pageRendersOwnHeader } from '@bitbaum/sitekit';
42
+
43
+ const result = validateSite(generatorOutput);
44
+ if (!result.success) throw new Error(result.errors.join('\n'));
45
+ const site = result.data; // fully typed SiteSpec from here on
46
+ ```
47
+
48
+ ```tsx
49
+ import Link from 'next/link';
50
+ import { SiteMasthead, SiteFooter, SiteSections } from '@bitbaum/sitekit/react';
51
+
52
+ <SiteMasthead chrome={site.chrome} navItems={siteNavItems(site.pages)} currentPath={path} Link={Link} />
53
+ <SiteSections sections={page.sections} />
54
+ <SiteFooter chrome={site.chrome} />
55
+ ```
56
+
57
+ `Link` is the one framework seam: it defaults to a plain `<a>`, which is
58
+ correct everywhere; a Next.js app passes `next/link` to get client-side
59
+ navigation back.
60
+
61
+ ## Provenance — the check behind "fabricate no facts"
62
+
63
+ A closed schema invites completion: a model handed a `stats` section wants
64
+ numbers in it. So absence is first-class and typed. The generator emits a
65
+ sidecar keyed by JSON path:
66
+
67
+ ```ts
68
+ import { assertDeliverable, notFoundPaths } from '@bitbaum/sitekit';
69
+
70
+ const provenance = {
71
+ 'chrome.name': { kind: 'scraped', url: 'https://cafe-beispiel.ch/' },
72
+ 'pages.0.sections.1.paragraphs.0':{ kind: 'operator' }, // a human typed it
73
+ 'pages.0.sections.4.cards.0.meta':{ kind: 'inferred' }, // a model invented it
74
+ 'pages.0.sections.7.rows.0.1': { kind: 'not-found' }, // we looked; it isn't public
75
+ };
76
+
77
+ assertDeliverable(provenance); // throws: names the inferred path
78
+ notFoundPaths(provenance); // the honest gaps — in a pitch, these ARE the pitch
79
+ ```
80
+
81
+ `not-found` is the difference between *unknown* and *empty*: it records that
82
+ the looking happened, which is what makes "here is what we could not find out
83
+ about you" an honest sentence instead of an excuse.
84
+
85
+ ## The styling contract
86
+
87
+ The renderers emit **semantic utility classes and nothing else** — no colours,
88
+ faces, or radii ship in this package. Each consumer defines the tokens, which
89
+ is how fifty sites share one renderer without looking like one company.
90
+ (Uniform everywhere except aesthetics. Do NOT point consumers at
91
+ `@fleet/design-tokens` values — those make things look like the fleet;
92
+ inherit the *shape*, choose your own values.)
93
+
94
+ Classes the renderers use, to be defined by the consumer's Tailwind theme:
95
+
96
+ | Group | Classes |
97
+ |---|---|
98
+ | Text | `text-fg-primary` `text-fg-secondary` `text-fg-tertiary` `text-fg-muted` |
99
+ | Surfaces | `bg-surface-page` `bg-surface-raised` |
100
+ | Borders | `border-subtle` `border-strong` `divide-subtle` |
101
+ | Accent & status | `bg-accent` `decoration-accent` `bg-status-positive` `bg-status-warning` `bg-status-negative` |
102
+ | Type | `font-heading` `font-mono` `tracking-caps` `tracking-display` |
103
+ | Layout | `max-w-shell` `max-w-prose` `scrollbar-hide` |
104
+
105
+ With Tailwind 4, define them as `@theme` variables (`--color-fg-primary`,
106
+ `--font-heading`, …) and add the package to content scanning:
107
+
108
+ ```css
109
+ @import 'tailwindcss';
110
+ @source '../node_modules/@bitbaum/sitekit/dist';
111
+ ```
112
+
113
+ Substrata's `app/globals.css` is the reference implementation of the contract.
114
+
115
+ ## Not in this package
116
+
117
+ - **No design tokens.** See above — that is the point.
118
+ - **No scraper.** `siteFromUrl()` (extraction from a live site, with
119
+ provenance) is the planned next module; the schema is its target.
120
+ - **No HTTP, no framework.** Same rule as every package in this fleet.
121
+
122
+ ## Verify
123
+
124
+ ```bash
125
+ pnpm run verify # lint + typecheck + build + test — same command CI runs
126
+ ```
127
+
128
+ Tests import the package by **name** (`@bitbaum/sitekit`, `@bitbaum/sitekit/react`), so a broken
129
+ `exports` map fails in CI, not at the first consumer.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @bitbaum/sitekit — a website as data.
3
+ *
4
+ * This root export is framework-free: schemas, inferred types, validation and
5
+ * provenance. The React renderers live behind `@bitbaum/sitekit/react` so a generator
6
+ * or a CI check can depend on the model without pulling in React.
7
+ */
8
+ export { siteSpecSchema, sitePageSchema, siteChromeSchema, siteSectionSchema, siteNavItemSchema, heroSectionSchema, proseSectionSchema, statsSectionSchema, meterSectionSchema, cardsSectionSchema, definitionsSectionSchema, featureSectionSchema, contactSectionSchema, faqSectionSchema, indexSectionSchema, tableSectionSchema, siteActionSchema, siteImageSchema, siteLocationSchema, siteFaqItemSchema, siteStatSchema, siteCardSchema, siteDefinitionSchema, siteIndexEntrySchema, type SiteSpec, type SitePage, type SiteChrome, type SiteSection, type SiteSectionKind, type SiteNavItem, type SiteStat, type SiteCard, type SiteDefinition, type SiteIndexEntry, type SiteAction, type SiteImage, type SiteLocation, type SiteFaqItem, } from './schema.js';
9
+ export { provenanceSchema, provenanceEntrySchema, type Provenance, type ProvenanceEntry, inferredPaths, notFoundPaths, assertDeliverable, } from './provenance.js';
10
+ export { validateSite, siteNavItems, pageRendersOwnHeader, sitePageAt, href } from './model.js';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @bitbaum/sitekit — a website as data.
3
+ *
4
+ * This root export is framework-free: schemas, inferred types, validation and
5
+ * provenance. The React renderers live behind `@bitbaum/sitekit/react` so a generator
6
+ * or a CI check can depend on the model without pulling in React.
7
+ */
8
+ export {
9
+ // schemas (Zod is the SSOT; every type below is inferred from one of these)
10
+ siteSpecSchema, sitePageSchema, siteChromeSchema, siteSectionSchema, siteNavItemSchema, heroSectionSchema, proseSectionSchema, statsSectionSchema, meterSectionSchema, cardsSectionSchema, definitionsSectionSchema, featureSectionSchema, contactSectionSchema, faqSectionSchema, indexSectionSchema, tableSectionSchema, siteActionSchema, siteImageSchema, siteLocationSchema, siteFaqItemSchema, siteStatSchema, siteCardSchema, siteDefinitionSchema, siteIndexEntrySchema, } from './schema.js';
11
+ export { provenanceSchema, provenanceEntrySchema, inferredPaths, notFoundPaths, assertDeliverable, } from './provenance.js';
12
+ export { validateSite, siteNavItems, pageRendersOwnHeader, sitePageAt, href } from './model.js';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Pure helpers over a validated site. No React, no framework — a Node script
3
+ * generating sites can use everything in this file.
4
+ */
5
+ import { type SiteNavItem, type SitePage, type SiteSpec } from './schema.js';
6
+ /**
7
+ * Validate anything claiming to be a site — typically a generator's raw
8
+ * output. Structured result in the house shape; `errors` are `path: message`
9
+ * lines a model (or a person) can act on directly.
10
+ */
11
+ export declare function validateSite(input: unknown): {
12
+ success: true;
13
+ data: SiteSpec;
14
+ } | {
15
+ success: false;
16
+ errors: string[];
17
+ };
18
+ /** Nav entries in page order. Pages without a `navLabel` are omitted. */
19
+ export declare function siteNavItems(pages: SitePage[]): SiteNavItem[];
20
+ /**
21
+ * True when the page opens with its own hero, in which case the shell must not
22
+ * also print a title block. One rule, checked in one place.
23
+ */
24
+ export declare function pageRendersOwnHeader(page: SitePage): boolean;
25
+ /** @returns the page at this path, or null. */
26
+ export declare function sitePageAt(pages: SitePage[], path: string): SitePage | null;
27
+ /** An in-site link. Root is '/', everything else '/segment'. */
28
+ export declare function href(path?: string): string;
package/dist/model.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pure helpers over a validated site. No React, no framework — a Node script
3
+ * generating sites can use everything in this file.
4
+ */
5
+ import { siteSpecSchema } from './schema.js';
6
+ /**
7
+ * Validate anything claiming to be a site — typically a generator's raw
8
+ * output. Structured result in the house shape; `errors` are `path: message`
9
+ * lines a model (or a person) can act on directly.
10
+ */
11
+ export function validateSite(input) {
12
+ const result = siteSpecSchema.safeParse(input);
13
+ if (result.success)
14
+ return { success: true, data: result.data };
15
+ return {
16
+ success: false,
17
+ errors: result.error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`),
18
+ };
19
+ }
20
+ /** Nav entries in page order. Pages without a `navLabel` are omitted. */
21
+ export function siteNavItems(pages) {
22
+ return pages
23
+ .filter((page) => Boolean(page.navLabel))
24
+ .map((page) => ({ path: page.path, label: page.navLabel }));
25
+ }
26
+ /**
27
+ * True when the page opens with its own hero, in which case the shell must not
28
+ * also print a title block. One rule, checked in one place.
29
+ */
30
+ export function pageRendersOwnHeader(page) {
31
+ return page.sections[0]?.kind === 'hero';
32
+ }
33
+ /** @returns the page at this path, or null. */
34
+ export function sitePageAt(pages, path) {
35
+ const normalised = path.replace(/^\/+|\/+$/g, '');
36
+ return pages.find((page) => page.path === normalised) ?? null;
37
+ }
38
+ /** An in-site link. Root is '/', everything else '/segment'. */
39
+ export function href(path = '') {
40
+ const clean = path.replace(/^\/+|\/+$/g, '');
41
+ return clean ? `/${clean}` : '/';
42
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Where every generated field came from — the check behind "we fabricate no
3
+ * facts".
4
+ *
5
+ * A closed schema invites completion: a model handed a `stats` section wants
6
+ * numbers in it, and a plausible-but-wrong street number is the one detail
7
+ * that actively damages the business a pitch is meant to win. So provenance
8
+ * makes absence first-class and typed:
9
+ *
10
+ * - `scraped` — read from the subject's own public web, with the URL
11
+ * - `operator` — a human typed it and answers for it
12
+ * - `inferred` — a model filled it in. NEVER deliverable in a real pitch.
13
+ * - `not-found` — we looked and could not find it. Unknown is not empty:
14
+ * this is the record that the looking happened, and it is
15
+ * what makes "here is what we could not find out about you"
16
+ * an honest sentence instead of an excuse.
17
+ *
18
+ * The map is a sidecar keyed by JSON path (e.g. `pages.0.sections.2.cards.1.body`)
19
+ * rather than annotations woven through the content, so the renderers stay
20
+ * ignorant of it and the check stays one function.
21
+ */
22
+ import { z } from 'zod';
23
+ export declare const provenanceEntrySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
24
+ kind: z.ZodLiteral<"scraped">;
25
+ url: z.ZodURL;
26
+ }, z.core.$strip>, z.ZodObject<{
27
+ kind: z.ZodLiteral<"operator">;
28
+ }, z.core.$strip>, z.ZodObject<{
29
+ kind: z.ZodLiteral<"inferred">;
30
+ }, z.core.$strip>, z.ZodObject<{
31
+ kind: z.ZodLiteral<"not-found">;
32
+ }, z.core.$strip>], "kind">;
33
+ export type ProvenanceEntry = z.infer<typeof provenanceEntrySchema>;
34
+ /** JSON path → where that field came from. */
35
+ export declare const provenanceSchema: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
36
+ kind: z.ZodLiteral<"scraped">;
37
+ url: z.ZodURL;
38
+ }, z.core.$strip>, z.ZodObject<{
39
+ kind: z.ZodLiteral<"operator">;
40
+ }, z.core.$strip>, z.ZodObject<{
41
+ kind: z.ZodLiteral<"inferred">;
42
+ }, z.core.$strip>, z.ZodObject<{
43
+ kind: z.ZodLiteral<"not-found">;
44
+ }, z.core.$strip>], "kind">>;
45
+ export type Provenance = z.infer<typeof provenanceSchema>;
46
+ /** Fields a model invented. A deliverable site has none. */
47
+ export declare function inferredPaths(provenance: Provenance): string[];
48
+ /**
49
+ * Fields we looked for and could not find. These are not defects — shown
50
+ * honestly, they are the pitch: what we could not learn about you in five
51
+ * minutes is what your customers also cannot.
52
+ */
53
+ export declare function notFoundPaths(provenance: Provenance): string[];
54
+ /**
55
+ * Throws unless every field is scraped, operator-entered, or honestly absent.
56
+ * Call this before anything generated is shown to, or handed to, a real
57
+ * business. The error names every offending path so the fix is a lookup, not
58
+ * a hunt.
59
+ */
60
+ export declare function assertDeliverable(provenance: Provenance): void;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Where every generated field came from — the check behind "we fabricate no
3
+ * facts".
4
+ *
5
+ * A closed schema invites completion: a model handed a `stats` section wants
6
+ * numbers in it, and a plausible-but-wrong street number is the one detail
7
+ * that actively damages the business a pitch is meant to win. So provenance
8
+ * makes absence first-class and typed:
9
+ *
10
+ * - `scraped` — read from the subject's own public web, with the URL
11
+ * - `operator` — a human typed it and answers for it
12
+ * - `inferred` — a model filled it in. NEVER deliverable in a real pitch.
13
+ * - `not-found` — we looked and could not find it. Unknown is not empty:
14
+ * this is the record that the looking happened, and it is
15
+ * what makes "here is what we could not find out about you"
16
+ * an honest sentence instead of an excuse.
17
+ *
18
+ * The map is a sidecar keyed by JSON path (e.g. `pages.0.sections.2.cards.1.body`)
19
+ * rather than annotations woven through the content, so the renderers stay
20
+ * ignorant of it and the check stays one function.
21
+ */
22
+ import { z } from 'zod';
23
+ export const provenanceEntrySchema = z.discriminatedUnion('kind', [
24
+ z.object({ kind: z.literal('scraped'), url: z.url() }),
25
+ z.object({ kind: z.literal('operator') }),
26
+ z.object({ kind: z.literal('inferred') }),
27
+ z.object({ kind: z.literal('not-found') }),
28
+ ]);
29
+ /** JSON path → where that field came from. */
30
+ export const provenanceSchema = z.record(z.string(), provenanceEntrySchema);
31
+ function pathsOf(provenance, kind) {
32
+ return Object.entries(provenance)
33
+ .filter(([, entry]) => entry.kind === kind)
34
+ .map(([path]) => path)
35
+ .sort();
36
+ }
37
+ /** Fields a model invented. A deliverable site has none. */
38
+ export function inferredPaths(provenance) {
39
+ return pathsOf(provenance, 'inferred');
40
+ }
41
+ /**
42
+ * Fields we looked for and could not find. These are not defects — shown
43
+ * honestly, they are the pitch: what we could not learn about you in five
44
+ * minutes is what your customers also cannot.
45
+ */
46
+ export function notFoundPaths(provenance) {
47
+ return pathsOf(provenance, 'not-found');
48
+ }
49
+ /**
50
+ * Throws unless every field is scraped, operator-entered, or honestly absent.
51
+ * Call this before anything generated is shown to, or handed to, a real
52
+ * business. The error names every offending path so the fix is a lookup, not
53
+ * a hunt.
54
+ */
55
+ export function assertDeliverable(provenance) {
56
+ const inferred = inferredPaths(provenance);
57
+ if (inferred.length > 0) {
58
+ throw new Error(`site is not deliverable: ${inferred.length} field(s) were inferred rather than ` +
59
+ `sourced or left honestly absent:\n ${inferred.join('\n ')}`);
60
+ }
61
+ }
@@ -0,0 +1,3 @@
1
+ export declare function NavAutoScroll({ currentPath }: {
2
+ currentPath: string;
3
+ }): null;
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+ /**
3
+ * The one client-side behaviour in the chrome, isolated so everything else
4
+ * stays server-renderable.
5
+ *
6
+ * With eight sections the nav scrolls sideways on a phone, and the active
7
+ * item is frequently off-screen at rest — a visitor arriving on a deep page
8
+ * would see a row of links with no indication that any of them is the page
9
+ * they are on. Scrolling it into view on mount fixes that; there is no
10
+ * CSS-only way to do it.
11
+ *
12
+ * Deliberately takes ONLY serialisable props. The nav itself (with its
13
+ * injected Link component) must stay a server component — a component
14
+ * function cannot cross the server→client boundary, which is exactly the
15
+ * build error this split prevents.
16
+ */
17
+ import { useEffect } from 'react';
18
+ export function NavAutoScroll({ currentPath }) {
19
+ useEffect(() => {
20
+ const active = document.querySelector('[data-sitekit-nav] [aria-current="page"]');
21
+ // `nearest` keeps the page itself still — `center` would scroll the whole
22
+ // document to the top of the masthead on every navigation.
23
+ active?.scrollIntoView({ inline: 'nearest', block: 'nearest' });
24
+ }, [currentPath]);
25
+ return null;
26
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Masthead and footer.
3
+ *
4
+ * The masthead is a rule, not a bar. It sticks so the nav stays reachable on
5
+ * a long page, but it carries no shadow, no fill beyond the page's own
6
+ * surface, and no logo — a hairline and a wordmark are enough, and anything
7
+ * heavier competes with the document.
8
+ *
9
+ * There is deliberately no "hosted on" or "built by" line. A generated site
10
+ * belongs to its subject; its footer has no business advertising anyone else.
11
+ */
12
+ import type { SiteChrome as SiteChromeSpec, SiteNavItem } from '../schema.js';
13
+ import { type LinkLike } from './link.js';
14
+ interface Props {
15
+ chrome: SiteChromeSpec;
16
+ navItems: SiteNavItem[];
17
+ currentPath: string;
18
+ /** Framework link component (e.g. next/link). Defaults to a plain anchor. */
19
+ Link?: LinkLike;
20
+ }
21
+ export declare function SiteMasthead({ chrome, navItems, currentPath, Link }: Props): import("react").JSX.Element;
22
+ export declare function SiteFooter({ chrome }: {
23
+ chrome: SiteChromeSpec;
24
+ }): import("react").JSX.Element;
25
+ export {};
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Masthead and footer.
4
+ *
5
+ * The masthead is a rule, not a bar. It sticks so the nav stays reachable on
6
+ * a long page, but it carries no shadow, no fill beyond the page's own
7
+ * surface, and no logo — a hairline and a wordmark are enough, and anything
8
+ * heavier competes with the document.
9
+ *
10
+ * There is deliberately no "hosted on" or "built by" line. A generated site
11
+ * belongs to its subject; its footer has no business advertising anyone else.
12
+ */
13
+ import { href } from '../model.js';
14
+ import { DefaultLink } from './link.js';
15
+ import { SiteNav } from './SiteNav.js';
16
+ export function SiteMasthead({ chrome, navItems, currentPath, Link = DefaultLink }) {
17
+ return (_jsx("header", { className: "sticky top-0 z-30 border-b border-subtle bg-surface-page/85 backdrop-blur", children: _jsx("div", { className: "mx-auto max-w-shell px-4 sm:px-6 lg:px-8", children: _jsxs("div", { className: "flex items-center justify-between gap-6 py-2", children: [_jsx(Link, { href: href(), className: "inline-flex min-h-11 shrink-0 items-center rounded focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent", children: _jsx("span", { className: "font-heading text-lg font-semibold tracking-display text-fg-primary", children: chrome.name }) }), _jsx(SiteNav, { items: navItems, currentPath: currentPath, Link: Link })] }) }) }));
18
+ }
19
+ export function SiteFooter({ chrome }) {
20
+ return (_jsx("footer", { className: "mt-24 border-t border-subtle", children: _jsxs("div", { className: "mx-auto max-w-shell px-4 py-12 sm:px-6 lg:px-8", children: [_jsx("p", { className: "max-w-prose text-sm leading-relaxed text-fg-secondary", children: chrome.footerNote }), _jsxs("div", { className: "mt-8 flex flex-col gap-2 border-t border-subtle pt-6 font-mono text-xs uppercase tracking-caps text-fg-muted sm:flex-row sm:items-center sm:justify-between", children: [chrome.host && _jsx("span", { children: chrome.host }), _jsxs("span", { className: "normal-case tracking-normal", children: ["\u00A9 ", chrome.name] })] })] }) }));
21
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The masthead's section nav.
3
+ *
4
+ * A server component, deliberately: the injected `Link` is a component
5
+ * function, and functions cannot cross into a 'use client' boundary. The one
6
+ * client behaviour — scrolling the active item into view — lives in
7
+ * `NavAutoScroll`, which takes only a string.
8
+ */
9
+ import type { SiteNavItem } from '../schema.js';
10
+ import { type LinkLike } from './link.js';
11
+ interface Props {
12
+ /**
13
+ * Deliberately NOT `SitePage[]`. Everything the nav receives ends up in the
14
+ * payload of every page it renders on.
15
+ */
16
+ items: SiteNavItem[];
17
+ currentPath: string;
18
+ /** Framework link component (e.g. next/link). Defaults to a plain anchor. */
19
+ Link?: LinkLike;
20
+ }
21
+ export declare function SiteNav({ items, currentPath, Link }: Props): import("react").JSX.Element;
22
+ export {};
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The masthead's section nav.
4
+ *
5
+ * A server component, deliberately: the injected `Link` is a component
6
+ * function, and functions cannot cross into a 'use client' boundary. The one
7
+ * client behaviour — scrolling the active item into view — lives in
8
+ * `NavAutoScroll`, which takes only a string.
9
+ */
10
+ import { href } from '../model.js';
11
+ import { DefaultLink } from './link.js';
12
+ import { NavAutoScroll } from './NavAutoScroll.js';
13
+ export function SiteNav({ items, currentPath, Link = DefaultLink }) {
14
+ return (_jsxs("nav", { "data-sitekit-nav": true, "aria-label": "Sections", className: "scrollbar-hide -mx-1 flex flex-nowrap items-center gap-x-1 overflow-x-auto", children: [items.map((item) => {
15
+ const isCurrent = item.path === currentPath;
16
+ return (_jsx(Link, { href: href(item.path), "aria-current": isCurrent ? 'page' : undefined, className: [
17
+ // min-h-11 is the 44px touch floor. `px-2 py-1` on text-xs came out
18
+ // around 28px, which is a fiddly target on a phone and below the
19
+ // floor the rest of the fleet holds. The masthead row drops from
20
+ // py-4 to py-2 to pay for it, so the sticky header does NOT get
21
+ // taller — see the comment in SiteChrome.
22
+ 'inline-flex min-h-11 shrink-0 items-center rounded px-2 font-mono text-xs uppercase tracking-caps transition-colors',
23
+ // A consumer's CSS reset can remove the UA focus ring, and this
24
+ // package shipped nothing to replace it — so keyboard users had no
25
+ // visible position in the nav on every site that installs it.
26
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
27
+ isCurrent
28
+ ? 'text-fg-primary underline decoration-accent decoration-2 underline-offset-8'
29
+ : 'text-fg-tertiary hover:text-fg-primary',
30
+ ].join(' '), children: item.label }, item.path || 'home'));
31
+ }), _jsx(NavAutoScroll, { currentPath: currentPath })] }));
32
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Dispatches a page's sections to their renderers, and numbers them.
3
+ *
4
+ * One renderer for every site is the point: it is what stops fifty customer
5
+ * websites becoming fifty stylesheets, and it is why a site inherits
6
+ * typography, dark mode and reading measure without its owner thinking about
7
+ * any of them. Section shapes are the closed set in the schema.
8
+ *
9
+ * Numbering counts only sections that carry a heading, so an unheaded lead
10
+ * paragraph or a bare table does not consume a number and leave a gap in the
11
+ * sequence a reader can see.
12
+ */
13
+ import type { SiteSection } from '../schema.js';
14
+ export declare function SiteSections({ sections }: {
15
+ sections: SiteSection[];
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { HeroSection } from './sections/HeroSection.js';
3
+ import { MeterSection, StatsSection } from './sections/FigureSections.js';
4
+ import { CardsSection, DefinitionsSection, ProseSection } from './sections/ProseSections.js';
5
+ import { IndexSection, TableSection } from './sections/DataSections.js';
6
+ import { ContactSection, FaqSection, FeatureSection } from './sections/BusinessSections.js';
7
+ /** True when a section shows a heading, and therefore takes the next number. */
8
+ function isNumbered(section) {
9
+ return section.kind !== 'hero' && section.kind !== 'table' && Boolean(section.heading);
10
+ }
11
+ export function SiteSections({ sections }) {
12
+ let counter = 0;
13
+ return (_jsx("div", { className: "space-y-16", children: sections.map((section, key) => {
14
+ const index = isNumbered(section) ? ++counter : undefined;
15
+ switch (section.kind) {
16
+ case 'hero':
17
+ return _jsx(HeroSection, { section: section }, key);
18
+ case 'prose':
19
+ return _jsx(ProseSection, { section: section, index: index }, key);
20
+ case 'stats':
21
+ return _jsx(StatsSection, { section: section, index: index }, key);
22
+ case 'meter':
23
+ return _jsx(MeterSection, { section: section, index: index }, key);
24
+ case 'cards':
25
+ return _jsx(CardsSection, { section: section, index: index }, key);
26
+ case 'definitions':
27
+ return _jsx(DefinitionsSection, { section: section, index: index }, key);
28
+ case 'feature':
29
+ return _jsx(FeatureSection, { section: section, index: index }, key);
30
+ case 'contact':
31
+ return _jsx(ContactSection, { section: section, index: index }, key);
32
+ case 'faq':
33
+ return _jsx(FaqSection, { section: section, index: index }, key);
34
+ case 'index':
35
+ return _jsx(IndexSection, { section: section, index: index }, key);
36
+ case 'table':
37
+ return _jsx(TableSection, { section: section }, key);
38
+ default:
39
+ return null;
40
+ }
41
+ }) }));
42
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @bitbaum/sitekit/react — the one set of renderers every generated site shares.
3
+ *
4
+ * Emits SEMANTIC utility classes only (`text-fg-primary`, `bg-surface-page`,
5
+ * `font-heading`, …) and ships no colours, faces or radii: each consumer
6
+ * defines those tokens in its own stylesheet. Uniform system, divergent
7
+ * aesthetics — the class contract is in the README.
8
+ */
9
+ export { SiteSections } from './SiteSections.js';
10
+ export { SiteMasthead, SiteFooter } from './SiteChrome.js';
11
+ export { SiteNav } from './SiteNav.js';
12
+ export { NavAutoScroll } from './NavAutoScroll.js';
13
+ export { DefaultLink, type LinkLike, type LinkProps } from './link.js';
14
+ export { SectionHeading, Blurb, SectionBody } from './sections/Primitives.js';
15
+ export { HeroSection } from './sections/HeroSection.js';
16
+ export { ProseSection, CardsSection, DefinitionsSection } from './sections/ProseSections.js';
17
+ export { StatsSection, MeterSection } from './sections/FigureSections.js';
18
+ export { IndexSection, TableSection } from './sections/DataSections.js';
19
+ export { FeatureSection, ContactSection, FaqSection } from './sections/BusinessSections.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @bitbaum/sitekit/react — the one set of renderers every generated site shares.
3
+ *
4
+ * Emits SEMANTIC utility classes only (`text-fg-primary`, `bg-surface-page`,
5
+ * `font-heading`, …) and ships no colours, faces or radii: each consumer
6
+ * defines those tokens in its own stylesheet. Uniform system, divergent
7
+ * aesthetics — the class contract is in the README.
8
+ */
9
+ export { SiteSections } from './SiteSections.js';
10
+ export { SiteMasthead, SiteFooter } from './SiteChrome.js';
11
+ export { SiteNav } from './SiteNav.js';
12
+ export { NavAutoScroll } from './NavAutoScroll.js';
13
+ export { DefaultLink } from './link.js';
14
+ export { SectionHeading, Blurb, SectionBody } from './sections/Primitives.js';
15
+ export { HeroSection } from './sections/HeroSection.js';
16
+ export { ProseSection, CardsSection, DefinitionsSection } from './sections/ProseSections.js';
17
+ export { StatsSection, MeterSection } from './sections/FigureSections.js';
18
+ export { IndexSection, TableSection } from './sections/DataSections.js';
19
+ export { FeatureSection, ContactSection, FaqSection } from './sections/BusinessSections.js';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The one framework seam in the package.
3
+ *
4
+ * The chrome needs to render links, and every framework has its own Link with
5
+ * its own prefetching. Rather than depending on any of them, components that
6
+ * link accept a `Link` component and default to a plain anchor — correct
7
+ * everywhere, and a Next.js consumer passes `next/link` to get client-side
8
+ * navigation back.
9
+ */
10
+ import { type ComponentType, type ReactNode } from 'react';
11
+ export interface LinkProps {
12
+ href: string;
13
+ className?: string;
14
+ 'aria-current'?: 'page';
15
+ children?: ReactNode;
16
+ }
17
+ export type LinkLike = ComponentType<LinkProps>;
18
+ export declare const DefaultLink: LinkLike;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The one framework seam in the package.
3
+ *
4
+ * The chrome needs to render links, and every framework has its own Link with
5
+ * its own prefetching. Rather than depending on any of them, components that
6
+ * link accept a `Link` component and default to a plain anchor — correct
7
+ * everywhere, and a Next.js consumer passes `next/link` to get client-side
8
+ * navigation back.
9
+ */
10
+ import { createElement } from 'react';
11
+ export const DefaultLink = (props) => createElement('a', props);