import type { ReactNode } from 'react' import type { A11yViolation, AuditOptions, CaseContext, DiffFn, DisplayCaseConfig, HierarchyLevel, RenderDriver, } from '../index' /** * The rendering-substrate contract: the replaceable unit that turns a case tree * into something viewable. * * Display Case's pipeline — discovery, the catalog/manifest, the browse chrome, * tweaks, flows, and most of `check` — is substrate-neutral. The substrate owns * the parts that are not: producing a frame from a case tree, serializing that * frame, the document the chrome embeds as the stage, the client runtime (if * any) that paints it, the axes the showcase varies over, and the render- * dependent check phases. * * **Experimental.** This contract is published from the `./core` subpath so a * substrate can be implemented out-of-tree, but it is not yet stable: it is * expected to change until a second, non-DOM implementation has been built * against it. The built-in DOM substrate is its first implementation. * * Everything here is renderer-agnostic by construction — nothing in this module * imports the DOM, a server, or a bundler. `Frame` is opaque to Display Case: * the core only ever hands it back to the substrate's own `serialize()` and * `document()`. A field the core needed to read off a frame would mean the * abstraction is leaking. */ export interface Substrate { /** * Stable identifier, used in build-cache keys, the baseline path segment, and * the manifest. The built-in DOM substrate's id is `'dom'`. Treat it as * durable: changing it re-keys a showcase's recorded baselines. */ id: string /** * The axes this substrate's renderings vary over — replacing what used to be * a hard-coded light/dark theme plus pixel viewport presets. The chrome's * variant controls, the manifest, the render addresses, and the checks' * variant enumeration all follow this declaration. */ variants: SubstrateVariantAxis[] /** * Render a case tree headlessly: no browser, no interactive client. Used by * the pre-scripting render, the render endpoint, the checks, publish, and the * `display-case render` subcommand — so whatever this produces is what every * surface delivers. * * MUST be deterministic for a deterministic case: the same tree, variant * values, and tweak values MUST produce a frame that serializes identically. */ render(tree: ReactNode, ctx: SubstrateRenderContext): Promise | Frame /** * Serialize a frame for transport, for visual baselines, and for the render * subcommand's stdout. `ext` is the file extension baselines are stored under * (`'png'`, `'txt'`, `'ansi'`, …) and decides whether a recorded baseline * reads as a reviewable text diff or as an opaque binary. */ serialize(frame: Frame): SerializedFrame /** * Produce the **entire** document served at a case's isolated render address * — the stage the browse chrome embeds. The substrate owns the whole envelope * (fonts, background, sizing, theming signals, script tags), because that * envelope is exactly where a medium's assumptions live. * * The chrome depends on only two things, both stable across substrates: the * render address shape and the stage message protocol (see * {@link StageMessage}). It never reaches into this document's content. */ document(frame: Frame, ctx: SubstrateDocumentContext): string /** * The client runtime that paints — and, where the substrate supports it, * takes over driving — a frame inside the stage document. * * Omit it entirely for a substrate whose frames are static: that is a * complete substrate, not a stub. Tweaks, flow steps, variant switches, and * deep links all remain functional, because each addresses a distinct * rendering the substrate produces afresh. */ stage?: SubstrateStage /** * Render-dependent check phases. Each omitted member marks that phase not * applicable for this substrate: the run reports it as such rather than * failing. A consumer-configured `providers` override still wins over * whatever is supplied here. */ checks?: SubstrateChecks /** * Display labels for the fixed design-hierarchy levels — e.g. presenting * `page` as "Screens" for a terminal showcase. Purely cosmetic: the level set * and its order are fixed across substrates, because classification, manifest * grouping, the browse-mode split, and the structure rules all operate on it. */ levelLabels?: Partial> /** * Module specifiers whose single shared copy every surface should resolve to * in a published build, always — the substrate's own rendering runtime. The * DOM substrate declares the React runtime here; a substrate whose stage is * not React-based is not forced to carry it. Merged with the consumer's * `share` and with {@link SubstrateStage.share}. */ alwaysShare?: string[] } /** A frame serialized to bytes, with the extension baselines store it under. */ export interface SerializedFrame { bytes: Uint8Array /** Baseline file extension, without a leading dot (`'png'`, `'txt'`). */ ext: string } /** * One axis a substrate's renderings vary over. * * The `kind` decides where the axis is honored, and it is the load-bearing * distinction — a `render` axis changes what the substrate produces, a `stage` * axis only changes how the unchanged frame is presented: * * - `'render'` — encoded in the case's address, passed to {@link * Substrate.render}, enumerated by the checks, and keyed into baseline paths. * The DOM substrate's light/dark theme axis is of this kind. * - `'stage'` — applied by the chrome around the stage without re-rendering. * The DOM substrate's viewport-width axis is of this kind: it constrains the * embedded stage, it does not change the document inside it. */ export interface SubstrateVariantAxis { /** Stable identifier, used as the address parameter name and the test-id key * (`'theme'`, `'viewport'`, `'cols'`, `'color'`). */ id: string /** Human-readable label for the chrome's control. */ label: string kind: 'render' | 'stage' /** The values this axis can take, in presentation order. */ values: SubstrateVariantValue[] /** Value used when the address names none. MUST be one of `values`. */ default: string } export interface SubstrateVariantValue { /** The value as it appears in an address and in a baseline's variant key. */ value: string /** Human-readable label for the chrome's control. */ label: string } /** The client runtime that paints a substrate's frames inside the stage. */ export interface SubstrateStage { /** * Module specifier for the stage runtime, included in each per-component * render bundle. It receives the delivered frame and paints it; for a * substrate that can render client-side it may then take over driving the * case in place. */ entry: string /** * Specifiers this stage runtime needs delivered once across a published * build, merged into the consumer's `share`. A terminal emulator is the * archetype: without this it would be inlined into every per-component * bundle, and the consumer would have to know its dependency names to * prevent that. */ share?: string[] } /** A case's address, decoded into the parts a substrate renders from. */ export interface SubstrateCaseAddress { componentId: string caseId: string /** Tweak values decoded from the address (the `t.*` parameters). */ tweaks: Record /** Values for every `render`-kind axis, with declared defaults filled in. */ variants: Record /** Remaining address parameters, for options a substrate defines itself. The * DOM substrate reads `fit`, `transparent`, and `width` from here. */ params: Record /** * The showcase's resolved configuration. A substrate is configured by the * showcase it renders, and needs this to honor the parts of the config that * are its own concern — the DOM substrate reads `theme` (root signals), * `styleEngines` (render-time CSS-in-JS), and `globalStyles` from here. * A substrate for another medium reads whatever its own factory options and * this config expose, and ignores the rest. */ config: DisplayCaseConfig } /** What {@link Substrate.render} is told about the case it is rendering. */ export interface SubstrateRenderContext extends SubstrateCaseAddress { /** Declared browser-only (or, generally, client-only): the case opts out of * headless rendering and is painted by the stage runtime instead. A * substrate SHOULD return an empty frame rather than attempt the render. */ clientOnly: boolean } /** What {@link Substrate.document} is told, beyond the frame itself. */ export interface SubstrateDocumentContext extends SubstrateCaseAddress { /** * URL of the built stage-runtime bundle for this component, when the * substrate declares a {@link SubstrateStage}. Absent for a static-frame * substrate — such a document carries no script of its own. */ scriptSrc?: string /** Importmap (bare specifier → shared vendor bundle URL) for the shared * runtime; `{}` when nothing is shared, which omits the map entirely. */ importmap: Record /** True when `frame` came from a headless render, false when the case is * client-only and the stage runtime must produce the first paint. */ prerendered: boolean /** Markup the host asks the substrate to place verbatim near the end of the * document — the dev server's live-reload and error-overlay scripts. Empty * in a published build, which carries no development machinery. */ hostScripts: string /** Presentation resources the host resolved from the showcase's * configuration. A substrate for a non-browser medium ignores these. */ resources: SubstrateDocumentResources } /** Host-resolved presentation resources offered to {@link Substrate.document}. */ export interface SubstrateDocumentResources { /** Concatenated text of the showcase's configured `globalStyles`; `''` when * none are configured. */ globalCss: string /** Display Case's own design-system stylesheet text, so a showcase that * dogfoods it paints before scripts. */ vitrineCss: string } /** Render-dependent check phases a substrate supplies. */ export interface SubstrateChecks { /** * The render-safety phase (surfaced as `--safety`, historically `--ssr`): * "this case renders headlessly without throwing". Report a finding per case * that cannot; return `[]` when every case is fine. */ safety?( tree: ReactNode, ctx: SubstrateRenderContext, ): Promise | CheckFinding[] /** * Open one variant of one case for inspection, yielding a session the visual * and accessibility phases both read. * * The two phases share a session rather than each getting their own call * because for some media producing the rendering is the expensive part and * both answers come from it. The DOM substrate's session wraps one painted * browser page — axe and the screenshot must see the *same* paint, and * opening two pages per variant would double the browser work for the common * run that asks for both. A substrate that serializes directly renders once * and answers both from that frame, so the session costs it nothing. * * Omit it and both phases report as not applicable for this substrate. */ openVariant?(ctx: SubstrateCaptureContext): Promise /** Default comparison for captured bytes. A consumer's `providers.diff` * overrides it. */ diff?: DiffFn /** * Design-token conformance: does the showcase reference only vocabulary it * defines? The DOM substrate checks CSS custom properties. Omit it and the * phase reports as not applicable rather than failing. */ tokens?(ctx: SubstrateTokensContext): Promise } /** * One opened variant, held only as long as both phases need it. * * Whoever opens a session MUST dispose it — the DOM substrate's session holds a * live browser page, and leaking one leaks a page per variant across the run. */ export interface VariantSession { /** * File extension of what {@link capture} returns, without a leading dot, and * the extension its baselines are stored under. * * This is the *capture* format, which is not always the substrate's * `serialize()` format: the DOM substrate serializes a frame to `html` but * captures a painted `png`, because a screenshot is what a visual regression * is actually about. A text substrate captures `txt` — which diffs better: * deterministic, and readable in a pull request. */ ext: string /** Bytes for the visual phase to compare against a baseline. */ capture(): Promise /** * Accessibility violations for this rendering. The DOM substrate runs axe * over the painted page; another medium audits what is meaningful there — for * a terminal, layout overflow, truncation, grapheme and wide-character * handling, and contrast over resolved colors. Return `[]` when the substrate * renders fine but has nothing to audit. */ audit(opts?: AuditOptions): Promise /** Release whatever the session holds (a browser page, a pty, a buffer). */ dispose(): Promise } /** What a capture is told about the case it is capturing. */ export interface SubstrateCaptureContext extends SubstrateRenderContext { /** Case identity in the shape snapshot providers already receive. */ case: CaseContext /** * Address of this case's isolated rendering on a running host, when one is * running. A capture that needs to paint the document (the DOM substrate's * browser screenshot) opens it; a capture that renders headlessly ignores it * — and lets the run skip starting a server at all. */ renderUrl?: string /** * The browser driver, when the run resolved one. Supplied by the caller * rather than held by the substrate so the optional Playwright/axe toolchain * stays lazily loaded and out of the substrate module's own graph. A * substrate that captures headlessly ignores it. */ driver?: RenderDriver } /** What the token-conformance phase is given. */ export interface SubstrateTokensContext { /** Absolute path to the package being checked. */ pkgDir: string /** Vocabulary names the showcase may reference without defining, from the * showcase's `tokens.allow` configuration. */ allow: string[] } /** A single finding from a substrate-supplied check phase. */ export interface CheckFinding { /** Component id the finding belongs to. */ componentId: string /** Case id, when the finding is attributable to one case. */ caseId?: string /** Package-relative source file, so the finding is navigable. */ sourcePath?: string /** Human-readable description of what is wrong. */ message: string /** `'error'` fails the run; `'warn'` is reported without failing. */ severity: 'error' | 'warn' } /** * The message protocol between the browse chrome and the embedded stage — one * half of the contract a substrate must honor (the other is the render address * shape, `/render//` plus tweak and axis parameters). * * The chrome sends {@link StageRenderMessage} to swap what the stage shows * without a reload. The stage announces readiness once, and reports a flow's * step changes back so the chrome's address and navigation stay in step. */ export type StageMessage = | StageRenderMessage | StageReadyMessage | StageStepChangedMessage /** Chrome → stage: show this case, under these tweak and axis values. */ export interface StageRenderMessage { type: 'dc-render' componentId: string caseId: string tweaks: Record /** Values for the substrate's declared axes. */ variants: Record /** Substrate-defined address options (`fit`, `transparent`, `width`). */ params: Record } /** Stage → chrome: the stage runtime has mounted and will accept messages. */ export interface StageReadyMessage { type: 'dc-ready' } /** Stage → chrome: a flow advanced to another step, so the address follows. */ export interface StageStepChangedMessage { type: 'dc-step-changed' caseId: string tweaks: Record }