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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "1.8.0";
2
+ var VERSION = "2.0.0";
3
3
 
4
4
  // ../state/src/signal.ts
5
5
  var _activeConsumer = null;
@@ -339,6 +339,198 @@ function resource(loader, options = {}) {
339
339
  };
340
340
  }
341
341
 
342
+ // ../state/src/mutation.ts
343
+ function mutation(mutator, options = {}) {
344
+ const status = signal("idle");
345
+ const data = signal(void 0);
346
+ const error = signal(void 0);
347
+ const pending = derived(() => status.get() === "loading");
348
+ let disposed = false;
349
+ let runId = 0;
350
+ const mutate = async (args) => {
351
+ if (disposed) {
352
+ return await mutator(args);
353
+ }
354
+ const myRun = ++runId;
355
+ batch(() => {
356
+ error.set(void 0);
357
+ status.set("loading");
358
+ });
359
+ try {
360
+ const result = await mutator(args);
361
+ if (!disposed && myRun === runId) {
362
+ batch(() => {
363
+ data.set(result);
364
+ error.set(void 0);
365
+ status.set("success");
366
+ });
367
+ }
368
+ if (!disposed && myRun === runId) {
369
+ await options.onSuccess?.(result, args);
370
+ await options.onSettled?.(args);
371
+ }
372
+ return result;
373
+ } catch (err) {
374
+ if (!disposed && myRun === runId) {
375
+ batch(() => {
376
+ error.set(err);
377
+ status.set("error");
378
+ });
379
+ await options.onError?.(err, args);
380
+ await options.onSettled?.(args);
381
+ }
382
+ throw err;
383
+ }
384
+ };
385
+ const reset = () => {
386
+ batch(() => {
387
+ status.set("idle");
388
+ data.set(void 0);
389
+ error.set(void 0);
390
+ });
391
+ };
392
+ const dispose = () => {
393
+ disposed = true;
394
+ };
395
+ if (options.onCleanup !== void 0) options.onCleanup(dispose);
396
+ return { status, data, error, pending, mutate, reset, dispose };
397
+ }
398
+
399
+ // ../state/src/client.ts
400
+ var HttpError = class extends Error {
401
+ status;
402
+ statusText;
403
+ url;
404
+ body;
405
+ constructor(status, statusText, url, body) {
406
+ super(`HTTP ${status} ${statusText} for ${url}`);
407
+ this.name = "HttpError";
408
+ this.status = status;
409
+ this.statusText = statusText;
410
+ this.url = url;
411
+ this.body = body;
412
+ }
413
+ };
414
+ function joinUrl(baseUrl, path) {
415
+ if (baseUrl === void 0 || baseUrl === "") return path;
416
+ const b = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
417
+ const p = path.startsWith("/") ? path : `/${path}`;
418
+ return `${b}${p}`;
419
+ }
420
+ function withQuery(url, query) {
421
+ if (query === void 0) return url;
422
+ const params = new URLSearchParams();
423
+ for (const [k, v] of Object.entries(query)) params.set(k, String(v));
424
+ const qs = params.toString();
425
+ if (qs === "") return url;
426
+ return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
427
+ }
428
+ function createClient(config = {}) {
429
+ const doFetch = config.fetch ?? ((input, init) => {
430
+ if (typeof fetch === "undefined") {
431
+ throw new Error("createClient: no global fetch; pass { fetch } explicitly");
432
+ }
433
+ return fetch(input, init);
434
+ });
435
+ async function request(method, path, body, reqConfig = {}) {
436
+ const url = withQuery(joinUrl(config.baseUrl, path), reqConfig.query);
437
+ const headers = { ...config.headers, ...reqConfig.headers };
438
+ const init = { method, headers };
439
+ if (reqConfig.signal !== void 0) init.signal = reqConfig.signal;
440
+ if (body !== void 0) {
441
+ if (headers["Content-Type"] === void 0) headers["Content-Type"] = "application/json";
442
+ init.body = JSON.stringify(body);
443
+ }
444
+ const response = await doFetch(url, init);
445
+ const parsed = await parseBody(response);
446
+ if (!response.ok) {
447
+ throw new HttpError(response.status, response.statusText, url, parsed);
448
+ }
449
+ return parsed;
450
+ }
451
+ return {
452
+ request,
453
+ get: (path, c) => request("GET", path, void 0, c),
454
+ post: (path, b, c) => request("POST", path, b, c),
455
+ put: (path, b, c) => request("PUT", path, b, c),
456
+ patch: (path, b, c) => request("PATCH", path, b, c),
457
+ del: (path, c) => request("DELETE", path, void 0, c),
458
+ resource: (path, options = {}) => {
459
+ const { query, ...resourceOptions } = options;
460
+ return resource(
461
+ (ctx) => request("GET", path, void 0, { signal: ctx.signal, ...query !== void 0 ? { query } : {} }),
462
+ resourceOptions
463
+ );
464
+ },
465
+ mutation: (method, path, options) => mutation((args) => request(method, path, args), options ?? {})
466
+ };
467
+ }
468
+ async function parseBody(response) {
469
+ const text = await response.text();
470
+ if (text === "") return void 0;
471
+ const type = response.headers.get("content-type") ?? "";
472
+ if (type.includes("application/json")) {
473
+ try {
474
+ return JSON.parse(text);
475
+ } catch {
476
+ return text;
477
+ }
478
+ }
479
+ return text;
480
+ }
481
+
482
+ // ../state/src/auth.ts
483
+ function createAuthSession(config) {
484
+ const session = resource(
485
+ (ctx) => config.loadUser(ctx),
486
+ {
487
+ ...config.immediate !== void 0 ? { immediate: config.immediate } : {},
488
+ ...config.onCleanup !== void 0 ? { onCleanup: config.onCleanup } : {}
489
+ }
490
+ );
491
+ const status = derived(() => {
492
+ const s = session.status.get();
493
+ const d = session.data.get();
494
+ if (s === "error") return "error";
495
+ if (s === "idle" || s === "loading" && d === void 0) return "loading";
496
+ return d === null || d === void 0 ? "unauthenticated" : "authenticated";
497
+ });
498
+ const user = derived(() => {
499
+ const d = session.data.get();
500
+ return d === null ? void 0 : d;
501
+ });
502
+ const authenticated = derived(() => status.get() === "authenticated");
503
+ const unauthenticated = derived(() => status.get() === "unauthenticated");
504
+ const loading = derived(() => status.get() === "loading");
505
+ const logoutMutation = mutation(
506
+ async () => {
507
+ await config.logout?.();
508
+ },
509
+ { onSuccess: () => session.refetch() }
510
+ );
511
+ const dispose = () => {
512
+ session.dispose();
513
+ status.dispose();
514
+ user.dispose();
515
+ authenticated.dispose();
516
+ unauthenticated.dispose();
517
+ loading.dispose();
518
+ logoutMutation.dispose();
519
+ };
520
+ return {
521
+ status,
522
+ user,
523
+ error: session.error,
524
+ authenticated,
525
+ unauthenticated,
526
+ loading,
527
+ loggingOut: logoutMutation.pending,
528
+ refresh: () => session.refetch(),
529
+ logout: () => logoutMutation.mutate(void 0),
530
+ dispose
531
+ };
532
+ }
533
+
342
534
  // ../core/src/a11y-ids.ts
343
535
  var UNSAFE = /[^A-Za-z0-9_-]+/g;
344
536
  function toIdToken(base) {
@@ -354,6 +546,9 @@ function a11yIds(base) {
354
546
  description: `${token}-description`,
355
547
  error: `${token}-error`,
356
548
  title: `${token}-title`,
549
+ trigger: `${token}-trigger`,
550
+ controls: `${token}-controls`,
551
+ owns: `${token}-owns`,
357
552
  id: (suffix) => `${token}-${toIdToken(suffix)}`
358
553
  };
359
554
  }
@@ -602,9 +797,13 @@ function formatDiagnosticContext(context) {
602
797
  const parts = [];
603
798
  if (context.package !== void 0) parts.push(`package=${context.package}`);
604
799
  if (context.operation !== void 0) parts.push(`operation=${context.operation}`);
800
+ if (context.component !== void 0) parts.push(`component=${context.component}`);
801
+ if (context.phase !== void 0) parts.push(`phase=${context.phase}`);
605
802
  if (context.nodeId !== void 0) parts.push(`node=${context.nodeId}`);
803
+ if (context.element !== void 0) parts.push(`element=${context.element}`);
606
804
  if (context.route !== void 0) parts.push(`route=${context.route}`);
607
805
  if (context.resource !== void 0) parts.push(`resource=${context.resource}`);
806
+ if (context.signal !== void 0) parts.push(`signal=${context.signal}`);
608
807
  return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
609
808
  }
610
809
  var StreetFrameworkError = class extends Error {
@@ -635,6 +834,42 @@ function consoleDiagnosticSink(logger = console) {
635
834
  error: (m, c) => logger.error?.(`${m}${formatDiagnosticContext(c)}`)
636
835
  };
637
836
  }
837
+ function describeErrorAt(error, context, options, depth) {
838
+ const isError = error instanceof Error;
839
+ const name = isError ? error.name : "Error";
840
+ const message = isError ? error.message : safeStringify(error);
841
+ const report = { name, message, isError };
842
+ if (context !== void 0) report.context = context;
843
+ if (options.includeStack === true && isError && typeof error.stack === "string") {
844
+ report.stack = error.stack;
845
+ }
846
+ if (options.includeCause === true && isError && depth < 4) {
847
+ const cause = error.cause;
848
+ if (cause !== void 0 && cause !== null) {
849
+ report.cause = describeErrorAt(cause, void 0, options, depth + 1);
850
+ }
851
+ }
852
+ return report;
853
+ }
854
+ function safeStringify(value) {
855
+ if (typeof value === "string") return value;
856
+ if (value === null) return "null";
857
+ if (value === void 0) return "undefined";
858
+ const t = typeof value;
859
+ if (t === "number" || t === "boolean" || t === "bigint" || t === "symbol") {
860
+ return String(value);
861
+ }
862
+ const ctor = t === "object" && value !== null ? value.constructor?.name ?? "Object" : t;
863
+ return `[non-Error ${ctor}]`;
864
+ }
865
+ function describeError(error, context, options = {}) {
866
+ return describeErrorAt(error, context, options, 0);
867
+ }
868
+ function reportError(sink, error, context, options = {}) {
869
+ const report = describeError(error, context, options);
870
+ reportDiagnostic(sink, "error", report.message, report.context);
871
+ return report;
872
+ }
638
873
 
639
874
  // ../graph/src/graph-node.ts
640
875
  var GraphNode = class _GraphNode {
@@ -804,6 +1039,8 @@ var ApplicationGraph = class {
804
1039
  this.handlers.delete(`__listplan__${node.id}`);
805
1040
  this.handlers.delete(`__overlay__${node.id}`);
806
1041
  this.handlers.delete(`__component__${node.id}`);
1042
+ this.handlers.delete(`__transition__${node.id}`);
1043
+ this.handlers.delete(`__head__${node.id}`);
807
1044
  }
808
1045
  // ── Handler registry ──────────────────────────────────────────────────────
809
1046
  registerHandler(key, fn) {
@@ -892,6 +1129,162 @@ var ApplicationGraph = class {
892
1129
  }
893
1130
  };
894
1131
 
1132
+ // ../dsl/src/transition.ts
1133
+ function classes(value) {
1134
+ if (value === void 0) return [];
1135
+ const out = [];
1136
+ for (const token of value.split(/\s+/)) {
1137
+ if (token.length > 0 && !out.includes(token)) out.push(token);
1138
+ }
1139
+ return out;
1140
+ }
1141
+ function merge(a, b) {
1142
+ const out = [...a];
1143
+ for (const token of b) if (!out.includes(token)) out.push(token);
1144
+ return out;
1145
+ }
1146
+ function resolveTransition(config) {
1147
+ const n = config.name;
1148
+ const enterActive = merge(
1149
+ classes(config.enter),
1150
+ classes(config.enterActive ?? (n !== void 0 ? `${n}-enter-active` : void 0))
1151
+ );
1152
+ const leaveActive = merge(
1153
+ classes(config.leave),
1154
+ classes(config.leaveActive ?? (n !== void 0 ? `${n}-leave-active` : void 0))
1155
+ );
1156
+ return {
1157
+ enterActive,
1158
+ enterFrom: classes(config.enterFrom ?? (n !== void 0 ? `${n}-enter-from` : void 0)),
1159
+ enterTo: classes(config.enterTo ?? (n !== void 0 ? `${n}-enter-to` : void 0)),
1160
+ leaveActive,
1161
+ leaveFrom: classes(config.leaveFrom ?? (n !== void 0 ? `${n}-leave-from` : void 0)),
1162
+ leaveTo: classes(config.leaveTo ?? (n !== void 0 ? `${n}-leave-to` : void 0)),
1163
+ appear: config.appear ?? false,
1164
+ duration: config.duration ?? 1e3
1165
+ };
1166
+ }
1167
+ function isTransitionConfig(value) {
1168
+ if (value === null || typeof value !== "object") return false;
1169
+ const o = value;
1170
+ return typeof o["name"] === "string" || typeof o["enter"] === "string" || typeof o["enterActive"] === "string" || typeof o["enterFrom"] === "string" || typeof o["leave"] === "string" || typeof o["leaveActive"] === "string" || typeof o["leaveFrom"] === "string";
1171
+ }
1172
+
1173
+ // ../dsl/src/head.ts
1174
+ function metaDedupKey(m) {
1175
+ if (m.charset !== void 0) return "meta:charset";
1176
+ if (m.name !== void 0) return `meta:name=${m.name}`;
1177
+ if (m.property !== void 0) return `meta:property=${m.property}`;
1178
+ if (m.httpEquiv !== void 0) return `meta:http-equiv=${m.httpEquiv}`;
1179
+ return void 0;
1180
+ }
1181
+ function metaAttrs(m) {
1182
+ const attrs = {};
1183
+ if (m.charset !== void 0) attrs["charset"] = m.charset;
1184
+ if (m.name !== void 0) attrs["name"] = m.name;
1185
+ if (m.property !== void 0) attrs["property"] = m.property;
1186
+ if (m.httpEquiv !== void 0) attrs["http-equiv"] = m.httpEquiv;
1187
+ if (m.content !== void 0) attrs["content"] = m.content;
1188
+ return attrs;
1189
+ }
1190
+ function linkAttrs(l) {
1191
+ const attrs = { rel: l.rel, href: l.href };
1192
+ if (l.sizes !== void 0) attrs["sizes"] = l.sizes;
1193
+ if (l.type !== void 0) attrs["type"] = l.type;
1194
+ if (l.media !== void 0) attrs["media"] = l.media;
1195
+ if (l.as !== void 0) attrs["as"] = l.as;
1196
+ if (l.crossorigin !== void 0) attrs["crossorigin"] = l.crossorigin;
1197
+ if (l.hreflang !== void 0) attrs["hreflang"] = l.hreflang;
1198
+ return attrs;
1199
+ }
1200
+ function resolveHead(config) {
1201
+ const entries = [];
1202
+ if (config.charset !== void 0) {
1203
+ entries.push({ tag: "meta", dedupKey: "meta:charset", attrs: { charset: config.charset } });
1204
+ }
1205
+ if (config.base !== void 0) {
1206
+ entries.push({ tag: "base", dedupKey: "base", attrs: { href: config.base } });
1207
+ }
1208
+ if (config.title !== void 0) {
1209
+ entries.push({ tag: "title", dedupKey: "title", attrs: {}, text: config.title });
1210
+ }
1211
+ if (config.description !== void 0) {
1212
+ entries.push({
1213
+ tag: "meta",
1214
+ dedupKey: "meta:name=description",
1215
+ attrs: { name: "description", content: config.description }
1216
+ });
1217
+ }
1218
+ if (config.canonical !== void 0) {
1219
+ entries.push({
1220
+ tag: "link",
1221
+ dedupKey: "link:rel=canonical",
1222
+ attrs: { rel: "canonical", href: config.canonical }
1223
+ });
1224
+ }
1225
+ if (config.robots !== void 0) {
1226
+ entries.push({
1227
+ tag: "meta",
1228
+ dedupKey: "meta:name=robots",
1229
+ attrs: { name: "robots", content: config.robots }
1230
+ });
1231
+ }
1232
+ if (config.themeColor !== void 0) {
1233
+ entries.push({
1234
+ tag: "meta",
1235
+ dedupKey: "meta:name=theme-color",
1236
+ attrs: { name: "theme-color", content: config.themeColor }
1237
+ });
1238
+ }
1239
+ if (config.viewport !== void 0) {
1240
+ entries.push({
1241
+ tag: "meta",
1242
+ dedupKey: "meta:name=viewport",
1243
+ attrs: { name: "viewport", content: config.viewport }
1244
+ });
1245
+ }
1246
+ if (config.favicon !== void 0) {
1247
+ const l = typeof config.favicon === "string" ? { rel: "icon", href: config.favicon } : config.favicon;
1248
+ entries.push({ tag: "link", dedupKey: `link:rel=${l.rel}`, attrs: linkAttrs(l) });
1249
+ }
1250
+ if (config.openGraph !== void 0) {
1251
+ for (const key of Object.keys(config.openGraph)) {
1252
+ const property = `og:${key}`;
1253
+ entries.push({
1254
+ tag: "meta",
1255
+ dedupKey: `meta:property=${property}`,
1256
+ attrs: { property, content: config.openGraph[key] }
1257
+ });
1258
+ }
1259
+ }
1260
+ if (config.twitter !== void 0) {
1261
+ for (const key of Object.keys(config.twitter)) {
1262
+ const name = `twitter:${key}`;
1263
+ entries.push({
1264
+ tag: "meta",
1265
+ dedupKey: `meta:name=${name}`,
1266
+ attrs: { name, content: config.twitter[key] }
1267
+ });
1268
+ }
1269
+ }
1270
+ if (config.meta !== void 0) {
1271
+ for (const m of config.meta) {
1272
+ const key = metaDedupKey(m);
1273
+ if (key === void 0) continue;
1274
+ entries.push({ tag: "meta", dedupKey: key, attrs: metaAttrs(m) });
1275
+ }
1276
+ }
1277
+ if (config.link !== void 0) {
1278
+ for (const l of config.link) {
1279
+ entries.push({ tag: "link", dedupKey: `link:rel=${l.rel}:href=${l.href}`, attrs: linkAttrs(l) });
1280
+ }
1281
+ }
1282
+ return { entries };
1283
+ }
1284
+ function isHeadContribution(value) {
1285
+ return value !== null && typeof value === "object" && Array.isArray(value["entries"]);
1286
+ }
1287
+
895
1288
  // ../dsl/src/builders.ts
896
1289
  function isSignal(v) {
897
1290
  return v !== null && typeof v === "object" && typeof v["get"] === "function" && typeof v["subscribe"] === "function";
@@ -919,6 +1312,18 @@ function applyA11yProps(props, options) {
919
1312
  if (options.ariaInvalid !== void 0) props["aria-invalid"] = String(options.ariaInvalid);
920
1313
  if (options.ariaRequired !== void 0) props["aria-required"] = String(options.ariaRequired);
921
1314
  if (options.ariaModal !== void 0) props["aria-modal"] = String(options.ariaModal);
1315
+ if (options.ariaOwns !== void 0) props["aria-owns"] = options.ariaOwns;
1316
+ if (options.ariaActiveDescendant !== void 0) props["aria-activedescendant"] = options.ariaActiveDescendant;
1317
+ if (options.ariaHasPopup !== void 0) props["aria-haspopup"] = String(options.ariaHasPopup);
1318
+ if (options.ariaSelected !== void 0) props["aria-selected"] = String(options.ariaSelected);
1319
+ }
1320
+ function registerTransition(graph, node, config) {
1321
+ if (config === void 0) return;
1322
+ const resolved = resolveTransition(config);
1323
+ graph.registerHandler(
1324
+ `__transition__${node.id}`,
1325
+ () => resolved
1326
+ );
922
1327
  }
923
1328
  function containerProps(options) {
924
1329
  const props = {};
@@ -979,6 +1384,7 @@ var OVERLAY_KINDS = {
979
1384
  role: "menu",
980
1385
  modal: false,
981
1386
  takesFocus: true,
1387
+ menu: true,
982
1388
  ariaModal: false,
983
1389
  defaultCloseOnEscape: true,
984
1390
  defaultRestoreFocus: true
@@ -1111,6 +1517,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1111
1517
  parent: this._node,
1112
1518
  props: containerProps(options)
1113
1519
  });
1520
+ registerTransition(this._graph, node, options.transition);
1114
1521
  builder(new SectionBuilderImpl(node, this._graph));
1115
1522
  }
1116
1523
  container(key, builder, options = {}) {
@@ -1119,6 +1526,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1119
1526
  parent: this._node,
1120
1527
  props: containerProps(options)
1121
1528
  });
1529
+ registerTransition(this._graph, node, options.transition);
1122
1530
  builder(new ContainerBuilderImpl(node, this._graph));
1123
1531
  }
1124
1532
  list(key, builder, options = {}) {
@@ -1127,6 +1535,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1127
1535
  parent: this._node,
1128
1536
  props: containerProps(options)
1129
1537
  });
1538
+ registerTransition(this._graph, node, options.transition);
1130
1539
  builder(new ListBuilderImpl(node, this._graph));
1131
1540
  }
1132
1541
  listOf(key, items, renderItem, options = {}) {
@@ -1136,6 +1545,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1136
1545
  parent: this._node,
1137
1546
  props: containerProps(options)
1138
1547
  });
1548
+ registerTransition(graph, node, options.transition);
1139
1549
  const signalId = `${node.id}:items`;
1140
1550
  node.stateRefs.push({ signalId, propKey: "items" });
1141
1551
  graph.registerHandler(`__signal__${signalId}`, items);
@@ -1157,6 +1567,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1157
1567
  }
1158
1568
  });
1159
1569
  renderItem(item, index, new ContainerBuilderImpl(itemNode, graph));
1570
+ registerTransition(graph, itemNode, options.itemTransition);
1160
1571
  return itemNode;
1161
1572
  };
1162
1573
  const buildPlan = (raw) => {
@@ -1188,6 +1599,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1188
1599
  parent: this._node,
1189
1600
  props
1190
1601
  });
1602
+ registerTransition(this._graph, node, options.transition);
1191
1603
  if (options.onSubmit !== void 0) {
1192
1604
  const handlerKey = `submit:${node.id}`;
1193
1605
  this._graph.registerHandler(handlerKey, options.onSubmit);
@@ -1195,18 +1607,20 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1195
1607
  }
1196
1608
  builder(new FormBuilderImpl(node, this._graph));
1197
1609
  }
1198
- when(condition, builder, elseBuilder) {
1610
+ when(condition, builder, elseBuilder, options = {}) {
1199
1611
  const graph = this._graph;
1200
1612
  const node = graph.createNode("conditional", {
1201
1613
  parent: this._node,
1202
1614
  props: containerProps({})
1203
1615
  });
1616
+ const branchTransition = options.transition !== void 0 && options.appear === true ? { ...options.transition, appear: true } : options.transition;
1204
1617
  const buildBranch = (build, tag) => {
1205
1618
  const branchKey = `when-${tag}:${node.id}`;
1206
1619
  const branch = graph.createNode("container", {
1207
1620
  key: branchKey,
1208
1621
  props: { key: branchKey }
1209
1622
  });
1623
+ registerTransition(graph, branch, branchTransition);
1210
1624
  build(new ContainerBuilderImpl(branch, graph));
1211
1625
  return branch;
1212
1626
  };
@@ -1252,8 +1666,14 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1252
1666
  this.container(id, (c) => {
1253
1667
  c.when(
1254
1668
  hasError,
1255
- // Error state → fallback.
1256
- (fb) => options.fallback(fb, readError(), retry),
1669
+ // Error state → fallback. The fallback branch mounts exactly when the
1670
+ // boundary enters its error state, so this is also where the optional
1671
+ // `onError` reporting hook fires (observe-only; §6/§7).
1672
+ (fb) => {
1673
+ const currentError = readError();
1674
+ options.onError?.(currentError);
1675
+ options.fallback(fb, currentError, retry);
1676
+ },
1257
1677
  // Healthy state → body, guarded against synchronous build throws.
1258
1678
  (body) => {
1259
1679
  try {
@@ -1265,6 +1685,24 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1265
1685
  );
1266
1686
  }, { id });
1267
1687
  }
1688
+ asyncBoundary(key, res, branches) {
1689
+ const isError = derived(() => res.status.get() === "error");
1690
+ const showSuccess = derived(
1691
+ () => res.status.get() !== "error" && res.data.get() !== void 0
1692
+ );
1693
+ const showLoading = derived(
1694
+ () => res.status.get() !== "error" && res.data.get() === void 0
1695
+ );
1696
+ const dataSignal = derived(() => res.data.get());
1697
+ const retry = () => {
1698
+ void res.refetch();
1699
+ };
1700
+ this.container(key, (c) => {
1701
+ c.when(isError, (fb) => branches.error?.(fb, res.error.peek(), retry));
1702
+ c.when(showSuccess, (sb) => branches.success(sb, dataSignal));
1703
+ c.when(showLoading, (lb) => branches.loading?.(lb));
1704
+ }, { key });
1705
+ }
1268
1706
  // ── Portals & overlays ──────────────────────────────────────────────────────
1269
1707
  portal(key, builder, options = {}) {
1270
1708
  const node = this._graph.createNode("portal", {
@@ -1274,6 +1712,31 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1274
1712
  });
1275
1713
  builder(new ContainerBuilderImpl(node, this._graph));
1276
1714
  }
1715
+ /**
1716
+ * Declare document metadata (2.0 §1–§3). Creates a `'head'` node — a neutral,
1717
+ * empty inline anchor at this position (one node / one element, so positional
1718
+ * hydration is preserved) — and registers a `__head__<id>` descriptor holding
1719
+ * this call's normalized, dedup-keyed {@link resolveHead} contribution. The
1720
+ * renderer applies it to `document.head` on the browser (adopting server tags
1721
+ * on hydration, cleaning up on unmount / route change) and emits the active
1722
+ * graph's merged metadata as a string on the server (`renderHead`).
1723
+ *
1724
+ * Multiple `head()` nodes may be live at once (app default + route + component)
1725
+ * — the renderer merges them and, per dedup key, the last in document order
1726
+ * wins (see head.ts). No new render path: this reuses the same graph-node +
1727
+ * handler-registry convention as overlays/transitions/components.
1728
+ */
1729
+ head(metadata) {
1730
+ const node = this._graph.createNode("head", {
1731
+ parent: this._node,
1732
+ props: { "data-streetui-head-anchor": "" }
1733
+ });
1734
+ const contribution = resolveHead(metadata);
1735
+ this._graph.registerHandler(
1736
+ `__head__${node.id}`,
1737
+ () => contribution
1738
+ );
1739
+ }
1277
1740
  /**
1278
1741
  * Shared assembly for every overlay kind: a `portal` node whose single child
1279
1742
  * is a `when(open, panel)` conditional. The panel container carries the
@@ -1301,13 +1764,24 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1301
1764
  ...options.ariaDescribedBy !== void 0 ? { ariaDescribedBy: options.ariaDescribedBy } : {}
1302
1765
  };
1303
1766
  const portalBuilder = new ContainerBuilderImpl(portalNode, graph);
1304
- portalBuilder.when(openSignal, (panelHost) => {
1305
- panelHost.container(`${key}__panel`, builder, panelOptions);
1306
- });
1767
+ portalBuilder.when(
1768
+ openSignal,
1769
+ (panelHost) => {
1770
+ panelHost.container(`${key}__panel`, builder, panelOptions);
1771
+ },
1772
+ void 0,
1773
+ // Overlay open/close rides the panel's `when`; a transition animates the
1774
+ // panel in on open and — via the reconciler's deferred-leave — plays the
1775
+ // leave before the panel is removed (§10). Focus is restored at close-
1776
+ // request time (see wireOverlayBehavior), so it never stays trapped inside
1777
+ // a panel that is animating away.
1778
+ options.transition !== void 0 ? { transition: options.transition } : {}
1779
+ );
1307
1780
  const descriptor = {
1308
1781
  open: openSignal,
1309
1782
  modal: kind.modal,
1310
1783
  takesFocus: kind.takesFocus,
1784
+ menu: kind.menu ?? false,
1311
1785
  closeOnEscape: options.closeOnEscape ?? kind.defaultCloseOnEscape,
1312
1786
  restoreFocus: options.restoreFocus ?? kind.defaultRestoreFocus,
1313
1787
  ...options.initialFocusId !== void 0 ? { initialFocusId: options.initialFocusId } : {},
@@ -1404,6 +1878,7 @@ var ListBuilderImpl = class extends ContentBuilderBase {
1404
1878
  parent: this._node,
1405
1879
  props: containerProps(options)
1406
1880
  });
1881
+ registerTransition(this._graph, node, options.transition);
1407
1882
  builder(new ContainerBuilderImpl(node, this._graph));
1408
1883
  }
1409
1884
  };
@@ -1633,8 +2108,8 @@ var RuntimeNodeInstance = class {
1633
2108
  this.children.push(instance);
1634
2109
  }
1635
2110
  /** Subscribe to a signal and register the unsubscribe for cleanup. */
1636
- trackSignal(signal2, handler) {
1637
- const unsub = signal2.subscribe(handler);
2111
+ trackSignal(signal3, handler) {
2112
+ const unsub = signal3.subscribe(handler);
1638
2113
  this.cleanup.add(unsub);
1639
2114
  }
1640
2115
  /** Register an arbitrary cleanup function (e.g. DOM event removal). */
@@ -2005,6 +2480,9 @@ var BrowserDOMAdapter = class {
2005
2480
  body() {
2006
2481
  return document.body ?? null;
2007
2482
  }
2483
+ head() {
2484
+ return document.head ?? null;
2485
+ }
2008
2486
  activeElement() {
2009
2487
  return document.activeElement ?? null;
2010
2488
  }
@@ -2384,6 +2862,9 @@ var ServerDOMAdapter = class {
2384
2862
  body() {
2385
2863
  return null;
2386
2864
  }
2865
+ head() {
2866
+ return null;
2867
+ }
2387
2868
  activeElement() {
2388
2869
  return null;
2389
2870
  }
@@ -2498,18 +2979,25 @@ function trapFocus(dom, container) {
2498
2979
  dom.addEventListener(container, "keydown", onKeydown);
2499
2980
  return () => dom.removeEventListener(container, "keydown", onKeydown);
2500
2981
  }
2982
+ var containmentStack = [];
2501
2983
  function containFocus(dom, container) {
2502
2984
  const body = dom.body();
2503
2985
  if (body === null) return () => {
2504
2986
  };
2987
+ containmentStack.push(container);
2505
2988
  const onFocusIn = (event) => {
2989
+ if (containmentStack[containmentStack.length - 1] !== container) return;
2506
2990
  const target = event.target;
2507
2991
  if (target !== null && !dom.contains(container, target)) {
2508
2992
  focusFirst(dom, container);
2509
2993
  }
2510
2994
  };
2511
2995
  dom.addEventListener(body, "focusin", onFocusIn);
2512
- return () => dom.removeEventListener(body, "focusin", onFocusIn);
2996
+ return () => {
2997
+ dom.removeEventListener(body, "focusin", onFocusIn);
2998
+ const index = containmentStack.lastIndexOf(container);
2999
+ if (index !== -1) containmentStack.splice(index, 1);
3000
+ };
2513
3001
  }
2514
3002
  function onEscape(dom, target, handler) {
2515
3003
  const onKeydown = (event) => {
@@ -2518,6 +3006,84 @@ function onEscape(dom, target, handler) {
2518
3006
  dom.addEventListener(target, "keydown", onKeydown);
2519
3007
  return () => dom.removeEventListener(target, "keydown", onKeydown);
2520
3008
  }
3009
+ function rovingMenu(dom, container, selector = FOCUSABLE_SELECTOR) {
3010
+ const onKeydown = (event) => {
3011
+ const key = event.key;
3012
+ const isActivate = key === "Enter" || key === " " || key === "Spacebar";
3013
+ const isMove = key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End";
3014
+ if (!isActivate && !isMove) return;
3015
+ const items = getFocusable(dom, container, selector);
3016
+ if (items.length === 0) return;
3017
+ const active = dom.activeElement();
3018
+ const index = active === null ? -1 : items.indexOf(active);
3019
+ if (isActivate) {
3020
+ if (index < 0) return;
3021
+ event.preventDefault();
3022
+ items[index].click?.();
3023
+ return;
3024
+ }
3025
+ event.preventDefault();
3026
+ let next;
3027
+ if (key === "Home") next = 0;
3028
+ else if (key === "End") next = items.length - 1;
3029
+ else if (key === "ArrowDown") next = index < 0 ? 0 : (index + 1) % items.length;
3030
+ else next = index <= 0 ? items.length - 1 : index - 1;
3031
+ dom.focus(items[next]);
3032
+ };
3033
+ dom.addEventListener(container, "keydown", onKeydown);
3034
+ return () => dom.removeEventListener(container, "keydown", onKeydown);
3035
+ }
3036
+
3037
+ // ../dom/src/live-region.ts
3038
+ var VISUALLY_HIDDEN = "position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;";
3039
+ function makeRegion(dom, politeness) {
3040
+ const el = dom.createElement("div");
3041
+ dom.setAttribute(el, "aria-live", politeness);
3042
+ dom.setAttribute(el, "aria-atomic", "true");
3043
+ dom.setAttribute(el, "role", politeness === "assertive" ? "alert" : "status");
3044
+ dom.setAttribute(el, "data-streetui-live", politeness);
3045
+ dom.setAttribute(el, "style", VISUALLY_HIDDEN);
3046
+ return el;
3047
+ }
3048
+ function createAnnouncer(dom) {
3049
+ const body = dom.body();
3050
+ if (body === null) {
3051
+ return { announce() {
3052
+ }, clear() {
3053
+ }, destroy() {
3054
+ } };
3055
+ }
3056
+ const polite = makeRegion(dom, "polite");
3057
+ const assertive = makeRegion(dom, "assertive");
3058
+ dom.appendChild(body, polite);
3059
+ dom.appendChild(body, assertive);
3060
+ let destroyed = false;
3061
+ const write = (region, message) => {
3062
+ dom.setTextContent(region, "");
3063
+ void Promise.resolve().then(() => {
3064
+ if (!destroyed) dom.setTextContent(region, message);
3065
+ });
3066
+ };
3067
+ return {
3068
+ announce(message, options) {
3069
+ if (destroyed) return;
3070
+ write(options?.assertive === true ? assertive : polite, message);
3071
+ },
3072
+ clear() {
3073
+ if (destroyed) return;
3074
+ dom.setTextContent(polite, "");
3075
+ dom.setTextContent(assertive, "");
3076
+ },
3077
+ destroy() {
3078
+ if (destroyed) return;
3079
+ destroyed = true;
3080
+ for (const region of [polite, assertive]) {
3081
+ const parent = dom.parentNode(region);
3082
+ if (parent !== null) dom.removeChild(parent, region);
3083
+ }
3084
+ }
3085
+ };
3086
+ }
2521
3087
 
2522
3088
  // ../renderer/src/render-context.ts
2523
3089
  function createRenderContext(dom, graph, container, hydrationDiagnostics, staticHTML) {
@@ -2681,7 +3247,13 @@ var TAG_MAP = {
2681
3247
  // its children are relocated to a document.body container on the browser
2682
3248
  // (see the portal branch in mount.ts). On the server (no body) it renders
2683
3249
  // inline, so the anchor tag is what SSR/hydration positionally match on.
2684
- portal: "div"
3250
+ portal: "div",
3251
+ // A `head()` node renders as a neutral, empty inline anchor <div> at its
3252
+ // declaration site (like a portal anchor). Its actual contribution — title/
3253
+ // meta/link/etc. — is applied to `document.head` by `wireHeadBehavior` on the
3254
+ // browser, and emitted separately by `renderHead()` on the server. Keeping a
3255
+ // one-node/one-element anchor preserves positional hydration.
3256
+ head: "div"
2685
3257
  };
2686
3258
  function resolveTag(type) {
2687
3259
  return TAG_MAP[type] ?? "div";
@@ -2729,7 +3301,7 @@ function patchNode(ctx, graphNode, propKey, newValue) {
2729
3301
  }
2730
3302
 
2731
3303
  // ../renderer/src/reconciliation.ts
2732
- function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
3304
+ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn, hooks) {
2733
3305
  const oldByKey = /* @__PURE__ */ new Map();
2734
3306
  for (const inst of oldInstances) {
2735
3307
  const key = inst.graphNode.key ?? inst.graphNode.id;
@@ -2750,28 +3322,33 @@ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
2750
3322
  }
2751
3323
  newInstances.push(existing);
2752
3324
  } else {
2753
- const inst = mountFn(newNode, parentDom);
2754
- newInstances.push(inst);
3325
+ const reclaimed = hooks?.takeLeaving(key);
3326
+ if (reclaimed !== void 0) {
3327
+ patchExistingInstance(ctx, reclaimed, newNode);
3328
+ reconcileItemChildren(ctx, reclaimed, newNode, mountFn);
3329
+ hooks?.onEnter(reclaimed);
3330
+ newInstances.push(reclaimed);
3331
+ } else {
3332
+ const inst = mountFn(newNode, parentDom);
3333
+ hooks?.onEnter(inst);
3334
+ newInstances.push(inst);
3335
+ }
2755
3336
  }
2756
3337
  }
2757
3338
  const removed = [];
2758
3339
  for (const inst of oldInstances) {
2759
3340
  const key = inst.graphNode.key ?? inst.graphNode.id;
2760
- if (!usedKeys.has(key)) {
2761
- removed.push(inst);
2762
- }
2763
- }
2764
- for (const inst of removed) {
3341
+ if (usedKeys.has(key)) continue;
3342
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
2765
3343
  const parent = ctx.dom.parentNode(inst.domNode);
2766
- if (parent !== null) {
2767
- ctx.dom.removeChild(parent, inst.domNode);
2768
- }
3344
+ if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
2769
3345
  inst.dispose();
3346
+ removed.push(inst);
2770
3347
  }
2771
3348
  reorderDom(ctx, parentDom, newInstances);
2772
3349
  return { instances: newInstances, removed };
2773
3350
  }
2774
- function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
3351
+ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn, hooks) {
2775
3352
  const oldByKey = /* @__PURE__ */ new Map();
2776
3353
  for (const inst of oldInstances) {
2777
3354
  oldByKey.set(inst.graphNode.key ?? inst.graphNode.id, inst);
@@ -2798,21 +3375,34 @@ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
2798
3375
  }
2799
3376
  newInstances.push(existing);
2800
3377
  } else {
2801
- const freshNode = entry.build();
2802
- built.push(freshNode);
2803
- const inst = mountFn(freshNode, parentDom);
2804
- newInstances.push(inst);
3378
+ const reclaimed = hooks?.takeLeaving(entry.key);
3379
+ if (reclaimed !== void 0) {
3380
+ const freshNode = entry.build();
3381
+ built.push(freshNode);
3382
+ patchExistingInstance(ctx, reclaimed, freshNode);
3383
+ reconcileItemChildren(ctx, reclaimed, freshNode, mountFn);
3384
+ reclaimed.graphNode.setProp("_sig", freshNode.getProp("_sig"));
3385
+ reclaimed.graphNode.setProp("_item", entry.item);
3386
+ hooks?.onEnter(reclaimed);
3387
+ newInstances.push(reclaimed);
3388
+ } else {
3389
+ const freshNode = entry.build();
3390
+ built.push(freshNode);
3391
+ const inst = mountFn(freshNode, parentDom);
3392
+ hooks?.onEnter(inst);
3393
+ newInstances.push(inst);
3394
+ }
2805
3395
  }
2806
3396
  }
2807
3397
  const removed = [];
2808
3398
  for (const inst of oldInstances) {
2809
3399
  const key = inst.graphNode.key ?? inst.graphNode.id;
2810
- if (!usedKeys.has(key)) removed.push(inst);
2811
- }
2812
- for (const inst of removed) {
3400
+ if (usedKeys.has(key)) continue;
3401
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
2813
3402
  const parent = ctx.dom.parentNode(inst.domNode);
2814
3403
  if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
2815
3404
  inst.dispose();
3405
+ removed.push(inst);
2816
3406
  }
2817
3407
  reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
2818
3408
  return { instances: newInstances, removed, built };
@@ -2938,6 +3528,365 @@ function patchExistingInstance(ctx, instance, newNode) {
2938
3528
  }
2939
3529
  }
2940
3530
 
3531
+ // ../renderer/src/transition.ts
3532
+ function getResolvedTransition(graph, nodeId) {
3533
+ const fn = graph.getHandler(`__transition__${nodeId}`);
3534
+ return fn === void 0 ? void 0 : fn();
3535
+ }
3536
+ function host() {
3537
+ return globalThis;
3538
+ }
3539
+ function nextFrame(cb) {
3540
+ const h = host();
3541
+ const raf = h.requestAnimationFrame;
3542
+ if (typeof raf === "function") {
3543
+ raf(() => raf(cb));
3544
+ } else {
3545
+ h.setTimeout(cb, 0);
3546
+ }
3547
+ }
3548
+ function splitClass(value) {
3549
+ if (value === null) return [];
3550
+ const out = [];
3551
+ for (const t of value.split(/\s+/)) if (t.length > 0) out.push(t);
3552
+ return out;
3553
+ }
3554
+ function addClasses(dom, el, classes2) {
3555
+ if (classes2.length === 0) return;
3556
+ const current = splitClass(dom.getAttribute(el, "class"));
3557
+ let changed = false;
3558
+ for (const c of classes2) {
3559
+ if (!current.includes(c)) {
3560
+ current.push(c);
3561
+ changed = true;
3562
+ }
3563
+ }
3564
+ if (changed) dom.setAttribute(el, "class", current.join(" "));
3565
+ }
3566
+ function removeClasses(dom, el, classes2) {
3567
+ if (classes2.length === 0) return;
3568
+ const current = splitClass(dom.getAttribute(el, "class"));
3569
+ const next = current.filter((c) => !classes2.includes(c));
3570
+ if (next.length !== current.length) {
3571
+ if (next.length === 0) dom.removeAttribute(el, "class");
3572
+ else dom.setAttribute(el, "class", next.join(" "));
3573
+ }
3574
+ }
3575
+ function startRun(dom, el, active, from, to, duration, onDone) {
3576
+ const h = host();
3577
+ let finished = false;
3578
+ let timer = null;
3579
+ const onEvent = (e) => {
3580
+ if (e.target !== el) return;
3581
+ finish();
3582
+ };
3583
+ const detach = () => {
3584
+ dom.removeEventListener(el, "transitionend", onEvent);
3585
+ dom.removeEventListener(el, "animationend", onEvent);
3586
+ if (timer !== null) {
3587
+ h.clearTimeout(timer);
3588
+ timer = null;
3589
+ }
3590
+ };
3591
+ const finish = () => {
3592
+ if (finished) return;
3593
+ finished = true;
3594
+ detach();
3595
+ removeClasses(dom, el, active);
3596
+ removeClasses(dom, el, to);
3597
+ removeClasses(dom, el, from);
3598
+ onDone();
3599
+ };
3600
+ addClasses(dom, el, from);
3601
+ addClasses(dom, el, active);
3602
+ dom.addEventListener(el, "transitionend", onEvent);
3603
+ dom.addEventListener(el, "animationend", onEvent);
3604
+ timer = h.setTimeout(finish, duration);
3605
+ nextFrame(() => {
3606
+ if (finished) return;
3607
+ removeClasses(dom, el, from);
3608
+ addClasses(dom, el, to);
3609
+ });
3610
+ return {
3611
+ cancel: () => {
3612
+ if (finished) return;
3613
+ finished = true;
3614
+ detach();
3615
+ removeClasses(dom, el, active);
3616
+ removeClasses(dom, el, to);
3617
+ removeClasses(dom, el, from);
3618
+ }
3619
+ };
3620
+ }
3621
+ function runElementTransition(dom, el, rt, phase, onDone) {
3622
+ if (dom.body() === null || !dom.isElement(el)) {
3623
+ onDone();
3624
+ return { cancel: () => {
3625
+ } };
3626
+ }
3627
+ const active = phase === "enter" ? rt.enterActive : rt.leaveActive;
3628
+ const from = phase === "enter" ? rt.enterFrom : rt.leaveFrom;
3629
+ const to = phase === "enter" ? rt.enterTo : rt.leaveTo;
3630
+ return startRun(dom, el, active, from, to, rt.duration, onDone);
3631
+ }
3632
+ var TransitionController = class {
3633
+ constructor(dom, graph, finalize) {
3634
+ this.dom = dom;
3635
+ this.graph = graph;
3636
+ this.finalize = finalize;
3637
+ }
3638
+ leaving = /* @__PURE__ */ new Map();
3639
+ /** True only in a real DOM environment (browser). */
3640
+ get browser() {
3641
+ return this.dom.body() !== null;
3642
+ }
3643
+ keyOf(inst) {
3644
+ return inst.graphNode.key ?? inst.graphNode.id;
3645
+ }
3646
+ resolved(node) {
3647
+ return getResolvedTransition(this.graph, node.id);
3648
+ }
3649
+ /** Run the enter animation for `inst` if it carries a transition (browser only). */
3650
+ enter(inst) {
3651
+ if (!this.browser) return;
3652
+ const rt = this.resolved(inst.graphNode);
3653
+ if (rt === void 0) return;
3654
+ const el = inst.domNode;
3655
+ if (!this.dom.isElement(el)) return;
3656
+ startRun(this.dom, el, rt.enterActive, rt.enterFrom, rt.enterTo, rt.duration, () => {
3657
+ });
3658
+ }
3659
+ /**
3660
+ * Play `appear` for any initial child that opted into it (fresh browser mount
3661
+ * only — hydration must never animate appear, §22, and this is called only on
3662
+ * the mount path).
3663
+ */
3664
+ appear(children) {
3665
+ if (!this.browser) return;
3666
+ for (const child of children) {
3667
+ const rt = this.resolved(child.graphNode);
3668
+ if (rt !== void 0 && rt.appear) this.enter(child);
3669
+ }
3670
+ }
3671
+ hooks() {
3672
+ return {
3673
+ takeLeaving: (key) => {
3674
+ const entry = this.leaving.get(key);
3675
+ if (entry === void 0) return void 0;
3676
+ entry.run.cancel();
3677
+ this.leaving.delete(key);
3678
+ return entry.inst;
3679
+ },
3680
+ beginLeave: (inst) => {
3681
+ if (!this.browser) return false;
3682
+ const rt = this.resolved(inst.graphNode);
3683
+ if (rt === void 0) return false;
3684
+ const el = inst.domNode;
3685
+ if (!this.dom.isElement(el)) return false;
3686
+ const key = this.keyOf(inst);
3687
+ const prior = this.leaving.get(key);
3688
+ if (prior !== void 0) prior.run.cancel();
3689
+ const run = startRun(
3690
+ this.dom,
3691
+ el,
3692
+ rt.leaveActive,
3693
+ rt.leaveFrom,
3694
+ rt.leaveTo,
3695
+ rt.duration,
3696
+ () => {
3697
+ const current = this.leaving.get(key);
3698
+ if (current !== void 0 && current.run === run) {
3699
+ this.leaving.delete(key);
3700
+ this.finalize(inst);
3701
+ }
3702
+ }
3703
+ );
3704
+ this.leaving.set(key, { inst, run });
3705
+ return true;
3706
+ },
3707
+ onEnter: (inst) => this.enter(inst)
3708
+ };
3709
+ }
3710
+ };
3711
+
3712
+ // ../renderer/src/head.ts
3713
+ var HEAD_MARKER = "data-streetui-head";
3714
+ var HEAD_KEY = "data-streetui-head-key";
3715
+ function isSignalLike(v) {
3716
+ return v !== null && typeof v === "object" && typeof v["subscribe"] === "function" && typeof v["peek"] === "function";
3717
+ }
3718
+ function readValue(v) {
3719
+ if (isSignalLike(v)) return String(v.peek() ?? "");
3720
+ return String(v ?? "");
3721
+ }
3722
+ function resolveEntry(entry) {
3723
+ const attrs = {};
3724
+ for (const key of Object.keys(entry.attrs)) {
3725
+ attrs[key] = readValue(entry.attrs[key]);
3726
+ }
3727
+ const resolved = { tag: entry.tag, attrs };
3728
+ if (entry.tag === "title") {
3729
+ return { ...resolved, text: readValue(entry.text) };
3730
+ }
3731
+ return resolved;
3732
+ }
3733
+ var HeadManager = class {
3734
+ _dom;
3735
+ _head;
3736
+ _contributions = /* @__PURE__ */ new Map();
3737
+ _applied = /* @__PURE__ */ new Map();
3738
+ _order = 0;
3739
+ _adopted = false;
3740
+ constructor(dom, head) {
3741
+ this._dom = dom;
3742
+ this._head = head;
3743
+ }
3744
+ /** Register (or replace) a node's contribution and re-apply the merged result. */
3745
+ register(nodeId, entries) {
3746
+ this._contributions.set(nodeId, { order: this._order++, entries });
3747
+ this.apply();
3748
+ }
3749
+ /** Withdraw a node's contribution (unmount / route change) and re-apply. */
3750
+ unregister(nodeId) {
3751
+ if (this._contributions.delete(nodeId)) this.apply();
3752
+ }
3753
+ /** Recompute the merged head and patch `document.head` to match. */
3754
+ apply() {
3755
+ if (!this._adopted) {
3756
+ this._adoptServerTags();
3757
+ this._adopted = true;
3758
+ }
3759
+ const ordered = [...this._contributions.values()].sort((a, b) => a.order - b.order);
3760
+ const merged = /* @__PURE__ */ new Map();
3761
+ for (const contribution of ordered) {
3762
+ for (const entry of contribution.entries) {
3763
+ merged.set(entry.dedupKey, resolveEntry(entry));
3764
+ }
3765
+ }
3766
+ for (const [key, desired] of merged) {
3767
+ const existing = this._applied.get(key);
3768
+ if (existing !== void 0 && existing.tag === desired.tag) {
3769
+ this._reconcileAttrs(existing, desired);
3770
+ } else {
3771
+ if (existing !== void 0) {
3772
+ this._dom.removeChild(this._head, existing.el);
3773
+ this._applied.delete(key);
3774
+ }
3775
+ const el = this._createTag(key, desired);
3776
+ this._dom.appendChild(this._head, el);
3777
+ this._applied.set(key, { el, attrKeys: new Set(Object.keys(desired.attrs)), tag: desired.tag });
3778
+ }
3779
+ }
3780
+ for (const [key, record] of [...this._applied]) {
3781
+ if (!merged.has(key)) {
3782
+ this._dom.removeChild(this._head, record.el);
3783
+ this._applied.delete(key);
3784
+ }
3785
+ }
3786
+ }
3787
+ _createTag(key, desired) {
3788
+ const el = this._dom.createElement(desired.tag);
3789
+ this._dom.setAttribute(el, HEAD_MARKER, "");
3790
+ this._dom.setAttribute(el, HEAD_KEY, key);
3791
+ for (const attr of Object.keys(desired.attrs)) {
3792
+ this._dom.setAttribute(el, attr, desired.attrs[attr]);
3793
+ }
3794
+ if (desired.tag === "title") this._dom.setTextContent(el, desired.text ?? "");
3795
+ return el;
3796
+ }
3797
+ _reconcileAttrs(record, desired) {
3798
+ const nextKeys = new Set(Object.keys(desired.attrs));
3799
+ for (const attr of record.attrKeys) {
3800
+ if (!nextKeys.has(attr)) this._dom.removeAttribute(record.el, attr);
3801
+ }
3802
+ for (const attr of nextKeys) {
3803
+ this._dom.setAttribute(record.el, attr, desired.attrs[attr]);
3804
+ }
3805
+ if (desired.tag === "title") this._dom.setTextContent(record.el, desired.text ?? "");
3806
+ record.attrKeys = nextKeys;
3807
+ }
3808
+ /**
3809
+ * Seed `_applied` from server-emitted `[data-streetui-head-key]` tags already
3810
+ * in `document.head`. The subsequent diff reuses these elements when the
3811
+ * client desires the same key (no duplicate), rewrites them if the value
3812
+ * changed, or removes them if the client graph no longer wants them.
3813
+ */
3814
+ _adoptServerTags() {
3815
+ for (const child of this._dom.childNodes(this._head)) {
3816
+ if (!this._dom.isElement(child)) continue;
3817
+ const el = child;
3818
+ const key = this._dom.getAttribute(el, HEAD_KEY);
3819
+ if (key === null) continue;
3820
+ this._applied.set(key, {
3821
+ el,
3822
+ attrKeys: new Set(this._attrNames(el)),
3823
+ tag: this._dom.tagName(el)
3824
+ });
3825
+ }
3826
+ }
3827
+ /** The framework-managed attribute names currently on a server tag. */
3828
+ _attrNames(el) {
3829
+ const names = [];
3830
+ if (this._dom.getAttribute(el, HEAD_MARKER) !== null) names.push(HEAD_MARKER);
3831
+ if (this._dom.getAttribute(el, HEAD_KEY) !== null) names.push(HEAD_KEY);
3832
+ return names;
3833
+ }
3834
+ };
3835
+ function getHeadManager(ctx) {
3836
+ const head = ctx.dom.head();
3837
+ if (head === null) return null;
3838
+ const mutable = ctx;
3839
+ if (mutable.head === void 0) mutable.head = new HeadManager(ctx.dom, head);
3840
+ return mutable.head;
3841
+ }
3842
+ function wireHeadBehavior(ctx, graphNode, instance) {
3843
+ const manager = getHeadManager(ctx);
3844
+ if (manager === null) return;
3845
+ const descFn = ctx.graph.getHandler(`__head__${graphNode.id}`);
3846
+ if (descFn === void 0) return;
3847
+ const contribution = descFn();
3848
+ const nodeId = graphNode.id;
3849
+ manager.register(nodeId, contribution.entries);
3850
+ for (const entry of contribution.entries) {
3851
+ for (const attrKey of Object.keys(entry.attrs)) {
3852
+ const v = entry.attrs[attrKey];
3853
+ if (isSignalLike(v)) {
3854
+ instance.trackCleanup(v.subscribe(() => manager.apply()));
3855
+ }
3856
+ }
3857
+ if (isSignalLike(entry.text)) {
3858
+ instance.trackCleanup(entry.text.subscribe(() => manager.apply()));
3859
+ }
3860
+ }
3861
+ instance.trackCleanup(() => manager.unregister(nodeId));
3862
+ }
3863
+ function renderHead(compiled) {
3864
+ const graph = compiled.graph;
3865
+ const merged = /* @__PURE__ */ new Map();
3866
+ graph.walk((node) => {
3867
+ if (node.type !== "head") return;
3868
+ const descFn = graph.getHandler(`__head__${node.id}`);
3869
+ if (descFn === void 0) return;
3870
+ for (const entry of descFn().entries) {
3871
+ merged.set(entry.dedupKey, resolveEntry(entry));
3872
+ }
3873
+ });
3874
+ if (merged.size === 0) return "";
3875
+ const dom = new ServerDOMAdapter();
3876
+ let out = "";
3877
+ for (const [key, desired] of merged) {
3878
+ const el = dom.createElement(desired.tag);
3879
+ dom.setAttribute(el, HEAD_MARKER, "");
3880
+ dom.setAttribute(el, HEAD_KEY, key);
3881
+ for (const attr of Object.keys(desired.attrs)) {
3882
+ dom.setAttribute(el, attr, desired.attrs[attr]);
3883
+ }
3884
+ if (desired.tag === "title") dom.setTextContent(el, desired.text ?? "");
3885
+ out += dom.serializeOuter(el);
3886
+ }
3887
+ return out;
3888
+ }
3889
+
2941
3890
  // ../renderer/src/mount.ts
2942
3891
  var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
2943
3892
  "text",
@@ -3060,6 +4009,9 @@ function mountNode(ctx, graphNode, parentDom) {
3060
4009
  const instance2 = new NodeInstance(graphNode, el2);
3061
4010
  ctx.instances.set(graphNode.id, instance2);
3062
4011
  wireEvents(dom, graph, graphNode, el2, instance2);
4012
+ if (graphNode.stateRefs.length !== 0) {
4013
+ wireSignalBindings(ctx, graphNode, instance2, linkUpdate(dom, el2));
4014
+ }
3063
4015
  dom.appendChild(parentDom, el2);
3064
4016
  return instance2;
3065
4017
  }
@@ -3090,7 +4042,7 @@ function mountNode(ctx, graphNode, parentDom) {
3090
4042
  instance2.addChild(childInstance);
3091
4043
  }
3092
4044
  dom.appendChild(parentDom, el2);
3093
- wireReactiveList(ctx, graphNode, instance2, el2);
4045
+ wireReactiveList(ctx, graphNode, instance2, el2, true);
3094
4046
  return instance2;
3095
4047
  }
3096
4048
  if (graphNode.type === "portal") {
@@ -3115,6 +4067,16 @@ function mountNode(ctx, graphNode, parentDom) {
3115
4067
  wireOverlayBehavior(ctx, graphNode, instance2, target);
3116
4068
  return instance2;
3117
4069
  }
4070
+ if (graphNode.type === "head") {
4071
+ const anchor = dom.createElement(resolveTag("head"));
4072
+ dom.setAttribute(anchor, "data-streetui-head-anchor", "");
4073
+ applyNodeProps(ctx, graphNode, anchor);
4074
+ const instance2 = new NodeInstance(graphNode, anchor);
4075
+ ctx.instances.set(graphNode.id, instance2);
4076
+ dom.appendChild(parentDom, anchor);
4077
+ wireHeadBehavior(ctx, graphNode, instance2);
4078
+ return instance2;
4079
+ }
3118
4080
  const tag = resolveTag(graphNode.type);
3119
4081
  const el = dom.createElement(tag);
3120
4082
  applyNodeProps(ctx, graphNode, el);
@@ -3179,6 +4141,17 @@ function buttonUpdate(dom, el) {
3179
4141
  }
3180
4142
  };
3181
4143
  }
4144
+ function linkUpdate(dom, el) {
4145
+ return (propKey, value) => {
4146
+ if (propKey === "label") {
4147
+ dom.setTextContent(el, String(value ?? ""));
4148
+ } else if (propKey === "href") {
4149
+ dom.setAttribute(el, "href", String(value ?? ""));
4150
+ } else {
4151
+ applyProp(dom, el, propKey, value);
4152
+ }
4153
+ };
4154
+ }
3182
4155
  function applyNodeProps(ctx, graphNode, el) {
3183
4156
  const props = graphNode.props;
3184
4157
  for (const key in props) {
@@ -3199,32 +4172,42 @@ function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
3199
4172
  instance.trackCleanup(unsub);
3200
4173
  }
3201
4174
  }
3202
- function wireReactiveList(ctx, graphNode, instance, el) {
4175
+ function wireReactiveList(ctx, graphNode, instance, el, runAppear = false) {
3203
4176
  const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
3204
4177
  const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
3205
4178
  if (plan === void 0 && build === void 0) return;
4179
+ const controller = new TransitionController(ctx.dom, ctx.graph, (leaving) => {
4180
+ const parent = ctx.dom.parentNode(leaving.domNode);
4181
+ if (parent !== null) ctx.dom.removeChild(parent, leaving.domNode);
4182
+ leaving.dispose();
4183
+ forgetInstance(ctx, leaving);
4184
+ ctx.graph.detachNode(leaving.graphNode);
4185
+ });
4186
+ const hooks = controller.hooks();
4187
+ if (runAppear) controller.appear(instance.children);
3206
4188
  for (const stateRef of graphNode.stateRefs) {
3207
4189
  if (stateRef.propKey !== "items") continue;
3208
4190
  const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
3209
4191
  if (sig === void 0 || typeof sig.subscribe !== "function") continue;
3210
4192
  const unsub = sig.subscribe((value) => {
3211
4193
  if (plan !== void 0) {
3212
- reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value));
4194
+ reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value), hooks);
3213
4195
  } else {
3214
- reconcileReactiveList(ctx, graphNode, instance, el, build(value));
4196
+ reconcileReactiveList(ctx, graphNode, instance, el, build(value), hooks);
3215
4197
  }
3216
4198
  });
3217
4199
  instance.trackCleanup(unsub);
3218
4200
  }
3219
4201
  }
3220
- function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
4202
+ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan, hooks) {
3221
4203
  const oldInstances = [...listInstance.children];
3222
4204
  const result = reconcileChildrenByPlan(
3223
4205
  ctx,
3224
4206
  listEl,
3225
4207
  oldInstances,
3226
4208
  plan,
3227
- (node, parent) => mountNode(ctx, node, parent)
4209
+ (node, parent) => mountNode(ctx, node, parent),
4210
+ hooks
3228
4211
  );
3229
4212
  listInstance.children.length = 0;
3230
4213
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -3239,14 +4222,15 @@ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan)
3239
4222
  for (const child of [...listNode.children]) listNode.removeChild(child);
3240
4223
  for (const inst of result.instances) listNode.appendChild(inst.graphNode);
3241
4224
  }
3242
- function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
4225
+ function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes, hooks) {
3243
4226
  const oldInstances = [...listInstance.children];
3244
4227
  const result = reconcileChildren(
3245
4228
  ctx,
3246
4229
  listEl,
3247
4230
  oldInstances,
3248
4231
  newNodes,
3249
- (node, parent) => mountNode(ctx, node, parent)
4232
+ (node, parent) => mountNode(ctx, node, parent),
4233
+ hooks
3250
4234
  );
3251
4235
  listInstance.children.length = 0;
3252
4236
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -3282,11 +4266,14 @@ function wireOverlayBehavior(ctx, graphNode, instance, target) {
3282
4266
  const onOpenChange = (isOpen) => {
3283
4267
  if (isOpen) {
3284
4268
  if (desc.restoreFocus) saved = saveFocus(dom);
3285
- if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
3286
4269
  if (desc.modal) {
3287
4270
  active.push(trapFocus(dom, target));
3288
4271
  active.push(containFocus(dom, target));
3289
4272
  }
4273
+ if (desc.menu) {
4274
+ active.push(rovingMenu(dom, target));
4275
+ }
4276
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
3290
4277
  if (desc.closeOnEscape && desc.onClose !== void 0) {
3291
4278
  active.push(onEscape(dom, target, desc.onClose));
3292
4279
  }
@@ -3399,7 +4386,12 @@ function hydrateNode(ctx, graphNode, domNode, path) {
3399
4386
  case "link": {
3400
4387
  const instance = new NodeInstance(graphNode, domNode);
3401
4388
  ctx.instances.set(graphNode.id, instance);
3402
- if (graphNode.type === "link") wireEvents(dom, graph, graphNode, domNode, instance);
4389
+ if (graphNode.type === "link") {
4390
+ wireEvents(dom, graph, graphNode, domNode, instance);
4391
+ if (graphNode.stateRefs.length !== 0) {
4392
+ wireSignalBindings(ctx, graphNode, instance, linkUpdate(dom, domNode));
4393
+ }
4394
+ }
3403
4395
  return instance;
3404
4396
  }
3405
4397
  case "reactive-list":
@@ -3429,6 +4421,12 @@ function hydrateNode(ctx, graphNode, domNode, path) {
3429
4421
  wireOverlayBehavior(ctx, graphNode, instance, target);
3430
4422
  return instance;
3431
4423
  }
4424
+ case "head": {
4425
+ const instance = new NodeInstance(graphNode, domNode);
4426
+ ctx.instances.set(graphNode.id, instance);
4427
+ wireHeadBehavior(ctx, graphNode, instance);
4428
+ return instance;
4429
+ }
3432
4430
  default: {
3433
4431
  const instance = new NodeInstance(graphNode, domNode);
3434
4432
  ctx.instances.set(graphNode.id, instance);
@@ -3675,7 +4673,8 @@ function analyzeGraph(graph) {
3675
4673
  const isConditional = node.type === "conditional";
3676
4674
  const isPortal = node.type === "portal";
3677
4675
  const isComponent = node.type === "component";
3678
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent;
4676
+ const isHead = node.type === "head";
4677
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent && !isHead;
3679
4678
  const isStaticSubtree = isStatic && allChildrenStatic;
3680
4679
  nodes.set(node.id, {
3681
4680
  isStatic,
@@ -4021,16 +5020,38 @@ function mountRouter(router, options) {
4021
5020
  } else {
4022
5021
  outlet = container;
4023
5022
  }
5023
+ const routeTransition = options.transition !== void 0 ? resolveTransition(options.transition) : void 0;
5024
+ const txDom = routeTransition !== void 0 ? new BrowserDOMAdapter() : void 0;
4024
5025
  let active = null;
5026
+ const pendingLeaves = /* @__PURE__ */ new Map();
4025
5027
  let firstRender = hydrateMode;
5028
+ const teardownRoute = (route) => {
5029
+ route.registry.run();
5030
+ route.mounted.unmount();
5031
+ if (route.host !== null && txDom !== void 0) {
5032
+ const parent = txDom.parentNode(route.host);
5033
+ if (parent !== null) txDom.removeChild(parent, route.host);
5034
+ }
5035
+ };
4026
5036
  const disposeActive = () => {
4027
5037
  if (active === null) return;
4028
- active.registry.run();
4029
- active.mounted.unmount();
5038
+ teardownRoute(active);
4030
5039
  active = null;
4031
5040
  };
4032
- const renderRoute = (match) => {
4033
- disposeActive();
5041
+ const wrapOutletChildren = (dom) => {
5042
+ const host2 = dom.createElement("div");
5043
+ dom.setAttribute(host2, "data-streetui-route", "");
5044
+ const moved = [];
5045
+ let child = dom.firstChild(outlet);
5046
+ while (child !== null) {
5047
+ moved.push(child);
5048
+ child = dom.nextSibling(child);
5049
+ }
5050
+ for (const n of moved) dom.appendChild(host2, n);
5051
+ dom.appendChild(outlet, host2);
5052
+ return host2;
5053
+ };
5054
+ const buildRoute = (match, target, hydrate) => {
4034
5055
  const registry = new CleanupRegistry();
4035
5056
  const ctx = {
4036
5057
  path: match.path,
@@ -4043,9 +5064,53 @@ function mountRouter(router, options) {
4043
5064
  routeApp.page("route", (page) => match.route.builder(page, ctx));
4044
5065
  const runtime = createRuntime({ renderer });
4045
5066
  const routeCompiled = compile(routeApp);
4046
- const mounted = firstRender ? runtime.hydrate(routeCompiled, outlet) : runtime.mount(routeCompiled, outlet);
5067
+ const mounted = hydrate ? runtime.hydrate(routeCompiled, target) : runtime.mount(routeCompiled, target);
5068
+ return { registry, mounted, host: null, enterRun: null };
5069
+ };
5070
+ const renderRoute = (match) => {
5071
+ if (routeTransition === void 0 || txDom === void 0) {
5072
+ disposeActive();
5073
+ active = buildRoute(match, outlet, firstRender);
5074
+ firstRender = false;
5075
+ return;
5076
+ }
5077
+ if (active === null) {
5078
+ if (firstRender) {
5079
+ active = buildRoute(match, outlet, true);
5080
+ active.host = wrapOutletChildren(txDom);
5081
+ } else {
5082
+ const host2 = txDom.createElement("div");
5083
+ txDom.setAttribute(host2, "data-streetui-route", "");
5084
+ txDom.appendChild(outlet, host2);
5085
+ active = buildRoute(match, host2, false);
5086
+ active.host = host2;
5087
+ }
5088
+ firstRender = false;
5089
+ return;
5090
+ }
4047
5091
  firstRender = false;
4048
- active = { registry, mounted };
5092
+ const leaving = active;
5093
+ leaving.enterRun?.cancel();
5094
+ leaving.enterRun = null;
5095
+ const leaveHost = leaving.host;
5096
+ const enterHost = txDom.createElement("div");
5097
+ txDom.setAttribute(enterHost, "data-streetui-route", "");
5098
+ txDom.appendChild(outlet, enterHost);
5099
+ const next = buildRoute(match, enterHost, false);
5100
+ next.host = enterHost;
5101
+ next.enterRun = runElementTransition(txDom, enterHost, routeTransition, "enter", () => {
5102
+ next.enterRun = null;
5103
+ });
5104
+ active = next;
5105
+ if (leaveHost !== null) {
5106
+ const run = runElementTransition(txDom, leaveHost, routeTransition, "leave", () => {
5107
+ pendingLeaves.delete(leaving);
5108
+ teardownRoute(leaving);
5109
+ });
5110
+ pendingLeaves.set(leaving, run);
5111
+ } else {
5112
+ teardownRoute(leaving);
5113
+ }
4049
5114
  };
4050
5115
  renderRoute(router.currentRoute.peek());
4051
5116
  const stopRouteSub = router.currentRoute.subscribe((match) => renderRoute(match));
@@ -4073,6 +5138,11 @@ function mountRouter(router, options) {
4073
5138
  unmount() {
4074
5139
  if (interceptLinks) container.removeEventListener("click", onClick);
4075
5140
  stopRouteSub();
5141
+ for (const [route, run] of pendingLeaves) {
5142
+ run.cancel();
5143
+ teardownRoute(route);
5144
+ }
5145
+ pendingLeaves.clear();
4076
5146
  disposeActive();
4077
5147
  shellMounted?.unmount();
4078
5148
  router.destroy();
@@ -4397,6 +5467,40 @@ function inspectComponents(graph) {
4397
5467
  });
4398
5468
  return out;
4399
5469
  }
5470
+ function inspectInteractions(graph) {
5471
+ const overlays = [];
5472
+ const transitions = [];
5473
+ graph.walk((node, depth) => {
5474
+ const overlayFn = graph.getHandler(`__overlay__${node.id}`);
5475
+ if (overlayFn !== void 0) {
5476
+ const d = overlayFn();
5477
+ overlays.push({
5478
+ id: node.id,
5479
+ key: node.key,
5480
+ open: d.open.peek(),
5481
+ modal: d.modal,
5482
+ takesFocus: d.takesFocus,
5483
+ menu: d.menu,
5484
+ closeOnEscape: d.closeOnEscape,
5485
+ restoreFocus: d.restoreFocus,
5486
+ depth
5487
+ });
5488
+ }
5489
+ const transitionFn = graph.getHandler(`__transition__${node.id}`);
5490
+ if (transitionFn !== void 0) {
5491
+ const t = transitionFn();
5492
+ transitions.push({
5493
+ id: node.id,
5494
+ key: node.key,
5495
+ nodeType: node.type,
5496
+ duration: t.duration,
5497
+ appear: t.appear,
5498
+ depth
5499
+ });
5500
+ }
5501
+ });
5502
+ return { overlays, transitions };
5503
+ }
4400
5504
 
4401
5505
  // ../devtools/src/application.ts
4402
5506
  function collectPerf(node, distinctSignals, acc) {
@@ -4632,11 +5736,22 @@ function capture(compiled, sources, options) {
4632
5736
  snapshot: app.perf,
4633
5737
  diagnostics: diagnosePerformance(compiled, options.perfThresholds)
4634
5738
  };
5739
+ const components = inspectComponents(compiled.graph);
5740
+ const interactions = inspectInteractions(compiled.graph);
5741
+ const diagnostics = {
5742
+ errors: app.diagnostics.errors,
5743
+ warnings: app.diagnostics.warnings,
5744
+ messages: app.diagnostics.messages
5745
+ };
4635
5746
  const snapshot = {
4636
5747
  application,
4637
5748
  graph: app.graph,
4638
5749
  signals,
4639
5750
  performance,
5751
+ components,
5752
+ overlays: interactions.overlays,
5753
+ transitions: interactions.transitions,
5754
+ diagnostics,
4640
5755
  ...sources.router !== void 0 ? { router: inspectRouter(sources.router) } : {},
4641
5756
  ...sources.resources !== void 0 ? { resources: mapInspect(sources.resources, (r) => inspectResource(r)) } : {},
4642
5757
  ...sources.forms !== void 0 ? { forms: mapInspect(sources.forms, (f) => inspectForm(f)) } : {},
@@ -4677,6 +5792,25 @@ function formatSnapshot(s) {
4677
5792
  for (const [label, sig] of Object.entries(s.signals.live)) {
4678
5793
  lines.push(` ${label} [${sig.kind}] = ${format(sig.value)} \xB7 observers ${sig.observerCount ?? "?"}`);
4679
5794
  }
5795
+ if (s.components.length > 0) {
5796
+ lines.push(`Components: ${s.components.length}`);
5797
+ for (const c of s.components) {
5798
+ lines.push(` ${" ".repeat(c.depth)}${c.name}${c.key !== void 0 ? ` (#${c.key})` : ""} \xB7 children ${c.childCount}`);
5799
+ }
5800
+ }
5801
+ if (s.overlays.length > 0) {
5802
+ lines.push(`Overlays: ${s.overlays.length}`);
5803
+ for (const o of s.overlays) {
5804
+ const kind = o.modal ? "modal" : o.menu ? "menu" : o.takesFocus ? "focusable" : "non-modal";
5805
+ lines.push(` ${o.key ?? o.id} [${kind}] ${o.open ? "open" : "closed"}`);
5806
+ }
5807
+ }
5808
+ if (s.transitions.length > 0) {
5809
+ lines.push(`Transitions: ${s.transitions.length}`);
5810
+ for (const t of s.transitions) {
5811
+ lines.push(` ${t.key ?? t.id} on <${t.nodeType}> \xB7 ${t.duration}ms${t.appear ? " \xB7 appear" : ""}`);
5812
+ }
5813
+ }
4680
5814
  if (s.router !== void 0) {
4681
5815
  lines.push(`Router: ${s.router.path} (${s.router.pattern})${s.router.isFallback ? " [fallback]" : ""}`);
4682
5816
  }
@@ -4710,6 +5844,12 @@ function formatSnapshot(s) {
4710
5844
  for (const d of s.performance.diagnostics) {
4711
5845
  lines.push(` ${d.code}: ${d.message}`);
4712
5846
  }
5847
+ lines.push(
5848
+ `Diagnostics: ${s.diagnostics.errors} error(s), ${s.diagnostics.warnings} warning(s)`
5849
+ );
5850
+ for (const m of s.diagnostics.messages) {
5851
+ lines.push(` ${m}`);
5852
+ }
4713
5853
  return lines.join("\n");
4714
5854
  }
4715
5855
  function format(value) {
@@ -4725,6 +5865,176 @@ function format(value) {
4725
5865
  return String(value);
4726
5866
  }
4727
5867
 
5868
+ // ../devtools/src/view.ts
5869
+ function escapeHtml(value) {
5870
+ const s = typeof value === "string" ? value : stringifyValue(value);
5871
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
5872
+ }
5873
+ function stringifyValue(value) {
5874
+ if (value === null || value === void 0) return String(value);
5875
+ if (typeof value === "object") {
5876
+ try {
5877
+ return JSON.stringify(value);
5878
+ } catch {
5879
+ return "[object]";
5880
+ }
5881
+ }
5882
+ return String(value);
5883
+ }
5884
+ function section(title, count, body) {
5885
+ const badge = count === void 0 ? "" : ` <span class="st-count">${count}</span>`;
5886
+ return `<section class="st-panel"><h2>${escapeHtml(title)}${badge}</h2>${body}</section>`;
5887
+ }
5888
+ function ul(items) {
5889
+ if (items.length === 0) return '<p class="st-empty">(none)</p>';
5890
+ return `<ul>${items.map((i) => `<li>${i}</li>`).join("")}</ul>`;
5891
+ }
5892
+ function renderDevToolsHTML(s) {
5893
+ const app = s.application;
5894
+ const parts = [];
5895
+ parts.push(
5896
+ section(
5897
+ "Application",
5898
+ void 0,
5899
+ `<p>${escapeHtml(app.identity.name)} <span class="st-dim">v${escapeHtml(app.identity.version)}</span></p><p class="st-dim">${app.nodeCount} nodes \xB7 depth ${app.maxDepth} \xB7 ${app.pages.length} page(s) \xB7 ${app.signalCount} signals \xB7 ${app.eventHandlers} handlers \xB7 ${app.stateBindings} bindings</p>`
5900
+ )
5901
+ );
5902
+ parts.push(
5903
+ section(
5904
+ "Components",
5905
+ s.components.length,
5906
+ ul(
5907
+ s.components.map(
5908
+ (c) => `<span class="st-depth" style="--d:${c.depth}"></span><code>${escapeHtml(c.name)}</code>` + (c.key !== void 0 ? ` <span class="st-key">#${escapeHtml(c.key)}</span>` : "") + ` <span class="st-dim">${c.childCount} child(ren)</span>`
5909
+ )
5910
+ )
5911
+ )
5912
+ );
5913
+ parts.push(section("Graph", void 0, `<pre class="st-tree">${escapeHtml(renderNodeTree(s.graph))}</pre>`));
5914
+ parts.push(
5915
+ section(
5916
+ "Signals",
5917
+ s.signals.boundSignalIds.length,
5918
+ `<p class="st-dim">${s.signals.boundSignalIds.length} bound in graph \xB7 effects shown as observer counts (no global effect registry)</p>` + ul(
5919
+ Object.entries(s.signals.live).map(
5920
+ ([label, sig]) => `<code>${escapeHtml(label)}</code> <span class="st-key">[${escapeHtml(sig.kind)}]</span> = <code>${escapeHtml(sig.value)}</code> <span class="st-dim">observers ${escapeHtml(sig.observerCount ?? "?")}</span>`
5921
+ )
5922
+ )
5923
+ )
5924
+ );
5925
+ if (s.router !== void 0) {
5926
+ parts.push(
5927
+ section(
5928
+ "Router",
5929
+ void 0,
5930
+ `<p><code>${escapeHtml(s.router.path)}</code> <span class="st-dim">(${escapeHtml(s.router.pattern)})${s.router.isFallback ? " \xB7 fallback" : ""}</span></p>`
5931
+ )
5932
+ );
5933
+ }
5934
+ if (s.resources !== void 0) {
5935
+ parts.push(
5936
+ section(
5937
+ "Resources",
5938
+ Object.keys(s.resources).length,
5939
+ ul(
5940
+ Object.entries(s.resources).map(
5941
+ ([label, r]) => `<code>${escapeHtml(label)}</code>: ${escapeHtml(r.status)}` + (r.loading ? ' <span class="st-dim">(loading)</span>' : "") + (r.hasError ? ` <span class="st-err">!${escapeHtml(r.errorName)}</span>` : "")
5942
+ )
5943
+ )
5944
+ )
5945
+ );
5946
+ }
5947
+ parts.push(
5948
+ section(
5949
+ "Overlays",
5950
+ s.overlays.length,
5951
+ ul(
5952
+ s.overlays.map((o) => {
5953
+ const kind = o.modal ? "modal" : o.menu ? "menu" : o.takesFocus ? "focusable" : "non-modal";
5954
+ return `<code>${escapeHtml(o.key ?? o.id)}</code> <span class="st-key">[${kind}]</span> <span class="st-dim">${o.open ? "open" : "closed"}${o.closeOnEscape ? " \xB7 esc" : ""}${o.restoreFocus ? " \xB7 restore" : ""}</span>`;
5955
+ })
5956
+ )
5957
+ )
5958
+ );
5959
+ parts.push(
5960
+ section(
5961
+ "Transitions",
5962
+ s.transitions.length,
5963
+ ul(
5964
+ s.transitions.map(
5965
+ (t) => `<code>${escapeHtml(t.key ?? t.id)}</code> on <code>&lt;${escapeHtml(t.nodeType)}&gt;</code> <span class="st-dim">${t.duration}ms${t.appear ? " \xB7 appear" : ""}</span>`
5966
+ )
5967
+ )
5968
+ )
5969
+ );
5970
+ if (s.forms !== void 0) {
5971
+ parts.push(
5972
+ section(
5973
+ "Forms",
5974
+ Object.keys(s.forms).length,
5975
+ ul(
5976
+ Object.entries(s.forms).map(
5977
+ ([label, f]) => `<code>${escapeHtml(label)}</code>: ${f.valid ? "valid" : "invalid"} \xB7 ${escapeHtml(f.status)} \xB7 ${f.fields.length} field(s)`
5978
+ )
5979
+ )
5980
+ )
5981
+ );
5982
+ }
5983
+ if (s.i18n !== void 0) {
5984
+ const missing = s.i18n.missingKeys;
5985
+ parts.push(
5986
+ section(
5987
+ "i18n",
5988
+ void 0,
5989
+ `<p><code>${escapeHtml(s.i18n.locale)}</code> of [${s.i18n.locales.map(escapeHtml).join(", ")}]${missing !== void 0 ? ` <span class="st-dim">\xB7 missing ${missing.length}</span>` : ""}</p>`
5990
+ )
5991
+ );
5992
+ }
5993
+ parts.push(
5994
+ section(
5995
+ "Performance",
5996
+ s.performance.diagnostics.length,
5997
+ `<p class="st-dim">Structural counts, not runtime timings \u2014 not a production profiler.</p>` + ul(s.performance.diagnostics.map((d) => `<code>${escapeHtml(d.code)}</code>: ${escapeHtml(d.message)}`))
5998
+ )
5999
+ );
6000
+ parts.push(
6001
+ section(
6002
+ "Diagnostics",
6003
+ s.diagnostics.errors + s.diagnostics.warnings,
6004
+ `<p class="st-dim">${s.diagnostics.errors} error(s) \xB7 ${s.diagnostics.warnings} warning(s)</p>` + ul(s.diagnostics.messages.map((m) => escapeHtml(m)))
6005
+ )
6006
+ );
6007
+ const title = `StreetUI DevTools \u2014 ${escapeHtml(app.identity.name)}`;
6008
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title><style>${DEVTOOLS_CSS}</style></head><body class="st-devtools"><header class="st-header"><h1>${title} <span class="st-dim">v${escapeHtml(app.identity.version)}</span></h1></header><main>${parts.join("")}</main></body></html>`;
6009
+ }
6010
+ function renderNodeTree(node) {
6011
+ const lines = [];
6012
+ const walk = (n) => {
6013
+ const indent = " ".repeat(n.depth);
6014
+ const key = n.key !== void 0 ? ` #${n.key}` : "";
6015
+ lines.push(`${indent}<${n.type}${key}> ${n.id}`);
6016
+ for (const c of n.children) walk(c);
6017
+ };
6018
+ walk(node);
6019
+ return lines.join("\n");
6020
+ }
6021
+ var DEVTOOLS_CSS = [
6022
+ ".st-devtools{font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;margin:0;color:#e6e6e6;background:#1e1e28}",
6023
+ ".st-header{padding:12px 16px;border-bottom:1px solid #333;background:#15151c}",
6024
+ ".st-header h1{font-size:14px;margin:0}",
6025
+ "main{padding:8px 16px}",
6026
+ ".st-panel{margin:12px 0;border:1px solid #2c2c38;border-radius:6px;overflow:hidden}",
6027
+ ".st-panel h2{font-size:12px;text-transform:uppercase;letter-spacing:.04em;margin:0;padding:6px 10px;background:#23232e}",
6028
+ ".st-panel ul{list-style:none;margin:0;padding:6px 10px}",
6029
+ ".st-panel li{padding:1px 0}",
6030
+ ".st-panel p{margin:6px 10px}",
6031
+ ".st-count{background:#3a3a4a;border-radius:10px;padding:0 7px;font-size:11px;float:right}",
6032
+ ".st-dim{color:#8a8a9a}.st-key{color:#7db4ff}.st-err{color:#ff8a8a}.st-empty{color:#6a6a7a}",
6033
+ ".st-tree{margin:6px 10px;white-space:pre;overflow:auto;color:#c8c8d4}",
6034
+ ".st-depth{display:inline-block}.st-depth{width:calc(var(--d,0)*12px)}",
6035
+ "code{color:#d7d7e0}"
6036
+ ].join("");
6037
+
4728
6038
  // src/config.ts
4729
6039
  function defineConfig(config) {
4730
6040
  return config;
@@ -4747,6 +6057,8 @@ export {
4747
6057
  FOCUSABLE_SELECTOR,
4748
6058
  FormBuilderImpl,
4749
6059
  GraphNode,
6060
+ HeadManager,
6061
+ HttpError,
4750
6062
  Lifecycle,
4751
6063
  ListBuilderImpl,
4752
6064
  NodeInstance,
@@ -4770,6 +6082,7 @@ export {
4770
6082
  StreetFrameworkError,
4771
6083
  StreetRenderHandle,
4772
6084
  StreetRendererImpl,
6085
+ TransitionController,
4773
6086
  VERSION,
4774
6087
  a11yIds,
4775
6088
  applyNodeProps,
@@ -4784,8 +6097,11 @@ export {
4784
6097
  consoleDiagnosticSink,
4785
6098
  consoleHydrationDiagnosticSink,
4786
6099
  containFocus,
6100
+ createAnnouncer,
4787
6101
  createApplication,
6102
+ createAuthSession,
4788
6103
  createBrowserHistory,
6104
+ createClient,
4789
6105
  createContext,
4790
6106
  createDevTools,
4791
6107
  createForm,
@@ -4801,10 +6117,12 @@ export {
4801
6117
  createStreetEvent,
4802
6118
  defineConfig,
4803
6119
  derived,
6120
+ describeError,
4804
6121
  diagnosePerformance,
4805
6122
  effect,
4806
6123
  email,
4807
6124
  environment,
6125
+ escapeHtml,
4808
6126
  escapeHtmlAttr,
4809
6127
  escapeHtmlText,
4810
6128
  flushSync,
@@ -4818,6 +6136,7 @@ export {
4818
6136
  generateApplicationId,
4819
6137
  generateNodeId,
4820
6138
  getFocusable,
6139
+ getResolvedTransition,
4821
6140
  globalEventBus,
4822
6141
  headingUpdate,
4823
6142
  hydrateGraph,
@@ -4828,12 +6147,16 @@ export {
4828
6147
  inspectForm,
4829
6148
  inspectGraph,
4830
6149
  inspectI18n,
6150
+ inspectInteractions,
4831
6151
  inspectResource,
4832
6152
  inspectRouter,
4833
6153
  inspectSignal,
4834
6154
  interpolate,
4835
6155
  isBatching,
4836
6156
  isComponentDefinition,
6157
+ isHeadContribution,
6158
+ isTransitionConfig,
6159
+ linkUpdate,
4837
6160
  matchPattern,
4838
6161
  matchRoutes,
4839
6162
  maxLength,
@@ -4841,6 +6164,7 @@ export {
4841
6164
  mountGraph,
4842
6165
  mountNode,
4843
6166
  mountRouter,
6167
+ mutation,
4844
6168
  nextId,
4845
6169
  nodeIdPrefix,
4846
6170
  nodeTypeStats,
@@ -4857,14 +6181,21 @@ export {
4857
6181
  readState,
4858
6182
  reconcileChildren,
4859
6183
  reconcileChildrenByPlan,
6184
+ renderDevToolsHTML,
6185
+ renderHead,
4860
6186
  renderToString,
4861
6187
  reportDiagnostic,
6188
+ reportError,
4862
6189
  required,
4863
6190
  resetIdCounter,
6191
+ resolveHead,
4864
6192
  resolveTag,
6193
+ resolveTransition,
4865
6194
  resource,
4866
6195
  restoreFocus,
4867
6196
  routerOutlet,
6197
+ rovingMenu,
6198
+ runElementTransition,
4868
6199
  runValidators,
4869
6200
  saveFocus,
4870
6201
  scheduleImmediate,
@@ -4885,6 +6216,7 @@ export {
4885
6216
  validateGraph,
4886
6217
  wireComponentBehavior,
4887
6218
  wireEvents,
6219
+ wireHeadBehavior,
4888
6220
  wireOverlayBehavior,
4889
6221
  wireReactiveList,
4890
6222
  wireSignalBindings