Typed controls
The recommended consumption path: typedFields(store) derives every key's exact value and meta types from the graph itself — the page imports
nothing but the form. The Slider component only compiles against
number/SliderDefMeta fields, and the state panel narrows the discriminated union with a
plain if. Source: src/routes/demo.
aspectRatio
Narrowing the union
snapshot.current is the discriminated union straight from the resolver — the
branch decides which keys exist, and TypeScript knows it:
mode === 'create' → 25 steps at cfg 7, 1:1. (s.scale doesn't
exist here.)
Switch modes, change values, switch back — scoped intent means each branch remembers its own values.
view source — demo-form.ts (40 lines)
import { z } from 'zod';
import { defineGraph } from '$lib/index.js';
import { enumOf, slider } from '$lib/defs/index.js';
// A compact form authored right next to the page that consumes it. Each field
// is one definition; conditional fields return null when their mode doesn't
// carry them; the graph's registry is what types the page's controls.
export const demoForm = defineGraph()
.field('mode', enumOf({
options: [
{ label: 'Create', value: 'create' },
{ label: 'Upscale', value: 'upscale' },
],
default: 'create',
}))
.field('prompt', {
input: z.string().optional(),
output: z.string().min(1, 'Prompt is required'),
default: '',
})
.field('scale', (c) => (c.mode === 'upscale' ? slider({ min: 2, max: 4, default: 2 }) : null))
.field('steps', (c) => (c.mode === 'create' ? slider({ min: 1, max: 50, default: 25 }) : null))
.field('cfgScale', (c) =>
c.mode === 'create' ? slider({ min: 1, max: 20, step: 0.5, default: 7 }) : null
)
.field('aspectRatio', (c) =>
c.mode === 'create'
? enumOf({
options: [
{ label: 'Square', value: '1:1' },
{ label: 'Portrait', value: '2:3' },
{ label: 'Landscape', value: '3:2' },
],
default: '1:1',
})
: null
);
export type DemoState = ReturnType<typeof demoForm.resolve>;