streetui 1.6.1 → 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.cjs CHANGED
@@ -72,6 +72,7 @@ __export(src_exports, {
72
72
  compileGraph: () => compileGraph,
73
73
  consoleDiagnosticSink: () => consoleDiagnosticSink,
74
74
  consoleHydrationDiagnosticSink: () => consoleHydrationDiagnosticSink,
75
+ containFocus: () => containFocus,
75
76
  createApplication: () => createApplication,
76
77
  createBrowserHistory: () => createBrowserHistory,
77
78
  createContext: () => createContext,
@@ -98,12 +99,14 @@ __export(src_exports, {
98
99
  flushSync: () => flushSync,
99
100
  focusById: () => focusById,
100
101
  focusFirst: () => focusFirst,
102
+ focusInitial: () => focusInitial,
101
103
  formatDiagnostic: () => formatDiagnostic,
102
104
  formatDiagnosticContext: () => formatDiagnosticContext,
103
105
  formatHydrationDiagnostic: () => formatHydrationDiagnostic,
104
106
  frameworkError: () => frameworkError,
105
107
  generateApplicationId: () => generateApplicationId,
106
108
  generateNodeId: () => generateNodeId,
109
+ getFocusable: () => getFocusable,
107
110
  globalEventBus: () => globalEventBus,
108
111
  headingUpdate: () => headingUpdate,
109
112
  hydrateGraph: () => hydrateGraph,
@@ -130,6 +133,7 @@ __export(src_exports, {
130
133
  nodeTypeStats: () => nodeTypeStats,
131
134
  normalizePath: () => normalizePath,
132
135
  observerCount: () => observerCount,
136
+ onEscape: () => onEscape,
133
137
  patchNode: () => patchNode,
134
138
  patchProp: () => patchProp,
135
139
  pattern: () => pattern,
@@ -146,8 +150,10 @@ __export(src_exports, {
146
150
  resetIdCounter: () => resetIdCounter,
147
151
  resolveTag: () => resolveTag,
148
152
  resource: () => resource,
153
+ restoreFocus: () => restoreFocus,
149
154
  routerOutlet: () => routerOutlet,
150
155
  runValidators: () => runValidators,
156
+ saveFocus: () => saveFocus,
151
157
  scheduleImmediate: () => scheduleImmediate,
152
158
  scheduleUpdate: () => scheduleUpdate,
153
159
  scheduler: () => scheduler,
@@ -162,15 +168,17 @@ __export(src_exports, {
162
168
  textUpdate: () => textUpdate,
163
169
  toIdToken: () => toIdToken,
164
170
  transformGraph: () => transformGraph,
171
+ trapFocus: () => trapFocus,
165
172
  validateGraph: () => validateGraph,
166
173
  wireEvents: () => wireEvents,
174
+ wireOverlayBehavior: () => wireOverlayBehavior,
167
175
  wireReactiveList: () => wireReactiveList,
168
176
  wireSignalBindings: () => wireSignalBindings
169
177
  });
170
178
  module.exports = __toCommonJS(src_exports);
171
179
 
172
180
  // src/version.ts
173
- var VERSION = "1.6.0";
181
+ var VERSION = "1.7.0";
174
182
 
175
183
  // ../state/src/signal.ts
176
184
  var _activeConsumer = null;
@@ -973,6 +981,7 @@ var ApplicationGraph = class {
973
981
  }
974
982
  this.handlers.delete(`__listbuild__${node.id}`);
975
983
  this.handlers.delete(`__listplan__${node.id}`);
984
+ this.handlers.delete(`__overlay__${node.id}`);
976
985
  }
977
986
  // ── Handler registry ──────────────────────────────────────────────────────
978
987
  registerHandler(key, fn) {
@@ -1087,6 +1096,7 @@ function applyA11yProps(props, options) {
1087
1096
  if (options.ariaCurrent !== void 0) props["aria-current"] = String(options.ariaCurrent);
1088
1097
  if (options.ariaInvalid !== void 0) props["aria-invalid"] = String(options.ariaInvalid);
1089
1098
  if (options.ariaRequired !== void 0) props["aria-required"] = String(options.ariaRequired);
1099
+ if (options.ariaModal !== void 0) props["aria-modal"] = String(options.ariaModal);
1090
1100
  }
1091
1101
  function containerProps(options) {
1092
1102
  const props = {};
@@ -1118,6 +1128,49 @@ function reactiveListItemSignature(item) {
1118
1128
  function reactiveListItemKey(item, index) {
1119
1129
  return itemIdentity(item, index);
1120
1130
  }
1131
+ var OVERLAY_KINDS = {
1132
+ dialog: {
1133
+ role: "dialog",
1134
+ modal: true,
1135
+ takesFocus: true,
1136
+ ariaModal: true,
1137
+ defaultCloseOnEscape: true,
1138
+ defaultRestoreFocus: true
1139
+ },
1140
+ popover: {
1141
+ role: "dialog",
1142
+ modal: false,
1143
+ takesFocus: true,
1144
+ ariaModal: false,
1145
+ defaultCloseOnEscape: true,
1146
+ defaultRestoreFocus: true
1147
+ },
1148
+ tooltip: {
1149
+ role: "tooltip",
1150
+ modal: false,
1151
+ takesFocus: false,
1152
+ ariaModal: false,
1153
+ defaultCloseOnEscape: false,
1154
+ defaultRestoreFocus: false
1155
+ },
1156
+ dropdown: {
1157
+ role: "menu",
1158
+ modal: false,
1159
+ takesFocus: true,
1160
+ ariaModal: false,
1161
+ defaultCloseOnEscape: true,
1162
+ defaultRestoreFocus: true
1163
+ },
1164
+ toast: {
1165
+ role: "status",
1166
+ modal: false,
1167
+ takesFocus: false,
1168
+ ariaModal: false,
1169
+ ariaLive: "polite",
1170
+ defaultCloseOnEscape: false,
1171
+ defaultRestoreFocus: false
1172
+ }
1173
+ };
1121
1174
  var ContentBuilderBase = class {
1122
1175
  constructor(_node, _graph) {
1123
1176
  this._node = _node;
@@ -1390,6 +1443,74 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1390
1443
  );
1391
1444
  }, { id });
1392
1445
  }
1446
+ // ── Portals & overlays ──────────────────────────────────────────────────────
1447
+ portal(key, builder, options = {}) {
1448
+ const node = this._graph.createNode("portal", {
1449
+ key,
1450
+ parent: this._node,
1451
+ props: containerProps(options)
1452
+ });
1453
+ builder(new ContainerBuilderImpl(node, this._graph));
1454
+ }
1455
+ /**
1456
+ * Shared assembly for every overlay kind: a `portal` node whose single child
1457
+ * is a `when(open, panel)` conditional. The panel container carries the
1458
+ * kind's ARIA semantics; `builder` fills it. An `__overlay__<portalId>`
1459
+ * descriptor is registered so the renderer wires focus/keyboard behavior to
1460
+ * the same `open` signal that drives the panel. Reuses existing primitives
1461
+ * (portal + when + container) — no new render path.
1462
+ */
1463
+ _overlay(kind, key, options, builder) {
1464
+ const graph = this._graph;
1465
+ const portalNode = graph.createNode("portal", {
1466
+ key,
1467
+ parent: this._node,
1468
+ props: { key }
1469
+ });
1470
+ const openBindable = options.open;
1471
+ const openSignal = isSignal(openBindable) ? openBindable : signal(openBindable);
1472
+ const panelOptions = {
1473
+ role: options.role ?? kind.role,
1474
+ ...kind.ariaModal ? { ariaModal: true } : {},
1475
+ ...kind.ariaLive !== void 0 ? { ariaLive: kind.ariaLive } : {},
1476
+ ...options.class !== void 0 ? { class: options.class } : {},
1477
+ ...options.ariaLabel !== void 0 ? { ariaLabel: options.ariaLabel } : {},
1478
+ ...options.ariaLabelledBy !== void 0 ? { ariaLabelledBy: options.ariaLabelledBy } : {},
1479
+ ...options.ariaDescribedBy !== void 0 ? { ariaDescribedBy: options.ariaDescribedBy } : {}
1480
+ };
1481
+ const portalBuilder = new ContainerBuilderImpl(portalNode, graph);
1482
+ portalBuilder.when(openSignal, (panelHost) => {
1483
+ panelHost.container(`${key}__panel`, builder, panelOptions);
1484
+ });
1485
+ const descriptor = {
1486
+ open: openSignal,
1487
+ modal: kind.modal,
1488
+ takesFocus: kind.takesFocus,
1489
+ closeOnEscape: options.closeOnEscape ?? kind.defaultCloseOnEscape,
1490
+ restoreFocus: options.restoreFocus ?? kind.defaultRestoreFocus,
1491
+ ...options.initialFocusId !== void 0 ? { initialFocusId: options.initialFocusId } : {},
1492
+ ...options.onClose !== void 0 ? { onClose: options.onClose } : {}
1493
+ };
1494
+ graph.registerHandler(
1495
+ `__overlay__${portalNode.id}`,
1496
+ () => descriptor
1497
+ );
1498
+ }
1499
+ dialog(key, options, builder) {
1500
+ this._overlay(OVERLAY_KINDS.dialog, key, options, builder);
1501
+ }
1502
+ popover(key, options, builder) {
1503
+ this._overlay(OVERLAY_KINDS.popover, key, options, builder);
1504
+ }
1505
+ tooltip(key, options, builder) {
1506
+ this._overlay(OVERLAY_KINDS.tooltip, key, options, builder);
1507
+ }
1508
+ dropdown(key, options, builder) {
1509
+ this._overlay(OVERLAY_KINDS.dropdown, key, options, builder);
1510
+ }
1511
+ toast(key, options, builder) {
1512
+ this._overlay(OVERLAY_KINDS.toast, key, options, builder);
1513
+ }
1393
1514
  };
1394
1515
  var SectionBuilderImpl = class extends ContainerBuilderBase {
1395
1516
  };
@@ -1990,6 +2111,18 @@ var BrowserDOMAdapter = class {
1990
2111
  focus(element) {
1991
2112
  element.focus?.();
1992
2113
  }
2114
+ body() {
2115
+ return document.body ?? null;
2116
+ }
2117
+ activeElement() {
2118
+ return document.activeElement ?? null;
2119
+ }
2120
+ contains(ancestor, node) {
2121
+ return ancestor.contains(node);
2122
+ }
2123
+ matches(element, selector) {
2124
+ return typeof element.matches === "function" && element.matches(selector);
2125
+ }
1993
2126
  isElement(node) {
1994
2127
  return node.nodeType === Node.ELEMENT_NODE;
1995
2128
  }
@@ -2357,6 +2490,18 @@ var ServerDOMAdapter = class {
2357
2490
  }
2358
2491
  focus() {
2359
2492
  }
2493
+ body() {
2494
+ return null;
2495
+ }
2496
+ activeElement() {
2497
+ return null;
2498
+ }
2499
+ contains(_ancestor, _node) {
2500
+ return false;
2501
+ }
2502
+ matches(_element, _selector) {
2503
+ return false;
2504
+ }
2360
2505
  isElement(node) {
2361
2506
  return asServer(node).kind === "element";
2362
2507
  }
@@ -2422,6 +2567,66 @@ function focusFirst(dom, container, selector = FOCUSABLE_SELECTOR) {
2422
2567
  dom.focus(el);
2423
2568
  return true;
2424
2569
  }
2570
+ function getFocusable(dom, container, selector = FOCUSABLE_SELECTOR) {
2571
+ return Array.from(dom.querySelectorAll(container, selector)).filter(
2572
+ (el) => dom.matches(el, selector)
2573
+ );
2574
+ }
2575
+ function saveFocus(dom) {
2576
+ return dom.activeElement();
2577
+ }
2578
+ function restoreFocus(dom, saved) {
2579
+ if (saved !== null) dom.focus(saved);
2580
+ }
2581
+ function focusInitial(dom, container, initialFocusId) {
2582
+ if (initialFocusId !== void 0 && focusById(dom, container, initialFocusId)) return;
2583
+ focusFirst(dom, container);
2584
+ }
2585
+ function trapFocus(dom, container) {
2586
+ const onKeydown = (event) => {
2587
+ if (event.key !== "Tab") return;
2588
+ const items = getFocusable(dom, container);
2589
+ if (items.length === 0) {
2590
+ event.preventDefault();
2591
+ return;
2592
+ }
2593
+ const first = items[0];
2594
+ const last = items[items.length - 1];
2595
+ const active = dom.activeElement();
2596
+ if (active === null || !dom.contains(container, active)) {
2597
+ event.preventDefault();
2598
+ dom.focus(first);
2599
+ } else if (event.shiftKey && active === first) {
2600
+ event.preventDefault();
2601
+ dom.focus(last);
2602
+ } else if (!event.shiftKey && active === last) {
2603
+ event.preventDefault();
2604
+ dom.focus(first);
2605
+ }
2606
+ };
2607
+ dom.addEventListener(container, "keydown", onKeydown);
2608
+ return () => dom.removeEventListener(container, "keydown", onKeydown);
2609
+ }
2610
+ function containFocus(dom, container) {
2611
+ const body = dom.body();
2612
+ if (body === null) return () => {
2613
+ };
2614
+ const onFocusIn = (event) => {
2615
+ const target = event.target;
2616
+ if (target !== null && !dom.contains(container, target)) {
2617
+ focusFirst(dom, container);
2618
+ }
2619
+ };
2620
+ dom.addEventListener(body, "focusin", onFocusIn);
2621
+ return () => dom.removeEventListener(body, "focusin", onFocusIn);
2622
+ }
2623
+ function onEscape(dom, target, handler) {
2624
+ const onKeydown = (event) => {
2625
+ if (event.key === "Escape") handler();
2626
+ };
2627
+ dom.addEventListener(target, "keydown", onKeydown);
2628
+ return () => dom.removeEventListener(target, "keydown", onKeydown);
2629
+ }
2425
2630
 
2426
2631
  // ../renderer/src/render-context.ts
2427
2632
  function createRenderContext(dom, graph, container, hydrationDiagnostics, staticHTML) {
@@ -2580,7 +2785,12 @@ var TAG_MAP = {
2580
2785
  component: "div",
2581
2786
  slot: "div",
2582
2787
  fragment: "div",
2583
- "reactive-list": "ul"
2788
+ "reactive-list": "ul",
2789
+ // A portal renders as a neutral inline anchor <div> at its declaration site;
2790
+ // its children are relocated to a document.body container on the browser
2791
+ // (see the portal branch in mount.ts). On the server (no body) it renders
2792
+ // inline, so the anchor tag is what SSR/hydration positionally match on.
2793
+ portal: "div"
2584
2794
  };
2585
2795
  function resolveTag(type) {
2586
2796
  return TAG_MAP[type] ?? "div";
@@ -2992,6 +3202,28 @@ function mountNode(ctx, graphNode, parentDom) {
2992
3202
  wireReactiveList(ctx, graphNode, instance2, el2);
2993
3203
  return instance2;
2994
3204
  }
3205
+ if (graphNode.type === "portal") {
3206
+ const anchor = dom.createElement(resolveTag("portal"));
3207
+ dom.setAttribute(anchor, "data-streetui-portal", "");
3208
+ applyNodeProps(ctx, graphNode, anchor);
3209
+ const instance2 = new NodeInstance(graphNode, anchor);
3210
+ ctx.instances.set(graphNode.id, instance2);
3211
+ const body = dom.body();
3212
+ let target = anchor;
3213
+ if (body !== null) {
3214
+ const portalContainer = dom.createElement("div");
3215
+ dom.setAttribute(portalContainer, "data-streetui-portal-container", "");
3216
+ dom.appendChild(body, portalContainer);
3217
+ instance2.trackCleanup(() => dom.removeChild(body, portalContainer));
3218
+ target = portalContainer;
3219
+ }
3220
+ for (const child of graphNode.children) {
3221
+ instance2.addChild(mountNode(ctx, child, target));
3222
+ }
3223
+ dom.appendChild(parentDom, anchor);
3224
+ wireOverlayBehavior(ctx, graphNode, instance2, target);
3225
+ return instance2;
3226
+ }
2995
3227
  const tag = resolveTag(graphNode.type);
2996
3228
  const el = dom.createElement(tag);
2997
3229
  applyNodeProps(ctx, graphNode, el);
@@ -3141,6 +3373,44 @@ function forgetInstance(ctx, instance) {
3141
3373
  ctx.instances.delete(instance.graphNode.id);
3142
3374
  for (const child of instance.children) forgetInstance(ctx, child);
3143
3375
  }
3376
+ function wireOverlayBehavior(ctx, graphNode, instance, target) {
3377
+ const { dom, graph } = ctx;
3378
+ if (dom.body() === null) return;
3379
+ const descFn = graph.getHandler(`__overlay__${graphNode.id}`);
3380
+ if (descFn === void 0) return;
3381
+ const desc = descFn();
3382
+ const openSig = desc.open;
3383
+ if (openSig === void 0 || typeof openSig.subscribe !== "function") return;
3384
+ let active = [];
3385
+ let saved = null;
3386
+ const teardown = () => {
3387
+ for (const fn of active) fn();
3388
+ active = [];
3389
+ };
3390
+ const onOpenChange = (isOpen) => {
3391
+ if (isOpen) {
3392
+ if (desc.restoreFocus) saved = saveFocus(dom);
3393
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
3394
+ if (desc.modal) {
3395
+ active.push(trapFocus(dom, target));
3396
+ active.push(containFocus(dom, target));
3397
+ }
3398
+ if (desc.closeOnEscape && desc.onClose !== void 0) {
3399
+ active.push(onEscape(dom, target, desc.onClose));
3400
+ }
3401
+ } else {
3402
+ teardown();
3403
+ if (desc.restoreFocus && saved !== null) {
3404
+ restoreFocus(dom, saved);
3405
+ saved = null;
3406
+ }
3407
+ }
3408
+ };
3409
+ const unsub = openSig.subscribe(onOpenChange);
3410
+ instance.trackCleanup(unsub);
3411
+ instance.trackCleanup(teardown);
3412
+ if (openSig.peek() === true) onOpenChange(true);
3413
+ }
3144
3414
 
3145
3415
  // ../renderer/src/hydration-diagnostics.ts
3146
3416
  function formatHydrationDiagnostic(d) {
@@ -3243,6 +3513,25 @@ function hydrateNode(ctx, graphNode, domNode, path) {
3243
3513
  wireReactiveList(ctx, graphNode, instance, domNode);
3244
3514
  return instance;
3245
3515
  }
3516
+ case "portal": {
3517
+ const instance = new NodeInstance(graphNode, domNode);
3518
+ ctx.instances.set(graphNode.id, instance);
3519
+ const body = dom.body();
3520
+ let target = domNode;
3521
+ if (body !== null) {
3522
+ const portalContainer = dom.createElement("div");
3523
+ dom.setAttribute(portalContainer, "data-streetui-portal-container", "");
3524
+ for (const child of dom.childNodes(domNode)) {
3525
+ dom.appendChild(portalContainer, child);
3526
+ }
3527
+ dom.appendChild(body, portalContainer);
3528
+ instance.trackCleanup(() => dom.removeChild(body, portalContainer));
3529
+ target = portalContainer;
3530
+ }
3531
+ hydrateChildren(ctx, graphNode, instance, target, path);
3532
+ wireOverlayBehavior(ctx, graphNode, instance, target);
3533
+ return instance;
3534
+ }
3246
3535
  default: {
3247
3536
  const instance = new NodeInstance(graphNode, domNode);
3248
3537
  ctx.instances.set(graphNode.id, instance);
@@ -3486,7 +3775,8 @@ function analyzeGraph(graph) {
3486
3775
  const hasEvents = node.events.length > 0;
3487
3776
  const isList = node.type === "reactive-list";
3488
3777
  const isConditional = node.type === "conditional";
3489
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional;
3778
+ const isPortal = node.type === "portal";
3779
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal;
3490
3780
  const isStaticSubtree = isStatic && allChildrenStatic;
3491
3781
  nodes.set(node.id, {
3492
3782
  isStatic,
@@ -4579,6 +4869,7 @@ function defineConfig(config) {
4579
4869
  compileGraph,
4580
4870
  consoleDiagnosticSink,
4581
4871
  consoleHydrationDiagnosticSink,
4872
+ containFocus,
4582
4873
  createApplication,
4583
4874
  createBrowserHistory,
4584
4875
  createContext,
@@ -4605,12 +4896,14 @@ function defineConfig(config) {
4605
4896
  flushSync,
4606
4897
  focusById,
4607
4898
  focusFirst,
4899
+ focusInitial,
4608
4900
  formatDiagnostic,
4609
4901
  formatDiagnosticContext,
4610
4902
  formatHydrationDiagnostic,
4611
4903
  frameworkError,
4612
4904
  generateApplicationId,
4613
4905
  generateNodeId,
4906
+ getFocusable,
4614
4907
  globalEventBus,
4615
4908
  headingUpdate,
4616
4909
  hydrateGraph,
@@ -4637,6 +4930,7 @@ function defineConfig(config) {
4637
4930
  nodeTypeStats,
4638
4931
  normalizePath,
4639
4932
  observerCount,
4933
+ onEscape,
4640
4934
  patchNode,
4641
4935
  patchProp,
4642
4936
  pattern,
@@ -4653,8 +4947,10 @@ function defineConfig(config) {
4653
4947
  resetIdCounter,
4654
4948
  resolveTag,
4655
4949
  resource,
4950
+ restoreFocus,
4656
4951
  routerOutlet,
4657
4952
  runValidators,
4953
+ saveFocus,
4658
4954
  scheduleImmediate,
4659
4955
  scheduleUpdate,
4660
4956
  scheduler,
@@ -4669,8 +4965,10 @@ function defineConfig(config) {
4669
4965
  textUpdate,
4670
4966
  toIdToken,
4671
4967
  transformGraph,
4968
+ trapFocus,
4672
4969
  validateGraph,
4673
4970
  wireEvents,
4971
+ wireOverlayBehavior,
4674
4972
  wireReactiveList,
4675
4973
  wireSignalBindings
4676
4974
  });