streetui 1.6.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@
7
7
  * (`streetui --version`) — the consolidated test-suite pins all three to the
8
8
  * same coordinated release so they can never silently drift apart.
9
9
  */
10
- declare const VERSION = "1.6.0";
10
+ declare const VERSION = "1.8.0";
11
11
 
12
12
  /**
13
13
  * StreetUI reactive signals — framework-owned reactivity, no external libraries.
@@ -195,7 +195,7 @@ declare function formatDiagnostic(d: Diagnostic): string;
195
195
  * in the Semantic Application Graph.
196
196
  */
197
197
 
198
- type SemanticNodeType = 'application' | 'page' | 'section' | 'container' | 'heading' | 'text' | 'button' | 'input' | 'form' | 'list' | 'list-item' | 'image' | 'link' | 'component' | 'slot' | 'fragment' | 'reactive-list' | 'conditional';
198
+ type SemanticNodeType = 'application' | 'page' | 'section' | 'container' | 'heading' | 'text' | 'button' | 'input' | 'form' | 'list' | 'list-item' | 'image' | 'link' | 'component' | 'slot' | 'fragment' | 'reactive-list' | 'conditional' | 'portal';
199
199
  interface NodeMetadata {
200
200
  readonly createdAt: number;
201
201
  readonly [key: string]: unknown;
@@ -341,6 +341,111 @@ interface SerializedGraph {
341
341
  readonly root: SerializedNode;
342
342
  }
343
343
 
344
+ /**
345
+ * First-class StreetUI-native components (§3–§9).
346
+ *
347
+ * A component is a *reusable unit that owns its own local state and lifecycle*.
348
+ * It is NOT a virtual-DOM element, a second reactive system, or a second
349
+ * renderer. It compiles into the EXISTING pipeline: `container.component(...)`
350
+ * creates a reserved `'component'` GraphNode (already a `SemanticNodeType`,
351
+ * already mapped to `<div>`), runs the component's `setup` synchronously during
352
+ * the same build-time descent every other builder uses, and routes any cleanups
353
+ * the setup registers into the node's `NodeInstance` at mount (so they run,
354
+ * children-first, when the component leaves the graph).
355
+ *
356
+ * Design (derived from the existing DSL architecture, not copied from React/Vue):
357
+ *
358
+ * const UserCard = component<{ name: Signal<string> }>((props, ctx) => {
359
+ * // ── setup: runs ONCE per instance, at build time ──
360
+ * const open = signal(false);
361
+ * ctx.effect(() => { ... }); // auto-disposed on unmount
362
+ * ctx.onCleanup(() => { ... }); // explicit teardown hook
363
+ * // ── render: fills the component's own container scope ──
364
+ * return (content) => {
365
+ * content.text(props.name); // fine-grained: signal prop, no re-setup
366
+ * content.when(open, (c) => c.text('expanded'));
367
+ * ctx.renderChildren(content); // where slotted children go (§6)
368
+ * };
369
+ * }, { name: 'UserCard' });
370
+ *
371
+ * page.component('card-1', UserCard, { name }, (slot) => slot.text('child'));
372
+ *
373
+ * Props are ordinary typed values (§5 — typing is a TypeScript concern, no
374
+ * runtime schema). Passing a `Signal<T>` prop and binding it in the render body
375
+ * gives fine-grained updates (§13) WITHOUT re-running `setup`: only the bound
376
+ * node mutates when the signal changes. `setup` re-runs only when the component
377
+ * is genuinely rebuilt (removed + re-created by a keyed list / conditional),
378
+ * at which point the previous instance is disposed first — so cleanup stays
379
+ * correct.
380
+ */
381
+
382
+ /**
383
+ * The render half of a component: fills the component's own container scope.
384
+ * Returned by `setup` so that per-instance state created in `setup` is captured
385
+ * by closure and the render body can read it.
386
+ */
387
+ type ComponentRender = (content: ContainerDSL) => void;
388
+ /**
389
+ * Lifecycle + composition surface handed to a component's `setup`. Local
390
+ * reactive state is created with the EXISTING `signal`/`derived`/`effect`/
391
+ * `batch` primitives (§8) — `ctx` only adds ownership: anything registered here
392
+ * is torn down automatically when the component leaves the graph (§9).
393
+ */
394
+ interface ComponentContext {
395
+ /** This instance's stable identity key (the `key` passed at the call site). */
396
+ readonly key: string;
397
+ /**
398
+ * Register a teardown callback. Runs when the component unmounts (children
399
+ * first, then this — mirroring `NodeInstance.dispose`). Use for resources,
400
+ * `form.dispose`, subscriptions, timers, etc.
401
+ */
402
+ onCleanup(fn: () => void): void;
403
+ /**
404
+ * Run a reactive effect owned by this component. Wraps the framework's
405
+ * `effect()`; the returned unsubscribe is auto-tracked and disposed on
406
+ * unmount, so component effects never leak (the pain point the audit ranked
407
+ * #1). The effect may itself return a cleanup, exactly like `effect()`.
408
+ */
409
+ effect(fn: () => void | (() => void)): void;
410
+ /**
411
+ * Render the caller-supplied children into `content` at this point (§6 native
412
+ * child composition / slots). No-op when the call site passed no children.
413
+ * Call it wherever the component wants its slotted content to appear.
414
+ */
415
+ renderChildren(content: ContainerDSL): void;
416
+ }
417
+ /**
418
+ * A component's setup function: receives typed props and the lifecycle context,
419
+ * creates any local state, and returns the render function. Runs synchronously
420
+ * during the build-time descent (like `errorBoundary`'s callback), so it must be
421
+ * SSR-safe — on the server it runs at render time and its cleanups run when the
422
+ * SSR root is disposed.
423
+ */
424
+ type ComponentSetup<P> = (props: P, ctx: ComponentContext) => ComponentRender;
425
+ /**
426
+ * The opaque, reusable definition produced by `component(...)`. Carries the
427
+ * `setup` and an inspectable `name`; the brand lets the builder method (and
428
+ * DevTools) recognise a definition at runtime without a class.
429
+ */
430
+ interface ComponentDefinition<P> {
431
+ readonly __streetui_component: true;
432
+ readonly name: string;
433
+ readonly setup: ComponentSetup<P>;
434
+ }
435
+ /**
436
+ * Define a reusable component. Returns a `ComponentDefinition` you render with
437
+ * `container.component(key, def, props, children?)`. This is a pure factory — it
438
+ * builds no graph and runs no `setup`; instantiation happens per call site.
439
+ *
440
+ * @param setup Runs once per instance: create local state, return the render fn.
441
+ * @param options.name Human-readable name for DevTools/`data-streetui-component`.
442
+ */
443
+ declare function component<P = Record<string, never>>(setup: ComponentSetup<P>, options?: {
444
+ name?: string;
445
+ }): ComponentDefinition<P>;
446
+ /** Runtime guard: is `value` a component definition? */
447
+ declare function isComponentDefinition(value: unknown): value is ComponentDefinition<unknown>;
448
+
344
449
  /**
345
450
  * StreetUI DSL type system.
346
451
  * All builder callbacks and option shapes live here.
@@ -385,6 +490,8 @@ interface A11yOptions {
385
490
  readonly ariaInvalid?: boolean;
386
491
  /** aria-required — mark a form field as required. */
387
492
  readonly ariaRequired?: boolean;
493
+ /** aria-modal — mark a dialog as modal (content outside is inert to AT). */
494
+ readonly ariaModal?: boolean;
388
495
  }
389
496
  interface TextOptions extends A11yOptions {
390
497
  readonly class?: string;
@@ -456,6 +563,30 @@ interface FormOptions extends ContainerOptions {
456
563
  }
457
564
  interface ListOptions extends ContainerOptions {
458
565
  }
566
+ /** Options for a plain portal (mount children into `document.body`). */
567
+ interface PortalOptions extends ContainerOptions {
568
+ }
569
+ /**
570
+ * Options shared by every overlay (dialog/popover/tooltip/dropdown/toast).
571
+ *
572
+ * An overlay is a portal + a reactive `when(open, …)` panel + focus/keyboard
573
+ * behavior. `open` drives visibility; the framework never mutates it — closing
574
+ * is cooperative: `onClose` fires on Escape (when `closeOnEscape`) and the app
575
+ * flips its own `open` signal there. Per-kind defaults (role, modality, focus,
576
+ * escape, restore) apply unless overridden here.
577
+ */
578
+ interface OverlayOptions extends ContainerOptions {
579
+ /** Reactive open/visibility state. When it flips, the panel mounts/unmounts. */
580
+ readonly open: Bindable<boolean>;
581
+ /** Requested-close callback (fired on Escape when `closeOnEscape`). Flip `open` here. */
582
+ readonly onClose?: () => void;
583
+ /** Restore focus to the previously-focused element on close. Default: per-kind. */
584
+ readonly restoreFocus?: boolean;
585
+ /** id of the element to focus first when the overlay opens (else first focusable). */
586
+ readonly initialFocusId?: string;
587
+ /** Escape key invokes `onClose`. Default: per-kind. */
588
+ readonly closeOnEscape?: boolean;
589
+ }
459
590
  type SectionBuilder = (section: SectionDSL) => void;
460
591
  type ContainerBuilder = (container: ContainerDSL) => void;
461
592
  type PageBuilder = (page: PageDSL) => void;
@@ -515,6 +646,54 @@ interface ContainerDSL extends ContentDSL {
515
646
  * removal. It does NOT trap arbitrary global errors; errors remain observable.
516
647
  */
517
648
  errorBoundary(id: string, builder: ContainerBuilder, options: ErrorBoundaryOptions): void;
649
+ /**
650
+ * Render `builder`'s subtree into `document.body` instead of inline at this
651
+ * position (a neutral inline anchor is left behind). On the server there is no
652
+ * body, so the content renders inline; hydration relocates it to a body
653
+ * container to match the browser. Use for content that must escape overflow/
654
+ * stacking contexts (overlays, toasts). Cleanup removes the body container.
655
+ */
656
+ portal(key: string, builder: ContainerBuilder, options?: PortalOptions): void;
657
+ /**
658
+ * Modal dialog: portal + `when(open, …)` panel with `role="dialog"`,
659
+ * `aria-modal="true"`, focus trap + containment, Escape-to-close, and focus
660
+ * restore on close. `builder` fills the dialog panel.
661
+ */
662
+ dialog(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
663
+ /**
664
+ * Non-modal popover: portal + `when(open, …)` panel with `role="dialog"`.
665
+ * Moves focus into the panel on open and restores it on close, but does not
666
+ * trap or contain focus. Escape closes by default.
667
+ */
668
+ popover(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
669
+ /**
670
+ * Tooltip: portal + `when(open, …)` panel with `role="tooltip"`. Non-modal
671
+ * and does not steal focus (tooltips describe another element); no Escape
672
+ * handling by default.
673
+ */
674
+ tooltip(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
675
+ /**
676
+ * Dropdown menu: portal + `when(open, …)` panel with `role="menu"`. Non-modal;
677
+ * moves focus into the menu on open, Escape closes, focus restored on close.
678
+ */
679
+ dropdown(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
680
+ /**
681
+ * Toast: portal + `when(open, …)` panel with `role="status"` and
682
+ * `aria-live="polite"`. Non-modal and never steals focus; no Escape handling.
683
+ */
684
+ toast(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
685
+ /**
686
+ * Instantiate a reusable `component()` at this position (§3–§9). Creates a
687
+ * `'component'` node (rendered as a `<div>` wrapper), runs the definition's
688
+ * `setup(props, ctx)` synchronously to obtain its render function, and fills
689
+ * the component's own container scope with it. Any `ctx.effect`/`ctx.onCleanup`
690
+ * registered by the setup is torn down automatically when the component leaves
691
+ * the graph. `props` are strongly typed by the definition's generic; pass
692
+ * `Signal` props for fine-grained updates that do NOT re-run `setup` (§13).
693
+ * The optional `children` builder is rendered wherever the component calls
694
+ * `ctx.renderChildren` (§6 native child composition).
695
+ */
696
+ component<P>(key: string, def: ComponentDefinition<P>, props: P, children?: ContainerBuilder): void;
518
697
  }
519
698
  interface SectionDSL extends ContainerDSL {
520
699
  }
@@ -576,6 +755,39 @@ declare class ContainerBuilderBase extends ContentBuilderBase implements Contain
576
755
  form(key: string, builder: FormBuilder, options?: FormOptions): void;
577
756
  when(condition: Bindable<boolean>, builder: ContainerBuilder, elseBuilder?: ContainerBuilder): void;
578
757
  errorBoundary(id: string, builder: ContainerBuilder, options: ErrorBoundaryOptions): void;
758
+ portal(key: string, builder: ContainerBuilder, options?: PortalOptions): void;
759
+ /**
760
+ * Shared assembly for every overlay kind: a `portal` node whose single child
761
+ * is a `when(open, panel)` conditional. The panel container carries the
762
+ * kind's ARIA semantics; `builder` fills it. An `__overlay__<portalId>`
763
+ * descriptor is registered so the renderer wires focus/keyboard behavior to
764
+ * the same `open` signal that drives the panel. Reuses existing primitives
765
+ * (portal + when + container) — no new render path.
766
+ */
767
+ private _overlay;
768
+ dialog(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
769
+ popover(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
770
+ tooltip(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
771
+ dropdown(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
772
+ toast(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
773
+ /**
774
+ * Instantiate a reusable component (§3–§9). Creates a `'component'` node
775
+ * (rendered as a `<div>` wrapper — preserves the one-node/one-element
776
+ * positional-hydration invariant), then runs `def.setup(props, ctx)`
777
+ * synchronously to obtain the render function and fills the component's own
778
+ * container scope with it — structurally identical to `container`/
779
+ * `errorBoundary`. Cleanups the setup registers via `ctx.effect`/
780
+ * `ctx.onCleanup` are collected into a closure and exposed to the renderer
781
+ * through a `__component__<id>` handler (mirroring `__overlay__`); the mount/
782
+ * hydrate paths read it and route each into `NodeInstance.trackCleanup`, so
783
+ * teardown runs (children-first) when the component leaves the graph.
784
+ *
785
+ * `setup` runs once per instance here at build time. When this component sits
786
+ * inside a keyed list / conditional, a rebuild disposes the old instance
787
+ * (running its cleanups + pruning its `__component__` entry) and re-runs this
788
+ * method for the new node — so re-invocation is safe and leak-free.
789
+ */
790
+ component<P>(key: string, def: ComponentDefinition<P>, props: P, children?: ContainerBuilder): void;
579
791
  }
580
792
  declare class SectionBuilderImpl extends ContainerBuilderBase implements SectionDSL {
581
793
  }
@@ -668,4 +880,4 @@ declare function compile(app: StreetApp, options?: CompileOptions): CompiledAppl
668
880
  */
669
881
  declare function compileGraph(graph: ApplicationGraph, options?: CompileOptions): CompiledApplication;
670
882
 
671
- export { type ListDSL as $, type ApplicationId as A, BaseNode as B, type CompiledApplication as C, DiagnosticCollector as D, type ErrorBoundaryOptions as E, type ErrorFallbackBuilder as F, GraphNode as G, type ErrorSource as H, type EventDescriptor as I, type FormBuilder as J, FormBuilderImpl as K, type FormDSL as L, type FormOptions as M, type GraphNodeData as N, type HandlerFn as O, type PageDSL as P, type HeadingOptions as Q, type ReadonlySignal as R, StreetApp as S, type ImageOptions as T, type Unsubscribe as U, VERSION as V, type InputOptions as W, type InputOptionsBase as X, type LinkOptions as Y, type ListBuilder as Z, ListBuilderImpl as _, Signal as a, type ListOptions as a0, type ListPlanEntry as a1, type NodeId as a2, type NodeMetadata as a3, type PageBuilder as a4, PageBuilderImpl as a5, type PropValue as a6, type Props as a7, type ReactiveConsumer as a8, type ReactiveSource as a9, signal as aA, signalKind as aB, streetui as aC, type SectionBuilder as aa, SectionBuilderImpl as ab, type SectionDSL as ac, type SectionOptions as ad, type SerializedGraph as ae, type SerializedNode as af, type StateRef as ag, type StreetUI as ah, type TextOptions as ai, type TextValue as aj, batch as ak, compile as al, compileGraph as am, createNodeId as an, derived as ao, effect as ap, formatDiagnostic as aq, generateApplicationId as ar, generateNodeId as as, isBatching as at, nextId as au, nodeIdPrefix as av, observerCount as aw, reactiveListItemKey as ax, reactiveListItemSignature as ay, resetIdCounter as az, type Subscriber as b, ApplicationGraph as c, type SemanticNodeType as d, type ContainerDSL as e, type SignalKind as f, type A11yOptions as g, AppBuilder as h, type AppDSL as i, type AppOptions as j, type ApplicationGraphOptions as k, type Bindable as l, type BindableText as m, type BoundInputOptions as n, type ButtonOptions as o, type CompileOptions as p, type ContainerBuilder as q, ContainerBuilderImpl as r, type ContainerOptions as s, type ContentDSL as t, type ControlledInputOptions as u, DerivedSignal as v, type Diagnostic as w, DiagnosticError as x, type DiagnosticLocation as y, type DiagnosticSeverity as z };
883
+ export { type InputOptionsBase as $, type ApplicationId as A, BaseNode as B, type CompiledApplication as C, DiagnosticCollector as D, type Diagnostic as E, DiagnosticError as F, GraphNode as G, type DiagnosticLocation as H, type DiagnosticSeverity as I, type ErrorBoundaryOptions as J, type ErrorFallbackBuilder as K, type ErrorSource as L, type EventDescriptor as M, type FormBuilder as N, FormBuilderImpl as O, type PageDSL as P, type FormDSL as Q, type ReadonlySignal as R, StreetApp as S, type FormOptions as T, type Unsubscribe as U, VERSION as V, type GraphNodeData as W, type HandlerFn as X, type HeadingOptions as Y, type ImageOptions as Z, type InputOptions as _, type ComponentDefinition as a, type LinkOptions as a0, type ListBuilder as a1, ListBuilderImpl as a2, type ListDSL as a3, type ListOptions as a4, type ListPlanEntry as a5, type NodeId as a6, type NodeMetadata as a7, type OverlayOptions as a8, type PageBuilder as a9, isBatching as aA, isComponentDefinition as aB, nextId as aC, nodeIdPrefix as aD, observerCount as aE, reactiveListItemKey as aF, reactiveListItemSignature as aG, resetIdCounter as aH, signal as aI, signalKind as aJ, streetui as aK, PageBuilderImpl as aa, type PortalOptions as ab, type PropValue as ac, type Props as ad, type ReactiveConsumer as ae, type ReactiveSource as af, type SectionBuilder as ag, SectionBuilderImpl as ah, type SectionDSL as ai, type SectionOptions as aj, type SerializedGraph as ak, type SerializedNode as al, type StateRef as am, type StreetUI as an, type TextOptions as ao, type TextValue as ap, batch as aq, compile as ar, compileGraph as as, component as at, createNodeId as au, derived as av, effect as aw, formatDiagnostic as ax, generateApplicationId as ay, generateNodeId as az, type ContainerBuilder as b, Signal as c, type Subscriber as d, ApplicationGraph as e, type SemanticNodeType as f, type ContainerDSL as g, type SignalKind as h, type A11yOptions as i, AppBuilder as j, type AppDSL as k, type AppOptions as l, type ApplicationGraphOptions as m, type Bindable as n, type BindableText as o, type BoundInputOptions as p, type ButtonOptions as q, type CompileOptions as r, type ComponentContext as s, type ComponentRender as t, type ComponentSetup as u, ContainerBuilderImpl as v, type ContainerOptions as w, type ContentDSL as x, type ControlledInputOptions as y, DerivedSignal as z };
@@ -7,7 +7,7 @@
7
7
  * (`streetui --version`) — the consolidated test-suite pins all three to the
8
8
  * same coordinated release so they can never silently drift apart.
9
9
  */
10
- declare const VERSION = "1.6.0";
10
+ declare const VERSION = "1.8.0";
11
11
 
12
12
  /**
13
13
  * StreetUI reactive signals — framework-owned reactivity, no external libraries.
@@ -195,7 +195,7 @@ declare function formatDiagnostic(d: Diagnostic): string;
195
195
  * in the Semantic Application Graph.
196
196
  */
197
197
 
198
- type SemanticNodeType = 'application' | 'page' | 'section' | 'container' | 'heading' | 'text' | 'button' | 'input' | 'form' | 'list' | 'list-item' | 'image' | 'link' | 'component' | 'slot' | 'fragment' | 'reactive-list' | 'conditional';
198
+ type SemanticNodeType = 'application' | 'page' | 'section' | 'container' | 'heading' | 'text' | 'button' | 'input' | 'form' | 'list' | 'list-item' | 'image' | 'link' | 'component' | 'slot' | 'fragment' | 'reactive-list' | 'conditional' | 'portal';
199
199
  interface NodeMetadata {
200
200
  readonly createdAt: number;
201
201
  readonly [key: string]: unknown;
@@ -341,6 +341,111 @@ interface SerializedGraph {
341
341
  readonly root: SerializedNode;
342
342
  }
343
343
 
344
+ /**
345
+ * First-class StreetUI-native components (§3–§9).
346
+ *
347
+ * A component is a *reusable unit that owns its own local state and lifecycle*.
348
+ * It is NOT a virtual-DOM element, a second reactive system, or a second
349
+ * renderer. It compiles into the EXISTING pipeline: `container.component(...)`
350
+ * creates a reserved `'component'` GraphNode (already a `SemanticNodeType`,
351
+ * already mapped to `<div>`), runs the component's `setup` synchronously during
352
+ * the same build-time descent every other builder uses, and routes any cleanups
353
+ * the setup registers into the node's `NodeInstance` at mount (so they run,
354
+ * children-first, when the component leaves the graph).
355
+ *
356
+ * Design (derived from the existing DSL architecture, not copied from React/Vue):
357
+ *
358
+ * const UserCard = component<{ name: Signal<string> }>((props, ctx) => {
359
+ * // ── setup: runs ONCE per instance, at build time ──
360
+ * const open = signal(false);
361
+ * ctx.effect(() => { ... }); // auto-disposed on unmount
362
+ * ctx.onCleanup(() => { ... }); // explicit teardown hook
363
+ * // ── render: fills the component's own container scope ──
364
+ * return (content) => {
365
+ * content.text(props.name); // fine-grained: signal prop, no re-setup
366
+ * content.when(open, (c) => c.text('expanded'));
367
+ * ctx.renderChildren(content); // where slotted children go (§6)
368
+ * };
369
+ * }, { name: 'UserCard' });
370
+ *
371
+ * page.component('card-1', UserCard, { name }, (slot) => slot.text('child'));
372
+ *
373
+ * Props are ordinary typed values (§5 — typing is a TypeScript concern, no
374
+ * runtime schema). Passing a `Signal<T>` prop and binding it in the render body
375
+ * gives fine-grained updates (§13) WITHOUT re-running `setup`: only the bound
376
+ * node mutates when the signal changes. `setup` re-runs only when the component
377
+ * is genuinely rebuilt (removed + re-created by a keyed list / conditional),
378
+ * at which point the previous instance is disposed first — so cleanup stays
379
+ * correct.
380
+ */
381
+
382
+ /**
383
+ * The render half of a component: fills the component's own container scope.
384
+ * Returned by `setup` so that per-instance state created in `setup` is captured
385
+ * by closure and the render body can read it.
386
+ */
387
+ type ComponentRender = (content: ContainerDSL) => void;
388
+ /**
389
+ * Lifecycle + composition surface handed to a component's `setup`. Local
390
+ * reactive state is created with the EXISTING `signal`/`derived`/`effect`/
391
+ * `batch` primitives (§8) — `ctx` only adds ownership: anything registered here
392
+ * is torn down automatically when the component leaves the graph (§9).
393
+ */
394
+ interface ComponentContext {
395
+ /** This instance's stable identity key (the `key` passed at the call site). */
396
+ readonly key: string;
397
+ /**
398
+ * Register a teardown callback. Runs when the component unmounts (children
399
+ * first, then this — mirroring `NodeInstance.dispose`). Use for resources,
400
+ * `form.dispose`, subscriptions, timers, etc.
401
+ */
402
+ onCleanup(fn: () => void): void;
403
+ /**
404
+ * Run a reactive effect owned by this component. Wraps the framework's
405
+ * `effect()`; the returned unsubscribe is auto-tracked and disposed on
406
+ * unmount, so component effects never leak (the pain point the audit ranked
407
+ * #1). The effect may itself return a cleanup, exactly like `effect()`.
408
+ */
409
+ effect(fn: () => void | (() => void)): void;
410
+ /**
411
+ * Render the caller-supplied children into `content` at this point (§6 native
412
+ * child composition / slots). No-op when the call site passed no children.
413
+ * Call it wherever the component wants its slotted content to appear.
414
+ */
415
+ renderChildren(content: ContainerDSL): void;
416
+ }
417
+ /**
418
+ * A component's setup function: receives typed props and the lifecycle context,
419
+ * creates any local state, and returns the render function. Runs synchronously
420
+ * during the build-time descent (like `errorBoundary`'s callback), so it must be
421
+ * SSR-safe — on the server it runs at render time and its cleanups run when the
422
+ * SSR root is disposed.
423
+ */
424
+ type ComponentSetup<P> = (props: P, ctx: ComponentContext) => ComponentRender;
425
+ /**
426
+ * The opaque, reusable definition produced by `component(...)`. Carries the
427
+ * `setup` and an inspectable `name`; the brand lets the builder method (and
428
+ * DevTools) recognise a definition at runtime without a class.
429
+ */
430
+ interface ComponentDefinition<P> {
431
+ readonly __streetui_component: true;
432
+ readonly name: string;
433
+ readonly setup: ComponentSetup<P>;
434
+ }
435
+ /**
436
+ * Define a reusable component. Returns a `ComponentDefinition` you render with
437
+ * `container.component(key, def, props, children?)`. This is a pure factory — it
438
+ * builds no graph and runs no `setup`; instantiation happens per call site.
439
+ *
440
+ * @param setup Runs once per instance: create local state, return the render fn.
441
+ * @param options.name Human-readable name for DevTools/`data-streetui-component`.
442
+ */
443
+ declare function component<P = Record<string, never>>(setup: ComponentSetup<P>, options?: {
444
+ name?: string;
445
+ }): ComponentDefinition<P>;
446
+ /** Runtime guard: is `value` a component definition? */
447
+ declare function isComponentDefinition(value: unknown): value is ComponentDefinition<unknown>;
448
+
344
449
  /**
345
450
  * StreetUI DSL type system.
346
451
  * All builder callbacks and option shapes live here.
@@ -385,6 +490,8 @@ interface A11yOptions {
385
490
  readonly ariaInvalid?: boolean;
386
491
  /** aria-required — mark a form field as required. */
387
492
  readonly ariaRequired?: boolean;
493
+ /** aria-modal — mark a dialog as modal (content outside is inert to AT). */
494
+ readonly ariaModal?: boolean;
388
495
  }
389
496
  interface TextOptions extends A11yOptions {
390
497
  readonly class?: string;
@@ -456,6 +563,30 @@ interface FormOptions extends ContainerOptions {
456
563
  }
457
564
  interface ListOptions extends ContainerOptions {
458
565
  }
566
+ /** Options for a plain portal (mount children into `document.body`). */
567
+ interface PortalOptions extends ContainerOptions {
568
+ }
569
+ /**
570
+ * Options shared by every overlay (dialog/popover/tooltip/dropdown/toast).
571
+ *
572
+ * An overlay is a portal + a reactive `when(open, …)` panel + focus/keyboard
573
+ * behavior. `open` drives visibility; the framework never mutates it — closing
574
+ * is cooperative: `onClose` fires on Escape (when `closeOnEscape`) and the app
575
+ * flips its own `open` signal there. Per-kind defaults (role, modality, focus,
576
+ * escape, restore) apply unless overridden here.
577
+ */
578
+ interface OverlayOptions extends ContainerOptions {
579
+ /** Reactive open/visibility state. When it flips, the panel mounts/unmounts. */
580
+ readonly open: Bindable<boolean>;
581
+ /** Requested-close callback (fired on Escape when `closeOnEscape`). Flip `open` here. */
582
+ readonly onClose?: () => void;
583
+ /** Restore focus to the previously-focused element on close. Default: per-kind. */
584
+ readonly restoreFocus?: boolean;
585
+ /** id of the element to focus first when the overlay opens (else first focusable). */
586
+ readonly initialFocusId?: string;
587
+ /** Escape key invokes `onClose`. Default: per-kind. */
588
+ readonly closeOnEscape?: boolean;
589
+ }
459
590
  type SectionBuilder = (section: SectionDSL) => void;
460
591
  type ContainerBuilder = (container: ContainerDSL) => void;
461
592
  type PageBuilder = (page: PageDSL) => void;
@@ -515,6 +646,54 @@ interface ContainerDSL extends ContentDSL {
515
646
  * removal. It does NOT trap arbitrary global errors; errors remain observable.
516
647
  */
517
648
  errorBoundary(id: string, builder: ContainerBuilder, options: ErrorBoundaryOptions): void;
649
+ /**
650
+ * Render `builder`'s subtree into `document.body` instead of inline at this
651
+ * position (a neutral inline anchor is left behind). On the server there is no
652
+ * body, so the content renders inline; hydration relocates it to a body
653
+ * container to match the browser. Use for content that must escape overflow/
654
+ * stacking contexts (overlays, toasts). Cleanup removes the body container.
655
+ */
656
+ portal(key: string, builder: ContainerBuilder, options?: PortalOptions): void;
657
+ /**
658
+ * Modal dialog: portal + `when(open, …)` panel with `role="dialog"`,
659
+ * `aria-modal="true"`, focus trap + containment, Escape-to-close, and focus
660
+ * restore on close. `builder` fills the dialog panel.
661
+ */
662
+ dialog(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
663
+ /**
664
+ * Non-modal popover: portal + `when(open, …)` panel with `role="dialog"`.
665
+ * Moves focus into the panel on open and restores it on close, but does not
666
+ * trap or contain focus. Escape closes by default.
667
+ */
668
+ popover(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
669
+ /**
670
+ * Tooltip: portal + `when(open, …)` panel with `role="tooltip"`. Non-modal
671
+ * and does not steal focus (tooltips describe another element); no Escape
672
+ * handling by default.
673
+ */
674
+ tooltip(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
675
+ /**
676
+ * Dropdown menu: portal + `when(open, …)` panel with `role="menu"`. Non-modal;
677
+ * moves focus into the menu on open, Escape closes, focus restored on close.
678
+ */
679
+ dropdown(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
680
+ /**
681
+ * Toast: portal + `when(open, …)` panel with `role="status"` and
682
+ * `aria-live="polite"`. Non-modal and never steals focus; no Escape handling.
683
+ */
684
+ toast(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
685
+ /**
686
+ * Instantiate a reusable `component()` at this position (§3–§9). Creates a
687
+ * `'component'` node (rendered as a `<div>` wrapper), runs the definition's
688
+ * `setup(props, ctx)` synchronously to obtain its render function, and fills
689
+ * the component's own container scope with it. Any `ctx.effect`/`ctx.onCleanup`
690
+ * registered by the setup is torn down automatically when the component leaves
691
+ * the graph. `props` are strongly typed by the definition's generic; pass
692
+ * `Signal` props for fine-grained updates that do NOT re-run `setup` (§13).
693
+ * The optional `children` builder is rendered wherever the component calls
694
+ * `ctx.renderChildren` (§6 native child composition).
695
+ */
696
+ component<P>(key: string, def: ComponentDefinition<P>, props: P, children?: ContainerBuilder): void;
518
697
  }
519
698
  interface SectionDSL extends ContainerDSL {
520
699
  }
@@ -576,6 +755,39 @@ declare class ContainerBuilderBase extends ContentBuilderBase implements Contain
576
755
  form(key: string, builder: FormBuilder, options?: FormOptions): void;
577
756
  when(condition: Bindable<boolean>, builder: ContainerBuilder, elseBuilder?: ContainerBuilder): void;
578
757
  errorBoundary(id: string, builder: ContainerBuilder, options: ErrorBoundaryOptions): void;
758
+ portal(key: string, builder: ContainerBuilder, options?: PortalOptions): void;
759
+ /**
760
+ * Shared assembly for every overlay kind: a `portal` node whose single child
761
+ * is a `when(open, panel)` conditional. The panel container carries the
762
+ * kind's ARIA semantics; `builder` fills it. An `__overlay__<portalId>`
763
+ * descriptor is registered so the renderer wires focus/keyboard behavior to
764
+ * the same `open` signal that drives the panel. Reuses existing primitives
765
+ * (portal + when + container) — no new render path.
766
+ */
767
+ private _overlay;
768
+ dialog(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
769
+ popover(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
770
+ tooltip(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
771
+ dropdown(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
772
+ toast(key: string, options: OverlayOptions, builder: ContainerBuilder): void;
773
+ /**
774
+ * Instantiate a reusable component (§3–§9). Creates a `'component'` node
775
+ * (rendered as a `<div>` wrapper — preserves the one-node/one-element
776
+ * positional-hydration invariant), then runs `def.setup(props, ctx)`
777
+ * synchronously to obtain the render function and fills the component's own
778
+ * container scope with it — structurally identical to `container`/
779
+ * `errorBoundary`. Cleanups the setup registers via `ctx.effect`/
780
+ * `ctx.onCleanup` are collected into a closure and exposed to the renderer
781
+ * through a `__component__<id>` handler (mirroring `__overlay__`); the mount/
782
+ * hydrate paths read it and route each into `NodeInstance.trackCleanup`, so
783
+ * teardown runs (children-first) when the component leaves the graph.
784
+ *
785
+ * `setup` runs once per instance here at build time. When this component sits
786
+ * inside a keyed list / conditional, a rebuild disposes the old instance
787
+ * (running its cleanups + pruning its `__component__` entry) and re-runs this
788
+ * method for the new node — so re-invocation is safe and leak-free.
789
+ */
790
+ component<P>(key: string, def: ComponentDefinition<P>, props: P, children?: ContainerBuilder): void;
579
791
  }
580
792
  declare class SectionBuilderImpl extends ContainerBuilderBase implements SectionDSL {
581
793
  }
@@ -668,4 +880,4 @@ declare function compile(app: StreetApp, options?: CompileOptions): CompiledAppl
668
880
  */
669
881
  declare function compileGraph(graph: ApplicationGraph, options?: CompileOptions): CompiledApplication;
670
882
 
671
- export { type ListDSL as $, type ApplicationId as A, BaseNode as B, type CompiledApplication as C, DiagnosticCollector as D, type ErrorBoundaryOptions as E, type ErrorFallbackBuilder as F, GraphNode as G, type ErrorSource as H, type EventDescriptor as I, type FormBuilder as J, FormBuilderImpl as K, type FormDSL as L, type FormOptions as M, type GraphNodeData as N, type HandlerFn as O, type PageDSL as P, type HeadingOptions as Q, type ReadonlySignal as R, StreetApp as S, type ImageOptions as T, type Unsubscribe as U, VERSION as V, type InputOptions as W, type InputOptionsBase as X, type LinkOptions as Y, type ListBuilder as Z, ListBuilderImpl as _, Signal as a, type ListOptions as a0, type ListPlanEntry as a1, type NodeId as a2, type NodeMetadata as a3, type PageBuilder as a4, PageBuilderImpl as a5, type PropValue as a6, type Props as a7, type ReactiveConsumer as a8, type ReactiveSource as a9, signal as aA, signalKind as aB, streetui as aC, type SectionBuilder as aa, SectionBuilderImpl as ab, type SectionDSL as ac, type SectionOptions as ad, type SerializedGraph as ae, type SerializedNode as af, type StateRef as ag, type StreetUI as ah, type TextOptions as ai, type TextValue as aj, batch as ak, compile as al, compileGraph as am, createNodeId as an, derived as ao, effect as ap, formatDiagnostic as aq, generateApplicationId as ar, generateNodeId as as, isBatching as at, nextId as au, nodeIdPrefix as av, observerCount as aw, reactiveListItemKey as ax, reactiveListItemSignature as ay, resetIdCounter as az, type Subscriber as b, ApplicationGraph as c, type SemanticNodeType as d, type ContainerDSL as e, type SignalKind as f, type A11yOptions as g, AppBuilder as h, type AppDSL as i, type AppOptions as j, type ApplicationGraphOptions as k, type Bindable as l, type BindableText as m, type BoundInputOptions as n, type ButtonOptions as o, type CompileOptions as p, type ContainerBuilder as q, ContainerBuilderImpl as r, type ContainerOptions as s, type ContentDSL as t, type ControlledInputOptions as u, DerivedSignal as v, type Diagnostic as w, DiagnosticError as x, type DiagnosticLocation as y, type DiagnosticSeverity as z };
883
+ export { type InputOptionsBase as $, type ApplicationId as A, BaseNode as B, type CompiledApplication as C, DiagnosticCollector as D, type Diagnostic as E, DiagnosticError as F, GraphNode as G, type DiagnosticLocation as H, type DiagnosticSeverity as I, type ErrorBoundaryOptions as J, type ErrorFallbackBuilder as K, type ErrorSource as L, type EventDescriptor as M, type FormBuilder as N, FormBuilderImpl as O, type PageDSL as P, type FormDSL as Q, type ReadonlySignal as R, StreetApp as S, type FormOptions as T, type Unsubscribe as U, VERSION as V, type GraphNodeData as W, type HandlerFn as X, type HeadingOptions as Y, type ImageOptions as Z, type InputOptions as _, type ComponentDefinition as a, type LinkOptions as a0, type ListBuilder as a1, ListBuilderImpl as a2, type ListDSL as a3, type ListOptions as a4, type ListPlanEntry as a5, type NodeId as a6, type NodeMetadata as a7, type OverlayOptions as a8, type PageBuilder as a9, isBatching as aA, isComponentDefinition as aB, nextId as aC, nodeIdPrefix as aD, observerCount as aE, reactiveListItemKey as aF, reactiveListItemSignature as aG, resetIdCounter as aH, signal as aI, signalKind as aJ, streetui as aK, PageBuilderImpl as aa, type PortalOptions as ab, type PropValue as ac, type Props as ad, type ReactiveConsumer as ae, type ReactiveSource as af, type SectionBuilder as ag, SectionBuilderImpl as ah, type SectionDSL as ai, type SectionOptions as aj, type SerializedGraph as ak, type SerializedNode as al, type StateRef as am, type StreetUI as an, type TextOptions as ao, type TextValue as ap, batch as aq, compile as ar, compileGraph as as, component as at, createNodeId as au, derived as av, effect as aw, formatDiagnostic as ax, generateApplicationId as ay, generateNodeId as az, type ContainerBuilder as b, Signal as c, type Subscriber as d, ApplicationGraph as e, type SemanticNodeType as f, type ContainerDSL as g, type SignalKind as h, type A11yOptions as i, AppBuilder as j, type AppDSL as k, type AppOptions as l, type ApplicationGraphOptions as m, type Bindable as n, type BindableText as o, type BoundInputOptions as p, type ButtonOptions as q, type CompileOptions as r, type ComponentContext as s, type ComponentRender as t, type ComponentSetup as u, ContainerBuilderImpl as v, type ContainerOptions as w, type ContentDSL as x, type ControlledInputOptions as y, DerivedSignal as z };
@@ -771,7 +771,7 @@ async function createProject(options) {
771
771
  }
772
772
 
773
773
  // ../cli/src/index.ts
774
- var CLI_VERSION = "1.6.1";
774
+ var CLI_VERSION = "1.8.0";
775
775
  var HELP = `streetui \u2014 the StreetUI application CLI
776
776
 
777
777
  Usage: