Page Builder
See also: Story Engine · Expressions and Math · Auth and Users · Story Categories · Context and Bindings
Two pieces make up the authoring surface: the library's PageBuilder component (libs/stretched-components/src/dynamic-host/page-builder/) and the app's /admin/story-builder page that wraps it with persistence and story metadata.
The PageBuilder component (<sc-page-builder>)
A SharePoint-style canvas over the Dynamic Component System: click a component to select it and edit its schema-driven fields in the right property pane; hover hotspots insert components anywhere in the tree; drag to reorder or move between containers; a story drawer holds the name, variables, and validators.
Files: page-builder.ts (shell + property pane), page-builder-node.ts (recursive canvas node), page-builder-state.ts (shared tree/selection state), section-editor.ts, page-tree.ts.
Output: DynamicFormDefinition
The builder emits the same DynamicFormDefinition as the older form-style sc-dynamic-builder, so it's a drop-in replacement for storage:
interface DynamicFormDefinition {
story: string; // the namespace — becomes Story.name
variables: VariableDeclaration[]; // { name, type, validators? }
root: BuiltConfig; // the component tree (always wrapped in a gridLayout page)
}
storyFromFormDefinition(definition, metadata) in storage/story-serializer.ts turns this into a storable Story (Story Engine). The builder emits a blank story until the page is named — the serializer refuses blank names, so an unnamed page can never be persisted under a fallback namespace. Story and variable names are normalized to camelCase identifiers on blur (toCamelIdentifier); renaming a story migrates the context namespace and rewrites every binding in the tree (renameStory + rewriteTreeBindings).
Canvas modes
edit (authoring chrome), view (the page through the real hosts — exactly what a StoryTeller consumer sees), and json (the emitted definition as JSON, with copy-to-clipboard).
The JSON-size chip
The toolbar chip shows the byte size of the emitted JSON — what actually ships to storage/CDN — and cycles raw → gzip → off. Gzip size is measured for real via CompressionStream (async, off the render path); when the API is unavailable it falls back to a ~40% estimate. Hosts can hide it entirely with [showJsonSize]="false" (e.g. if the builder is ever surfaced to end users).
Variables, validators, and the live equation preview
The story drawer manages typed variable declarations and their validators (Context and Bindings). Each validator row has:
- an expression input with
$value/$name/$story.nameautocomplete (sc-variable-ref-input); - a live syntax check (
checkExpressionSyntax) shown inline; - when the expression parses, a live equation preview — an inline
<sc-equation>typesets the condition as math (KaTeX → MathML) so you can read the rule as you write it (Expressions and Math).
Property pane & bindings
The pane builds its field controls from the selected component's Zod schema (buildFieldDefs in builder/field-descriptor.ts); children is excluded (the canvas owns the tree), and story-section text/slots are managed by the dedicated section editor. For a selected serviced-input, a bind dropdown lists only variables whose type matches the entry's produces; binding a variable also marks its field touched so validation surfaces immediately in the preview. Section ${key} slots are component-first: pick any input component and the variable's type follows what that input produces.
The app page: /admin/story-builder
apps/stretched/src/app/pages/admin/story-builder/story-builder.component.ts — a deliberately plain internal tool: author in <sc-page-builder>, save as a Firestore Story, preview stored stories through <sc-story-teller>.
- Persistence — depends only on the
STORY_STOREseam; the route (apps/stretched/src/app/app.routes.ts) binds{ provide: STORY_STORE, useExisting: StoryStoreService }, the app's Firestore implementation (stories/{name}docs,Date⇄Timestampat the boundary). See Story Engine. - Category chip picker — toggle chips over
STORY_CATEGORY_LIST(icon + label per category) setStory.categories; a summary line lists the selected labels. See Story Categories. - Metadata row — version and feature-flag inputs; Save story is disabled until the draft has a story name. Save runs
storyFromFormDefinitionwith the collected metadata and refreshes the stored-stories list; loading a story also restores its categories into the picker. - Admin guard — the route is
canActivate: [adminGuard](apps/stretched/src/app/core/guards/admin.guard.ts): only users with theadmin: truecustom auth claim get in; everyone else is redirected home. The guard is the UX layer — the Firestorestoriesrules enforce the same claim server-side (Auth and Users, Firestore and Security Rules).
Trying it without the app
Storybook runs the whole authoring loop Firebase-free: Dynamic → Page Builder stories, including the "JSON store (save / export / import — no Firebase)" demo backed by LocalStoryStore. See Local Development.