Skip to main content

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.name autocomplete (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_STORE seam; the route (apps/stretched/src/app/app.routes.ts) binds { provide: STORY_STORE, useExisting: StoryStoreService }, the app's Firestore implementation (stories/{name} docs, Date ⇄ Timestamp at the boundary). See Story Engine.
  • Category chip picker — toggle chips over STORY_CATEGORY_LIST (icon + label per category) set Story.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 storyFromFormDefinition with 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 the admin: true custom auth claim get in; everyone else is redirected home. The guard is the UX layer — the Firestore stories rules 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.