streetui 1.9.0 → 2.1.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/server.cjs CHANGED
@@ -24,6 +24,7 @@ __export(server_exports, {
24
24
  ServerDOMAdapter: () => ServerDOMAdapter,
25
25
  VERSION: () => VERSION,
26
26
  readState: () => readState,
27
+ renderHead: () => renderHead,
27
28
  renderToString: () => renderToString,
28
29
  serializeState: () => serializeState
29
30
  });
@@ -531,6 +532,9 @@ var ServerDOMAdapter = class {
531
532
  body() {
532
533
  return null;
533
534
  }
535
+ head() {
536
+ return null;
537
+ }
534
538
  activeElement() {
535
539
  return null;
536
540
  }
@@ -722,7 +726,13 @@ var TAG_MAP = {
722
726
  // its children are relocated to a document.body container on the browser
723
727
  // (see the portal branch in mount.ts). On the server (no body) it renders
724
728
  // inline, so the anchor tag is what SSR/hydration positionally match on.
725
- portal: "div"
729
+ portal: "div",
730
+ // A `head()` node renders as a neutral, empty inline anchor <div> at its
731
+ // declaration site (like a portal anchor). Its actual contribution — title/
732
+ // meta/link/etc. — is applied to `document.head` by `wireHeadBehavior` on the
733
+ // browser, and emitted separately by `renderHead()` on the server. Keeping a
734
+ // one-node/one-element anchor preserves positional hydration.
735
+ head: "div"
726
736
  };
727
737
  function resolveTag(type) {
728
738
  return TAG_MAP[type] ?? "div";
@@ -1167,6 +1177,184 @@ var TransitionController = class {
1167
1177
  }
1168
1178
  };
1169
1179
 
1180
+ // ../renderer/src/head.ts
1181
+ var HEAD_MARKER = "data-streetui-head";
1182
+ var HEAD_KEY = "data-streetui-head-key";
1183
+ function isSignalLike(v) {
1184
+ return v !== null && typeof v === "object" && typeof v["subscribe"] === "function" && typeof v["peek"] === "function";
1185
+ }
1186
+ function readValue(v) {
1187
+ if (isSignalLike(v)) return String(v.peek() ?? "");
1188
+ return String(v ?? "");
1189
+ }
1190
+ function resolveEntry(entry) {
1191
+ const attrs = {};
1192
+ for (const key of Object.keys(entry.attrs)) {
1193
+ attrs[key] = readValue(entry.attrs[key]);
1194
+ }
1195
+ const resolved = { tag: entry.tag, attrs };
1196
+ if (entry.tag === "title") {
1197
+ return { ...resolved, text: readValue(entry.text) };
1198
+ }
1199
+ return resolved;
1200
+ }
1201
+ var HeadManager = class {
1202
+ _dom;
1203
+ _head;
1204
+ _contributions = /* @__PURE__ */ new Map();
1205
+ _applied = /* @__PURE__ */ new Map();
1206
+ _order = 0;
1207
+ _adopted = false;
1208
+ constructor(dom, head) {
1209
+ this._dom = dom;
1210
+ this._head = head;
1211
+ }
1212
+ /** Register (or replace) a node's contribution and re-apply the merged result. */
1213
+ register(nodeId, entries) {
1214
+ this._contributions.set(nodeId, { order: this._order++, entries });
1215
+ this.apply();
1216
+ }
1217
+ /** Withdraw a node's contribution (unmount / route change) and re-apply. */
1218
+ unregister(nodeId) {
1219
+ if (this._contributions.delete(nodeId)) this.apply();
1220
+ }
1221
+ /** Recompute the merged head and patch `document.head` to match. */
1222
+ apply() {
1223
+ if (!this._adopted) {
1224
+ this._adoptServerTags();
1225
+ this._adopted = true;
1226
+ }
1227
+ const ordered = [...this._contributions.values()].sort((a, b) => a.order - b.order);
1228
+ const merged = /* @__PURE__ */ new Map();
1229
+ for (const contribution of ordered) {
1230
+ for (const entry of contribution.entries) {
1231
+ merged.set(entry.dedupKey, resolveEntry(entry));
1232
+ }
1233
+ }
1234
+ for (const [key, desired] of merged) {
1235
+ const existing = this._applied.get(key);
1236
+ if (existing !== void 0 && existing.tag === desired.tag) {
1237
+ this._reconcileAttrs(existing, desired);
1238
+ } else {
1239
+ if (existing !== void 0) {
1240
+ this._dom.removeChild(this._head, existing.el);
1241
+ this._applied.delete(key);
1242
+ }
1243
+ const el = this._createTag(key, desired);
1244
+ this._dom.appendChild(this._head, el);
1245
+ this._applied.set(key, { el, attrKeys: new Set(Object.keys(desired.attrs)), tag: desired.tag });
1246
+ }
1247
+ }
1248
+ for (const [key, record] of [...this._applied]) {
1249
+ if (!merged.has(key)) {
1250
+ this._dom.removeChild(this._head, record.el);
1251
+ this._applied.delete(key);
1252
+ }
1253
+ }
1254
+ }
1255
+ _createTag(key, desired) {
1256
+ const el = this._dom.createElement(desired.tag);
1257
+ this._dom.setAttribute(el, HEAD_MARKER, "");
1258
+ this._dom.setAttribute(el, HEAD_KEY, key);
1259
+ for (const attr of Object.keys(desired.attrs)) {
1260
+ this._dom.setAttribute(el, attr, desired.attrs[attr]);
1261
+ }
1262
+ if (desired.tag === "title") this._dom.setTextContent(el, desired.text ?? "");
1263
+ return el;
1264
+ }
1265
+ _reconcileAttrs(record, desired) {
1266
+ const nextKeys = new Set(Object.keys(desired.attrs));
1267
+ for (const attr of record.attrKeys) {
1268
+ if (!nextKeys.has(attr)) this._dom.removeAttribute(record.el, attr);
1269
+ }
1270
+ for (const attr of nextKeys) {
1271
+ this._dom.setAttribute(record.el, attr, desired.attrs[attr]);
1272
+ }
1273
+ if (desired.tag === "title") this._dom.setTextContent(record.el, desired.text ?? "");
1274
+ record.attrKeys = nextKeys;
1275
+ }
1276
+ /**
1277
+ * Seed `_applied` from server-emitted `[data-streetui-head-key]` tags already
1278
+ * in `document.head`. The subsequent diff reuses these elements when the
1279
+ * client desires the same key (no duplicate), rewrites them if the value
1280
+ * changed, or removes them if the client graph no longer wants them.
1281
+ */
1282
+ _adoptServerTags() {
1283
+ for (const child of this._dom.childNodes(this._head)) {
1284
+ if (!this._dom.isElement(child)) continue;
1285
+ const el = child;
1286
+ const key = this._dom.getAttribute(el, HEAD_KEY);
1287
+ if (key === null) continue;
1288
+ this._applied.set(key, {
1289
+ el,
1290
+ attrKeys: new Set(this._attrNames(el)),
1291
+ tag: this._dom.tagName(el)
1292
+ });
1293
+ }
1294
+ }
1295
+ /** The framework-managed attribute names currently on a server tag. */
1296
+ _attrNames(el) {
1297
+ const names = [];
1298
+ if (this._dom.getAttribute(el, HEAD_MARKER) !== null) names.push(HEAD_MARKER);
1299
+ if (this._dom.getAttribute(el, HEAD_KEY) !== null) names.push(HEAD_KEY);
1300
+ return names;
1301
+ }
1302
+ };
1303
+ function getHeadManager(ctx) {
1304
+ const head = ctx.dom.head();
1305
+ if (head === null) return null;
1306
+ const mutable = ctx;
1307
+ if (mutable.head === void 0) mutable.head = new HeadManager(ctx.dom, head);
1308
+ return mutable.head;
1309
+ }
1310
+ function wireHeadBehavior(ctx, graphNode, instance) {
1311
+ const manager = getHeadManager(ctx);
1312
+ if (manager === null) return;
1313
+ const descFn = ctx.graph.getHandler(`__head__${graphNode.id}`);
1314
+ if (descFn === void 0) return;
1315
+ const contribution = descFn();
1316
+ const nodeId = graphNode.id;
1317
+ manager.register(nodeId, contribution.entries);
1318
+ for (const entry of contribution.entries) {
1319
+ for (const attrKey of Object.keys(entry.attrs)) {
1320
+ const v = entry.attrs[attrKey];
1321
+ if (isSignalLike(v)) {
1322
+ instance.trackCleanup(v.subscribe(() => manager.apply()));
1323
+ }
1324
+ }
1325
+ if (isSignalLike(entry.text)) {
1326
+ instance.trackCleanup(entry.text.subscribe(() => manager.apply()));
1327
+ }
1328
+ }
1329
+ instance.trackCleanup(() => manager.unregister(nodeId));
1330
+ }
1331
+ function renderHead(compiled) {
1332
+ const graph = compiled.graph;
1333
+ const merged = /* @__PURE__ */ new Map();
1334
+ graph.walk((node) => {
1335
+ if (node.type !== "head") return;
1336
+ const descFn = graph.getHandler(`__head__${node.id}`);
1337
+ if (descFn === void 0) return;
1338
+ for (const entry of descFn().entries) {
1339
+ merged.set(entry.dedupKey, resolveEntry(entry));
1340
+ }
1341
+ });
1342
+ if (merged.size === 0) return "";
1343
+ const dom = new ServerDOMAdapter();
1344
+ let out = "";
1345
+ for (const [key, desired] of merged) {
1346
+ const el = dom.createElement(desired.tag);
1347
+ dom.setAttribute(el, HEAD_MARKER, "");
1348
+ dom.setAttribute(el, HEAD_KEY, key);
1349
+ for (const attr of Object.keys(desired.attrs)) {
1350
+ dom.setAttribute(el, attr, desired.attrs[attr]);
1351
+ }
1352
+ if (desired.tag === "title") dom.setTextContent(el, desired.text ?? "");
1353
+ out += dom.serializeOuter(el);
1354
+ }
1355
+ return out;
1356
+ }
1357
+
1170
1358
  // ../renderer/src/mount.ts
1171
1359
  var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
1172
1360
  "text",
@@ -1289,6 +1477,9 @@ function mountNode(ctx, graphNode, parentDom) {
1289
1477
  const instance2 = new NodeInstance(graphNode, el2);
1290
1478
  ctx.instances.set(graphNode.id, instance2);
1291
1479
  wireEvents(dom, graph, graphNode, el2, instance2);
1480
+ if (graphNode.stateRefs.length !== 0) {
1481
+ wireSignalBindings(ctx, graphNode, instance2, linkUpdate(dom, el2));
1482
+ }
1292
1483
  dom.appendChild(parentDom, el2);
1293
1484
  return instance2;
1294
1485
  }
@@ -1344,6 +1535,16 @@ function mountNode(ctx, graphNode, parentDom) {
1344
1535
  wireOverlayBehavior(ctx, graphNode, instance2, target);
1345
1536
  return instance2;
1346
1537
  }
1538
+ if (graphNode.type === "head") {
1539
+ const anchor = dom.createElement(resolveTag("head"));
1540
+ dom.setAttribute(anchor, "data-streetui-head-anchor", "");
1541
+ applyNodeProps(ctx, graphNode, anchor);
1542
+ const instance2 = new NodeInstance(graphNode, anchor);
1543
+ ctx.instances.set(graphNode.id, instance2);
1544
+ dom.appendChild(parentDom, anchor);
1545
+ wireHeadBehavior(ctx, graphNode, instance2);
1546
+ return instance2;
1547
+ }
1347
1548
  const tag = resolveTag(graphNode.type);
1348
1549
  const el = dom.createElement(tag);
1349
1550
  applyNodeProps(ctx, graphNode, el);
@@ -1408,6 +1609,17 @@ function buttonUpdate(dom, el) {
1408
1609
  }
1409
1610
  };
1410
1611
  }
1612
+ function linkUpdate(dom, el) {
1613
+ return (propKey, value) => {
1614
+ if (propKey === "label") {
1615
+ dom.setTextContent(el, String(value ?? ""));
1616
+ } else if (propKey === "href") {
1617
+ dom.setAttribute(el, "href", String(value ?? ""));
1618
+ } else {
1619
+ applyProp(dom, el, propKey, value);
1620
+ }
1621
+ };
1622
+ }
1411
1623
  function applyNodeProps(ctx, graphNode, el) {
1412
1624
  const props = graphNode.props;
1413
1625
  for (const key in props) {
@@ -1619,7 +1831,8 @@ function analyzeGraph(graph) {
1619
1831
  const isConditional = node.type === "conditional";
1620
1832
  const isPortal = node.type === "portal";
1621
1833
  const isComponent = node.type === "component";
1622
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent;
1834
+ const isHead = node.type === "head";
1835
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent && !isHead;
1623
1836
  const isStaticSubtree = isStatic && allChildrenStatic;
1624
1837
  nodes.set(node.id, {
1625
1838
  isStatic,
@@ -1706,13 +1919,14 @@ function renderToString(compiled, options = {}) {
1706
1919
  }
1707
1920
 
1708
1921
  // src/version.ts
1709
- var VERSION = "1.9.0";
1922
+ var VERSION = "2.1.0";
1710
1923
  // Annotate the CommonJS export names for ESM import in node:
1711
1924
  0 && (module.exports = {
1712
1925
  STATE_MARKER_ATTR,
1713
1926
  ServerDOMAdapter,
1714
1927
  VERSION,
1715
1928
  readState,
1929
+ renderHead,
1716
1930
  renderToString,
1717
1931
  serializeState
1718
1932
  });