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.cjs CHANGED
@@ -37,6 +37,8 @@ __export(src_exports, {
37
37
  FOCUSABLE_SELECTOR: () => FOCUSABLE_SELECTOR,
38
38
  FormBuilderImpl: () => FormBuilderImpl,
39
39
  GraphNode: () => GraphNode,
40
+ HeadManager: () => HeadManager,
41
+ HttpError: () => HttpError,
40
42
  Lifecycle: () => Lifecycle,
41
43
  ListBuilderImpl: () => ListBuilderImpl,
42
44
  NodeInstance: () => NodeInstance,
@@ -60,6 +62,7 @@ __export(src_exports, {
60
62
  StreetFrameworkError: () => StreetFrameworkError,
61
63
  StreetRenderHandle: () => StreetRenderHandle,
62
64
  StreetRendererImpl: () => StreetRendererImpl,
65
+ TransitionController: () => TransitionController,
63
66
  VERSION: () => VERSION,
64
67
  a11yIds: () => a11yIds,
65
68
  applyNodeProps: () => applyNodeProps,
@@ -74,8 +77,11 @@ __export(src_exports, {
74
77
  consoleDiagnosticSink: () => consoleDiagnosticSink,
75
78
  consoleHydrationDiagnosticSink: () => consoleHydrationDiagnosticSink,
76
79
  containFocus: () => containFocus,
80
+ createAnnouncer: () => createAnnouncer,
77
81
  createApplication: () => createApplication,
82
+ createAuthSession: () => createAuthSession,
78
83
  createBrowserHistory: () => createBrowserHistory,
84
+ createClient: () => createClient,
79
85
  createContext: () => createContext,
80
86
  createDevTools: () => createDevTools,
81
87
  createForm: () => createForm,
@@ -91,10 +97,12 @@ __export(src_exports, {
91
97
  createStreetEvent: () => createStreetEvent,
92
98
  defineConfig: () => defineConfig,
93
99
  derived: () => derived,
100
+ describeError: () => describeError,
94
101
  diagnosePerformance: () => diagnosePerformance,
95
102
  effect: () => effect,
96
103
  email: () => email,
97
104
  environment: () => environment,
105
+ escapeHtml: () => escapeHtml,
98
106
  escapeHtmlAttr: () => escapeHtmlAttr,
99
107
  escapeHtmlText: () => escapeHtmlText,
100
108
  flushSync: () => flushSync,
@@ -108,6 +116,7 @@ __export(src_exports, {
108
116
  generateApplicationId: () => generateApplicationId,
109
117
  generateNodeId: () => generateNodeId,
110
118
  getFocusable: () => getFocusable,
119
+ getResolvedTransition: () => getResolvedTransition,
111
120
  globalEventBus: () => globalEventBus,
112
121
  headingUpdate: () => headingUpdate,
113
122
  hydrateGraph: () => hydrateGraph,
@@ -118,12 +127,16 @@ __export(src_exports, {
118
127
  inspectForm: () => inspectForm,
119
128
  inspectGraph: () => inspectGraph,
120
129
  inspectI18n: () => inspectI18n,
130
+ inspectInteractions: () => inspectInteractions,
121
131
  inspectResource: () => inspectResource,
122
132
  inspectRouter: () => inspectRouter,
123
133
  inspectSignal: () => inspectSignal,
124
134
  interpolate: () => interpolate,
125
135
  isBatching: () => isBatching,
126
136
  isComponentDefinition: () => isComponentDefinition,
137
+ isHeadContribution: () => isHeadContribution,
138
+ isTransitionConfig: () => isTransitionConfig,
139
+ linkUpdate: () => linkUpdate,
127
140
  matchPattern: () => matchPattern,
128
141
  matchRoutes: () => matchRoutes,
129
142
  maxLength: () => maxLength,
@@ -131,6 +144,7 @@ __export(src_exports, {
131
144
  mountGraph: () => mountGraph,
132
145
  mountNode: () => mountNode,
133
146
  mountRouter: () => mountRouter,
147
+ mutation: () => mutation,
134
148
  nextId: () => nextId,
135
149
  nodeIdPrefix: () => nodeIdPrefix,
136
150
  nodeTypeStats: () => nodeTypeStats,
@@ -147,14 +161,21 @@ __export(src_exports, {
147
161
  readState: () => readState,
148
162
  reconcileChildren: () => reconcileChildren,
149
163
  reconcileChildrenByPlan: () => reconcileChildrenByPlan,
164
+ renderDevToolsHTML: () => renderDevToolsHTML,
165
+ renderHead: () => renderHead,
150
166
  renderToString: () => renderToString,
151
167
  reportDiagnostic: () => reportDiagnostic,
168
+ reportError: () => reportError,
152
169
  required: () => required,
153
170
  resetIdCounter: () => resetIdCounter,
171
+ resolveHead: () => resolveHead,
154
172
  resolveTag: () => resolveTag,
173
+ resolveTransition: () => resolveTransition,
155
174
  resource: () => resource,
156
175
  restoreFocus: () => restoreFocus,
157
176
  routerOutlet: () => routerOutlet,
177
+ rovingMenu: () => rovingMenu,
178
+ runElementTransition: () => runElementTransition,
158
179
  runValidators: () => runValidators,
159
180
  saveFocus: () => saveFocus,
160
181
  scheduleImmediate: () => scheduleImmediate,
@@ -175,6 +196,7 @@ __export(src_exports, {
175
196
  validateGraph: () => validateGraph,
176
197
  wireComponentBehavior: () => wireComponentBehavior,
177
198
  wireEvents: () => wireEvents,
199
+ wireHeadBehavior: () => wireHeadBehavior,
178
200
  wireOverlayBehavior: () => wireOverlayBehavior,
179
201
  wireReactiveList: () => wireReactiveList,
180
202
  wireSignalBindings: () => wireSignalBindings
@@ -182,7 +204,7 @@ __export(src_exports, {
182
204
  module.exports = __toCommonJS(src_exports);
183
205
 
184
206
  // src/version.ts
185
- var VERSION = "1.8.0";
207
+ var VERSION = "2.0.0";
186
208
 
187
209
  // ../state/src/signal.ts
188
210
  var _activeConsumer = null;
@@ -522,6 +544,198 @@ function resource(loader, options = {}) {
522
544
  };
523
545
  }
524
546
 
547
+ // ../state/src/mutation.ts
548
+ function mutation(mutator, options = {}) {
549
+ const status = signal("idle");
550
+ const data = signal(void 0);
551
+ const error = signal(void 0);
552
+ const pending = derived(() => status.get() === "loading");
553
+ let disposed = false;
554
+ let runId = 0;
555
+ const mutate = async (args) => {
556
+ if (disposed) {
557
+ return await mutator(args);
558
+ }
559
+ const myRun = ++runId;
560
+ batch(() => {
561
+ error.set(void 0);
562
+ status.set("loading");
563
+ });
564
+ try {
565
+ const result = await mutator(args);
566
+ if (!disposed && myRun === runId) {
567
+ batch(() => {
568
+ data.set(result);
569
+ error.set(void 0);
570
+ status.set("success");
571
+ });
572
+ }
573
+ if (!disposed && myRun === runId) {
574
+ await options.onSuccess?.(result, args);
575
+ await options.onSettled?.(args);
576
+ }
577
+ return result;
578
+ } catch (err) {
579
+ if (!disposed && myRun === runId) {
580
+ batch(() => {
581
+ error.set(err);
582
+ status.set("error");
583
+ });
584
+ await options.onError?.(err, args);
585
+ await options.onSettled?.(args);
586
+ }
587
+ throw err;
588
+ }
589
+ };
590
+ const reset = () => {
591
+ batch(() => {
592
+ status.set("idle");
593
+ data.set(void 0);
594
+ error.set(void 0);
595
+ });
596
+ };
597
+ const dispose = () => {
598
+ disposed = true;
599
+ };
600
+ if (options.onCleanup !== void 0) options.onCleanup(dispose);
601
+ return { status, data, error, pending, mutate, reset, dispose };
602
+ }
603
+
604
+ // ../state/src/client.ts
605
+ var HttpError = class extends Error {
606
+ status;
607
+ statusText;
608
+ url;
609
+ body;
610
+ constructor(status, statusText, url, body) {
611
+ super(`HTTP ${status} ${statusText} for ${url}`);
612
+ this.name = "HttpError";
613
+ this.status = status;
614
+ this.statusText = statusText;
615
+ this.url = url;
616
+ this.body = body;
617
+ }
618
+ };
619
+ function joinUrl(baseUrl, path) {
620
+ if (baseUrl === void 0 || baseUrl === "") return path;
621
+ const b = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
622
+ const p = path.startsWith("/") ? path : `/${path}`;
623
+ return `${b}${p}`;
624
+ }
625
+ function withQuery(url, query) {
626
+ if (query === void 0) return url;
627
+ const params = new URLSearchParams();
628
+ for (const [k, v] of Object.entries(query)) params.set(k, String(v));
629
+ const qs = params.toString();
630
+ if (qs === "") return url;
631
+ return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
632
+ }
633
+ function createClient(config = {}) {
634
+ const doFetch = config.fetch ?? ((input, init) => {
635
+ if (typeof fetch === "undefined") {
636
+ throw new Error("createClient: no global fetch; pass { fetch } explicitly");
637
+ }
638
+ return fetch(input, init);
639
+ });
640
+ async function request(method, path, body, reqConfig = {}) {
641
+ const url = withQuery(joinUrl(config.baseUrl, path), reqConfig.query);
642
+ const headers = { ...config.headers, ...reqConfig.headers };
643
+ const init = { method, headers };
644
+ if (reqConfig.signal !== void 0) init.signal = reqConfig.signal;
645
+ if (body !== void 0) {
646
+ if (headers["Content-Type"] === void 0) headers["Content-Type"] = "application/json";
647
+ init.body = JSON.stringify(body);
648
+ }
649
+ const response = await doFetch(url, init);
650
+ const parsed = await parseBody(response);
651
+ if (!response.ok) {
652
+ throw new HttpError(response.status, response.statusText, url, parsed);
653
+ }
654
+ return parsed;
655
+ }
656
+ return {
657
+ request,
658
+ get: (path, c) => request("GET", path, void 0, c),
659
+ post: (path, b, c) => request("POST", path, b, c),
660
+ put: (path, b, c) => request("PUT", path, b, c),
661
+ patch: (path, b, c) => request("PATCH", path, b, c),
662
+ del: (path, c) => request("DELETE", path, void 0, c),
663
+ resource: (path, options = {}) => {
664
+ const { query, ...resourceOptions } = options;
665
+ return resource(
666
+ (ctx) => request("GET", path, void 0, { signal: ctx.signal, ...query !== void 0 ? { query } : {} }),
667
+ resourceOptions
668
+ );
669
+ },
670
+ mutation: (method, path, options) => mutation((args) => request(method, path, args), options ?? {})
671
+ };
672
+ }
673
+ async function parseBody(response) {
674
+ const text = await response.text();
675
+ if (text === "") return void 0;
676
+ const type = response.headers.get("content-type") ?? "";
677
+ if (type.includes("application/json")) {
678
+ try {
679
+ return JSON.parse(text);
680
+ } catch {
681
+ return text;
682
+ }
683
+ }
684
+ return text;
685
+ }
686
+
687
+ // ../state/src/auth.ts
688
+ function createAuthSession(config) {
689
+ const session = resource(
690
+ (ctx) => config.loadUser(ctx),
691
+ {
692
+ ...config.immediate !== void 0 ? { immediate: config.immediate } : {},
693
+ ...config.onCleanup !== void 0 ? { onCleanup: config.onCleanup } : {}
694
+ }
695
+ );
696
+ const status = derived(() => {
697
+ const s = session.status.get();
698
+ const d = session.data.get();
699
+ if (s === "error") return "error";
700
+ if (s === "idle" || s === "loading" && d === void 0) return "loading";
701
+ return d === null || d === void 0 ? "unauthenticated" : "authenticated";
702
+ });
703
+ const user = derived(() => {
704
+ const d = session.data.get();
705
+ return d === null ? void 0 : d;
706
+ });
707
+ const authenticated = derived(() => status.get() === "authenticated");
708
+ const unauthenticated = derived(() => status.get() === "unauthenticated");
709
+ const loading = derived(() => status.get() === "loading");
710
+ const logoutMutation = mutation(
711
+ async () => {
712
+ await config.logout?.();
713
+ },
714
+ { onSuccess: () => session.refetch() }
715
+ );
716
+ const dispose = () => {
717
+ session.dispose();
718
+ status.dispose();
719
+ user.dispose();
720
+ authenticated.dispose();
721
+ unauthenticated.dispose();
722
+ loading.dispose();
723
+ logoutMutation.dispose();
724
+ };
725
+ return {
726
+ status,
727
+ user,
728
+ error: session.error,
729
+ authenticated,
730
+ unauthenticated,
731
+ loading,
732
+ loggingOut: logoutMutation.pending,
733
+ refresh: () => session.refetch(),
734
+ logout: () => logoutMutation.mutate(void 0),
735
+ dispose
736
+ };
737
+ }
738
+
525
739
  // ../core/src/a11y-ids.ts
526
740
  var UNSAFE = /[^A-Za-z0-9_-]+/g;
527
741
  function toIdToken(base) {
@@ -537,6 +751,9 @@ function a11yIds(base) {
537
751
  description: `${token}-description`,
538
752
  error: `${token}-error`,
539
753
  title: `${token}-title`,
754
+ trigger: `${token}-trigger`,
755
+ controls: `${token}-controls`,
756
+ owns: `${token}-owns`,
540
757
  id: (suffix) => `${token}-${toIdToken(suffix)}`
541
758
  };
542
759
  }
@@ -785,9 +1002,13 @@ function formatDiagnosticContext(context) {
785
1002
  const parts = [];
786
1003
  if (context.package !== void 0) parts.push(`package=${context.package}`);
787
1004
  if (context.operation !== void 0) parts.push(`operation=${context.operation}`);
1005
+ if (context.component !== void 0) parts.push(`component=${context.component}`);
1006
+ if (context.phase !== void 0) parts.push(`phase=${context.phase}`);
788
1007
  if (context.nodeId !== void 0) parts.push(`node=${context.nodeId}`);
1008
+ if (context.element !== void 0) parts.push(`element=${context.element}`);
789
1009
  if (context.route !== void 0) parts.push(`route=${context.route}`);
790
1010
  if (context.resource !== void 0) parts.push(`resource=${context.resource}`);
1011
+ if (context.signal !== void 0) parts.push(`signal=${context.signal}`);
791
1012
  return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
792
1013
  }
793
1014
  var StreetFrameworkError = class extends Error {
@@ -818,6 +1039,42 @@ function consoleDiagnosticSink(logger = console) {
818
1039
  error: (m, c) => logger.error?.(`${m}${formatDiagnosticContext(c)}`)
819
1040
  };
820
1041
  }
1042
+ function describeErrorAt(error, context, options, depth) {
1043
+ const isError = error instanceof Error;
1044
+ const name = isError ? error.name : "Error";
1045
+ const message = isError ? error.message : safeStringify(error);
1046
+ const report = { name, message, isError };
1047
+ if (context !== void 0) report.context = context;
1048
+ if (options.includeStack === true && isError && typeof error.stack === "string") {
1049
+ report.stack = error.stack;
1050
+ }
1051
+ if (options.includeCause === true && isError && depth < 4) {
1052
+ const cause = error.cause;
1053
+ if (cause !== void 0 && cause !== null) {
1054
+ report.cause = describeErrorAt(cause, void 0, options, depth + 1);
1055
+ }
1056
+ }
1057
+ return report;
1058
+ }
1059
+ function safeStringify(value) {
1060
+ if (typeof value === "string") return value;
1061
+ if (value === null) return "null";
1062
+ if (value === void 0) return "undefined";
1063
+ const t = typeof value;
1064
+ if (t === "number" || t === "boolean" || t === "bigint" || t === "symbol") {
1065
+ return String(value);
1066
+ }
1067
+ const ctor = t === "object" && value !== null ? value.constructor?.name ?? "Object" : t;
1068
+ return `[non-Error ${ctor}]`;
1069
+ }
1070
+ function describeError(error, context, options = {}) {
1071
+ return describeErrorAt(error, context, options, 0);
1072
+ }
1073
+ function reportError(sink, error, context, options = {}) {
1074
+ const report = describeError(error, context, options);
1075
+ reportDiagnostic(sink, "error", report.message, report.context);
1076
+ return report;
1077
+ }
821
1078
 
822
1079
  // ../graph/src/graph-node.ts
823
1080
  var GraphNode = class _GraphNode {
@@ -987,6 +1244,8 @@ var ApplicationGraph = class {
987
1244
  this.handlers.delete(`__listplan__${node.id}`);
988
1245
  this.handlers.delete(`__overlay__${node.id}`);
989
1246
  this.handlers.delete(`__component__${node.id}`);
1247
+ this.handlers.delete(`__transition__${node.id}`);
1248
+ this.handlers.delete(`__head__${node.id}`);
990
1249
  }
991
1250
  // ── Handler registry ──────────────────────────────────────────────────────
992
1251
  registerHandler(key, fn) {
@@ -1075,6 +1334,162 @@ var ApplicationGraph = class {
1075
1334
  }
1076
1335
  };
1077
1336
 
1337
+ // ../dsl/src/transition.ts
1338
+ function classes(value) {
1339
+ if (value === void 0) return [];
1340
+ const out = [];
1341
+ for (const token of value.split(/\s+/)) {
1342
+ if (token.length > 0 && !out.includes(token)) out.push(token);
1343
+ }
1344
+ return out;
1345
+ }
1346
+ function merge(a, b) {
1347
+ const out = [...a];
1348
+ for (const token of b) if (!out.includes(token)) out.push(token);
1349
+ return out;
1350
+ }
1351
+ function resolveTransition(config) {
1352
+ const n = config.name;
1353
+ const enterActive = merge(
1354
+ classes(config.enter),
1355
+ classes(config.enterActive ?? (n !== void 0 ? `${n}-enter-active` : void 0))
1356
+ );
1357
+ const leaveActive = merge(
1358
+ classes(config.leave),
1359
+ classes(config.leaveActive ?? (n !== void 0 ? `${n}-leave-active` : void 0))
1360
+ );
1361
+ return {
1362
+ enterActive,
1363
+ enterFrom: classes(config.enterFrom ?? (n !== void 0 ? `${n}-enter-from` : void 0)),
1364
+ enterTo: classes(config.enterTo ?? (n !== void 0 ? `${n}-enter-to` : void 0)),
1365
+ leaveActive,
1366
+ leaveFrom: classes(config.leaveFrom ?? (n !== void 0 ? `${n}-leave-from` : void 0)),
1367
+ leaveTo: classes(config.leaveTo ?? (n !== void 0 ? `${n}-leave-to` : void 0)),
1368
+ appear: config.appear ?? false,
1369
+ duration: config.duration ?? 1e3
1370
+ };
1371
+ }
1372
+ function isTransitionConfig(value) {
1373
+ if (value === null || typeof value !== "object") return false;
1374
+ const o = value;
1375
+ 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";
1376
+ }
1377
+
1378
+ // ../dsl/src/head.ts
1379
+ function metaDedupKey(m) {
1380
+ if (m.charset !== void 0) return "meta:charset";
1381
+ if (m.name !== void 0) return `meta:name=${m.name}`;
1382
+ if (m.property !== void 0) return `meta:property=${m.property}`;
1383
+ if (m.httpEquiv !== void 0) return `meta:http-equiv=${m.httpEquiv}`;
1384
+ return void 0;
1385
+ }
1386
+ function metaAttrs(m) {
1387
+ const attrs = {};
1388
+ if (m.charset !== void 0) attrs["charset"] = m.charset;
1389
+ if (m.name !== void 0) attrs["name"] = m.name;
1390
+ if (m.property !== void 0) attrs["property"] = m.property;
1391
+ if (m.httpEquiv !== void 0) attrs["http-equiv"] = m.httpEquiv;
1392
+ if (m.content !== void 0) attrs["content"] = m.content;
1393
+ return attrs;
1394
+ }
1395
+ function linkAttrs(l) {
1396
+ const attrs = { rel: l.rel, href: l.href };
1397
+ if (l.sizes !== void 0) attrs["sizes"] = l.sizes;
1398
+ if (l.type !== void 0) attrs["type"] = l.type;
1399
+ if (l.media !== void 0) attrs["media"] = l.media;
1400
+ if (l.as !== void 0) attrs["as"] = l.as;
1401
+ if (l.crossorigin !== void 0) attrs["crossorigin"] = l.crossorigin;
1402
+ if (l.hreflang !== void 0) attrs["hreflang"] = l.hreflang;
1403
+ return attrs;
1404
+ }
1405
+ function resolveHead(config) {
1406
+ const entries = [];
1407
+ if (config.charset !== void 0) {
1408
+ entries.push({ tag: "meta", dedupKey: "meta:charset", attrs: { charset: config.charset } });
1409
+ }
1410
+ if (config.base !== void 0) {
1411
+ entries.push({ tag: "base", dedupKey: "base", attrs: { href: config.base } });
1412
+ }
1413
+ if (config.title !== void 0) {
1414
+ entries.push({ tag: "title", dedupKey: "title", attrs: {}, text: config.title });
1415
+ }
1416
+ if (config.description !== void 0) {
1417
+ entries.push({
1418
+ tag: "meta",
1419
+ dedupKey: "meta:name=description",
1420
+ attrs: { name: "description", content: config.description }
1421
+ });
1422
+ }
1423
+ if (config.canonical !== void 0) {
1424
+ entries.push({
1425
+ tag: "link",
1426
+ dedupKey: "link:rel=canonical",
1427
+ attrs: { rel: "canonical", href: config.canonical }
1428
+ });
1429
+ }
1430
+ if (config.robots !== void 0) {
1431
+ entries.push({
1432
+ tag: "meta",
1433
+ dedupKey: "meta:name=robots",
1434
+ attrs: { name: "robots", content: config.robots }
1435
+ });
1436
+ }
1437
+ if (config.themeColor !== void 0) {
1438
+ entries.push({
1439
+ tag: "meta",
1440
+ dedupKey: "meta:name=theme-color",
1441
+ attrs: { name: "theme-color", content: config.themeColor }
1442
+ });
1443
+ }
1444
+ if (config.viewport !== void 0) {
1445
+ entries.push({
1446
+ tag: "meta",
1447
+ dedupKey: "meta:name=viewport",
1448
+ attrs: { name: "viewport", content: config.viewport }
1449
+ });
1450
+ }
1451
+ if (config.favicon !== void 0) {
1452
+ const l = typeof config.favicon === "string" ? { rel: "icon", href: config.favicon } : config.favicon;
1453
+ entries.push({ tag: "link", dedupKey: `link:rel=${l.rel}`, attrs: linkAttrs(l) });
1454
+ }
1455
+ if (config.openGraph !== void 0) {
1456
+ for (const key of Object.keys(config.openGraph)) {
1457
+ const property = `og:${key}`;
1458
+ entries.push({
1459
+ tag: "meta",
1460
+ dedupKey: `meta:property=${property}`,
1461
+ attrs: { property, content: config.openGraph[key] }
1462
+ });
1463
+ }
1464
+ }
1465
+ if (config.twitter !== void 0) {
1466
+ for (const key of Object.keys(config.twitter)) {
1467
+ const name = `twitter:${key}`;
1468
+ entries.push({
1469
+ tag: "meta",
1470
+ dedupKey: `meta:name=${name}`,
1471
+ attrs: { name, content: config.twitter[key] }
1472
+ });
1473
+ }
1474
+ }
1475
+ if (config.meta !== void 0) {
1476
+ for (const m of config.meta) {
1477
+ const key = metaDedupKey(m);
1478
+ if (key === void 0) continue;
1479
+ entries.push({ tag: "meta", dedupKey: key, attrs: metaAttrs(m) });
1480
+ }
1481
+ }
1482
+ if (config.link !== void 0) {
1483
+ for (const l of config.link) {
1484
+ entries.push({ tag: "link", dedupKey: `link:rel=${l.rel}:href=${l.href}`, attrs: linkAttrs(l) });
1485
+ }
1486
+ }
1487
+ return { entries };
1488
+ }
1489
+ function isHeadContribution(value) {
1490
+ return value !== null && typeof value === "object" && Array.isArray(value["entries"]);
1491
+ }
1492
+
1078
1493
  // ../dsl/src/builders.ts
1079
1494
  function isSignal(v) {
1080
1495
  return v !== null && typeof v === "object" && typeof v["get"] === "function" && typeof v["subscribe"] === "function";
@@ -1102,6 +1517,18 @@ function applyA11yProps(props, options) {
1102
1517
  if (options.ariaInvalid !== void 0) props["aria-invalid"] = String(options.ariaInvalid);
1103
1518
  if (options.ariaRequired !== void 0) props["aria-required"] = String(options.ariaRequired);
1104
1519
  if (options.ariaModal !== void 0) props["aria-modal"] = String(options.ariaModal);
1520
+ if (options.ariaOwns !== void 0) props["aria-owns"] = options.ariaOwns;
1521
+ if (options.ariaActiveDescendant !== void 0) props["aria-activedescendant"] = options.ariaActiveDescendant;
1522
+ if (options.ariaHasPopup !== void 0) props["aria-haspopup"] = String(options.ariaHasPopup);
1523
+ if (options.ariaSelected !== void 0) props["aria-selected"] = String(options.ariaSelected);
1524
+ }
1525
+ function registerTransition(graph, node, config) {
1526
+ if (config === void 0) return;
1527
+ const resolved = resolveTransition(config);
1528
+ graph.registerHandler(
1529
+ `__transition__${node.id}`,
1530
+ () => resolved
1531
+ );
1105
1532
  }
1106
1533
  function containerProps(options) {
1107
1534
  const props = {};
@@ -1162,6 +1589,7 @@ var OVERLAY_KINDS = {
1162
1589
  role: "menu",
1163
1590
  modal: false,
1164
1591
  takesFocus: true,
1592
+ menu: true,
1165
1593
  ariaModal: false,
1166
1594
  defaultCloseOnEscape: true,
1167
1595
  defaultRestoreFocus: true
@@ -1294,6 +1722,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1294
1722
  parent: this._node,
1295
1723
  props: containerProps(options)
1296
1724
  });
1725
+ registerTransition(this._graph, node, options.transition);
1297
1726
  builder(new SectionBuilderImpl(node, this._graph));
1298
1727
  }
1299
1728
  container(key, builder, options = {}) {
@@ -1302,6 +1731,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1302
1731
  parent: this._node,
1303
1732
  props: containerProps(options)
1304
1733
  });
1734
+ registerTransition(this._graph, node, options.transition);
1305
1735
  builder(new ContainerBuilderImpl(node, this._graph));
1306
1736
  }
1307
1737
  list(key, builder, options = {}) {
@@ -1310,6 +1740,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1310
1740
  parent: this._node,
1311
1741
  props: containerProps(options)
1312
1742
  });
1743
+ registerTransition(this._graph, node, options.transition);
1313
1744
  builder(new ListBuilderImpl(node, this._graph));
1314
1745
  }
1315
1746
  listOf(key, items, renderItem, options = {}) {
@@ -1319,6 +1750,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1319
1750
  parent: this._node,
1320
1751
  props: containerProps(options)
1321
1752
  });
1753
+ registerTransition(graph, node, options.transition);
1322
1754
  const signalId = `${node.id}:items`;
1323
1755
  node.stateRefs.push({ signalId, propKey: "items" });
1324
1756
  graph.registerHandler(`__signal__${signalId}`, items);
@@ -1340,6 +1772,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1340
1772
  }
1341
1773
  });
1342
1774
  renderItem(item, index, new ContainerBuilderImpl(itemNode, graph));
1775
+ registerTransition(graph, itemNode, options.itemTransition);
1343
1776
  return itemNode;
1344
1777
  };
1345
1778
  const buildPlan = (raw) => {
@@ -1371,6 +1804,7 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1371
1804
  parent: this._node,
1372
1805
  props
1373
1806
  });
1807
+ registerTransition(this._graph, node, options.transition);
1374
1808
  if (options.onSubmit !== void 0) {
1375
1809
  const handlerKey = `submit:${node.id}`;
1376
1810
  this._graph.registerHandler(handlerKey, options.onSubmit);
@@ -1378,18 +1812,20 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1378
1812
  }
1379
1813
  builder(new FormBuilderImpl(node, this._graph));
1380
1814
  }
1381
- when(condition, builder, elseBuilder) {
1815
+ when(condition, builder, elseBuilder, options = {}) {
1382
1816
  const graph = this._graph;
1383
1817
  const node = graph.createNode("conditional", {
1384
1818
  parent: this._node,
1385
1819
  props: containerProps({})
1386
1820
  });
1821
+ const branchTransition = options.transition !== void 0 && options.appear === true ? { ...options.transition, appear: true } : options.transition;
1387
1822
  const buildBranch = (build, tag) => {
1388
1823
  const branchKey = `when-${tag}:${node.id}`;
1389
1824
  const branch = graph.createNode("container", {
1390
1825
  key: branchKey,
1391
1826
  props: { key: branchKey }
1392
1827
  });
1828
+ registerTransition(graph, branch, branchTransition);
1393
1829
  build(new ContainerBuilderImpl(branch, graph));
1394
1830
  return branch;
1395
1831
  };
@@ -1435,8 +1871,14 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1435
1871
  this.container(id, (c) => {
1436
1872
  c.when(
1437
1873
  hasError,
1438
- // Error state → fallback.
1439
- (fb) => options.fallback(fb, readError(), retry),
1874
+ // Error state → fallback. The fallback branch mounts exactly when the
1875
+ // boundary enters its error state, so this is also where the optional
1876
+ // `onError` reporting hook fires (observe-only; §6/§7).
1877
+ (fb) => {
1878
+ const currentError = readError();
1879
+ options.onError?.(currentError);
1880
+ options.fallback(fb, currentError, retry);
1881
+ },
1440
1882
  // Healthy state → body, guarded against synchronous build throws.
1441
1883
  (body) => {
1442
1884
  try {
@@ -1448,6 +1890,24 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1448
1890
  );
1449
1891
  }, { id });
1450
1892
  }
1893
+ asyncBoundary(key, res, branches) {
1894
+ const isError = derived(() => res.status.get() === "error");
1895
+ const showSuccess = derived(
1896
+ () => res.status.get() !== "error" && res.data.get() !== void 0
1897
+ );
1898
+ const showLoading = derived(
1899
+ () => res.status.get() !== "error" && res.data.get() === void 0
1900
+ );
1901
+ const dataSignal = derived(() => res.data.get());
1902
+ const retry = () => {
1903
+ void res.refetch();
1904
+ };
1905
+ this.container(key, (c) => {
1906
+ c.when(isError, (fb) => branches.error?.(fb, res.error.peek(), retry));
1907
+ c.when(showSuccess, (sb) => branches.success(sb, dataSignal));
1908
+ c.when(showLoading, (lb) => branches.loading?.(lb));
1909
+ }, { key });
1910
+ }
1451
1911
  // ── Portals & overlays ──────────────────────────────────────────────────────
1452
1912
  portal(key, builder, options = {}) {
1453
1913
  const node = this._graph.createNode("portal", {
@@ -1457,6 +1917,31 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1457
1917
  });
1458
1918
  builder(new ContainerBuilderImpl(node, this._graph));
1459
1919
  }
1920
+ /**
1921
+ * Declare document metadata (2.0 §1–§3). Creates a `'head'` node — a neutral,
1922
+ * empty inline anchor at this position (one node / one element, so positional
1923
+ * hydration is preserved) — and registers a `__head__<id>` descriptor holding
1924
+ * this call's normalized, dedup-keyed {@link resolveHead} contribution. The
1925
+ * renderer applies it to `document.head` on the browser (adopting server tags
1926
+ * on hydration, cleaning up on unmount / route change) and emits the active
1927
+ * graph's merged metadata as a string on the server (`renderHead`).
1928
+ *
1929
+ * Multiple `head()` nodes may be live at once (app default + route + component)
1930
+ * — the renderer merges them and, per dedup key, the last in document order
1931
+ * wins (see head.ts). No new render path: this reuses the same graph-node +
1932
+ * handler-registry convention as overlays/transitions/components.
1933
+ */
1934
+ head(metadata) {
1935
+ const node = this._graph.createNode("head", {
1936
+ parent: this._node,
1937
+ props: { "data-streetui-head-anchor": "" }
1938
+ });
1939
+ const contribution = resolveHead(metadata);
1940
+ this._graph.registerHandler(
1941
+ `__head__${node.id}`,
1942
+ () => contribution
1943
+ );
1944
+ }
1460
1945
  /**
1461
1946
  * Shared assembly for every overlay kind: a `portal` node whose single child
1462
1947
  * is a `when(open, panel)` conditional. The panel container carries the
@@ -1484,13 +1969,24 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1484
1969
  ...options.ariaDescribedBy !== void 0 ? { ariaDescribedBy: options.ariaDescribedBy } : {}
1485
1970
  };
1486
1971
  const portalBuilder = new ContainerBuilderImpl(portalNode, graph);
1487
- portalBuilder.when(openSignal, (panelHost) => {
1488
- panelHost.container(`${key}__panel`, builder, panelOptions);
1489
- });
1972
+ portalBuilder.when(
1973
+ openSignal,
1974
+ (panelHost) => {
1975
+ panelHost.container(`${key}__panel`, builder, panelOptions);
1976
+ },
1977
+ void 0,
1978
+ // Overlay open/close rides the panel's `when`; a transition animates the
1979
+ // panel in on open and — via the reconciler's deferred-leave — plays the
1980
+ // leave before the panel is removed (§10). Focus is restored at close-
1981
+ // request time (see wireOverlayBehavior), so it never stays trapped inside
1982
+ // a panel that is animating away.
1983
+ options.transition !== void 0 ? { transition: options.transition } : {}
1984
+ );
1490
1985
  const descriptor = {
1491
1986
  open: openSignal,
1492
1987
  modal: kind.modal,
1493
1988
  takesFocus: kind.takesFocus,
1989
+ menu: kind.menu ?? false,
1494
1990
  closeOnEscape: options.closeOnEscape ?? kind.defaultCloseOnEscape,
1495
1991
  restoreFocus: options.restoreFocus ?? kind.defaultRestoreFocus,
1496
1992
  ...options.initialFocusId !== void 0 ? { initialFocusId: options.initialFocusId } : {},
@@ -1587,6 +2083,7 @@ var ListBuilderImpl = class extends ContentBuilderBase {
1587
2083
  parent: this._node,
1588
2084
  props: containerProps(options)
1589
2085
  });
2086
+ registerTransition(this._graph, node, options.transition);
1590
2087
  builder(new ContainerBuilderImpl(node, this._graph));
1591
2088
  }
1592
2089
  };
@@ -1816,8 +2313,8 @@ var RuntimeNodeInstance = class {
1816
2313
  this.children.push(instance);
1817
2314
  }
1818
2315
  /** Subscribe to a signal and register the unsubscribe for cleanup. */
1819
- trackSignal(signal2, handler) {
1820
- const unsub = signal2.subscribe(handler);
2316
+ trackSignal(signal3, handler) {
2317
+ const unsub = signal3.subscribe(handler);
1821
2318
  this.cleanup.add(unsub);
1822
2319
  }
1823
2320
  /** Register an arbitrary cleanup function (e.g. DOM event removal). */
@@ -2188,6 +2685,9 @@ var BrowserDOMAdapter = class {
2188
2685
  body() {
2189
2686
  return document.body ?? null;
2190
2687
  }
2688
+ head() {
2689
+ return document.head ?? null;
2690
+ }
2191
2691
  activeElement() {
2192
2692
  return document.activeElement ?? null;
2193
2693
  }
@@ -2567,6 +3067,9 @@ var ServerDOMAdapter = class {
2567
3067
  body() {
2568
3068
  return null;
2569
3069
  }
3070
+ head() {
3071
+ return null;
3072
+ }
2570
3073
  activeElement() {
2571
3074
  return null;
2572
3075
  }
@@ -2681,18 +3184,25 @@ function trapFocus(dom, container) {
2681
3184
  dom.addEventListener(container, "keydown", onKeydown);
2682
3185
  return () => dom.removeEventListener(container, "keydown", onKeydown);
2683
3186
  }
3187
+ var containmentStack = [];
2684
3188
  function containFocus(dom, container) {
2685
3189
  const body = dom.body();
2686
3190
  if (body === null) return () => {
2687
3191
  };
3192
+ containmentStack.push(container);
2688
3193
  const onFocusIn = (event) => {
3194
+ if (containmentStack[containmentStack.length - 1] !== container) return;
2689
3195
  const target = event.target;
2690
3196
  if (target !== null && !dom.contains(container, target)) {
2691
3197
  focusFirst(dom, container);
2692
3198
  }
2693
3199
  };
2694
3200
  dom.addEventListener(body, "focusin", onFocusIn);
2695
- return () => dom.removeEventListener(body, "focusin", onFocusIn);
3201
+ return () => {
3202
+ dom.removeEventListener(body, "focusin", onFocusIn);
3203
+ const index = containmentStack.lastIndexOf(container);
3204
+ if (index !== -1) containmentStack.splice(index, 1);
3205
+ };
2696
3206
  }
2697
3207
  function onEscape(dom, target, handler) {
2698
3208
  const onKeydown = (event) => {
@@ -2701,6 +3211,84 @@ function onEscape(dom, target, handler) {
2701
3211
  dom.addEventListener(target, "keydown", onKeydown);
2702
3212
  return () => dom.removeEventListener(target, "keydown", onKeydown);
2703
3213
  }
3214
+ function rovingMenu(dom, container, selector = FOCUSABLE_SELECTOR) {
3215
+ const onKeydown = (event) => {
3216
+ const key = event.key;
3217
+ const isActivate = key === "Enter" || key === " " || key === "Spacebar";
3218
+ const isMove = key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End";
3219
+ if (!isActivate && !isMove) return;
3220
+ const items = getFocusable(dom, container, selector);
3221
+ if (items.length === 0) return;
3222
+ const active = dom.activeElement();
3223
+ const index = active === null ? -1 : items.indexOf(active);
3224
+ if (isActivate) {
3225
+ if (index < 0) return;
3226
+ event.preventDefault();
3227
+ items[index].click?.();
3228
+ return;
3229
+ }
3230
+ event.preventDefault();
3231
+ let next;
3232
+ if (key === "Home") next = 0;
3233
+ else if (key === "End") next = items.length - 1;
3234
+ else if (key === "ArrowDown") next = index < 0 ? 0 : (index + 1) % items.length;
3235
+ else next = index <= 0 ? items.length - 1 : index - 1;
3236
+ dom.focus(items[next]);
3237
+ };
3238
+ dom.addEventListener(container, "keydown", onKeydown);
3239
+ return () => dom.removeEventListener(container, "keydown", onKeydown);
3240
+ }
3241
+
3242
+ // ../dom/src/live-region.ts
3243
+ 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;";
3244
+ function makeRegion(dom, politeness) {
3245
+ const el = dom.createElement("div");
3246
+ dom.setAttribute(el, "aria-live", politeness);
3247
+ dom.setAttribute(el, "aria-atomic", "true");
3248
+ dom.setAttribute(el, "role", politeness === "assertive" ? "alert" : "status");
3249
+ dom.setAttribute(el, "data-streetui-live", politeness);
3250
+ dom.setAttribute(el, "style", VISUALLY_HIDDEN);
3251
+ return el;
3252
+ }
3253
+ function createAnnouncer(dom) {
3254
+ const body = dom.body();
3255
+ if (body === null) {
3256
+ return { announce() {
3257
+ }, clear() {
3258
+ }, destroy() {
3259
+ } };
3260
+ }
3261
+ const polite = makeRegion(dom, "polite");
3262
+ const assertive = makeRegion(dom, "assertive");
3263
+ dom.appendChild(body, polite);
3264
+ dom.appendChild(body, assertive);
3265
+ let destroyed = false;
3266
+ const write = (region, message) => {
3267
+ dom.setTextContent(region, "");
3268
+ void Promise.resolve().then(() => {
3269
+ if (!destroyed) dom.setTextContent(region, message);
3270
+ });
3271
+ };
3272
+ return {
3273
+ announce(message, options) {
3274
+ if (destroyed) return;
3275
+ write(options?.assertive === true ? assertive : polite, message);
3276
+ },
3277
+ clear() {
3278
+ if (destroyed) return;
3279
+ dom.setTextContent(polite, "");
3280
+ dom.setTextContent(assertive, "");
3281
+ },
3282
+ destroy() {
3283
+ if (destroyed) return;
3284
+ destroyed = true;
3285
+ for (const region of [polite, assertive]) {
3286
+ const parent = dom.parentNode(region);
3287
+ if (parent !== null) dom.removeChild(parent, region);
3288
+ }
3289
+ }
3290
+ };
3291
+ }
2704
3292
 
2705
3293
  // ../renderer/src/render-context.ts
2706
3294
  function createRenderContext(dom, graph, container, hydrationDiagnostics, staticHTML) {
@@ -2864,7 +3452,13 @@ var TAG_MAP = {
2864
3452
  // its children are relocated to a document.body container on the browser
2865
3453
  // (see the portal branch in mount.ts). On the server (no body) it renders
2866
3454
  // inline, so the anchor tag is what SSR/hydration positionally match on.
2867
- portal: "div"
3455
+ portal: "div",
3456
+ // A `head()` node renders as a neutral, empty inline anchor <div> at its
3457
+ // declaration site (like a portal anchor). Its actual contribution — title/
3458
+ // meta/link/etc. — is applied to `document.head` by `wireHeadBehavior` on the
3459
+ // browser, and emitted separately by `renderHead()` on the server. Keeping a
3460
+ // one-node/one-element anchor preserves positional hydration.
3461
+ head: "div"
2868
3462
  };
2869
3463
  function resolveTag(type) {
2870
3464
  return TAG_MAP[type] ?? "div";
@@ -2912,7 +3506,7 @@ function patchNode(ctx, graphNode, propKey, newValue) {
2912
3506
  }
2913
3507
 
2914
3508
  // ../renderer/src/reconciliation.ts
2915
- function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
3509
+ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn, hooks) {
2916
3510
  const oldByKey = /* @__PURE__ */ new Map();
2917
3511
  for (const inst of oldInstances) {
2918
3512
  const key = inst.graphNode.key ?? inst.graphNode.id;
@@ -2933,28 +3527,33 @@ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
2933
3527
  }
2934
3528
  newInstances.push(existing);
2935
3529
  } else {
2936
- const inst = mountFn(newNode, parentDom);
2937
- newInstances.push(inst);
3530
+ const reclaimed = hooks?.takeLeaving(key);
3531
+ if (reclaimed !== void 0) {
3532
+ patchExistingInstance(ctx, reclaimed, newNode);
3533
+ reconcileItemChildren(ctx, reclaimed, newNode, mountFn);
3534
+ hooks?.onEnter(reclaimed);
3535
+ newInstances.push(reclaimed);
3536
+ } else {
3537
+ const inst = mountFn(newNode, parentDom);
3538
+ hooks?.onEnter(inst);
3539
+ newInstances.push(inst);
3540
+ }
2938
3541
  }
2939
3542
  }
2940
3543
  const removed = [];
2941
3544
  for (const inst of oldInstances) {
2942
3545
  const key = inst.graphNode.key ?? inst.graphNode.id;
2943
- if (!usedKeys.has(key)) {
2944
- removed.push(inst);
2945
- }
2946
- }
2947
- for (const inst of removed) {
3546
+ if (usedKeys.has(key)) continue;
3547
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
2948
3548
  const parent = ctx.dom.parentNode(inst.domNode);
2949
- if (parent !== null) {
2950
- ctx.dom.removeChild(parent, inst.domNode);
2951
- }
3549
+ if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
2952
3550
  inst.dispose();
3551
+ removed.push(inst);
2953
3552
  }
2954
3553
  reorderDom(ctx, parentDom, newInstances);
2955
3554
  return { instances: newInstances, removed };
2956
3555
  }
2957
- function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
3556
+ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn, hooks) {
2958
3557
  const oldByKey = /* @__PURE__ */ new Map();
2959
3558
  for (const inst of oldInstances) {
2960
3559
  oldByKey.set(inst.graphNode.key ?? inst.graphNode.id, inst);
@@ -2981,21 +3580,34 @@ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
2981
3580
  }
2982
3581
  newInstances.push(existing);
2983
3582
  } else {
2984
- const freshNode = entry.build();
2985
- built.push(freshNode);
2986
- const inst = mountFn(freshNode, parentDom);
2987
- newInstances.push(inst);
3583
+ const reclaimed = hooks?.takeLeaving(entry.key);
3584
+ if (reclaimed !== void 0) {
3585
+ const freshNode = entry.build();
3586
+ built.push(freshNode);
3587
+ patchExistingInstance(ctx, reclaimed, freshNode);
3588
+ reconcileItemChildren(ctx, reclaimed, freshNode, mountFn);
3589
+ reclaimed.graphNode.setProp("_sig", freshNode.getProp("_sig"));
3590
+ reclaimed.graphNode.setProp("_item", entry.item);
3591
+ hooks?.onEnter(reclaimed);
3592
+ newInstances.push(reclaimed);
3593
+ } else {
3594
+ const freshNode = entry.build();
3595
+ built.push(freshNode);
3596
+ const inst = mountFn(freshNode, parentDom);
3597
+ hooks?.onEnter(inst);
3598
+ newInstances.push(inst);
3599
+ }
2988
3600
  }
2989
3601
  }
2990
3602
  const removed = [];
2991
3603
  for (const inst of oldInstances) {
2992
3604
  const key = inst.graphNode.key ?? inst.graphNode.id;
2993
- if (!usedKeys.has(key)) removed.push(inst);
2994
- }
2995
- for (const inst of removed) {
3605
+ if (usedKeys.has(key)) continue;
3606
+ if (hooks !== void 0 && hooks.beginLeave(inst)) continue;
2996
3607
  const parent = ctx.dom.parentNode(inst.domNode);
2997
3608
  if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
2998
3609
  inst.dispose();
3610
+ removed.push(inst);
2999
3611
  }
3000
3612
  reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
3001
3613
  return { instances: newInstances, removed, built };
@@ -3121,6 +3733,365 @@ function patchExistingInstance(ctx, instance, newNode) {
3121
3733
  }
3122
3734
  }
3123
3735
 
3736
+ // ../renderer/src/transition.ts
3737
+ function getResolvedTransition(graph, nodeId) {
3738
+ const fn = graph.getHandler(`__transition__${nodeId}`);
3739
+ return fn === void 0 ? void 0 : fn();
3740
+ }
3741
+ function host() {
3742
+ return globalThis;
3743
+ }
3744
+ function nextFrame(cb) {
3745
+ const h = host();
3746
+ const raf = h.requestAnimationFrame;
3747
+ if (typeof raf === "function") {
3748
+ raf(() => raf(cb));
3749
+ } else {
3750
+ h.setTimeout(cb, 0);
3751
+ }
3752
+ }
3753
+ function splitClass(value) {
3754
+ if (value === null) return [];
3755
+ const out = [];
3756
+ for (const t of value.split(/\s+/)) if (t.length > 0) out.push(t);
3757
+ return out;
3758
+ }
3759
+ function addClasses(dom, el, classes2) {
3760
+ if (classes2.length === 0) return;
3761
+ const current = splitClass(dom.getAttribute(el, "class"));
3762
+ let changed = false;
3763
+ for (const c of classes2) {
3764
+ if (!current.includes(c)) {
3765
+ current.push(c);
3766
+ changed = true;
3767
+ }
3768
+ }
3769
+ if (changed) dom.setAttribute(el, "class", current.join(" "));
3770
+ }
3771
+ function removeClasses(dom, el, classes2) {
3772
+ if (classes2.length === 0) return;
3773
+ const current = splitClass(dom.getAttribute(el, "class"));
3774
+ const next = current.filter((c) => !classes2.includes(c));
3775
+ if (next.length !== current.length) {
3776
+ if (next.length === 0) dom.removeAttribute(el, "class");
3777
+ else dom.setAttribute(el, "class", next.join(" "));
3778
+ }
3779
+ }
3780
+ function startRun(dom, el, active, from, to, duration, onDone) {
3781
+ const h = host();
3782
+ let finished = false;
3783
+ let timer = null;
3784
+ const onEvent = (e) => {
3785
+ if (e.target !== el) return;
3786
+ finish();
3787
+ };
3788
+ const detach = () => {
3789
+ dom.removeEventListener(el, "transitionend", onEvent);
3790
+ dom.removeEventListener(el, "animationend", onEvent);
3791
+ if (timer !== null) {
3792
+ h.clearTimeout(timer);
3793
+ timer = null;
3794
+ }
3795
+ };
3796
+ const finish = () => {
3797
+ if (finished) return;
3798
+ finished = true;
3799
+ detach();
3800
+ removeClasses(dom, el, active);
3801
+ removeClasses(dom, el, to);
3802
+ removeClasses(dom, el, from);
3803
+ onDone();
3804
+ };
3805
+ addClasses(dom, el, from);
3806
+ addClasses(dom, el, active);
3807
+ dom.addEventListener(el, "transitionend", onEvent);
3808
+ dom.addEventListener(el, "animationend", onEvent);
3809
+ timer = h.setTimeout(finish, duration);
3810
+ nextFrame(() => {
3811
+ if (finished) return;
3812
+ removeClasses(dom, el, from);
3813
+ addClasses(dom, el, to);
3814
+ });
3815
+ return {
3816
+ cancel: () => {
3817
+ if (finished) return;
3818
+ finished = true;
3819
+ detach();
3820
+ removeClasses(dom, el, active);
3821
+ removeClasses(dom, el, to);
3822
+ removeClasses(dom, el, from);
3823
+ }
3824
+ };
3825
+ }
3826
+ function runElementTransition(dom, el, rt, phase, onDone) {
3827
+ if (dom.body() === null || !dom.isElement(el)) {
3828
+ onDone();
3829
+ return { cancel: () => {
3830
+ } };
3831
+ }
3832
+ const active = phase === "enter" ? rt.enterActive : rt.leaveActive;
3833
+ const from = phase === "enter" ? rt.enterFrom : rt.leaveFrom;
3834
+ const to = phase === "enter" ? rt.enterTo : rt.leaveTo;
3835
+ return startRun(dom, el, active, from, to, rt.duration, onDone);
3836
+ }
3837
+ var TransitionController = class {
3838
+ constructor(dom, graph, finalize) {
3839
+ this.dom = dom;
3840
+ this.graph = graph;
3841
+ this.finalize = finalize;
3842
+ }
3843
+ leaving = /* @__PURE__ */ new Map();
3844
+ /** True only in a real DOM environment (browser). */
3845
+ get browser() {
3846
+ return this.dom.body() !== null;
3847
+ }
3848
+ keyOf(inst) {
3849
+ return inst.graphNode.key ?? inst.graphNode.id;
3850
+ }
3851
+ resolved(node) {
3852
+ return getResolvedTransition(this.graph, node.id);
3853
+ }
3854
+ /** Run the enter animation for `inst` if it carries a transition (browser only). */
3855
+ enter(inst) {
3856
+ if (!this.browser) return;
3857
+ const rt = this.resolved(inst.graphNode);
3858
+ if (rt === void 0) return;
3859
+ const el = inst.domNode;
3860
+ if (!this.dom.isElement(el)) return;
3861
+ startRun(this.dom, el, rt.enterActive, rt.enterFrom, rt.enterTo, rt.duration, () => {
3862
+ });
3863
+ }
3864
+ /**
3865
+ * Play `appear` for any initial child that opted into it (fresh browser mount
3866
+ * only — hydration must never animate appear, §22, and this is called only on
3867
+ * the mount path).
3868
+ */
3869
+ appear(children) {
3870
+ if (!this.browser) return;
3871
+ for (const child of children) {
3872
+ const rt = this.resolved(child.graphNode);
3873
+ if (rt !== void 0 && rt.appear) this.enter(child);
3874
+ }
3875
+ }
3876
+ hooks() {
3877
+ return {
3878
+ takeLeaving: (key) => {
3879
+ const entry = this.leaving.get(key);
3880
+ if (entry === void 0) return void 0;
3881
+ entry.run.cancel();
3882
+ this.leaving.delete(key);
3883
+ return entry.inst;
3884
+ },
3885
+ beginLeave: (inst) => {
3886
+ if (!this.browser) return false;
3887
+ const rt = this.resolved(inst.graphNode);
3888
+ if (rt === void 0) return false;
3889
+ const el = inst.domNode;
3890
+ if (!this.dom.isElement(el)) return false;
3891
+ const key = this.keyOf(inst);
3892
+ const prior = this.leaving.get(key);
3893
+ if (prior !== void 0) prior.run.cancel();
3894
+ const run = startRun(
3895
+ this.dom,
3896
+ el,
3897
+ rt.leaveActive,
3898
+ rt.leaveFrom,
3899
+ rt.leaveTo,
3900
+ rt.duration,
3901
+ () => {
3902
+ const current = this.leaving.get(key);
3903
+ if (current !== void 0 && current.run === run) {
3904
+ this.leaving.delete(key);
3905
+ this.finalize(inst);
3906
+ }
3907
+ }
3908
+ );
3909
+ this.leaving.set(key, { inst, run });
3910
+ return true;
3911
+ },
3912
+ onEnter: (inst) => this.enter(inst)
3913
+ };
3914
+ }
3915
+ };
3916
+
3917
+ // ../renderer/src/head.ts
3918
+ var HEAD_MARKER = "data-streetui-head";
3919
+ var HEAD_KEY = "data-streetui-head-key";
3920
+ function isSignalLike(v) {
3921
+ return v !== null && typeof v === "object" && typeof v["subscribe"] === "function" && typeof v["peek"] === "function";
3922
+ }
3923
+ function readValue(v) {
3924
+ if (isSignalLike(v)) return String(v.peek() ?? "");
3925
+ return String(v ?? "");
3926
+ }
3927
+ function resolveEntry(entry) {
3928
+ const attrs = {};
3929
+ for (const key of Object.keys(entry.attrs)) {
3930
+ attrs[key] = readValue(entry.attrs[key]);
3931
+ }
3932
+ const resolved = { tag: entry.tag, attrs };
3933
+ if (entry.tag === "title") {
3934
+ return { ...resolved, text: readValue(entry.text) };
3935
+ }
3936
+ return resolved;
3937
+ }
3938
+ var HeadManager = class {
3939
+ _dom;
3940
+ _head;
3941
+ _contributions = /* @__PURE__ */ new Map();
3942
+ _applied = /* @__PURE__ */ new Map();
3943
+ _order = 0;
3944
+ _adopted = false;
3945
+ constructor(dom, head) {
3946
+ this._dom = dom;
3947
+ this._head = head;
3948
+ }
3949
+ /** Register (or replace) a node's contribution and re-apply the merged result. */
3950
+ register(nodeId, entries) {
3951
+ this._contributions.set(nodeId, { order: this._order++, entries });
3952
+ this.apply();
3953
+ }
3954
+ /** Withdraw a node's contribution (unmount / route change) and re-apply. */
3955
+ unregister(nodeId) {
3956
+ if (this._contributions.delete(nodeId)) this.apply();
3957
+ }
3958
+ /** Recompute the merged head and patch `document.head` to match. */
3959
+ apply() {
3960
+ if (!this._adopted) {
3961
+ this._adoptServerTags();
3962
+ this._adopted = true;
3963
+ }
3964
+ const ordered = [...this._contributions.values()].sort((a, b) => a.order - b.order);
3965
+ const merged = /* @__PURE__ */ new Map();
3966
+ for (const contribution of ordered) {
3967
+ for (const entry of contribution.entries) {
3968
+ merged.set(entry.dedupKey, resolveEntry(entry));
3969
+ }
3970
+ }
3971
+ for (const [key, desired] of merged) {
3972
+ const existing = this._applied.get(key);
3973
+ if (existing !== void 0 && existing.tag === desired.tag) {
3974
+ this._reconcileAttrs(existing, desired);
3975
+ } else {
3976
+ if (existing !== void 0) {
3977
+ this._dom.removeChild(this._head, existing.el);
3978
+ this._applied.delete(key);
3979
+ }
3980
+ const el = this._createTag(key, desired);
3981
+ this._dom.appendChild(this._head, el);
3982
+ this._applied.set(key, { el, attrKeys: new Set(Object.keys(desired.attrs)), tag: desired.tag });
3983
+ }
3984
+ }
3985
+ for (const [key, record] of [...this._applied]) {
3986
+ if (!merged.has(key)) {
3987
+ this._dom.removeChild(this._head, record.el);
3988
+ this._applied.delete(key);
3989
+ }
3990
+ }
3991
+ }
3992
+ _createTag(key, desired) {
3993
+ const el = this._dom.createElement(desired.tag);
3994
+ this._dom.setAttribute(el, HEAD_MARKER, "");
3995
+ this._dom.setAttribute(el, HEAD_KEY, key);
3996
+ for (const attr of Object.keys(desired.attrs)) {
3997
+ this._dom.setAttribute(el, attr, desired.attrs[attr]);
3998
+ }
3999
+ if (desired.tag === "title") this._dom.setTextContent(el, desired.text ?? "");
4000
+ return el;
4001
+ }
4002
+ _reconcileAttrs(record, desired) {
4003
+ const nextKeys = new Set(Object.keys(desired.attrs));
4004
+ for (const attr of record.attrKeys) {
4005
+ if (!nextKeys.has(attr)) this._dom.removeAttribute(record.el, attr);
4006
+ }
4007
+ for (const attr of nextKeys) {
4008
+ this._dom.setAttribute(record.el, attr, desired.attrs[attr]);
4009
+ }
4010
+ if (desired.tag === "title") this._dom.setTextContent(record.el, desired.text ?? "");
4011
+ record.attrKeys = nextKeys;
4012
+ }
4013
+ /**
4014
+ * Seed `_applied` from server-emitted `[data-streetui-head-key]` tags already
4015
+ * in `document.head`. The subsequent diff reuses these elements when the
4016
+ * client desires the same key (no duplicate), rewrites them if the value
4017
+ * changed, or removes them if the client graph no longer wants them.
4018
+ */
4019
+ _adoptServerTags() {
4020
+ for (const child of this._dom.childNodes(this._head)) {
4021
+ if (!this._dom.isElement(child)) continue;
4022
+ const el = child;
4023
+ const key = this._dom.getAttribute(el, HEAD_KEY);
4024
+ if (key === null) continue;
4025
+ this._applied.set(key, {
4026
+ el,
4027
+ attrKeys: new Set(this._attrNames(el)),
4028
+ tag: this._dom.tagName(el)
4029
+ });
4030
+ }
4031
+ }
4032
+ /** The framework-managed attribute names currently on a server tag. */
4033
+ _attrNames(el) {
4034
+ const names = [];
4035
+ if (this._dom.getAttribute(el, HEAD_MARKER) !== null) names.push(HEAD_MARKER);
4036
+ if (this._dom.getAttribute(el, HEAD_KEY) !== null) names.push(HEAD_KEY);
4037
+ return names;
4038
+ }
4039
+ };
4040
+ function getHeadManager(ctx) {
4041
+ const head = ctx.dom.head();
4042
+ if (head === null) return null;
4043
+ const mutable = ctx;
4044
+ if (mutable.head === void 0) mutable.head = new HeadManager(ctx.dom, head);
4045
+ return mutable.head;
4046
+ }
4047
+ function wireHeadBehavior(ctx, graphNode, instance) {
4048
+ const manager = getHeadManager(ctx);
4049
+ if (manager === null) return;
4050
+ const descFn = ctx.graph.getHandler(`__head__${graphNode.id}`);
4051
+ if (descFn === void 0) return;
4052
+ const contribution = descFn();
4053
+ const nodeId = graphNode.id;
4054
+ manager.register(nodeId, contribution.entries);
4055
+ for (const entry of contribution.entries) {
4056
+ for (const attrKey of Object.keys(entry.attrs)) {
4057
+ const v = entry.attrs[attrKey];
4058
+ if (isSignalLike(v)) {
4059
+ instance.trackCleanup(v.subscribe(() => manager.apply()));
4060
+ }
4061
+ }
4062
+ if (isSignalLike(entry.text)) {
4063
+ instance.trackCleanup(entry.text.subscribe(() => manager.apply()));
4064
+ }
4065
+ }
4066
+ instance.trackCleanup(() => manager.unregister(nodeId));
4067
+ }
4068
+ function renderHead(compiled) {
4069
+ const graph = compiled.graph;
4070
+ const merged = /* @__PURE__ */ new Map();
4071
+ graph.walk((node) => {
4072
+ if (node.type !== "head") return;
4073
+ const descFn = graph.getHandler(`__head__${node.id}`);
4074
+ if (descFn === void 0) return;
4075
+ for (const entry of descFn().entries) {
4076
+ merged.set(entry.dedupKey, resolveEntry(entry));
4077
+ }
4078
+ });
4079
+ if (merged.size === 0) return "";
4080
+ const dom = new ServerDOMAdapter();
4081
+ let out = "";
4082
+ for (const [key, desired] of merged) {
4083
+ const el = dom.createElement(desired.tag);
4084
+ dom.setAttribute(el, HEAD_MARKER, "");
4085
+ dom.setAttribute(el, HEAD_KEY, key);
4086
+ for (const attr of Object.keys(desired.attrs)) {
4087
+ dom.setAttribute(el, attr, desired.attrs[attr]);
4088
+ }
4089
+ if (desired.tag === "title") dom.setTextContent(el, desired.text ?? "");
4090
+ out += dom.serializeOuter(el);
4091
+ }
4092
+ return out;
4093
+ }
4094
+
3124
4095
  // ../renderer/src/mount.ts
3125
4096
  var SKIP_PROP_KEYS = /* @__PURE__ */ new Set([
3126
4097
  "text",
@@ -3243,6 +4214,9 @@ function mountNode(ctx, graphNode, parentDom) {
3243
4214
  const instance2 = new NodeInstance(graphNode, el2);
3244
4215
  ctx.instances.set(graphNode.id, instance2);
3245
4216
  wireEvents(dom, graph, graphNode, el2, instance2);
4217
+ if (graphNode.stateRefs.length !== 0) {
4218
+ wireSignalBindings(ctx, graphNode, instance2, linkUpdate(dom, el2));
4219
+ }
3246
4220
  dom.appendChild(parentDom, el2);
3247
4221
  return instance2;
3248
4222
  }
@@ -3273,7 +4247,7 @@ function mountNode(ctx, graphNode, parentDom) {
3273
4247
  instance2.addChild(childInstance);
3274
4248
  }
3275
4249
  dom.appendChild(parentDom, el2);
3276
- wireReactiveList(ctx, graphNode, instance2, el2);
4250
+ wireReactiveList(ctx, graphNode, instance2, el2, true);
3277
4251
  return instance2;
3278
4252
  }
3279
4253
  if (graphNode.type === "portal") {
@@ -3298,6 +4272,16 @@ function mountNode(ctx, graphNode, parentDom) {
3298
4272
  wireOverlayBehavior(ctx, graphNode, instance2, target);
3299
4273
  return instance2;
3300
4274
  }
4275
+ if (graphNode.type === "head") {
4276
+ const anchor = dom.createElement(resolveTag("head"));
4277
+ dom.setAttribute(anchor, "data-streetui-head-anchor", "");
4278
+ applyNodeProps(ctx, graphNode, anchor);
4279
+ const instance2 = new NodeInstance(graphNode, anchor);
4280
+ ctx.instances.set(graphNode.id, instance2);
4281
+ dom.appendChild(parentDom, anchor);
4282
+ wireHeadBehavior(ctx, graphNode, instance2);
4283
+ return instance2;
4284
+ }
3301
4285
  const tag = resolveTag(graphNode.type);
3302
4286
  const el = dom.createElement(tag);
3303
4287
  applyNodeProps(ctx, graphNode, el);
@@ -3362,6 +4346,17 @@ function buttonUpdate(dom, el) {
3362
4346
  }
3363
4347
  };
3364
4348
  }
4349
+ function linkUpdate(dom, el) {
4350
+ return (propKey, value) => {
4351
+ if (propKey === "label") {
4352
+ dom.setTextContent(el, String(value ?? ""));
4353
+ } else if (propKey === "href") {
4354
+ dom.setAttribute(el, "href", String(value ?? ""));
4355
+ } else {
4356
+ applyProp(dom, el, propKey, value);
4357
+ }
4358
+ };
4359
+ }
3365
4360
  function applyNodeProps(ctx, graphNode, el) {
3366
4361
  const props = graphNode.props;
3367
4362
  for (const key in props) {
@@ -3382,32 +4377,42 @@ function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
3382
4377
  instance.trackCleanup(unsub);
3383
4378
  }
3384
4379
  }
3385
- function wireReactiveList(ctx, graphNode, instance, el) {
4380
+ function wireReactiveList(ctx, graphNode, instance, el, runAppear = false) {
3386
4381
  const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
3387
4382
  const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
3388
4383
  if (plan === void 0 && build === void 0) return;
4384
+ const controller = new TransitionController(ctx.dom, ctx.graph, (leaving) => {
4385
+ const parent = ctx.dom.parentNode(leaving.domNode);
4386
+ if (parent !== null) ctx.dom.removeChild(parent, leaving.domNode);
4387
+ leaving.dispose();
4388
+ forgetInstance(ctx, leaving);
4389
+ ctx.graph.detachNode(leaving.graphNode);
4390
+ });
4391
+ const hooks = controller.hooks();
4392
+ if (runAppear) controller.appear(instance.children);
3389
4393
  for (const stateRef of graphNode.stateRefs) {
3390
4394
  if (stateRef.propKey !== "items") continue;
3391
4395
  const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
3392
4396
  if (sig === void 0 || typeof sig.subscribe !== "function") continue;
3393
4397
  const unsub = sig.subscribe((value) => {
3394
4398
  if (plan !== void 0) {
3395
- reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value));
4399
+ reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value), hooks);
3396
4400
  } else {
3397
- reconcileReactiveList(ctx, graphNode, instance, el, build(value));
4401
+ reconcileReactiveList(ctx, graphNode, instance, el, build(value), hooks);
3398
4402
  }
3399
4403
  });
3400
4404
  instance.trackCleanup(unsub);
3401
4405
  }
3402
4406
  }
3403
- function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
4407
+ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan, hooks) {
3404
4408
  const oldInstances = [...listInstance.children];
3405
4409
  const result = reconcileChildrenByPlan(
3406
4410
  ctx,
3407
4411
  listEl,
3408
4412
  oldInstances,
3409
4413
  plan,
3410
- (node, parent) => mountNode(ctx, node, parent)
4414
+ (node, parent) => mountNode(ctx, node, parent),
4415
+ hooks
3411
4416
  );
3412
4417
  listInstance.children.length = 0;
3413
4418
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -3422,14 +4427,15 @@ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan)
3422
4427
  for (const child of [...listNode.children]) listNode.removeChild(child);
3423
4428
  for (const inst of result.instances) listNode.appendChild(inst.graphNode);
3424
4429
  }
3425
- function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
4430
+ function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes, hooks) {
3426
4431
  const oldInstances = [...listInstance.children];
3427
4432
  const result = reconcileChildren(
3428
4433
  ctx,
3429
4434
  listEl,
3430
4435
  oldInstances,
3431
4436
  newNodes,
3432
- (node, parent) => mountNode(ctx, node, parent)
4437
+ (node, parent) => mountNode(ctx, node, parent),
4438
+ hooks
3433
4439
  );
3434
4440
  listInstance.children.length = 0;
3435
4441
  for (const inst of result.instances) listInstance.children.push(inst);
@@ -3465,11 +4471,14 @@ function wireOverlayBehavior(ctx, graphNode, instance, target) {
3465
4471
  const onOpenChange = (isOpen) => {
3466
4472
  if (isOpen) {
3467
4473
  if (desc.restoreFocus) saved = saveFocus(dom);
3468
- if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
3469
4474
  if (desc.modal) {
3470
4475
  active.push(trapFocus(dom, target));
3471
4476
  active.push(containFocus(dom, target));
3472
4477
  }
4478
+ if (desc.menu) {
4479
+ active.push(rovingMenu(dom, target));
4480
+ }
4481
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
3473
4482
  if (desc.closeOnEscape && desc.onClose !== void 0) {
3474
4483
  active.push(onEscape(dom, target, desc.onClose));
3475
4484
  }
@@ -3582,7 +4591,12 @@ function hydrateNode(ctx, graphNode, domNode, path) {
3582
4591
  case "link": {
3583
4592
  const instance = new NodeInstance(graphNode, domNode);
3584
4593
  ctx.instances.set(graphNode.id, instance);
3585
- if (graphNode.type === "link") wireEvents(dom, graph, graphNode, domNode, instance);
4594
+ if (graphNode.type === "link") {
4595
+ wireEvents(dom, graph, graphNode, domNode, instance);
4596
+ if (graphNode.stateRefs.length !== 0) {
4597
+ wireSignalBindings(ctx, graphNode, instance, linkUpdate(dom, domNode));
4598
+ }
4599
+ }
3586
4600
  return instance;
3587
4601
  }
3588
4602
  case "reactive-list":
@@ -3612,6 +4626,12 @@ function hydrateNode(ctx, graphNode, domNode, path) {
3612
4626
  wireOverlayBehavior(ctx, graphNode, instance, target);
3613
4627
  return instance;
3614
4628
  }
4629
+ case "head": {
4630
+ const instance = new NodeInstance(graphNode, domNode);
4631
+ ctx.instances.set(graphNode.id, instance);
4632
+ wireHeadBehavior(ctx, graphNode, instance);
4633
+ return instance;
4634
+ }
3615
4635
  default: {
3616
4636
  const instance = new NodeInstance(graphNode, domNode);
3617
4637
  ctx.instances.set(graphNode.id, instance);
@@ -3858,7 +4878,8 @@ function analyzeGraph(graph) {
3858
4878
  const isConditional = node.type === "conditional";
3859
4879
  const isPortal = node.type === "portal";
3860
4880
  const isComponent = node.type === "component";
3861
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent;
4881
+ const isHead = node.type === "head";
4882
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent && !isHead;
3862
4883
  const isStaticSubtree = isStatic && allChildrenStatic;
3863
4884
  nodes.set(node.id, {
3864
4885
  isStatic,
@@ -4204,16 +5225,38 @@ function mountRouter(router, options) {
4204
5225
  } else {
4205
5226
  outlet = container;
4206
5227
  }
5228
+ const routeTransition = options.transition !== void 0 ? resolveTransition(options.transition) : void 0;
5229
+ const txDom = routeTransition !== void 0 ? new BrowserDOMAdapter() : void 0;
4207
5230
  let active = null;
5231
+ const pendingLeaves = /* @__PURE__ */ new Map();
4208
5232
  let firstRender = hydrateMode;
5233
+ const teardownRoute = (route) => {
5234
+ route.registry.run();
5235
+ route.mounted.unmount();
5236
+ if (route.host !== null && txDom !== void 0) {
5237
+ const parent = txDom.parentNode(route.host);
5238
+ if (parent !== null) txDom.removeChild(parent, route.host);
5239
+ }
5240
+ };
4209
5241
  const disposeActive = () => {
4210
5242
  if (active === null) return;
4211
- active.registry.run();
4212
- active.mounted.unmount();
5243
+ teardownRoute(active);
4213
5244
  active = null;
4214
5245
  };
4215
- const renderRoute = (match) => {
4216
- disposeActive();
5246
+ const wrapOutletChildren = (dom) => {
5247
+ const host2 = dom.createElement("div");
5248
+ dom.setAttribute(host2, "data-streetui-route", "");
5249
+ const moved = [];
5250
+ let child = dom.firstChild(outlet);
5251
+ while (child !== null) {
5252
+ moved.push(child);
5253
+ child = dom.nextSibling(child);
5254
+ }
5255
+ for (const n of moved) dom.appendChild(host2, n);
5256
+ dom.appendChild(outlet, host2);
5257
+ return host2;
5258
+ };
5259
+ const buildRoute = (match, target, hydrate) => {
4217
5260
  const registry = new CleanupRegistry();
4218
5261
  const ctx = {
4219
5262
  path: match.path,
@@ -4226,9 +5269,53 @@ function mountRouter(router, options) {
4226
5269
  routeApp.page("route", (page) => match.route.builder(page, ctx));
4227
5270
  const runtime = createRuntime({ renderer });
4228
5271
  const routeCompiled = compile(routeApp);
4229
- const mounted = firstRender ? runtime.hydrate(routeCompiled, outlet) : runtime.mount(routeCompiled, outlet);
5272
+ const mounted = hydrate ? runtime.hydrate(routeCompiled, target) : runtime.mount(routeCompiled, target);
5273
+ return { registry, mounted, host: null, enterRun: null };
5274
+ };
5275
+ const renderRoute = (match) => {
5276
+ if (routeTransition === void 0 || txDom === void 0) {
5277
+ disposeActive();
5278
+ active = buildRoute(match, outlet, firstRender);
5279
+ firstRender = false;
5280
+ return;
5281
+ }
5282
+ if (active === null) {
5283
+ if (firstRender) {
5284
+ active = buildRoute(match, outlet, true);
5285
+ active.host = wrapOutletChildren(txDom);
5286
+ } else {
5287
+ const host2 = txDom.createElement("div");
5288
+ txDom.setAttribute(host2, "data-streetui-route", "");
5289
+ txDom.appendChild(outlet, host2);
5290
+ active = buildRoute(match, host2, false);
5291
+ active.host = host2;
5292
+ }
5293
+ firstRender = false;
5294
+ return;
5295
+ }
4230
5296
  firstRender = false;
4231
- active = { registry, mounted };
5297
+ const leaving = active;
5298
+ leaving.enterRun?.cancel();
5299
+ leaving.enterRun = null;
5300
+ const leaveHost = leaving.host;
5301
+ const enterHost = txDom.createElement("div");
5302
+ txDom.setAttribute(enterHost, "data-streetui-route", "");
5303
+ txDom.appendChild(outlet, enterHost);
5304
+ const next = buildRoute(match, enterHost, false);
5305
+ next.host = enterHost;
5306
+ next.enterRun = runElementTransition(txDom, enterHost, routeTransition, "enter", () => {
5307
+ next.enterRun = null;
5308
+ });
5309
+ active = next;
5310
+ if (leaveHost !== null) {
5311
+ const run = runElementTransition(txDom, leaveHost, routeTransition, "leave", () => {
5312
+ pendingLeaves.delete(leaving);
5313
+ teardownRoute(leaving);
5314
+ });
5315
+ pendingLeaves.set(leaving, run);
5316
+ } else {
5317
+ teardownRoute(leaving);
5318
+ }
4232
5319
  };
4233
5320
  renderRoute(router.currentRoute.peek());
4234
5321
  const stopRouteSub = router.currentRoute.subscribe((match) => renderRoute(match));
@@ -4256,6 +5343,11 @@ function mountRouter(router, options) {
4256
5343
  unmount() {
4257
5344
  if (interceptLinks) container.removeEventListener("click", onClick);
4258
5345
  stopRouteSub();
5346
+ for (const [route, run] of pendingLeaves) {
5347
+ run.cancel();
5348
+ teardownRoute(route);
5349
+ }
5350
+ pendingLeaves.clear();
4259
5351
  disposeActive();
4260
5352
  shellMounted?.unmount();
4261
5353
  router.destroy();
@@ -4580,6 +5672,40 @@ function inspectComponents(graph) {
4580
5672
  });
4581
5673
  return out;
4582
5674
  }
5675
+ function inspectInteractions(graph) {
5676
+ const overlays = [];
5677
+ const transitions = [];
5678
+ graph.walk((node, depth) => {
5679
+ const overlayFn = graph.getHandler(`__overlay__${node.id}`);
5680
+ if (overlayFn !== void 0) {
5681
+ const d = overlayFn();
5682
+ overlays.push({
5683
+ id: node.id,
5684
+ key: node.key,
5685
+ open: d.open.peek(),
5686
+ modal: d.modal,
5687
+ takesFocus: d.takesFocus,
5688
+ menu: d.menu,
5689
+ closeOnEscape: d.closeOnEscape,
5690
+ restoreFocus: d.restoreFocus,
5691
+ depth
5692
+ });
5693
+ }
5694
+ const transitionFn = graph.getHandler(`__transition__${node.id}`);
5695
+ if (transitionFn !== void 0) {
5696
+ const t = transitionFn();
5697
+ transitions.push({
5698
+ id: node.id,
5699
+ key: node.key,
5700
+ nodeType: node.type,
5701
+ duration: t.duration,
5702
+ appear: t.appear,
5703
+ depth
5704
+ });
5705
+ }
5706
+ });
5707
+ return { overlays, transitions };
5708
+ }
4583
5709
 
4584
5710
  // ../devtools/src/application.ts
4585
5711
  function collectPerf(node, distinctSignals, acc) {
@@ -4815,11 +5941,22 @@ function capture(compiled, sources, options) {
4815
5941
  snapshot: app.perf,
4816
5942
  diagnostics: diagnosePerformance(compiled, options.perfThresholds)
4817
5943
  };
5944
+ const components = inspectComponents(compiled.graph);
5945
+ const interactions = inspectInteractions(compiled.graph);
5946
+ const diagnostics = {
5947
+ errors: app.diagnostics.errors,
5948
+ warnings: app.diagnostics.warnings,
5949
+ messages: app.diagnostics.messages
5950
+ };
4818
5951
  const snapshot = {
4819
5952
  application,
4820
5953
  graph: app.graph,
4821
5954
  signals,
4822
5955
  performance,
5956
+ components,
5957
+ overlays: interactions.overlays,
5958
+ transitions: interactions.transitions,
5959
+ diagnostics,
4823
5960
  ...sources.router !== void 0 ? { router: inspectRouter(sources.router) } : {},
4824
5961
  ...sources.resources !== void 0 ? { resources: mapInspect(sources.resources, (r) => inspectResource(r)) } : {},
4825
5962
  ...sources.forms !== void 0 ? { forms: mapInspect(sources.forms, (f) => inspectForm(f)) } : {},
@@ -4860,6 +5997,25 @@ function formatSnapshot(s) {
4860
5997
  for (const [label, sig] of Object.entries(s.signals.live)) {
4861
5998
  lines.push(` ${label} [${sig.kind}] = ${format(sig.value)} \xB7 observers ${sig.observerCount ?? "?"}`);
4862
5999
  }
6000
+ if (s.components.length > 0) {
6001
+ lines.push(`Components: ${s.components.length}`);
6002
+ for (const c of s.components) {
6003
+ lines.push(` ${" ".repeat(c.depth)}${c.name}${c.key !== void 0 ? ` (#${c.key})` : ""} \xB7 children ${c.childCount}`);
6004
+ }
6005
+ }
6006
+ if (s.overlays.length > 0) {
6007
+ lines.push(`Overlays: ${s.overlays.length}`);
6008
+ for (const o of s.overlays) {
6009
+ const kind = o.modal ? "modal" : o.menu ? "menu" : o.takesFocus ? "focusable" : "non-modal";
6010
+ lines.push(` ${o.key ?? o.id} [${kind}] ${o.open ? "open" : "closed"}`);
6011
+ }
6012
+ }
6013
+ if (s.transitions.length > 0) {
6014
+ lines.push(`Transitions: ${s.transitions.length}`);
6015
+ for (const t of s.transitions) {
6016
+ lines.push(` ${t.key ?? t.id} on <${t.nodeType}> \xB7 ${t.duration}ms${t.appear ? " \xB7 appear" : ""}`);
6017
+ }
6018
+ }
4863
6019
  if (s.router !== void 0) {
4864
6020
  lines.push(`Router: ${s.router.path} (${s.router.pattern})${s.router.isFallback ? " [fallback]" : ""}`);
4865
6021
  }
@@ -4893,6 +6049,12 @@ function formatSnapshot(s) {
4893
6049
  for (const d of s.performance.diagnostics) {
4894
6050
  lines.push(` ${d.code}: ${d.message}`);
4895
6051
  }
6052
+ lines.push(
6053
+ `Diagnostics: ${s.diagnostics.errors} error(s), ${s.diagnostics.warnings} warning(s)`
6054
+ );
6055
+ for (const m of s.diagnostics.messages) {
6056
+ lines.push(` ${m}`);
6057
+ }
4896
6058
  return lines.join("\n");
4897
6059
  }
4898
6060
  function format(value) {
@@ -4908,6 +6070,176 @@ function format(value) {
4908
6070
  return String(value);
4909
6071
  }
4910
6072
 
6073
+ // ../devtools/src/view.ts
6074
+ function escapeHtml(value) {
6075
+ const s = typeof value === "string" ? value : stringifyValue(value);
6076
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
6077
+ }
6078
+ function stringifyValue(value) {
6079
+ if (value === null || value === void 0) return String(value);
6080
+ if (typeof value === "object") {
6081
+ try {
6082
+ return JSON.stringify(value);
6083
+ } catch {
6084
+ return "[object]";
6085
+ }
6086
+ }
6087
+ return String(value);
6088
+ }
6089
+ function section(title, count, body) {
6090
+ const badge = count === void 0 ? "" : ` <span class="st-count">${count}</span>`;
6091
+ return `<section class="st-panel"><h2>${escapeHtml(title)}${badge}</h2>${body}</section>`;
6092
+ }
6093
+ function ul(items) {
6094
+ if (items.length === 0) return '<p class="st-empty">(none)</p>';
6095
+ return `<ul>${items.map((i) => `<li>${i}</li>`).join("")}</ul>`;
6096
+ }
6097
+ function renderDevToolsHTML(s) {
6098
+ const app = s.application;
6099
+ const parts = [];
6100
+ parts.push(
6101
+ section(
6102
+ "Application",
6103
+ void 0,
6104
+ `<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>`
6105
+ )
6106
+ );
6107
+ parts.push(
6108
+ section(
6109
+ "Components",
6110
+ s.components.length,
6111
+ ul(
6112
+ s.components.map(
6113
+ (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>`
6114
+ )
6115
+ )
6116
+ )
6117
+ );
6118
+ parts.push(section("Graph", void 0, `<pre class="st-tree">${escapeHtml(renderNodeTree(s.graph))}</pre>`));
6119
+ parts.push(
6120
+ section(
6121
+ "Signals",
6122
+ s.signals.boundSignalIds.length,
6123
+ `<p class="st-dim">${s.signals.boundSignalIds.length} bound in graph \xB7 effects shown as observer counts (no global effect registry)</p>` + ul(
6124
+ Object.entries(s.signals.live).map(
6125
+ ([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>`
6126
+ )
6127
+ )
6128
+ )
6129
+ );
6130
+ if (s.router !== void 0) {
6131
+ parts.push(
6132
+ section(
6133
+ "Router",
6134
+ void 0,
6135
+ `<p><code>${escapeHtml(s.router.path)}</code> <span class="st-dim">(${escapeHtml(s.router.pattern)})${s.router.isFallback ? " \xB7 fallback" : ""}</span></p>`
6136
+ )
6137
+ );
6138
+ }
6139
+ if (s.resources !== void 0) {
6140
+ parts.push(
6141
+ section(
6142
+ "Resources",
6143
+ Object.keys(s.resources).length,
6144
+ ul(
6145
+ Object.entries(s.resources).map(
6146
+ ([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>` : "")
6147
+ )
6148
+ )
6149
+ )
6150
+ );
6151
+ }
6152
+ parts.push(
6153
+ section(
6154
+ "Overlays",
6155
+ s.overlays.length,
6156
+ ul(
6157
+ s.overlays.map((o) => {
6158
+ const kind = o.modal ? "modal" : o.menu ? "menu" : o.takesFocus ? "focusable" : "non-modal";
6159
+ 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>`;
6160
+ })
6161
+ )
6162
+ )
6163
+ );
6164
+ parts.push(
6165
+ section(
6166
+ "Transitions",
6167
+ s.transitions.length,
6168
+ ul(
6169
+ s.transitions.map(
6170
+ (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>`
6171
+ )
6172
+ )
6173
+ )
6174
+ );
6175
+ if (s.forms !== void 0) {
6176
+ parts.push(
6177
+ section(
6178
+ "Forms",
6179
+ Object.keys(s.forms).length,
6180
+ ul(
6181
+ Object.entries(s.forms).map(
6182
+ ([label, f]) => `<code>${escapeHtml(label)}</code>: ${f.valid ? "valid" : "invalid"} \xB7 ${escapeHtml(f.status)} \xB7 ${f.fields.length} field(s)`
6183
+ )
6184
+ )
6185
+ )
6186
+ );
6187
+ }
6188
+ if (s.i18n !== void 0) {
6189
+ const missing = s.i18n.missingKeys;
6190
+ parts.push(
6191
+ section(
6192
+ "i18n",
6193
+ void 0,
6194
+ `<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>`
6195
+ )
6196
+ );
6197
+ }
6198
+ parts.push(
6199
+ section(
6200
+ "Performance",
6201
+ s.performance.diagnostics.length,
6202
+ `<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)}`))
6203
+ )
6204
+ );
6205
+ parts.push(
6206
+ section(
6207
+ "Diagnostics",
6208
+ s.diagnostics.errors + s.diagnostics.warnings,
6209
+ `<p class="st-dim">${s.diagnostics.errors} error(s) \xB7 ${s.diagnostics.warnings} warning(s)</p>` + ul(s.diagnostics.messages.map((m) => escapeHtml(m)))
6210
+ )
6211
+ );
6212
+ const title = `StreetUI DevTools \u2014 ${escapeHtml(app.identity.name)}`;
6213
+ 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>`;
6214
+ }
6215
+ function renderNodeTree(node) {
6216
+ const lines = [];
6217
+ const walk = (n) => {
6218
+ const indent = " ".repeat(n.depth);
6219
+ const key = n.key !== void 0 ? ` #${n.key}` : "";
6220
+ lines.push(`${indent}<${n.type}${key}> ${n.id}`);
6221
+ for (const c of n.children) walk(c);
6222
+ };
6223
+ walk(node);
6224
+ return lines.join("\n");
6225
+ }
6226
+ var DEVTOOLS_CSS = [
6227
+ ".st-devtools{font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;margin:0;color:#e6e6e6;background:#1e1e28}",
6228
+ ".st-header{padding:12px 16px;border-bottom:1px solid #333;background:#15151c}",
6229
+ ".st-header h1{font-size:14px;margin:0}",
6230
+ "main{padding:8px 16px}",
6231
+ ".st-panel{margin:12px 0;border:1px solid #2c2c38;border-radius:6px;overflow:hidden}",
6232
+ ".st-panel h2{font-size:12px;text-transform:uppercase;letter-spacing:.04em;margin:0;padding:6px 10px;background:#23232e}",
6233
+ ".st-panel ul{list-style:none;margin:0;padding:6px 10px}",
6234
+ ".st-panel li{padding:1px 0}",
6235
+ ".st-panel p{margin:6px 10px}",
6236
+ ".st-count{background:#3a3a4a;border-radius:10px;padding:0 7px;font-size:11px;float:right}",
6237
+ ".st-dim{color:#8a8a9a}.st-key{color:#7db4ff}.st-err{color:#ff8a8a}.st-empty{color:#6a6a7a}",
6238
+ ".st-tree{margin:6px 10px;white-space:pre;overflow:auto;color:#c8c8d4}",
6239
+ ".st-depth{display:inline-block}.st-depth{width:calc(var(--d,0)*12px)}",
6240
+ "code{color:#d7d7e0}"
6241
+ ].join("");
6242
+
4911
6243
  // src/config.ts
4912
6244
  function defineConfig(config) {
4913
6245
  return config;
@@ -4931,6 +6263,8 @@ function defineConfig(config) {
4931
6263
  FOCUSABLE_SELECTOR,
4932
6264
  FormBuilderImpl,
4933
6265
  GraphNode,
6266
+ HeadManager,
6267
+ HttpError,
4934
6268
  Lifecycle,
4935
6269
  ListBuilderImpl,
4936
6270
  NodeInstance,
@@ -4954,6 +6288,7 @@ function defineConfig(config) {
4954
6288
  StreetFrameworkError,
4955
6289
  StreetRenderHandle,
4956
6290
  StreetRendererImpl,
6291
+ TransitionController,
4957
6292
  VERSION,
4958
6293
  a11yIds,
4959
6294
  applyNodeProps,
@@ -4968,8 +6303,11 @@ function defineConfig(config) {
4968
6303
  consoleDiagnosticSink,
4969
6304
  consoleHydrationDiagnosticSink,
4970
6305
  containFocus,
6306
+ createAnnouncer,
4971
6307
  createApplication,
6308
+ createAuthSession,
4972
6309
  createBrowserHistory,
6310
+ createClient,
4973
6311
  createContext,
4974
6312
  createDevTools,
4975
6313
  createForm,
@@ -4985,10 +6323,12 @@ function defineConfig(config) {
4985
6323
  createStreetEvent,
4986
6324
  defineConfig,
4987
6325
  derived,
6326
+ describeError,
4988
6327
  diagnosePerformance,
4989
6328
  effect,
4990
6329
  email,
4991
6330
  environment,
6331
+ escapeHtml,
4992
6332
  escapeHtmlAttr,
4993
6333
  escapeHtmlText,
4994
6334
  flushSync,
@@ -5002,6 +6342,7 @@ function defineConfig(config) {
5002
6342
  generateApplicationId,
5003
6343
  generateNodeId,
5004
6344
  getFocusable,
6345
+ getResolvedTransition,
5005
6346
  globalEventBus,
5006
6347
  headingUpdate,
5007
6348
  hydrateGraph,
@@ -5012,12 +6353,16 @@ function defineConfig(config) {
5012
6353
  inspectForm,
5013
6354
  inspectGraph,
5014
6355
  inspectI18n,
6356
+ inspectInteractions,
5015
6357
  inspectResource,
5016
6358
  inspectRouter,
5017
6359
  inspectSignal,
5018
6360
  interpolate,
5019
6361
  isBatching,
5020
6362
  isComponentDefinition,
6363
+ isHeadContribution,
6364
+ isTransitionConfig,
6365
+ linkUpdate,
5021
6366
  matchPattern,
5022
6367
  matchRoutes,
5023
6368
  maxLength,
@@ -5025,6 +6370,7 @@ function defineConfig(config) {
5025
6370
  mountGraph,
5026
6371
  mountNode,
5027
6372
  mountRouter,
6373
+ mutation,
5028
6374
  nextId,
5029
6375
  nodeIdPrefix,
5030
6376
  nodeTypeStats,
@@ -5041,14 +6387,21 @@ function defineConfig(config) {
5041
6387
  readState,
5042
6388
  reconcileChildren,
5043
6389
  reconcileChildrenByPlan,
6390
+ renderDevToolsHTML,
6391
+ renderHead,
5044
6392
  renderToString,
5045
6393
  reportDiagnostic,
6394
+ reportError,
5046
6395
  required,
5047
6396
  resetIdCounter,
6397
+ resolveHead,
5048
6398
  resolveTag,
6399
+ resolveTransition,
5049
6400
  resource,
5050
6401
  restoreFocus,
5051
6402
  routerOutlet,
6403
+ rovingMenu,
6404
+ runElementTransition,
5052
6405
  runValidators,
5053
6406
  saveFocus,
5054
6407
  scheduleImmediate,
@@ -5069,6 +6422,7 @@ function defineConfig(config) {
5069
6422
  validateGraph,
5070
6423
  wireComponentBehavior,
5071
6424
  wireEvents,
6425
+ wireHeadBehavior,
5072
6426
  wireOverlayBehavior,
5073
6427
  wireReactiveList,
5074
6428
  wireSignalBindings