streetui 1.9.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.
package/dist/testing.cjs CHANGED
@@ -315,6 +315,9 @@ var BrowserDOMAdapter = class {
315
315
  body() {
316
316
  return document.body ?? null;
317
317
  }
318
+ head() {
319
+ return document.head ?? null;
320
+ }
318
321
  activeElement() {
319
322
  return document.activeElement ?? null;
320
323
  }
@@ -693,6 +696,9 @@ var ServerDOMAdapter = class {
693
696
  body() {
694
697
  return null;
695
698
  }
699
+ head() {
700
+ return null;
701
+ }
696
702
  activeElement() {
697
703
  return null;
698
704
  }
@@ -1023,7 +1029,13 @@ var TAG_MAP = {
1023
1029
  // its children are relocated to a document.body container on the browser
1024
1030
  // (see the portal branch in mount.ts). On the server (no body) it renders
1025
1031
  // inline, so the anchor tag is what SSR/hydration positionally match on.
1026
- portal: "div"
1032
+ portal: "div",
1033
+ // A `head()` node renders as a neutral, empty inline anchor <div> at its
1034
+ // declaration site (like a portal anchor). Its actual contribution — title/
1035
+ // meta/link/etc. — is applied to `document.head` by `wireHeadBehavior` on the
1036
+ // browser, and emitted separately by `renderHead()` on the server. Keeping a
1037
+ // one-node/one-element anchor preserves positional hydration.
1038
+ head: "div"
1027
1039
  };
1028
1040
  function resolveTag(type) {
1029
1041
  return TAG_MAP[type] ?? "div";
@@ -1468,6 +1480,158 @@ var TransitionController = class {
1468
1480
  }
1469
1481
  };
1470
1482
 
1483
+ // ../renderer/src/head.ts
1484
+ var HEAD_MARKER = "data-streetui-head";
1485
+ var HEAD_KEY = "data-streetui-head-key";
1486
+ function isSignalLike(v) {
1487
+ return v !== null && typeof v === "object" && typeof v["subscribe"] === "function" && typeof v["peek"] === "function";
1488
+ }
1489
+ function readValue(v) {
1490
+ if (isSignalLike(v)) return String(v.peek() ?? "");
1491
+ return String(v ?? "");
1492
+ }
1493
+ function resolveEntry(entry) {
1494
+ const attrs = {};
1495
+ for (const key of Object.keys(entry.attrs)) {
1496
+ attrs[key] = readValue(entry.attrs[key]);
1497
+ }
1498
+ const resolved = { tag: entry.tag, attrs };
1499
+ if (entry.tag === "title") {
1500
+ return { ...resolved, text: readValue(entry.text) };
1501
+ }
1502
+ return resolved;
1503
+ }
1504
+ var HeadManager = class {
1505
+ _dom;
1506
+ _head;
1507
+ _contributions = /* @__PURE__ */ new Map();
1508
+ _applied = /* @__PURE__ */ new Map();
1509
+ _order = 0;
1510
+ _adopted = false;
1511
+ constructor(dom, head) {
1512
+ this._dom = dom;
1513
+ this._head = head;
1514
+ }
1515
+ /** Register (or replace) a node's contribution and re-apply the merged result. */
1516
+ register(nodeId, entries) {
1517
+ this._contributions.set(nodeId, { order: this._order++, entries });
1518
+ this.apply();
1519
+ }
1520
+ /** Withdraw a node's contribution (unmount / route change) and re-apply. */
1521
+ unregister(nodeId) {
1522
+ if (this._contributions.delete(nodeId)) this.apply();
1523
+ }
1524
+ /** Recompute the merged head and patch `document.head` to match. */
1525
+ apply() {
1526
+ if (!this._adopted) {
1527
+ this._adoptServerTags();
1528
+ this._adopted = true;
1529
+ }
1530
+ const ordered = [...this._contributions.values()].sort((a, b) => a.order - b.order);
1531
+ const merged = /* @__PURE__ */ new Map();
1532
+ for (const contribution of ordered) {
1533
+ for (const entry of contribution.entries) {
1534
+ merged.set(entry.dedupKey, resolveEntry(entry));
1535
+ }
1536
+ }
1537
+ for (const [key, desired] of merged) {
1538
+ const existing = this._applied.get(key);
1539
+ if (existing !== void 0 && existing.tag === desired.tag) {
1540
+ this._reconcileAttrs(existing, desired);
1541
+ } else {
1542
+ if (existing !== void 0) {
1543
+ this._dom.removeChild(this._head, existing.el);
1544
+ this._applied.delete(key);
1545
+ }
1546
+ const el = this._createTag(key, desired);
1547
+ this._dom.appendChild(this._head, el);
1548
+ this._applied.set(key, { el, attrKeys: new Set(Object.keys(desired.attrs)), tag: desired.tag });
1549
+ }
1550
+ }
1551
+ for (const [key, record] of [...this._applied]) {
1552
+ if (!merged.has(key)) {
1553
+ this._dom.removeChild(this._head, record.el);
1554
+ this._applied.delete(key);
1555
+ }
1556
+ }
1557
+ }
1558
+ _createTag(key, desired) {
1559
+ const el = this._dom.createElement(desired.tag);
1560
+ this._dom.setAttribute(el, HEAD_MARKER, "");
1561
+ this._dom.setAttribute(el, HEAD_KEY, key);
1562
+ for (const attr of Object.keys(desired.attrs)) {
1563
+ this._dom.setAttribute(el, attr, desired.attrs[attr]);
1564
+ }
1565
+ if (desired.tag === "title") this._dom.setTextContent(el, desired.text ?? "");
1566
+ return el;
1567
+ }
1568
+ _reconcileAttrs(record, desired) {
1569
+ const nextKeys = new Set(Object.keys(desired.attrs));
1570
+ for (const attr of record.attrKeys) {
1571
+ if (!nextKeys.has(attr)) this._dom.removeAttribute(record.el, attr);
1572
+ }
1573
+ for (const attr of nextKeys) {
1574
+ this._dom.setAttribute(record.el, attr, desired.attrs[attr]);
1575
+ }
1576
+ if (desired.tag === "title") this._dom.setTextContent(record.el, desired.text ?? "");
1577
+ record.attrKeys = nextKeys;
1578
+ }
1579
+ /**
1580
+ * Seed `_applied` from server-emitted `[data-streetui-head-key]` tags already
1581
+ * in `document.head`. The subsequent diff reuses these elements when the
1582
+ * client desires the same key (no duplicate), rewrites them if the value
1583
+ * changed, or removes them if the client graph no longer wants them.
1584
+ */
1585
+ _adoptServerTags() {
1586
+ for (const child of this._dom.childNodes(this._head)) {
1587
+ if (!this._dom.isElement(child)) continue;
1588
+ const el = child;
1589
+ const key = this._dom.getAttribute(el, HEAD_KEY);
1590
+ if (key === null) continue;
1591
+ this._applied.set(key, {
1592
+ el,
1593
+ attrKeys: new Set(this._attrNames(el)),
1594
+ tag: this._dom.tagName(el)
1595
+ });
1596
+ }
1597
+ }
1598
+ /** The framework-managed attribute names currently on a server tag. */
1599
+ _attrNames(el) {
1600
+ const names = [];
1601
+ if (this._dom.getAttribute(el, HEAD_MARKER) !== null) names.push(HEAD_MARKER);
1602
+ if (this._dom.getAttribute(el, HEAD_KEY) !== null) names.push(HEAD_KEY);
1603
+ return names;
1604
+ }
1605
+ };
1606
+ function getHeadManager(ctx) {
1607
+ const head = ctx.dom.head();
1608
+ if (head === null) return null;
1609
+ const mutable = ctx;
1610
+ if (mutable.head === void 0) mutable.head = new HeadManager(ctx.dom, head);
1611
+ return mutable.head;
1612
+ }
1613
+ function wireHeadBehavior(ctx, graphNode, instance) {
1614
+ const manager = getHeadManager(ctx);
1615
+ if (manager === null) return;
1616
+ const descFn = ctx.graph.getHandler(`__head__${graphNode.id}`);
1617
+ if (descFn === void 0) return;
1618
+ const contribution = descFn();
1619
+ const nodeId = graphNode.id;
1620
+ manager.register(nodeId, contribution.entries);
1621
+ for (const entry of contribution.entries) {
1622
+ for (const attrKey of Object.keys(entry.attrs)) {
1623
+ const v = entry.attrs[attrKey];
1624
+ if (isSignalLike(v)) {
1625
+ instance.trackCleanup(v.subscribe(() => manager.apply()));
1626
+ }
1627
+ }
1628
+ if (isSignalLike(entry.text)) {
1629
+ instance.trackCleanup(entry.text.subscribe(() => manager.apply()));
1630
+ }
1631
+ }
1632
+ instance.trackCleanup(() => manager.unregister(nodeId));
1633
+ }
1634
+
1471
1635
  // ../renderer/src/mount.ts
1472
1636
  var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
1473
1637
  "text",
@@ -1590,6 +1754,9 @@ function mountNode(ctx, graphNode, parentDom) {
1590
1754
  const instance2 = new NodeInstance(graphNode, el2);
1591
1755
  ctx.instances.set(graphNode.id, instance2);
1592
1756
  wireEvents(dom, graph, graphNode, el2, instance2);
1757
+ if (graphNode.stateRefs.length !== 0) {
1758
+ wireSignalBindings(ctx, graphNode, instance2, linkUpdate(dom, el2));
1759
+ }
1593
1760
  dom.appendChild(parentDom, el2);
1594
1761
  return instance2;
1595
1762
  }
@@ -1645,6 +1812,16 @@ function mountNode(ctx, graphNode, parentDom) {
1645
1812
  wireOverlayBehavior(ctx, graphNode, instance2, target);
1646
1813
  return instance2;
1647
1814
  }
1815
+ if (graphNode.type === "head") {
1816
+ const anchor = dom.createElement(resolveTag("head"));
1817
+ dom.setAttribute(anchor, "data-streetui-head-anchor", "");
1818
+ applyNodeProps(ctx, graphNode, anchor);
1819
+ const instance2 = new NodeInstance(graphNode, anchor);
1820
+ ctx.instances.set(graphNode.id, instance2);
1821
+ dom.appendChild(parentDom, anchor);
1822
+ wireHeadBehavior(ctx, graphNode, instance2);
1823
+ return instance2;
1824
+ }
1648
1825
  const tag = resolveTag(graphNode.type);
1649
1826
  const el = dom.createElement(tag);
1650
1827
  applyNodeProps(ctx, graphNode, el);
@@ -1709,6 +1886,17 @@ function buttonUpdate(dom, el) {
1709
1886
  }
1710
1887
  };
1711
1888
  }
1889
+ function linkUpdate(dom, el) {
1890
+ return (propKey, value) => {
1891
+ if (propKey === "label") {
1892
+ dom.setTextContent(el, String(value ?? ""));
1893
+ } else if (propKey === "href") {
1894
+ dom.setAttribute(el, "href", String(value ?? ""));
1895
+ } else {
1896
+ applyProp(dom, el, propKey, value);
1897
+ }
1898
+ };
1899
+ }
1712
1900
  function applyNodeProps(ctx, graphNode, el) {
1713
1901
  const props = graphNode.props;
1714
1902
  for (const key in props) {
@@ -1936,7 +2124,12 @@ function hydrateNode(ctx, graphNode, domNode, path) {
1936
2124
  case "link": {
1937
2125
  const instance = new NodeInstance(graphNode, domNode);
1938
2126
  ctx.instances.set(graphNode.id, instance);
1939
- if (graphNode.type === "link") wireEvents(dom, graph, graphNode, domNode, instance);
2127
+ if (graphNode.type === "link") {
2128
+ wireEvents(dom, graph, graphNode, domNode, instance);
2129
+ if (graphNode.stateRefs.length !== 0) {
2130
+ wireSignalBindings(ctx, graphNode, instance, linkUpdate(dom, domNode));
2131
+ }
2132
+ }
1940
2133
  return instance;
1941
2134
  }
1942
2135
  case "reactive-list":
@@ -1966,6 +2159,12 @@ function hydrateNode(ctx, graphNode, domNode, path) {
1966
2159
  wireOverlayBehavior(ctx, graphNode, instance, target);
1967
2160
  return instance;
1968
2161
  }
2162
+ case "head": {
2163
+ const instance = new NodeInstance(graphNode, domNode);
2164
+ ctx.instances.set(graphNode.id, instance);
2165
+ wireHeadBehavior(ctx, graphNode, instance);
2166
+ return instance;
2167
+ }
1969
2168
  default: {
1970
2169
  const instance = new NodeInstance(graphNode, domNode);
1971
2170
  ctx.instances.set(graphNode.id, instance);
@@ -2176,7 +2375,8 @@ function analyzeGraph(graph) {
2176
2375
  const isConditional = node.type === "conditional";
2177
2376
  const isPortal = node.type === "portal";
2178
2377
  const isComponent = node.type === "component";
2179
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent;
2378
+ const isHead = node.type === "head";
2379
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent && !isHead;
2180
2380
  const isStaticSubtree = isStatic && allChildrenStatic;
2181
2381
  nodes.set(node.id, {
2182
2382
  isStatic,
@@ -2668,6 +2868,118 @@ function resolveTransition(config) {
2668
2868
  };
2669
2869
  }
2670
2870
 
2871
+ // ../dsl/src/head.ts
2872
+ function metaDedupKey(m) {
2873
+ if (m.charset !== void 0) return "meta:charset";
2874
+ if (m.name !== void 0) return `meta:name=${m.name}`;
2875
+ if (m.property !== void 0) return `meta:property=${m.property}`;
2876
+ if (m.httpEquiv !== void 0) return `meta:http-equiv=${m.httpEquiv}`;
2877
+ return void 0;
2878
+ }
2879
+ function metaAttrs(m) {
2880
+ const attrs = {};
2881
+ if (m.charset !== void 0) attrs["charset"] = m.charset;
2882
+ if (m.name !== void 0) attrs["name"] = m.name;
2883
+ if (m.property !== void 0) attrs["property"] = m.property;
2884
+ if (m.httpEquiv !== void 0) attrs["http-equiv"] = m.httpEquiv;
2885
+ if (m.content !== void 0) attrs["content"] = m.content;
2886
+ return attrs;
2887
+ }
2888
+ function linkAttrs(l) {
2889
+ const attrs = { rel: l.rel, href: l.href };
2890
+ if (l.sizes !== void 0) attrs["sizes"] = l.sizes;
2891
+ if (l.type !== void 0) attrs["type"] = l.type;
2892
+ if (l.media !== void 0) attrs["media"] = l.media;
2893
+ if (l.as !== void 0) attrs["as"] = l.as;
2894
+ if (l.crossorigin !== void 0) attrs["crossorigin"] = l.crossorigin;
2895
+ if (l.hreflang !== void 0) attrs["hreflang"] = l.hreflang;
2896
+ return attrs;
2897
+ }
2898
+ function resolveHead(config) {
2899
+ const entries = [];
2900
+ if (config.charset !== void 0) {
2901
+ entries.push({ tag: "meta", dedupKey: "meta:charset", attrs: { charset: config.charset } });
2902
+ }
2903
+ if (config.base !== void 0) {
2904
+ entries.push({ tag: "base", dedupKey: "base", attrs: { href: config.base } });
2905
+ }
2906
+ if (config.title !== void 0) {
2907
+ entries.push({ tag: "title", dedupKey: "title", attrs: {}, text: config.title });
2908
+ }
2909
+ if (config.description !== void 0) {
2910
+ entries.push({
2911
+ tag: "meta",
2912
+ dedupKey: "meta:name=description",
2913
+ attrs: { name: "description", content: config.description }
2914
+ });
2915
+ }
2916
+ if (config.canonical !== void 0) {
2917
+ entries.push({
2918
+ tag: "link",
2919
+ dedupKey: "link:rel=canonical",
2920
+ attrs: { rel: "canonical", href: config.canonical }
2921
+ });
2922
+ }
2923
+ if (config.robots !== void 0) {
2924
+ entries.push({
2925
+ tag: "meta",
2926
+ dedupKey: "meta:name=robots",
2927
+ attrs: { name: "robots", content: config.robots }
2928
+ });
2929
+ }
2930
+ if (config.themeColor !== void 0) {
2931
+ entries.push({
2932
+ tag: "meta",
2933
+ dedupKey: "meta:name=theme-color",
2934
+ attrs: { name: "theme-color", content: config.themeColor }
2935
+ });
2936
+ }
2937
+ if (config.viewport !== void 0) {
2938
+ entries.push({
2939
+ tag: "meta",
2940
+ dedupKey: "meta:name=viewport",
2941
+ attrs: { name: "viewport", content: config.viewport }
2942
+ });
2943
+ }
2944
+ if (config.favicon !== void 0) {
2945
+ const l = typeof config.favicon === "string" ? { rel: "icon", href: config.favicon } : config.favicon;
2946
+ entries.push({ tag: "link", dedupKey: `link:rel=${l.rel}`, attrs: linkAttrs(l) });
2947
+ }
2948
+ if (config.openGraph !== void 0) {
2949
+ for (const key of Object.keys(config.openGraph)) {
2950
+ const property = `og:${key}`;
2951
+ entries.push({
2952
+ tag: "meta",
2953
+ dedupKey: `meta:property=${property}`,
2954
+ attrs: { property, content: config.openGraph[key] }
2955
+ });
2956
+ }
2957
+ }
2958
+ if (config.twitter !== void 0) {
2959
+ for (const key of Object.keys(config.twitter)) {
2960
+ const name = `twitter:${key}`;
2961
+ entries.push({
2962
+ tag: "meta",
2963
+ dedupKey: `meta:name=${name}`,
2964
+ attrs: { name, content: config.twitter[key] }
2965
+ });
2966
+ }
2967
+ }
2968
+ if (config.meta !== void 0) {
2969
+ for (const m of config.meta) {
2970
+ const key = metaDedupKey(m);
2971
+ if (key === void 0) continue;
2972
+ entries.push({ tag: "meta", dedupKey: key, attrs: metaAttrs(m) });
2973
+ }
2974
+ }
2975
+ if (config.link !== void 0) {
2976
+ for (const l of config.link) {
2977
+ entries.push({ tag: "link", dedupKey: `link:rel=${l.rel}:href=${l.href}`, attrs: linkAttrs(l) });
2978
+ }
2979
+ }
2980
+ return { entries };
2981
+ }
2982
+
2671
2983
  // ../state/src/signal.ts
2672
2984
  var _activeConsumer = null;
2673
2985
  function withConsumer(consumer, fn) {
@@ -3231,8 +3543,14 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
3231
3543
  this.container(id, (c) => {
3232
3544
  c.when(
3233
3545
  hasError,
3234
- // Error state → fallback.
3235
- (fb) => options.fallback(fb, readError(), retry),
3546
+ // Error state → fallback. The fallback branch mounts exactly when the
3547
+ // boundary enters its error state, so this is also where the optional
3548
+ // `onError` reporting hook fires (observe-only; §6/§7).
3549
+ (fb) => {
3550
+ const currentError = readError();
3551
+ options.onError?.(currentError);
3552
+ options.fallback(fb, currentError, retry);
3553
+ },
3236
3554
  // Healthy state → body, guarded against synchronous build throws.
3237
3555
  (body) => {
3238
3556
  try {
@@ -3244,6 +3562,24 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
3244
3562
  );
3245
3563
  }, { id });
3246
3564
  }
3565
+ asyncBoundary(key, res, branches) {
3566
+ const isError = derived(() => res.status.get() === "error");
3567
+ const showSuccess = derived(
3568
+ () => res.status.get() !== "error" && res.data.get() !== void 0
3569
+ );
3570
+ const showLoading = derived(
3571
+ () => res.status.get() !== "error" && res.data.get() === void 0
3572
+ );
3573
+ const dataSignal = derived(() => res.data.get());
3574
+ const retry = () => {
3575
+ void res.refetch();
3576
+ };
3577
+ this.container(key, (c) => {
3578
+ c.when(isError, (fb) => branches.error?.(fb, res.error.peek(), retry));
3579
+ c.when(showSuccess, (sb) => branches.success(sb, dataSignal));
3580
+ c.when(showLoading, (lb) => branches.loading?.(lb));
3581
+ }, { key });
3582
+ }
3247
3583
  // ── Portals & overlays ──────────────────────────────────────────────────────
3248
3584
  portal(key, builder, options = {}) {
3249
3585
  const node = this._graph.createNode("portal", {
@@ -3253,6 +3589,31 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
3253
3589
  });
3254
3590
  builder(new ContainerBuilderImpl(node, this._graph));
3255
3591
  }
3592
+ /**
3593
+ * Declare document metadata (2.0 §1–§3). Creates a `'head'` node — a neutral,
3594
+ * empty inline anchor at this position (one node / one element, so positional
3595
+ * hydration is preserved) — and registers a `__head__<id>` descriptor holding
3596
+ * this call's normalized, dedup-keyed {@link resolveHead} contribution. The
3597
+ * renderer applies it to `document.head` on the browser (adopting server tags
3598
+ * on hydration, cleaning up on unmount / route change) and emits the active
3599
+ * graph's merged metadata as a string on the server (`renderHead`).
3600
+ *
3601
+ * Multiple `head()` nodes may be live at once (app default + route + component)
3602
+ * — the renderer merges them and, per dedup key, the last in document order
3603
+ * wins (see head.ts). No new render path: this reuses the same graph-node +
3604
+ * handler-registry convention as overlays/transitions/components.
3605
+ */
3606
+ head(metadata) {
3607
+ const node = this._graph.createNode("head", {
3608
+ parent: this._node,
3609
+ props: { "data-streetui-head-anchor": "" }
3610
+ });
3611
+ const contribution = resolveHead(metadata);
3612
+ this._graph.registerHandler(
3613
+ `__head__${node.id}`,
3614
+ () => contribution
3615
+ );
3616
+ }
3256
3617
  /**
3257
3618
  * Shared assembly for every overlay kind: a `portal` node whose single child
3258
3619
  * is a `when(open, panel)` conditional. The panel container carries the
@@ -3583,6 +3944,7 @@ var ApplicationGraph = class {
3583
3944
  this.handlers.delete(`__overlay__${node.id}`);
3584
3945
  this.handlers.delete(`__component__${node.id}`);
3585
3946
  this.handlers.delete(`__transition__${node.id}`);
3947
+ this.handlers.delete(`__head__${node.id}`);
3586
3948
  }
3587
3949
  // ── Handler registry ──────────────────────────────────────────────────────
3588
3950
  registerHandler(key, fn) {
@@ -3760,7 +4122,7 @@ function trigger(el, type, init = {}) {
3760
4122
  }
3761
4123
 
3762
4124
  // src/version.ts
3763
- var VERSION = "1.9.0";
4125
+ var VERSION = "2.0.0";
3764
4126
  // Annotate the CommonJS export names for ESM import in node:
3765
4127
  0 && (module.exports = {
3766
4128
  VERSION,