The engine package
What @takazudo/zudo-sg owns, what a host keeps, and how the routes reach a build.
@takazudo/zudo-sg is a workspace package (packages/styleguide in this repo) built the same way @takazudo/zudo-doc is built: an installable zfb package that injects routes, registers virtual modules, and ships a CLI, rather than host-owned files a project copies and lets drift.
The zudo-doc analogy
Anyone familiar with @takazudo/zudo-doc's package-owned routes (the /, /, / routes this same host already gets from that package) already knows the shape zudo-sg follows for its own four routes — /, /, /, and /. Both packages:
inject route entrypoints from their own package realpath, not from files copied into the host;
pass data across the plugin boundary through
virtual:*modules rather than direct imports;let a host
pages/file at the same URL pattern silently shadow the injected route as an escape hatch, not an error.
zudo-sg's routes plugin (@takazudo/) in fact requires zudo-doc's routes plugin to already be registered — the engine's route entrypoints call zudo-doc's chrome factories (createRouteContext, createChrome) and import zudo-doc's own virtual modules to do it. The engine builds its catalog chrome on top of zudo-doc's, it does not reimplement it.
What the engine owns
Route entrypoints for the four catalog URLs. Each one only links preview CSS, reads its data from the virtual modules, and calls the zudo-doc chrome factories — it never reaches into a plugin's internals directly.
Virtual modules:
virtual:zudo-sg-context(a plain JSONsgContext— base path, route patterns, category order, the preview CSS URL, catalog title/intro),virtual:zudo-sg-registry(a re-export of the host's generated story registry), andvirtual:zudo-sg-tokens(the design-token manifest that feeds the/route's dashboards, when the host supplies one).tokens Three plugins, composed by
zudoSg()/withZudoSg()(@takazudo/): the routes plugin above, a preview-CSS plugin (see Tokens and preview CSS), and a dev-only design-token-panel apply proxy that persists live panel tweaks back into host CSS source.zudo- sg/ config A header trigger for the preview token panel, appended by
withZudoSg()behind aheaderTokenTriggeroption (defaulttrue): an{ type: "html" }item added to the zudo-doc routes plugin'sheaderRightItemsin its own pass, run after (and independent of) the chrome-defaults pass below — so it reaches a host with its ownheaderNav, not just a bare scaffold. The button ships hidden and an inline script reveals it only on the engine's own routes. See Design Token Panel for the reader-facing behaviour anddocs/for why it bypasses chrome defaults.adr/ styleguide- engine. md A CLI (
zudo-sg, from the package'sbin):gen-registry [--check]discovers*.stories.tsxfiles and writes the host's story registry;new-component <name> --category <c> [--nested] [--skip-barrel]scaffolds a component;gen-token-manifest [--check]regenerates the design-token manifest the/route reads.tokens A dev-hydration seed,
@takazudo/— see the note on islands below.zudo- sg/ islands
What the host keeps
zudo-sg.config.mjs— the host's own data: where component story roots live, the generated registry's output path, category order, the UI package name, the preview stylesheet entry, and token manifest paths. This is whatwithZudoSg(preset, options)reads to build the plugin descriptors above.The generated registry (
src/in this host) and the generated token manifest — both are CLI output, committed so the build never has to run codegen implicitly, and checked in CI with the CLI'sstyleguide/ sg- registry. ts --checkflags.The preview stylesheet entry the preview-CSS plugin compiles (this host's
src/).styles/ preview- entry. css The doc-chrome bindings (
chromeBindingsModule) that let the host override specific chrome slots — this project overrides onlyBodyEndIslands(so the engine's preview token panel, the image/mermaid enlarge dialogs, and the sidebar-resizer init mount on every route, including the package-injected ones) anddocHistoryMeta, leaving every other slot at the package default. There is deliberately noHeaderbinding: zudo-doc 5.25.0 added thesidebarNodesprop the engine needed, so every route now renders the package header.One page,
pages/, kept host-owned rather than injected — it is the site's identity page and, not incidentally, the host's static-import root for site-wide islands (see below).index. tsx
The story model, at a glance
The catalog is entirely data-driven from *.stories.tsx files. Each file default-exports a StoryMeta — a display title, a category, a one-line description, and a usage snippet — plus one or more named variants, each pairing a render function with a label. The catalog groups every component by category, lists its variants, and (for variants that opt in) exposes live-editable controls that re-render the same preview with tweaked props.
Categories are plain strings, not a fixed enum: a host lists the categories it wants to appear first, in order, via categoryOrder in zudo-sg.config.mjs, and any category a story uses that isn't in that list is still rendered, appended alphabetically after the named ones. Nothing needs to be registered up front for a new category to show up.
This is the concept-level shape only. The full authoring contract — control types, source-display rules, the live preview-props channel, and the scaffolding CLI — is the package's own documentation, not this site (see Relationship to zudo-doc for why the root site and the package docs stay two separate places).
A second story source for hosts outside zudo-react module mode
Everything above describes module mode — stories as *.stories.tsx files with real render functions that use zfb's zudo-react runtime. A host using another framework, or no component framework, switches to descriptor mode (registry: { mode: "descriptor" }): stories become plain-data StoryDescriptors with no render function, and the host serves its own preview document at externalPreview.url instead of relying on the engine's in-engine / route. The engine still renders the catalog, nav, search, and the detail page's chrome either way — only the preview document's contents change hands. See Adopting the engine for the framework-scope framing and the doc/ workspace's Descriptor mode & external preview for the full contract.
No staging
Earlier zfb releases required a package injecting routes from outside a project's own source tree to stage its route entrypoints into a project-local hidden directory before the bundler would resolve them. That gap is closed as of the zfb release this engine targets: a route entrypoint's virtual-module imports resolve correctly straight from the package's own realpath, whether the package is workspace-linked or installed from a tarball into node_modules. The engine therefore ships its route entrypoints as .tsx source (not compiled .js — the bundler extracts each route's dynamic paths() by parsing the source file) and injects them directly, with nothing to stage and nothing under the host's own tree to .gitignore.
Islands and the dev server
One consequence of the routes travelling through virtual modules used to be worth calling out because it shaped how every "use client" island in the catalog got wired up: on zfb releases before 2.18.0, a client island reached only through a virtual:* re-export did not reliably register with the bundler's island scanner, because the scanner did not resolve bare virtual: specifiers. Since 2.18.0 the scanner does resolve virtual: specifiers, so an island reached only through the registry virtual module registers the same way a direct, real-file import always did.
The engine's own islands (the component detail workbench, the code panel, the catalog filter, the preview design-token button and panel) are collected into one side-effect module, @takazudo/, that statically imports every one of them. A host may import that module once from an actual pages/ file — in this project, a one-line shim (pages/) imported by pages/ — and doing so is harmless, but it does not do anything the engine's peer floor doesn't already do on its own.
The scanner limitation that once required this seed was fixed in zfb 2.18.0. The engine's current zfb peer floor, 4.4.0, includes that behavior: the dev scanner reads injected package routes, and package wind manifests supply the engine's utility candidates. The seed is therefore a no-op kept for API compatibility.