streetui 1.8.0 → 2.0.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.8.0";
10
+ declare const VERSION = "2.0.0";
11
11
 
12
12
  /**
13
13
  * StreetUI reactive signals — framework-owned reactivity, no external libraries.
@@ -128,6 +128,86 @@ declare function signalKind(source: ReadonlySignal<unknown>): SignalKind;
128
128
  */
129
129
  declare function observerCount(source: ReadonlySignal<unknown>): number | undefined;
130
130
 
131
+ /**
132
+ * StreetUI async resources — framework-native asynchronous data.
133
+ *
134
+ * A `resource` wraps a Promise-returning loader and exposes its lifecycle as
135
+ * ordinary StreetUI signals (status / data / error), so it composes with
136
+ * `derived`, `effect`, `when()`, `listOf` and the renderer with no second
137
+ * reactive system.
138
+ *
139
+ * State machine:
140
+ *
141
+ * idle ──(load)──▶ loading ──(resolve)──▶ success
142
+ * │
143
+ * └────(reject)──────▶ error
144
+ *
145
+ * Refetch keeps the previously-loaded `data` visible while `status` is
146
+ * `'loading'` again (see `isRefetching`) — there is no separate `'refetching'`
147
+ * status; it is expressed through `status === 'loading'` with `data` still set.
148
+ *
149
+ * The resource is transport-agnostic: the loader is any function returning a
150
+ * value or a Promise. When it accepts the provided `AbortSignal`, in-flight
151
+ * work is cancelled on `dispose()` or when a newer request supersedes it.
152
+ */
153
+
154
+ type ResourceStatus = 'idle' | 'loading' | 'success' | 'error';
155
+ /** Context handed to the loader; carries an AbortSignal for cancellation. */
156
+ interface ResourceLoaderContext {
157
+ readonly signal: AbortSignal;
158
+ }
159
+ /** Any value-or-Promise producing function. Receives an abort-aware context. */
160
+ type ResourceLoader<T> = (ctx: ResourceLoaderContext) => Promise<T> | T;
161
+ interface ResourceOptions<T = unknown> {
162
+ /** Load immediately on creation. Defaults to `true`. When `false`, stays `idle` until `refetch()`. */
163
+ readonly immediate?: boolean;
164
+ /**
165
+ * Explicit reactive dependencies. When any listed signal changes, the
166
+ * resource refetches. Dependencies are explicit (not auto-tracked from the
167
+ * loader body) so there is no risk of an accidental infinite refetch loop.
168
+ */
169
+ readonly watch?: ReadonlyArray<ReadonlySignal<unknown>>;
170
+ /**
171
+ * Optional teardown registrar (e.g. a route's `ctx.onCleanup`). When given,
172
+ * the resource registers its own `dispose` so it is cleaned up automatically
173
+ * when its owner is removed.
174
+ */
175
+ readonly onCleanup?: (fn: () => void) => void;
176
+ /**
177
+ * Server-provided initial value for hydration. When present the resource
178
+ * starts in `'success'` with this data already visible, and the initial
179
+ * auto-load is skipped (so the client does not refetch data the server
180
+ * already resolved). This is the client half of SSR resource transfer; the
181
+ * server side awaits `refetch()` before serializing. Set `immediate: true`
182
+ * explicitly to force a client refetch anyway.
183
+ */
184
+ readonly initialData?: T;
185
+ /** Server-provided initial error for hydration (mirrors `initialData`). */
186
+ readonly initialError?: unknown;
187
+ /**
188
+ * Explicit initial status override. Rarely needed — inferred as `'success'`
189
+ * from `initialData` or `'error'` from `initialError`.
190
+ */
191
+ readonly initialStatus?: ResourceStatus;
192
+ }
193
+ interface Resource<T> {
194
+ /** Reactive lifecycle status. */
195
+ readonly status: ReadonlySignal<ResourceStatus>;
196
+ /** The last successfully-loaded value, or `undefined` before first success. */
197
+ readonly data: ReadonlySignal<T | undefined>;
198
+ /** The most recent error, or `undefined` when there is none. Typed `unknown` — never `any`. */
199
+ readonly error: ReadonlySignal<unknown>;
200
+ /** Convenience: `status === 'loading'`. */
201
+ readonly loading: ReadonlySignal<boolean>;
202
+ /** Convenience: loading while previously-loaded data is still present (a refetch). */
203
+ readonly isRefetching: ReadonlySignal<boolean>;
204
+ /** Trigger a new request. Resolves when the request settles (or is superseded). */
205
+ refetch(): Promise<void>;
206
+ /** Cancel in-flight work, drop watchers, and ignore any late results. Idempotent. */
207
+ dispose(): void;
208
+ }
209
+ declare function resource<T>(loader: ResourceLoader<T>, options?: ResourceOptions<T>): Resource<T>;
210
+
131
211
  /**
132
212
  * Node and application identity utilities.
133
213
  * Every node in the semantic graph has a stable, unique identity.
@@ -195,7 +275,7 @@ declare function formatDiagnostic(d: Diagnostic): string;
195
275
  * in the Semantic Application Graph.
196
276
  */
197
277
 
198
- type SemanticNodeType = 'application' | 'page' | 'section' | 'container' | 'heading' | 'text' | 'button' | 'input' | 'form' | 'list' | 'list-item' | 'image' | 'link' | 'component' | 'slot' | 'fragment' | 'reactive-list' | 'conditional' | 'portal';
278
+ type SemanticNodeType = 'application' | 'page' | 'section' | 'container' | 'heading' | 'text' | 'button' | 'input' | 'form' | 'list' | 'list-item' | 'image' | 'link' | 'component' | 'slot' | 'fragment' | 'reactive-list' | 'conditional' | 'portal' | 'head';
199
279
  interface NodeMetadata {
200
280
  readonly createdAt: number;
201
281
  readonly [key: string]: unknown;
@@ -446,6 +526,218 @@ declare function component<P = Record<string, never>>(setup: ComponentSetup<P>,
446
526
  /** Runtime guard: is `value` a component definition? */
447
527
  declare function isComponentDefinition(value: unknown): value is ComponentDefinition<unknown>;
448
528
 
529
+ /**
530
+ * Transition configuration (§2, §4).
531
+ *
532
+ * A `TransitionConfig` is a pure, declarative description of a CSS class-based
533
+ * enter/leave transition — the engine choice for this milestone. It contains NO
534
+ * DOM references, NO timers and NO browser-only APIs, so it is safe to build on
535
+ * the server (where it is simply ignored — see the renderer's SSR guard) and to
536
+ * carry on a graph handler (`__transition__<nodeId>`) alongside the existing
537
+ * `__overlay__`/`__component__` descriptors.
538
+ *
539
+ * The class model follows the widely-understood enter/leave convention:
540
+ *
541
+ * enter: [enterActive (+ enter) whole phase] · enterFrom (start) → enterTo (end)
542
+ * leave: [leaveActive (+ leave) whole phase] · leaveFrom (start) → leaveTo (end)
543
+ *
544
+ * `name` is a shorthand that expands to `${name}-enter-from`,
545
+ * `${name}-enter-active`, `${name}-enter-to` and the leave equivalents; explicit
546
+ * class fields override the derived ones. Because the classes are just strings,
547
+ * SSR output is deterministic (no class is applied on the server at all — the
548
+ * controller is browser-only), satisfying §4/§21.
549
+ */
550
+ interface TransitionConfig {
551
+ /** Shorthand base: expands to `${name}-enter-from`, `${name}-enter-active`, … */
552
+ readonly name?: string;
553
+ /** Class(es) present for the whole enter phase (in addition to `enterActive`). */
554
+ readonly enter?: string;
555
+ /** Class(es) applied at the start of enter, removed on the next frame. */
556
+ readonly enterFrom?: string;
557
+ /** Class(es) present for the whole enter phase (where the CSS `transition` lives). */
558
+ readonly enterActive?: string;
559
+ /** Class(es) added on the next frame, removed when enter completes. */
560
+ readonly enterTo?: string;
561
+ /** Class(es) present for the whole leave phase (in addition to `leaveActive`). */
562
+ readonly leave?: string;
563
+ /** Class(es) applied at the start of leave, removed on the next frame. */
564
+ readonly leaveFrom?: string;
565
+ /** Class(es) present for the whole leave phase (where the CSS `transition` lives). */
566
+ readonly leaveActive?: string;
567
+ /** Class(es) added on the next frame, removed when leave completes. */
568
+ readonly leaveTo?: string;
569
+ /**
570
+ * Also animate the very first appearance (initial mount). Hydration never
571
+ * animates appear (the DOM is already present and correct); this only affects
572
+ * fresh browser mounts. Defaults to false.
573
+ */
574
+ readonly appear?: boolean;
575
+ /**
576
+ * Fallback completion timeout in milliseconds. A transition normally completes
577
+ * on the element's `transitionend`/`animationend`. This timeout is the safety
578
+ * net for (a) transitions that fire no such event and (b) test DOMs like
579
+ * happy-dom that dispatch no transition events at all — making tests
580
+ * deterministic without a real browser (§24). Defaults to 1000. Use a small
581
+ * value (or 0 → next macrotask) in tests.
582
+ */
583
+ readonly duration?: number;
584
+ }
585
+ /**
586
+ * The resolved, ready-to-apply form of a {@link TransitionConfig}: each phase's
587
+ * classes are pre-split into arrays so the controller applies/removes them with
588
+ * no per-run string parsing. Produced once by {@link resolveTransition} at wire
589
+ * time (browser only).
590
+ */
591
+ interface ResolvedTransition {
592
+ /** enter classes present for the whole phase (base `enter` + `enterActive`). */
593
+ readonly enterActive: readonly string[];
594
+ /** enter start classes (removed next frame). */
595
+ readonly enterFrom: readonly string[];
596
+ /** enter end classes (added next frame). */
597
+ readonly enterTo: readonly string[];
598
+ readonly leaveActive: readonly string[];
599
+ readonly leaveFrom: readonly string[];
600
+ readonly leaveTo: readonly string[];
601
+ readonly appear: boolean;
602
+ readonly duration: number;
603
+ }
604
+ /**
605
+ * Resolve a {@link TransitionConfig} into applied class arrays. The `name`
606
+ * shorthand supplies defaults; any explicit field overrides the derived class
607
+ * for that phase-slot (still merged with `enter`/`leave` base classes).
608
+ */
609
+ declare function resolveTransition(config: TransitionConfig): ResolvedTransition;
610
+ /** Runtime brand check for a transition descriptor value. */
611
+ declare function isTransitionConfig(value: unknown): value is TransitionConfig;
612
+
613
+ /**
614
+ * Document head / metadata model (2.0 §1–§3).
615
+ *
616
+ * `head({...})` is a first-class StreetUI primitive for declaring document
617
+ * metadata — title, meta, link, canonical, Open Graph, Twitter/X, robots,
618
+ * theme-color and favicon. It is NOT a copy of another framework's API: it is
619
+ * expressed on StreetUI's own component/graph model. A `head()` call creates a
620
+ * `'head'` graph node (rendered as a neutral inline anchor, like a portal) and
621
+ * registers a `__head__<nodeId>` descriptor — exactly the handler-registry
622
+ * convention used by `__overlay__`/`__transition__`/`__component__`. The
623
+ * renderer reads that descriptor to:
624
+ *
625
+ * • apply the contribution to `document.head` on the browser
626
+ * (`wireHeadBehavior`), adopting server-emitted tags on hydration so there
627
+ * are no duplicates, and cleaning up its own tags on unmount / route change;
628
+ * • emit only the active graph's merged metadata as an HTML string on the
629
+ * server (`renderHead`).
630
+ *
631
+ * This module is pure and DOM-free (like `transition.ts`): it defines the config
632
+ * shape and normalises it into an ordered list of {@link HeadEntry} with stable
633
+ * *dedup keys*. All merge/precedence/DOM work happens in the renderer, keyed by
634
+ * these entries.
635
+ *
636
+ * ── Deduplication & precedence (§3) ──────────────────────────────────────────
637
+ * Every entry carries a `dedupKey`. When several `head()` nodes are live at once
638
+ * (e.g. an app-level default, a route-level `head()`, and a component-level
639
+ * `head()`), the renderer merges all of their entries and, for each `dedupKey`,
640
+ * the LAST contribution in document order wins. Document order is pre-order DFS
641
+ * = mount order, so a `head()` declared deeper/later (a route or a component
642
+ * nested inside the app shell) deterministically overrides an app-level default
643
+ * for the same key. Removing that node (navigating away, unmounting the
644
+ * component) re-exposes the previously-shadowed default. `<title>` and each
645
+ * single-instance meta/link (description, robots, theme-color, viewport,
646
+ * charset, canonical, favicon, and each og: or twitter: property) collapse to one
647
+ * effective tag; generic `meta[]`/`link[]` array entries are keyed by their
648
+ * identifying attributes so independent tags coexist.
649
+ */
650
+
651
+ /** A head value that may be a literal string or a reactive signal of a string. */
652
+ type BindableString = string | ReadonlySignal<string> | Signal<string>;
653
+ /** A single `<meta>` descriptor. Provide exactly one identifying key. */
654
+ interface MetaDescriptor {
655
+ /** `name="…"` (e.g. "description", "robots", "theme-color", "twitter:card"). */
656
+ readonly name?: string;
657
+ /** `property="…"` (e.g. "og:title", "og:image") — the Open Graph convention. */
658
+ readonly property?: string;
659
+ /** `http-equiv="…"` (e.g. "content-security-policy"). */
660
+ readonly httpEquiv?: string;
661
+ /** `charset="…"` (e.g. "utf-8"). Standalone; no `content`. */
662
+ readonly charset?: string;
663
+ /** The tag's `content`. May be reactive. */
664
+ readonly content?: BindableString;
665
+ }
666
+ /** A single `<link>` descriptor. `rel`+`href` identify it. */
667
+ interface LinkDescriptor {
668
+ readonly rel: string;
669
+ readonly href: string;
670
+ readonly sizes?: string;
671
+ readonly type?: string;
672
+ readonly media?: string;
673
+ readonly as?: string;
674
+ readonly crossorigin?: string;
675
+ readonly hreflang?: string;
676
+ }
677
+ /**
678
+ * Declarative document metadata. Every field is optional; convenience fields
679
+ * (title/description/canonical/robots/themeColor/viewport/charset/favicon/
680
+ * openGraph/twitter) expand into the same normalized entries as the raw
681
+ * `meta`/`link` arrays, with single-instance dedup keys so a later `head()`
682
+ * cleanly overrides an earlier one.
683
+ */
684
+ interface HeadMetadata {
685
+ /** `<title>` text. May be reactive. Single-instance (dedup key `title`). */
686
+ readonly title?: BindableString;
687
+ /** `<meta name="description">`. May be reactive. */
688
+ readonly description?: BindableString;
689
+ /** `<link rel="canonical">` href. */
690
+ readonly canonical?: string;
691
+ /** `<meta name="robots">` (e.g. "index,follow" / "noindex"). */
692
+ readonly robots?: string;
693
+ /** `<meta name="theme-color">`. */
694
+ readonly themeColor?: string;
695
+ /** `<meta name="viewport">`. */
696
+ readonly viewport?: string;
697
+ /** `<meta charset>`. */
698
+ readonly charset?: string;
699
+ /** Favicon: a shorthand for `<link rel="icon">`. String = href, or a full descriptor. */
700
+ readonly favicon?: string | LinkDescriptor;
701
+ /** Open Graph properties — each key `k` becomes `<meta property="og:k">`. Values may be reactive. */
702
+ readonly openGraph?: Readonly<Record<string, BindableString>>;
703
+ /** Twitter/X card properties — each key `k` becomes `<meta name="twitter:k">`. Values may be reactive. */
704
+ readonly twitter?: Readonly<Record<string, BindableString>>;
705
+ /** Raw `<meta>` tags (for anything the convenience fields don't cover). */
706
+ readonly meta?: readonly MetaDescriptor[];
707
+ /** Raw `<link>` tags (stylesheets, preload, alternate, etc.). */
708
+ readonly link?: readonly LinkDescriptor[];
709
+ /** `<base href>` — single-instance. */
710
+ readonly base?: string;
711
+ }
712
+ /**
713
+ * A normalized head tag: one `<title>`, `<meta>`, `<link>` or `<base>`. Attr
714
+ * values and the title's text may still be reactive (`BindableString`); the
715
+ * renderer peeks them for SSR and subscribes to them on the browser. `dedupKey`
716
+ * is what the merge collapses on.
717
+ */
718
+ interface HeadEntry {
719
+ readonly tag: 'title' | 'meta' | 'link' | 'base';
720
+ readonly dedupKey: string;
721
+ /** Static + reactive attributes (no `undefined` values). */
722
+ readonly attrs: Readonly<Record<string, BindableString>>;
723
+ /** Text content — only meaningful for `tag === 'title'`. */
724
+ readonly text?: BindableString;
725
+ }
726
+ /** The value a `__head__<id>` handler returns: this node's ordered contribution. */
727
+ interface HeadContribution {
728
+ readonly entries: readonly HeadEntry[];
729
+ }
730
+ /**
731
+ * Normalise a {@link HeadMetadata} into an ordered list of {@link HeadEntry}.
732
+ * Emission order within one `head()` is: charset → base → title → description →
733
+ * canonical → robots → theme-color → viewport → favicon → openGraph → twitter →
734
+ * explicit meta[] → explicit link[]. (Merge across nodes is document order; this
735
+ * per-node order only affects the sequence of same-priority tags.)
736
+ */
737
+ declare function resolveHead(config: HeadMetadata): HeadContribution;
738
+ /** Runtime brand check for a head-contribution descriptor value. */
739
+ declare function isHeadContribution(value: unknown): value is HeadContribution;
740
+
449
741
  /**
450
742
  * StreetUI DSL type system.
451
743
  * All builder callbacks and option shapes live here.
@@ -492,6 +784,14 @@ interface A11yOptions {
492
784
  readonly ariaRequired?: boolean;
493
785
  /** aria-modal — mark a dialog as modal (content outside is inert to AT). */
494
786
  readonly ariaModal?: boolean;
787
+ /** aria-owns — id(s) of elements owned by this one when the DOM can't express it. */
788
+ readonly ariaOwns?: string;
789
+ /** aria-activedescendant — id of the active option in a composite widget (menu/listbox/combobox). */
790
+ readonly ariaActiveDescendant?: string;
791
+ /** aria-haspopup — the element opens a popup ('menu' | 'listbox' | 'dialog' | 'grid' | 'tree' | true). */
792
+ readonly ariaHasPopup?: boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';
793
+ /** aria-selected — selection state within a composite widget. */
794
+ readonly ariaSelected?: boolean;
495
795
  }
496
796
  interface TextOptions extends A11yOptions {
497
797
  readonly class?: string;
@@ -555,6 +855,15 @@ interface ContainerOptions extends A11yOptions {
555
855
  readonly class?: string;
556
856
  readonly id?: string;
557
857
  readonly key?: string;
858
+ /**
859
+ * Enter/leave transition for this element (§2). CSS class-based and
860
+ * browser-only: on the server it is ignored (deterministic SSR output). The
861
+ * enter animation runs when the element is added by a reactive `when`/`listOf`
862
+ * change (or on initial mount when `appear` is set); the leave animation runs
863
+ * before the element is removed and disposed — the reconciler defers teardown
864
+ * until the transition completes.
865
+ */
866
+ readonly transition?: TransitionConfig;
558
867
  }
559
868
  interface SectionOptions extends ContainerOptions {
560
869
  }
@@ -562,6 +871,21 @@ interface FormOptions extends ContainerOptions {
562
871
  readonly onSubmit?: (e: Event) => void;
563
872
  }
564
873
  interface ListOptions extends ContainerOptions {
874
+ /**
875
+ * Enter/leave transition applied to each list item (§7). Preserves keyed
876
+ * identity: reordering reuses items (no leave/enter), append/prepend enter,
877
+ * remove leaves before disposal, and a removed key that reappears mid-leave is
878
+ * reclaimed (leave→enter). `transition` (inherited) applies to the list
879
+ * container itself; `itemTransition` applies to its rows.
880
+ */
881
+ readonly itemTransition?: TransitionConfig;
882
+ }
883
+ /** Options for `when()` (§2 conditional transitions). */
884
+ interface WhenOptions {
885
+ /** Transition applied to the active branch as it mounts/unmounts. */
886
+ readonly transition?: TransitionConfig;
887
+ /** Also animate the branch present on the initial mount (appear). */
888
+ readonly appear?: boolean;
565
889
  }
566
890
  /** Options for a plain portal (mount children into `document.body`). */
567
891
  interface PortalOptions extends ContainerOptions {
@@ -606,6 +930,40 @@ interface ErrorBoundaryOptions {
606
930
  readonly source?: ErrorSource | ReadonlyArray<ErrorSource>;
607
931
  /** Invoked by the fallback's `retry()`, before the body is re-attempted (e.g. `resource.refetch`). */
608
932
  readonly onRetry?: () => void;
933
+ /**
934
+ * Error-reporting hook (§6/§7). Called with the current error each time the
935
+ * boundary ENTERS its error state (i.e. when the fallback mounts), including
936
+ * on a re-entry after a failed retry. Use it to forward the error to a
937
+ * production diagnostics sink. It observes only — it never changes the
938
+ * boundary's behavior, and receives the same `unknown` error the fallback
939
+ * sees (no sensitive framework internals are injected).
940
+ */
941
+ readonly onError?: (error: unknown) => void;
942
+ }
943
+ /**
944
+ * Branch builders for {@link ContainerDSL.asyncBoundary} — the loading / error /
945
+ * success states of a {@link Resource}. This is deliberately NOT a new async
946
+ * system: it is thin sugar over the existing `resource` state machine and
947
+ * `when()`. Exactly one branch is live at a time, chosen by the resource's
948
+ * reactive `status`/`data` (error takes precedence, then resolved data, then
949
+ * loading), so mounting/unmounting and cleanup all reuse the conditional
950
+ * machinery. Branch signature mirrors `errorBoundary`'s fallback: the content
951
+ * scope comes first.
952
+ */
953
+ interface AsyncBoundaryBranches<T> {
954
+ /** Shown while the resource is idle or performing its first load (no data yet). */
955
+ readonly loading?: (content: ContainerDSL) => void;
956
+ /**
957
+ * Shown while the resource is in its error state. Receives the current error
958
+ * and a `retry()` that re-runs the loader (a thin wrapper over `refetch`).
959
+ */
960
+ readonly error?: (content: ContainerDSL, error: unknown, retry: () => void) => void;
961
+ /**
962
+ * Shown once the resource has data (including while a refetch keeps the old
963
+ * value visible). Receives the data as a `ReadonlySignal<T>` so the branch can
964
+ * bind it reactively and update in place without remounting.
965
+ */
966
+ readonly success: (content: ContainerDSL, data: ReadonlySignal<T>) => void;
609
967
  }
610
968
  interface ContentDSL {
611
969
  heading(text: BindableText, options?: HeadingOptions): void;
@@ -634,7 +992,7 @@ interface ContainerDSL extends ContentDSL {
634
992
  * renders while the condition is false. Compiles into the same reactive
635
993
  * reconciliation machinery as `listOf` — there is no separate render path.
636
994
  */
637
- when(condition: Bindable<boolean>, builder: ContainerBuilder, elseBuilder?: ContainerBuilder): void;
995
+ when(condition: Bindable<boolean>, builder: ContainerBuilder, elseBuilder?: ContainerBuilder, options?: WhenOptions): void;
638
996
  /**
639
997
  * Render `builder`, but swap to `options.fallback` when the boundary enters an
640
998
  * error state. A boundary enters that state when (a) any observed `source`
@@ -646,6 +1004,23 @@ interface ContainerDSL extends ContentDSL {
646
1004
  * removal. It does NOT trap arbitrary global errors; errors remain observable.
647
1005
  */
648
1006
  errorBoundary(id: string, builder: ContainerBuilder, options: ErrorBoundaryOptions): void;
1007
+ /**
1008
+ * Render the loading / error / success states of an async {@link Resource}
1009
+ * (§4). This is sugar over the existing `resource` state machine and `when()`
1010
+ * — NOT a second async system and NOT a literal port of React Suspense. It
1011
+ * renders exactly one branch at a time based on the resource's reactive state:
1012
+ * the `error` branch while `status === 'error'` (with a `retry()` that calls
1013
+ * `refetch`), otherwise the `success` branch once data is present (data passed
1014
+ * as a `ReadonlySignal<T>` so it updates in place, and stays visible while a
1015
+ * refetch is in flight), otherwise the `loading` branch. Because it is built
1016
+ * from `when()`, SSR renders whichever branch matches the resource's current
1017
+ * (peeked) state — so a server that awaits the resource before serializing
1018
+ * emits the resolved `success` branch, and hydration (seeded via the
1019
+ * resource's `initialData`) reuses it with no duplicate work. Resource
1020
+ * cleanup/cancellation is the resource's own concern (pass its `dispose` to
1021
+ * the owning scope's `onCleanup`, e.g. a component's `ctx.onCleanup`).
1022
+ */
1023
+ asyncBoundary<T>(key: string, resource: Resource<T>, branches: AsyncBoundaryBranches<T>): void;
649
1024
  /**
650
1025
  * Render `builder`'s subtree into `document.body` instead of inline at this
651
1026
  * position (a neutral inline anchor is left behind). On the server there is no
@@ -694,6 +1069,18 @@ interface ContainerDSL extends ContentDSL {
694
1069
  * `ctx.renderChildren` (§6 native child composition).
695
1070
  */
696
1071
  component<P>(key: string, def: ComponentDefinition<P>, props: P, children?: ContainerBuilder): void;
1072
+ /**
1073
+ * Declare document metadata (§1–§3): title, meta, link, canonical, Open Graph,
1074
+ * Twitter/X, robots, theme-color, favicon. Renders nothing visible at this
1075
+ * position (a neutral empty anchor); the framework applies the metadata to
1076
+ * `document.head` on the browser and emits it via `renderHead()` on the
1077
+ * server. Several `head()` calls compose — an app-level default, a route-level
1078
+ * `head()`, and a component-level `head()` are merged and, per metadata key,
1079
+ * the last declared (deepest/latest in document order) wins. Removing a
1080
+ * `head()` node (route change / component unmount) re-exposes the previously
1081
+ * shadowed value, and server tags are adopted on hydration without duplication.
1082
+ */
1083
+ head(metadata: HeadMetadata): void;
697
1084
  }
698
1085
  interface SectionDSL extends ContainerDSL {
699
1086
  }
@@ -753,9 +1140,25 @@ declare class ContainerBuilderBase extends ContentBuilderBase implements Contain
753
1140
  list(key: string, builder: ListBuilder, options?: ListOptions): void;
754
1141
  listOf<T>(key: string, items: Signal<T[]> | ReadonlySignal<T[]>, renderItem: (item: T, index: number, content: ContentDSL) => void, options?: ListOptions): void;
755
1142
  form(key: string, builder: FormBuilder, options?: FormOptions): void;
756
- when(condition: Bindable<boolean>, builder: ContainerBuilder, elseBuilder?: ContainerBuilder): void;
1143
+ when(condition: Bindable<boolean>, builder: ContainerBuilder, elseBuilder?: ContainerBuilder, options?: WhenOptions): void;
757
1144
  errorBoundary(id: string, builder: ContainerBuilder, options: ErrorBoundaryOptions): void;
1145
+ asyncBoundary<T>(key: string, res: Resource<T>, branches: AsyncBoundaryBranches<T>): void;
758
1146
  portal(key: string, builder: ContainerBuilder, options?: PortalOptions): void;
1147
+ /**
1148
+ * Declare document metadata (2.0 §1–§3). Creates a `'head'` node — a neutral,
1149
+ * empty inline anchor at this position (one node / one element, so positional
1150
+ * hydration is preserved) — and registers a `__head__<id>` descriptor holding
1151
+ * this call's normalized, dedup-keyed {@link resolveHead} contribution. The
1152
+ * renderer applies it to `document.head` on the browser (adopting server tags
1153
+ * on hydration, cleaning up on unmount / route change) and emits the active
1154
+ * graph's merged metadata as a string on the server (`renderHead`).
1155
+ *
1156
+ * Multiple `head()` nodes may be live at once (app default + route + component)
1157
+ * — the renderer merges them and, per dedup key, the last in document order
1158
+ * wins (see head.ts). No new render path: this reuses the same graph-node +
1159
+ * handler-registry convention as overlays/transitions/components.
1160
+ */
1161
+ head(metadata: HeadMetadata): void;
759
1162
  /**
760
1163
  * Shared assembly for every overlay kind: a `portal` node whose single child
761
1164
  * is a `when(open, panel)` conditional. The panel container carries the
@@ -880,4 +1283,67 @@ declare function compile(app: StreetApp, options?: CompileOptions): CompiledAppl
880
1283
  */
881
1284
  declare function compileGraph(graph: ApplicationGraph, options?: CompileOptions): CompiledApplication;
882
1285
 
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 };
1286
+ /**
1287
+ * Hydration diagnostics — dev-only, opt-in explanations of hydration mismatches.
1288
+ *
1289
+ * Hydration is self-repairing: when the server-rendered DOM does not match the
1290
+ * graph at a position, the renderer mounts a fresh subtree in place and drops
1291
+ * the offending element (see `hydrateChildren` in `hydrate.ts`). That recovery
1292
+ * is silent by design — a local mismatch must never tear down the whole app.
1293
+ *
1294
+ * During development, though, a silent repair hides a real problem (usually a
1295
+ * server/client divergence). A `HydrationDiagnosticSink` can be attached to the
1296
+ * renderer to *observe* those repairs without changing them: for every mismatch
1297
+ * the renderer reports what it expected, what it found, where, and what it did
1298
+ * to recover. Nothing is thrown, nothing is mutated differently, and when no
1299
+ * sink is attached there is zero additional work on the hydration path.
1300
+ */
1301
+ /** What kind of divergence the hydrator encountered at a position. */
1302
+ type HydrationMismatchType = 'tag-mismatch' | 'missing-element' | 'surplus-element';
1303
+ /** A single, fully-described hydration divergence and the repair taken. */
1304
+ interface HydrationDiagnostic {
1305
+ /** The category of mismatch. */
1306
+ readonly type: HydrationMismatchType;
1307
+ /** The tag the graph expected at this position (null for a surplus element). */
1308
+ readonly expected: string | null;
1309
+ /** The tag actually found in the server DOM (null for a missing element). */
1310
+ readonly found: string | null;
1311
+ /** A human-readable path to the position, e.g. `app / page[0] / section[1]`. */
1312
+ readonly path: string;
1313
+ /** The graph node id involved, when one exists (null for surplus DOM). */
1314
+ readonly nodeId: string | null;
1315
+ /** The semantic node type involved, when one exists (null for surplus DOM). */
1316
+ readonly nodeType: string | null;
1317
+ /** The recovery action the renderer performed. */
1318
+ readonly action: string;
1319
+ /** A single-line, developer-facing summary of the whole diagnostic. */
1320
+ readonly message: string;
1321
+ }
1322
+ /**
1323
+ * Receives hydration diagnostics as they are discovered. Kept intentionally
1324
+ * tiny so any logger — `console`, a test collector, a `DiagnosticSink` — can
1325
+ * satisfy it. Implementations must not throw.
1326
+ */
1327
+ interface HydrationDiagnosticSink {
1328
+ report(diagnostic: HydrationDiagnostic): void;
1329
+ }
1330
+ /** Build the canonical one-line message for a diagnostic. */
1331
+ declare function formatHydrationDiagnostic(d: Omit<HydrationDiagnostic, 'message'>): string;
1332
+ /**
1333
+ * A ready-made sink that accumulates diagnostics into an array — the shape most
1334
+ * useful for tests and for a DevTools panel. The returned `diagnostics` array is
1335
+ * appended to in-place as repairs happen.
1336
+ */
1337
+ declare function createHydrationDiagnosticCollector(): {
1338
+ readonly sink: HydrationDiagnosticSink;
1339
+ readonly diagnostics: HydrationDiagnostic[];
1340
+ };
1341
+ /**
1342
+ * A sink that forwards each diagnostic to a `console`-like logger as a single
1343
+ * warning line. Handy default when you just want the messages surfaced in dev.
1344
+ */
1345
+ declare function consoleHydrationDiagnosticSink(logger?: {
1346
+ warn(message: string): void;
1347
+ }): HydrationDiagnosticSink;
1348
+
1349
+ export { type FormBuilder as $, type ApplicationId as A, BaseNode as B, type CompiledApplication as C, DiagnosticCollector as D, type ComponentRender as E, type ComponentSetup as F, GraphNode as G, type HydrationDiagnostic as H, ContainerBuilderImpl as I, type ContainerOptions as J, type ContentDSL as K, type ControlledInputOptions as L, DerivedSignal as M, type Diagnostic as N, DiagnosticError as O, type PageDSL as P, type DiagnosticLocation as Q, type ReadonlySignal as R, StreetApp as S, type TransitionConfig as T, type Unsubscribe as U, VERSION as V, type DiagnosticSeverity as W, type ErrorBoundaryOptions as X, type ErrorFallbackBuilder as Y, type ErrorSource as Z, type EventDescriptor as _, type ComponentDefinition as a, observerCount as a$, FormBuilderImpl as a0, type FormDSL as a1, type FormOptions as a2, type GraphNodeData as a3, type HandlerFn as a4, type HeadContribution as a5, type HeadEntry as a6, type HeadMetadata as a7, type HeadingOptions as a8, type HydrationMismatchType as a9, type SectionOptions as aA, type SerializedGraph as aB, type SerializedNode as aC, type StateRef as aD, type StreetUI as aE, type TextOptions as aF, type TextValue as aG, type WhenOptions as aH, batch as aI, compile as aJ, compileGraph as aK, component as aL, consoleHydrationDiagnosticSink as aM, createHydrationDiagnosticCollector as aN, createNodeId as aO, derived as aP, effect as aQ, formatDiagnostic as aR, formatHydrationDiagnostic as aS, generateApplicationId as aT, generateNodeId as aU, isBatching as aV, isComponentDefinition as aW, isHeadContribution as aX, isTransitionConfig as aY, nextId as aZ, nodeIdPrefix as a_, type ImageOptions as aa, type InputOptions as ab, type InputOptionsBase as ac, type LinkDescriptor as ad, type LinkOptions as ae, type ListBuilder as af, ListBuilderImpl as ag, type ListDSL as ah, type ListOptions as ai, type ListPlanEntry as aj, type MetaDescriptor as ak, type NodeId as al, type NodeMetadata as am, type OverlayOptions as an, type PageBuilder as ao, PageBuilderImpl as ap, type PortalOptions as aq, type PropValue as ar, type Props as as, type ReactiveConsumer as at, type ReactiveSource as au, type ResolvedTransition as av, type ResourceLoader as aw, type SectionBuilder as ax, SectionBuilderImpl as ay, type SectionDSL as az, type ContainerBuilder as b, reactiveListItemKey as b0, reactiveListItemSignature as b1, resetIdCounter as b2, resolveHead as b3, resolveTransition as b4, resource as b5, signal as b6, signalKind as b7, streetui as b8, Signal as c, type Subscriber as d, type ResourceStatus as e, type ResourceOptions as f, type Resource as g, type ResourceLoaderContext as h, ApplicationGraph as i, type HydrationDiagnosticSink as j, type SemanticNodeType as k, type ContainerDSL as l, type SignalKind as m, type A11yOptions as n, AppBuilder as o, type AppDSL as p, type AppOptions as q, type ApplicationGraphOptions as r, type AsyncBoundaryBranches as s, type Bindable as t, type BindableString as u, type BindableText as v, type BoundInputOptions as w, type ButtonOptions as x, type CompileOptions as y, type ComponentContext as z };