` contract for Zazz pages.
* @description The single owner of everything a Zazz page loads before its own
* content: meta tags, the Geist fonts block, the one `index.css` stylesheet
* link, the feature polyfill, the third-party **import map**, the `index.js`
* module tag, and the theme-persistence script. The docs preview iframe, the
* kit's example pages (via `scripts/generate-heads.mjs`), and the docs page
* that teaches head structure all render from this module: there is no other
* copy to drift.
*
* This is a Node/server-side string builder (used at build/render time), not a
* browser runtime module; it attaches nothing to `window`.
*
* Third-party policy: one CDN provider (jsDelivr), exact pinned versions,
* static package files only (never dynamically generated `/+esm` bundles,
* as jsDelivr regenerates those when its bundler toolchain updates, which would
* silently invalidate SRI hashes), `sha384` integrity on every URL. ES modules
* resolve through the import map; the polyfill loads as its own module tag.
*
* @example
* import { buildHead } from "@zazz-ui/core/head";
* const head = buildHead({ base: "./zazz" });
*/
import { PRIMITIVES, resolveClosure } from "./manifest.ts";
// --- Third-party dependency manifest ---
/** One pinned third-party file served from jsDelivr. */
interface CdnDependency {
/** npm package name: doubles as the import-map specifier for ESM deps. */
name: string;
/** Exact pinned version. Bump deliberately; then refresh `integrity`. */
version: string;
/** Static file within the package (never a generated `/+esm` bundle). */
file: string;
/**
* `sha384` SRI hash of the pinned file. Regenerate after a version bump:
* `curl -sL | openssl dgst -sha384 -binary | openssl base64 -A`
*/
integrity: string;
}
/** The one CDN provider for every third-party resource. */
const CDN = "https://cdn.jsdelivr.net/npm";
/** The kit's own published package name (CDN mode serves files out of it). */
const PACKAGE_NAME = "@zazz-ui/core";
/**
* Exact-version pin required in every kit CDN URL. SRI hashes are per-byte,
* so floating specs (`latest`, `0.3`) would break integrity on each release —
* and unpinned URLs defeat jsDelivr's permanent caching (ticket 06).
*/
const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
/**
* Base stylesheets in the exact order `src/index.css` loads them around the
* primitive imports: `PRE` before (layer declaration first), `POST` after
* (utilities and layout stay the final normal override layer). The granular
* CDN head mirrors this split; `head.test.ts` guards it against `index.css`.
*/
const BASE_CSS_PRE = [
"base/_layers.css",
"base/_variables.css",
"base/_reset.css",
"base/_typography.css",
"base/_view-transitions.css",
];
const BASE_CSS_POST = ["base/_utilities.css", "base/_layout.css"];
/**
* Core runtime modules reached by relative import from primitive scripts
* (never via their own script tag, except `dialog-lifecycle`, which is a
* side-effect module the granular head loads explicitly). Listed so the
* import map's `integrity` section can cover their transitive loads.
*/
const CORE_RUNTIME_JS = [
"base/dialog-lifecycle.js",
"base/utils.js",
"base/signals.js",
"base/zazz-element.js",
];
/**
* @description Builds the pinned jsDelivr URL for a dependency.
*
* @param dep - The dependency entry.
* @returns The versioned URL.
* @private
*/
function cdnUrl(dep: CdnDependency): string {
return `${CDN}/${dep.name}@${dep.version}/${dep.file}`;
}
/**
* ES-module dependencies the kit's module graph imports by bare specifier.
* The import map points each specifier at its pinned static file; in tests and
* bundlers the same specifiers resolve from `node_modules` instead (the
* versions here must match the installed packages: `head.test.ts` pins that).
*/
const ESM_DEPENDENCIES: readonly CdnDependency[] = [
{
name: "signal-polyfill",
version: "0.2.2",
file: "dist/index.js",
integrity: "sha384-0OqM7OXGT6Oz1z98u/geiFn/FUyz4iWexkXHCvbIGXRRDXl6bp6IKc0j78AQ6DEK",
},
{
name: "embla-carousel",
version: "8.6.0",
file: "esm/embla-carousel.esm.js",
integrity: "sha384-GXjfyU1fSHHeweoYd3rwo7OBKYtkq+JcsDoe8VDRc6znCty2ZTzyVvX1DwqCE+9t",
},
{
name: "embla-carousel-autoplay",
version: "8.6.0",
file: "esm/embla-carousel-autoplay.esm.js",
integrity: "sha384-AX49yBc7zd/CT8ZqNMRiiqqVSV9xfxb27VwEGziPioT3wL4k4C5BW2qwefdWuN4J",
},
{
name: "embla-carousel-auto-scroll",
version: "8.6.0",
file: "esm/embla-carousel-auto-scroll.esm.js",
integrity: "sha384-SPdnL1eic73WfM72zFEf0RJ5PdUC7TL3Yb5jOl1JC3/l4D7EsVxwO8wFnnezlMSu",
},
{
name: "embla-carousel-class-names",
version: "8.6.0",
file: "esm/embla-carousel-class-names.esm.js",
integrity: "sha384-ZWno/FqzYeWE5Ils19GIhep1AZ5bMl0mp9wmm6baTJE4+pjMjOdTnE55SjUQAdVh",
},
];
/**
* Feature polyfills loaded ahead of the kit module. Exactly one entry:
* **Interest Invokers** (`interestfor`), which drives tooltip triggers and the
* optional hover/focus open on menu, menubar, and navigation-menu. Chromium
* 142+ ships it; Firefox and Safari do not, so this is the kit's one API below
* the browser-support floor that a polyfill can cover (ADR-0011).
*
* Deliberately *not* polyfilled any more (all native across the support
* window): the **Popover API** (Chrome 114, Firefox 125, Safari 17, iOS 18.3)
* and **Invoker Commands** `command`/`commandfor` (Chrome 135, Firefox 144,
* Safari 26.2). CSS anchor positioning is below the floor but has no polyfill
* here — the components gate it behind `@supports` instead.
*/
const POLYFILLS: readonly CdnDependency[] = [
{
name: "invokers",
version: "2.2.2",
file: "dist/esm/production/interest.js",
integrity: "sha384-hR2BVNtsS7fIIMwOm+f7MY0JZfuByGhQQfevCBB6evFATKzBsTw0JzH/RxD94z2T",
},
];
// --- Head fragments ---
/**
* @description Renders the Geist fonts block (preconnect + preload + stylesheet).
*
* @param display - The `font-display` strategy for the Google Fonts request.
* @returns The fonts markup.
* @private
*/
function fontsBlock(display: "swap" | "optional"): string {
return `
`;
}
/**
* @description Renders the import map that resolves the kit's bare specifiers
* (`signal-polyfill`, `embla-carousel`, …) to pinned jsDelivr URLs, with an
* `integrity` section covering every mapped file.
*
* @returns The import-map script tag.
* @private
*/
function importMapBlock(extraIntegrity: Record = {}): string {
const imports: Record = {};
const integrity: Record = { ...extraIntegrity };
for (const dep of ESM_DEPENDENCIES) {
const url = cdnUrl(dep);
imports[dep.name] = url;
integrity[url] = dep.integrity;
}
const json = JSON.stringify({ imports, integrity }, null, 2);
return `
`;
}
/**
* @description Renders the polyfill script tag. The invokers interest build is
* an ES module, so it is deferred by default and executes in document order —
* ahead of the `index.js` module tag that follows it.
*
* @returns The polyfill markup.
* @private
*/
function polyfillsBlock(): string {
const [interest] = POLYFILLS;
return `
`;
}
/**
* The theme-persistence script. Runs inline (no `defer`) while the parser is
* still in ``, so `.dark` lands on `` before first paint. The
* try/catch covers contexts where storage access throws (sandboxed iframes,
* blocked site data).
*/
const THEME_SCRIPT = `
`;
// --- Public API ---
/** CDN mode for `buildHead` — kit files served from jsDelivr (ticket 06). */
export interface CdnHeadOptions {
/** Exact published `@zazz-ui/core` version (`"0.1.0"`; never a dist-tag). */
version: string;
/**
* Granular grain: the primitives this page uses. Their dependency closure
* (via the kit manifest) decides which css/js files load. Omit for the
* bundle grain (`dist/zazz.css` + `dist/zazz.js`, whole kit, two requests).
*/
primitives?: string[];
/**
* The version's `dist/sri.json` contents (package-relative path → sha384).
* When provided, every kit URL gets `integrity` + `crossorigin` (links and
* script tags directly; transitive module imports via the import map's
* `integrity` section). Omitted → plain pinned URLs.
*/
sri?: Record;
}
/** Options for `buildHead`. */
export interface HeadOptions {
/**
* URL prefix to the kit's `src/` contents, no trailing slash — where
* `index.css` and `index.js` live. Default `"./zazz"` (the documented copy
* location); the docs preview iframe passes `"/zazz/src"`.
*/
base?: string;
/**
* Serve the kit from jsDelivr instead of a local copy: pinned, optionally
* SRI-checked URLs into the published package. Mutually exclusive with
* `base`.
*/
cdn?: CdnHeadOptions;
/**
* Load component behavior: the import map, the polyfills, and the
* `index.js` module. `false` renders a style-only head. Default `true`.
*/
scripts?: boolean;
/** Geist font loading; `false` skips the block entirely. Default `"swap"`. */
fontDisplay?: "swap" | "optional" | false;
/** Include the inline theme-persistence script (last in head). Default `true`. */
theme?: boolean;
}
/**
* @description Builds the canonical Zazz `` contents: meta tags, fonts,
* the single stylesheet link, and (unless `scripts: false`) the import map,
* polyfills, and `index.js` module tag, ending with the theme script. Page
* specifics (``, prefetch hints, override stylesheets) belong after
* this block, outside the contract.
*
* @param options - See `HeadOptions`.
* @returns The head markup (no surrounding `` tag).
* @example
* buildHead(); // full head for "./zazz"
* buildHead({ base: "/zazz/src", scripts: false, fontDisplay: "optional" });
*/
export function buildHead(options: HeadOptions = {}): string {
const { base = "./zazz", cdn, scripts = true, fontDisplay = "swap", theme = true } = options;
const parts: string[] = [
``,
``,
``,
``,
];
if (fontDisplay !== false) parts.push(fontsBlock(fontDisplay));
if (cdn) {
parts.push(...cdnBlocks(cdn, scripts));
} else {
parts.push(
``,
``,
);
if (scripts) {
parts.push(
// The import map must precede EVERY module load — including the
// modulepreload hint — or the browser rejects it and bare specifiers fail.
importMapBlock(),
``,
polyfillsBlock(),
``,
``,
);
}
}
if (theme) parts.push(THEME_SCRIPT);
return parts.join("\n");
}
// --- CDN mode ---
/**
* @description Renders the style/behavior blocks for CDN mode: the bundle
* grain (two `dist/` requests, whole kit) or, when `primitives` is given, the
* granular grain — base layers in cascade order, the dependency closure's
* stylesheets, and one module tag per closure script (relative imports between
* kit files resolve natively on jsDelivr; only bare specifiers need the map).
*
* @param cdn - The CDN options (exact version, optional primitives + sri).
* @param scripts - Whether behavior loads at all (`HeadOptions.scripts`).
* @returns The head fragments between the fonts block and the theme script.
* @private
*/
function cdnBlocks(cdn: CdnHeadOptions, scripts: boolean): string[] {
const { version, primitives, sri } = cdn;
if (!EXACT_VERSION.test(version)) {
throw new Error(
`CDN URLs must pin an exact version (got "${version}"); ` +
`dist-tags and ranges break SRI and permanent caching`,
);
}
const url = (path: string): string => `${CDN}/${PACKAGE_NAME}@${version}/${path}`;
const attrs = (path: string): string => {
const hash = sri?.[path];
return hash ? ` integrity="${hash}" crossorigin="anonymous"` : "";
};
const parts: string[] = [];
if (!primitives) {
parts.push(
``,
``,
);
if (scripts) {
parts.push(
importMapBlock(),
``,
polyfillsBlock(),
``,
``,
);
}
return parts;
}
const closure = resolveClosure(primitives);
const css = [
...BASE_CSS_PRE,
...closure.flatMap((name) => PRIMITIVES[name]?.css ?? []),
...BASE_CSS_POST,
];
parts.push(
``,
...css.map(
(path) => ``,
),
);
if (scripts) {
// Side-effect modules need their own tag (nothing imports them); the rest
// of each primitive's chain loads through native relative imports. The
// core dialog-lifecycle module always leads: dialogs, menus, and popovers
// assume its lifecycle events. The polyfill stays in even for css-only
// closures — tooltip is styles-only but its trigger is `interestfor`.
const scriptFiles = [
...new Set([
"base/dialog-lifecycle.js",
...closure.flatMap((name) => {
const entry = PRIMITIVES[name];
return entry ? [...entry.base, ...entry.js] : [];
}),
]),
];
// Transitively imported core modules never get a tag, so their integrity
// rides in the import map's integrity section instead.
const transitive: Record = {};
if (sri) {
for (const path of CORE_RUNTIME_JS) {
const hash = sri[`src/${path}`];
if (hash) transitive[url(`src/${path}`)] = hash;
}
}
parts.push(
importMapBlock(transitive),
polyfillsBlock(),
``,
...scriptFiles.map(
(path) =>
``,
),
);
}
return parts;
}
export { ESM_DEPENDENCIES, POLYFILLS, cdnUrl };
export type { CdnDependency };