@streetui/renderer 1.6.0 → 1.7.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.
package/dist/index.d.ts CHANGED
@@ -110,8 +110,18 @@ interface RenderContext {
110
110
  * DevTools/diagnostics stay off the production runtime path.
111
111
  */
112
112
  readonly hydrationDiagnostics?: HydrationDiagnosticSink;
113
+ /**
114
+ * Optional SSR-only static-subtree plan (v1.7). Maps a maximal
115
+ * static-subtree root GraphNode.id → its precomputed, verbatim HTML string.
116
+ * Present only on the server render path when a plan has been built; on the
117
+ * browser mount path it is always `undefined`, so the client hot path is
118
+ * unaffected (a single `=== undefined` check short-circuits). When a mounted
119
+ * node's id is in this map, the renderer emits the precomputed HTML via
120
+ * `dom.createRawHTML` instead of recursively constructing the subtree.
121
+ */
122
+ readonly staticHTML?: ReadonlyMap<string, string>;
113
123
  }
114
- declare function createRenderContext(dom: DOMAdapter, graph: ApplicationGraph, container: Element, hydrationDiagnostics?: HydrationDiagnosticSink): RenderContext;
124
+ declare function createRenderContext(dom: DOMAdapter, graph: ApplicationGraph, container: Element, hydrationDiagnostics?: HydrationDiagnosticSink, staticHTML?: ReadonlyMap<string, string>): RenderContext;
115
125
 
116
126
  /**
117
127
  * Attribute and property application helpers.
@@ -168,6 +178,20 @@ declare function wireSignalBindings(ctx: RenderContext, graphNode: GraphNode, in
168
178
  * eager build factory (it only ever renders 0..1 branch, so eager is fine).
169
179
  */
170
180
  declare function wireReactiveList(ctx: RenderContext, graphNode: GraphNode, instance: NodeInstance, el: Element): void;
181
+ /**
182
+ * Attach overlay focus/keyboard behavior to a mounted portal. Server-safe: on
183
+ * the server `dom.body()` is null so this returns immediately (SSR emits inert
184
+ * markup, no focus concept). A plain portal has no `__overlay__` descriptor, so
185
+ * this also returns immediately — the behavior is purely additive.
186
+ *
187
+ * The panel is mounted/unmounted by the portal's inner `when(open, …)`, whose
188
+ * signal subscription is registered *before* this one (the conditional child is
189
+ * mounted earlier in the portal branch). Signal subscribers fire synchronously
190
+ * in subscription order, so on open→true the panel DOM exists before we move
191
+ * focus into it, and on open→false the panel is torn down before we restore
192
+ * focus. All listeners are tracked on the instance and torn down on unmount.
193
+ */
194
+ declare function wireOverlayBehavior(ctx: RenderContext, graphNode: GraphNode, instance: NodeInstance, target: Element): void;
171
195
 
172
196
  /**
173
197
  * Patch — targeted DOM updates driven by signal changes.
@@ -393,6 +417,18 @@ interface RenderToStringOptions {
393
417
  * `ServerDOMAdapter` per call so concurrent renders never share state.
394
418
  */
395
419
  readonly domAdapter?: ServerDOMAdapter;
420
+ /**
421
+ * @internal — testing/benchmark knob for the v1.7 static SSR plan.
422
+ *
423
+ * `undefined` (default): use the per-app cached plan (build once, reuse).
424
+ * `null`: disable the plan entirely — the exact v1.6 runtime mount path, used
425
+ * by the byte-identity gate and A/B benchmark as the "legacy" baseline.
426
+ * a map: use this explicit plan.
427
+ *
428
+ * Not part of the supported public API; output is byte-identical regardless
429
+ * of this value (§8).
430
+ */
431
+ readonly staticPlan?: ReadonlyMap<string, string> | null;
396
432
  }
397
433
  /**
398
434
  * Render a compiled StreetUI application to an HTML string.
@@ -409,4 +445,4 @@ declare function renderToString(compiled: CompiledApplication, options?: RenderT
409
445
 
410
446
  declare function resolveTag(type: SemanticNodeType): string;
411
447
 
412
- export { type HydrationDiagnostic, type HydrationDiagnosticSink, type HydrationMismatchType, type MountFn, NodeInstance, type PlanEntry, type ReconcileResult, type RenderContext, type RenderToStringOptions, STATE_MARKER_ATTR, StreetRenderHandle, StreetRendererImpl, type StreetRendererOptions, applyNodeProps, applyProp, buttonUpdate, consoleHydrationDiagnosticSink, createHydrationDiagnosticCollector, createRenderContext, createRenderer, formatHydrationDiagnostic, headingUpdate, hydrateGraph, inputUpdate, mountGraph, mountNode, patchNode, patchProp, readState, reconcileChildren, reconcileChildrenByPlan, renderToString, resolveTag, serializeState, textUpdate, wireEvents, wireReactiveList, wireSignalBindings };
448
+ export { type HydrationDiagnostic, type HydrationDiagnosticSink, type HydrationMismatchType, type MountFn, NodeInstance, type PlanEntry, type ReconcileResult, type RenderContext, type RenderToStringOptions, STATE_MARKER_ATTR, StreetRenderHandle, StreetRendererImpl, type StreetRendererOptions, applyNodeProps, applyProp, buttonUpdate, consoleHydrationDiagnosticSink, createHydrationDiagnosticCollector, createRenderContext, createRenderer, formatHydrationDiagnostic, headingUpdate, hydrateGraph, inputUpdate, mountGraph, mountNode, patchNode, patchProp, readState, reconcileChildren, reconcileChildrenByPlan, renderToString, resolveTag, serializeState, textUpdate, wireEvents, wireOverlayBehavior, wireReactiveList, wireSignalBindings };
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  // src/render-context.ts
2
- function createRenderContext(dom, graph, container, hydrationDiagnostics) {
2
+ function createRenderContext(dom, graph, container, hydrationDiagnostics, staticHTML) {
3
3
  return {
4
4
  dom,
5
5
  graph,
6
6
  instances: /* @__PURE__ */ new Map(),
7
7
  container,
8
- ...hydrationDiagnostics !== void 0 ? { hydrationDiagnostics } : {}
8
+ ...hydrationDiagnostics !== void 0 ? { hydrationDiagnostics } : {},
9
+ ...staticHTML !== void 0 ? { staticHTML } : {}
9
10
  };
10
11
  }
11
12
 
@@ -137,6 +138,16 @@ function wireEvents(dom, graph, node, element, instance) {
137
138
  }
138
139
  }
139
140
 
141
+ // src/mount.ts
142
+ import {
143
+ focusInitial,
144
+ trapFocus,
145
+ containFocus,
146
+ onEscape,
147
+ saveFocus,
148
+ restoreFocus
149
+ } from "@streetui/dom";
150
+
140
151
  // src/tag-map.ts
141
152
  var TAG_MAP = {
142
153
  application: "div",
@@ -155,7 +166,12 @@ var TAG_MAP = {
155
166
  component: "div",
156
167
  slot: "div",
157
168
  fragment: "div",
158
- "reactive-list": "ul"
169
+ "reactive-list": "ul",
170
+ // A portal renders as a neutral inline anchor <div> at its declaration site;
171
+ // its children are relocated to a document.body container on the browser
172
+ // (see the portal branch in mount.ts). On the server (no body) it renders
173
+ // inline, so the anchor tag is what SSR/hydration positionally match on.
174
+ portal: "div"
159
175
  };
160
176
  function resolveTag(type) {
161
177
  return TAG_MAP[type] ?? "div";
@@ -434,6 +450,17 @@ function mountGraph(ctx) {
434
450
  }
435
451
  function mountNode(ctx, graphNode, parentDom) {
436
452
  const { dom, graph } = ctx;
453
+ const staticHTML = ctx.staticHTML;
454
+ if (staticHTML !== void 0 && dom.createRawHTML !== void 0) {
455
+ const precomputed = staticHTML.get(graphNode.id);
456
+ if (precomputed !== void 0) {
457
+ const raw = dom.createRawHTML(precomputed);
458
+ dom.appendChild(parentDom, raw);
459
+ const instance2 = new NodeInstance(graphNode, raw);
460
+ ctx.instances.set(graphNode.id, instance2);
461
+ return instance2;
462
+ }
463
+ }
437
464
  if (graphNode.type === "application") {
438
465
  const instance2 = new NodeInstance(graphNode, parentDom);
439
466
  ctx.instances.set(graphNode.id, instance2);
@@ -556,6 +583,28 @@ function mountNode(ctx, graphNode, parentDom) {
556
583
  wireReactiveList(ctx, graphNode, instance2, el2);
557
584
  return instance2;
558
585
  }
586
+ if (graphNode.type === "portal") {
587
+ const anchor = dom.createElement(resolveTag("portal"));
588
+ dom.setAttribute(anchor, "data-streetui-portal", "");
589
+ applyNodeProps(ctx, graphNode, anchor);
590
+ const instance2 = new NodeInstance(graphNode, anchor);
591
+ ctx.instances.set(graphNode.id, instance2);
592
+ const body = dom.body();
593
+ let target = anchor;
594
+ if (body !== null) {
595
+ const portalContainer = dom.createElement("div");
596
+ dom.setAttribute(portalContainer, "data-streetui-portal-container", "");
597
+ dom.appendChild(body, portalContainer);
598
+ instance2.trackCleanup(() => dom.removeChild(body, portalContainer));
599
+ target = portalContainer;
600
+ }
601
+ for (const child of graphNode.children) {
602
+ instance2.addChild(mountNode(ctx, child, target));
603
+ }
604
+ dom.appendChild(parentDom, anchor);
605
+ wireOverlayBehavior(ctx, graphNode, instance2, target);
606
+ return instance2;
607
+ }
559
608
  const tag = resolveTag(graphNode.type);
560
609
  const el = dom.createElement(tag);
561
610
  applyNodeProps(ctx, graphNode, el);
@@ -705,6 +754,44 @@ function forgetInstance(ctx, instance) {
705
754
  ctx.instances.delete(instance.graphNode.id);
706
755
  for (const child of instance.children) forgetInstance(ctx, child);
707
756
  }
757
+ function wireOverlayBehavior(ctx, graphNode, instance, target) {
758
+ const { dom, graph } = ctx;
759
+ if (dom.body() === null) return;
760
+ const descFn = graph.getHandler(`__overlay__${graphNode.id}`);
761
+ if (descFn === void 0) return;
762
+ const desc = descFn();
763
+ const openSig = desc.open;
764
+ if (openSig === void 0 || typeof openSig.subscribe !== "function") return;
765
+ let active = [];
766
+ let saved = null;
767
+ const teardown = () => {
768
+ for (const fn of active) fn();
769
+ active = [];
770
+ };
771
+ const onOpenChange = (isOpen) => {
772
+ if (isOpen) {
773
+ if (desc.restoreFocus) saved = saveFocus(dom);
774
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
775
+ if (desc.modal) {
776
+ active.push(trapFocus(dom, target));
777
+ active.push(containFocus(dom, target));
778
+ }
779
+ if (desc.closeOnEscape && desc.onClose !== void 0) {
780
+ active.push(onEscape(dom, target, desc.onClose));
781
+ }
782
+ } else {
783
+ teardown();
784
+ if (desc.restoreFocus && saved !== null) {
785
+ restoreFocus(dom, saved);
786
+ saved = null;
787
+ }
788
+ }
789
+ };
790
+ const unsub = openSig.subscribe(onOpenChange);
791
+ instance.trackCleanup(unsub);
792
+ instance.trackCleanup(teardown);
793
+ if (openSig.peek() === true) onOpenChange(true);
794
+ }
708
795
 
709
796
  // src/renderer.ts
710
797
  import { BrowserDOMAdapter } from "@streetui/dom";
@@ -810,6 +897,25 @@ function hydrateNode(ctx, graphNode, domNode, path) {
810
897
  wireReactiveList(ctx, graphNode, instance, domNode);
811
898
  return instance;
812
899
  }
900
+ case "portal": {
901
+ const instance = new NodeInstance(graphNode, domNode);
902
+ ctx.instances.set(graphNode.id, instance);
903
+ const body = dom.body();
904
+ let target = domNode;
905
+ if (body !== null) {
906
+ const portalContainer = dom.createElement("div");
907
+ dom.setAttribute(portalContainer, "data-streetui-portal-container", "");
908
+ for (const child of dom.childNodes(domNode)) {
909
+ dom.appendChild(portalContainer, child);
910
+ }
911
+ dom.appendChild(body, portalContainer);
912
+ instance.trackCleanup(() => dom.removeChild(body, portalContainer));
913
+ target = portalContainer;
914
+ }
915
+ hydrateChildren(ctx, graphNode, instance, target, path);
916
+ wireOverlayBehavior(ctx, graphNode, instance, target);
917
+ return instance;
918
+ }
813
919
  default: {
814
920
  const instance = new NodeInstance(graphNode, domNode);
815
921
  ctx.instances.set(graphNode.id, instance);
@@ -1025,11 +1131,64 @@ function readState(dom, root) {
1025
1131
  }
1026
1132
 
1027
1133
  // src/ssr.ts
1134
+ import { ServerDOMAdapter as ServerDOMAdapter2 } from "@streetui/dom";
1135
+
1136
+ // src/static-ssr-plan.ts
1137
+ import { analyzeGraph } from "@streetui/compiler/diagnostics";
1028
1138
  import { ServerDOMAdapter } from "@streetui/dom";
1139
+ function collectMaximalStaticRoots(graph) {
1140
+ const analysis = analyzeGraph(graph);
1141
+ const roots = [];
1142
+ const walk = (node) => {
1143
+ if (node.type !== "application") {
1144
+ const a = analysis.nodes.get(node.id);
1145
+ if (a !== void 0 && a.isStaticSubtree) {
1146
+ roots.push(node);
1147
+ return;
1148
+ }
1149
+ }
1150
+ for (const child of node.children) walk(child);
1151
+ };
1152
+ walk(graph.root);
1153
+ return roots;
1154
+ }
1155
+ function serializeStaticSubtree(dom, graph, root) {
1156
+ const container = dom.createElement("div");
1157
+ const ctx = createRenderContext(dom, graph, container);
1158
+ const instance = mountNode(ctx, root, container);
1159
+ const html = dom.serializeInner(container);
1160
+ instance.dispose();
1161
+ ctx.instances.clear();
1162
+ return html;
1163
+ }
1164
+ function buildStaticSSRPlan(compiled) {
1165
+ const graph = compiled.graph;
1166
+ const roots = collectMaximalStaticRoots(graph);
1167
+ const plan = /* @__PURE__ */ new Map();
1168
+ if (roots.length === 0) return plan;
1169
+ const dom = new ServerDOMAdapter();
1170
+ for (const root of roots) {
1171
+ plan.set(root.id, serializeStaticSubtree(dom, graph, root));
1172
+ }
1173
+ return plan;
1174
+ }
1175
+ var PLAN_CACHE = /* @__PURE__ */ new WeakMap();
1176
+ function getStaticSSRPlan(compiled) {
1177
+ let plan = PLAN_CACHE.get(compiled);
1178
+ if (plan === void 0) {
1179
+ plan = buildStaticSSRPlan(compiled);
1180
+ PLAN_CACHE.set(compiled, plan);
1181
+ }
1182
+ return plan;
1183
+ }
1184
+
1185
+ // src/ssr.ts
1029
1186
  function renderToString(compiled, options = {}) {
1030
- const dom = options.domAdapter ?? new ServerDOMAdapter();
1187
+ const dom = options.domAdapter ?? new ServerDOMAdapter2();
1188
+ const plan = options.staticPlan === null ? void 0 : options.staticPlan ?? getStaticSSRPlan(compiled);
1189
+ const staticHTML = plan !== void 0 && plan.size > 0 ? plan : void 0;
1031
1190
  const container = dom.createElement("div");
1032
- const ctx = createRenderContext(dom, compiled.graph, container);
1191
+ const ctx = createRenderContext(dom, compiled.graph, container, void 0, staticHTML);
1033
1192
  const rootInstance = mountGraph(ctx);
1034
1193
  const html = dom.serializeInner(container);
1035
1194
  rootInstance.dispose();
@@ -1064,6 +1223,7 @@ export {
1064
1223
  serializeState,
1065
1224
  textUpdate,
1066
1225
  wireEvents,
1226
+ wireOverlayBehavior,
1067
1227
  wireReactiveList,
1068
1228
  wireSignalBindings
1069
1229
  };