@hypen-space/web 0.5.2 → 0.5.3

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.
Files changed (104) hide show
  1. package/dist/canvas/accessibility.d.ts +118 -11
  2. package/dist/canvas/accessibility.js +470 -108
  3. package/dist/canvas/accessibility.js.map +1 -1
  4. package/dist/canvas/dispatch.d.ts +24 -0
  5. package/dist/canvas/dispatch.js +62 -0
  6. package/dist/canvas/dispatch.js.map +1 -0
  7. package/dist/canvas/editing.d.ts +178 -0
  8. package/dist/canvas/editing.js +590 -0
  9. package/dist/canvas/editing.js.map +1 -0
  10. package/dist/canvas/events.d.ts +28 -23
  11. package/dist/canvas/events.js +76 -128
  12. package/dist/canvas/events.js.map +1 -1
  13. package/dist/canvas/focus.d.ts +78 -0
  14. package/dist/canvas/focus.js +182 -0
  15. package/dist/canvas/focus.js.map +1 -0
  16. package/dist/canvas/index.d.ts +3 -1
  17. package/dist/canvas/index.js +3 -1
  18. package/dist/canvas/index.js.map +1 -1
  19. package/dist/canvas/paint.d.ts +16 -3
  20. package/dist/canvas/paint.js +283 -41
  21. package/dist/canvas/paint.js.map +1 -1
  22. package/dist/canvas/renderer.d.ts +20 -1
  23. package/dist/canvas/renderer.js +181 -50
  24. package/dist/canvas/renderer.js.map +1 -1
  25. package/dist/canvas/selection.js +31 -177
  26. package/dist/canvas/selection.js.map +1 -1
  27. package/dist/canvas/text-geometry.d.ts +72 -0
  28. package/dist/canvas/text-geometry.js +244 -0
  29. package/dist/canvas/text-geometry.js.map +1 -0
  30. package/dist/canvas/text.js +42 -9
  31. package/dist/canvas/text.js.map +1 -1
  32. package/dist/canvas/types.d.ts +7 -1
  33. package/dist/canvas/variants.d.ts +4 -1
  34. package/dist/canvas/variants.js +22 -3
  35. package/dist/canvas/variants.js.map +1 -1
  36. package/dist/dom/a11y-styles.d.ts +20 -0
  37. package/dist/dom/a11y-styles.js +61 -0
  38. package/dist/dom/a11y-styles.js.map +1 -0
  39. package/dist/dom/applicators/aria.d.ts +19 -0
  40. package/dist/dom/applicators/aria.js +36 -0
  41. package/dist/dom/applicators/aria.js.map +1 -0
  42. package/dist/dom/applicators/events.d.ts +7 -0
  43. package/dist/dom/applicators/events.js +35 -8
  44. package/dist/dom/applicators/events.js.map +1 -1
  45. package/dist/dom/applicators/index.js +4 -0
  46. package/dist/dom/applicators/index.js.map +1 -1
  47. package/dist/dom/components/hypenapp.d.ts +25 -1
  48. package/dist/dom/components/hypenapp.js +213 -245
  49. package/dist/dom/components/hypenapp.js.map +1 -1
  50. package/dist/dom/components/index.js +7 -0
  51. package/dist/dom/components/index.js.map +1 -1
  52. package/dist/dom/components/spinner.js +24 -10
  53. package/dist/dom/components/spinner.js.map +1 -1
  54. package/dist/dom/components/tabs.d.ts +27 -0
  55. package/dist/dom/components/tabs.js +69 -0
  56. package/dist/dom/components/tabs.js.map +1 -0
  57. package/dist/dom/components/visuallyhidden.d.ts +9 -0
  58. package/dist/dom/components/visuallyhidden.js +26 -0
  59. package/dist/dom/components/visuallyhidden.js.map +1 -0
  60. package/dist/dom/operability.d.ts +100 -0
  61. package/dist/dom/operability.js +298 -0
  62. package/dist/dom/operability.js.map +1 -0
  63. package/dist/dom/renderer.d.ts +98 -5
  64. package/dist/dom/renderer.js +398 -63
  65. package/dist/dom/renderer.js.map +1 -1
  66. package/dist/dom/route-focus.d.ts +42 -0
  67. package/dist/dom/route-focus.js +88 -0
  68. package/dist/dom/route-focus.js.map +1 -0
  69. package/dist/dom/semantics.d.ts +35 -0
  70. package/dist/dom/semantics.js +184 -0
  71. package/dist/dom/semantics.js.map +1 -0
  72. package/package.json +2 -2
  73. package/src/canvas/QUICKSTART.md +4 -4
  74. package/src/canvas/README.md +55 -29
  75. package/src/canvas/accessibility.ts +507 -115
  76. package/src/canvas/dispatch.ts +78 -0
  77. package/src/canvas/editing.ts +697 -0
  78. package/src/canvas/events.ts +89 -142
  79. package/src/canvas/focus.ts +216 -0
  80. package/src/canvas/index.ts +8 -1
  81. package/src/canvas/paint.ts +339 -42
  82. package/src/canvas/renderer.ts +218 -70
  83. package/src/canvas/selection.ts +52 -210
  84. package/src/canvas/text-geometry.ts +311 -0
  85. package/src/canvas/text.ts +43 -9
  86. package/src/canvas/types.ts +14 -1
  87. package/src/canvas/variants.ts +24 -5
  88. package/src/dom/a11y-styles.ts +65 -0
  89. package/src/dom/applicators/aria.ts +41 -0
  90. package/src/dom/applicators/events.ts +38 -7
  91. package/src/dom/applicators/index.ts +5 -0
  92. package/src/dom/components/hypenapp.ts +244 -272
  93. package/src/dom/components/index.ts +7 -0
  94. package/src/dom/components/spinner.ts +31 -13
  95. package/src/dom/components/tabs.ts +76 -0
  96. package/src/dom/components/visuallyhidden.ts +30 -0
  97. package/src/dom/operability.ts +312 -0
  98. package/src/dom/renderer.ts +455 -63
  99. package/src/dom/route-focus.ts +98 -0
  100. package/src/dom/semantics.ts +199 -0
  101. package/dist/canvas/input.d.ts +0 -76
  102. package/dist/canvas/input.js +0 -207
  103. package/dist/canvas/input.js.map +0 -1
  104. package/src/canvas/input.ts +0 -251
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Keyboard operability for actionable elements.
3
+ *
4
+ * Native interactive elements (`<button>`, `<a href>`) are keyboard-operable
5
+ * for free — the browser activates them on Enter/Space. But Hypen's actionable
6
+ * `Card` renders a plain `<div>`, which is neither focusable nor
7
+ * keyboard-activatable. This module closes that gap: a non-native actionable
8
+ * host is given a `button` role, made focusable (`tabindex=0`), and wired so
9
+ * Enter/Space dispatch the same action as a click.
10
+ *
11
+ * This is intentionally client-side: focus and key handling are synchronous
12
+ * interactions the streaming engine cannot mediate.
13
+ */
14
+ import { triggerElementAction } from "./applicators/events.js";
15
+ import { getMeta, setMeta } from "./element-data.js";
16
+ import { focusRouteTarget } from "./route-focus.js";
17
+ /** Host tags that the browser already makes keyboard-operable. */
18
+ const NATIVE_INTERACTIVE = new Set(["BUTTON", "A"]);
19
+ /**
20
+ * Make a non-native actionable element keyboard-operable. Idempotent across
21
+ * re-renders (guarded by a dataset flag). No-op for native interactive tags.
22
+ */
23
+ export function makeKeyboardActivatable(element, actionValue) {
24
+ const tag = element.tagName?.toUpperCase();
25
+ if (tag && NATIVE_INTERACTIVE.has(tag))
26
+ return;
27
+ // The keydown listener reads the action from element meta, so a later
28
+ // re-apply with a different action retargets keyboard activation exactly
29
+ // as it does the click listener — no stale action, no second listener.
30
+ setMeta(element, "kbdAction", actionValue);
31
+ // Wire the keydown listener only once.
32
+ if (element.dataset?.hypenKbd)
33
+ return;
34
+ if (element.dataset)
35
+ element.dataset.hypenKbd = "1";
36
+ // Announce as a button, make it reachable, and activate on Enter/Space.
37
+ element.setAttribute("role", "button");
38
+ element.tabIndex = 0;
39
+ element.addEventListener("keydown", (event) => {
40
+ const key = event.key;
41
+ if (key === "Enter" || key === " " || key === "Spacebar") {
42
+ // Space would otherwise scroll the page.
43
+ event.preventDefault?.();
44
+ triggerElementAction(element, getMeta(element, "kbdAction"));
45
+ }
46
+ });
47
+ }
48
+ /** CSS selector for the elements that can hold keyboard focus. */
49
+ const FOCUSABLE_SELECTOR = "a,button,input,textarea,select,[tabindex]";
50
+ /**
51
+ * Pick the focus target for a Tab keypress within a trapped container, wrapping
52
+ * at the ends.
53
+ *
54
+ * Pure over the ordered list of focusable descendants so it is testable without
55
+ * a real DOM: given the currently-focused element (`active`) and the Shift
56
+ * state, returns the element that should receive focus next, or `null` when
57
+ * there is nothing to focus. Tab past the last element wraps to the first;
58
+ * Shift+Tab past the first wraps to the last. When focus is outside the set,
59
+ * Tab lands on the first element and Shift+Tab on the last.
60
+ */
61
+ export function nextTrapFocus(focusable, active, shift) {
62
+ if (focusable.length === 0)
63
+ return null;
64
+ const first = focusable[0];
65
+ const last = focusable[focusable.length - 1];
66
+ const index = active == null ? -1 : focusable.indexOf(active);
67
+ if (index === -1)
68
+ return shift ? last : first;
69
+ if (shift)
70
+ return index === 0 ? last : focusable[index - 1];
71
+ return index === focusable.length - 1 ? first : focusable[index + 1];
72
+ }
73
+ /**
74
+ * Pick the tab that should receive focus for a keypress inside a roving
75
+ * tablist (WAI-ARIA APG "Tabs" keyboard contract). Pure over the ordered tab
76
+ * list so it is testable without a real DOM: Arrow Right/Down move to the
77
+ * next tab (wrapping), Arrow Left/Up to the previous (wrapping), Home/End to
78
+ * the first/last. Any other key — or an empty list — returns `null` (leave
79
+ * focus alone).
80
+ */
81
+ export function nextRovingFocus(tabs, active, key) {
82
+ if (tabs.length === 0)
83
+ return null;
84
+ const first = tabs[0];
85
+ const last = tabs[tabs.length - 1];
86
+ if (key === "Home")
87
+ return first;
88
+ if (key === "End")
89
+ return last;
90
+ const forward = key === "ArrowRight" || key === "ArrowDown";
91
+ const backward = key === "ArrowLeft" || key === "ArrowUp";
92
+ if (!forward && !backward)
93
+ return null;
94
+ const index = active == null ? -1 : tabs.indexOf(active);
95
+ if (index === -1)
96
+ return first;
97
+ if (forward)
98
+ return index === tabs.length - 1 ? first : tabs[index + 1];
99
+ return index === 0 ? last : tabs[index - 1];
100
+ }
101
+ /**
102
+ * Pick the item typeahead should move focus to (WAI-ARIA APG first-character
103
+ * navigation). Pure over the ordered item list so it is testable without a
104
+ * real DOM: the search is a case-insensitive `startsWith` over `textOf(item)`,
105
+ * starting from the item AFTER `active` and wrapping; `active` itself is the
106
+ * last candidate, so a query the current item already matches keeps focus in
107
+ * place (multi-char accumulation refining onto the same item). Returns `null`
108
+ * when nothing matches or the query is empty. Query accumulation and its
109
+ * timeout live at the caller.
110
+ */
111
+ export function nextTypeaheadFocus(items, active, query, textOf) {
112
+ if (items.length === 0 || query.length === 0)
113
+ return null;
114
+ const q = query.toLowerCase();
115
+ const start = active == null ? -1 : items.indexOf(active);
116
+ for (let step = 1; step <= items.length; step++) {
117
+ const item = items[(start + step + items.length) % items.length];
118
+ if (textOf(item).trim().toLowerCase().startsWith(q))
119
+ return item;
120
+ }
121
+ return null;
122
+ }
123
+ /** Pause after which an accumulated typeahead query starts over. */
124
+ const TYPEAHEAD_RESET_MS = 500;
125
+ /**
126
+ * Shared roving-tabindex installer for composite widgets (tablist, listbox):
127
+ * arrow keys move focus among the `itemSelector` descendants (wrapping),
128
+ * Home/End jump to the ends, and only the focused item stays in the page Tab
129
+ * order (`tabindex=0`; the rest drop to `-1`) so a single Tab keypress leaves
130
+ * the widget. Printable characters accumulate into a short-lived typeahead
131
+ * query (reset after a 500ms pause, tracked by timestamp — no timers) that
132
+ * roves to the next item whose text starts with the query. Space is excluded:
133
+ * it activates the focused item, and stealing it would break native Button
134
+ * hosts. Activation stays with the item's own click/Enter handling —
135
+ * selection is app state, not focus state. Idempotent across re-renders,
136
+ * guarded by a dataset flag.
137
+ */
138
+ function installRovingWidget(container, itemSelector) {
139
+ if (container.dataset?.hypenRoving)
140
+ return;
141
+ if (container.dataset)
142
+ container.dataset.hypenRoving = "1";
143
+ const itemsOf = () => Array.from(container.querySelectorAll(itemSelector));
144
+ const rove = (target, items) => {
145
+ for (const item of items) {
146
+ item.tabIndex = item === target ? 0 : -1;
147
+ }
148
+ target.focus?.();
149
+ };
150
+ let typeahead = "";
151
+ let typeaheadAt = 0;
152
+ container.addEventListener("keydown", (event) => {
153
+ const keyEvent = event;
154
+ const key = keyEvent.key;
155
+ const items = itemsOf();
156
+ const focused = (container.ownerDocument?.activeElement ?? null);
157
+ const active = focused && items.includes(focused) ? focused : null;
158
+ let target = nextRovingFocus(items, active, key);
159
+ if (!target &&
160
+ key.length === 1 &&
161
+ key !== " " &&
162
+ !keyEvent.ctrlKey &&
163
+ !keyEvent.metaKey &&
164
+ !keyEvent.altKey) {
165
+ const now = Date.now();
166
+ if (now - typeaheadAt > TYPEAHEAD_RESET_MS)
167
+ typeahead = "";
168
+ typeaheadAt = now;
169
+ typeahead += key;
170
+ target = nextTypeaheadFocus(items, active, typeahead, (item) => item.textContent ?? "");
171
+ }
172
+ if (target) {
173
+ event.preventDefault?.();
174
+ rove(target, items);
175
+ }
176
+ });
177
+ // First entry into the widget establishes the roving state (focused item
178
+ // reachable, siblings parked at -1) without needing any keydown first.
179
+ container.addEventListener("focusin", (event) => {
180
+ const items = itemsOf();
181
+ const focused = event.target;
182
+ if (focused && items.includes(focused)) {
183
+ for (const item of items) {
184
+ item.tabIndex = item === focused ? 0 : -1;
185
+ }
186
+ }
187
+ });
188
+ }
189
+ /**
190
+ * Install the WAI-ARIA APG "Tabs" keyboard contract on a tablist container:
191
+ * roving tabindex over the `role="tab"` descendants plus first-character
192
+ * typeahead (see `installRovingWidget`).
193
+ */
194
+ export function makeRovingTablist(container) {
195
+ installRovingWidget(container, '[role="tab"]');
196
+ }
197
+ /**
198
+ * Install the WAI-ARIA APG "Listbox" keyboard contract on a listbox
199
+ * container: roving tabindex over the `role="option"` descendants plus
200
+ * first-character typeahead (see `installRovingWidget`). No-op for a native
201
+ * `<select>` host (Hypen's `Select`), where the browser already owns arrow
202
+ * and typeahead behaviour.
203
+ */
204
+ export function makeRovingListbox(container) {
205
+ if (container.tagName?.toUpperCase() === "SELECT")
206
+ return;
207
+ installRovingWidget(container, '[role="option"]');
208
+ }
209
+ /**
210
+ * Install a focus trap on a dialog-like container: Tab / Shift+Tab cycle focus
211
+ * among the container's focusable descendants and wrap at the ends. Closing is
212
+ * app state: Escape is handled by `installDialogEscape` only when the dialog
213
+ * declares an `onClose` action. Idempotent across re-renders, guarded by a
214
+ * dataset flag.
215
+ */
216
+ export function makeFocusTrap(container) {
217
+ if (container.dataset?.hypenTrap)
218
+ return;
219
+ if (container.dataset)
220
+ container.dataset.hypenTrap = "1";
221
+ container.addEventListener("keydown", (event) => {
222
+ if (event.key !== "Tab")
223
+ return;
224
+ const focusable = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
225
+ const active = (container.ownerDocument?.activeElement ?? null);
226
+ const target = nextTrapFocus(focusable, active, event.shiftKey === true);
227
+ if (target) {
228
+ event.preventDefault?.();
229
+ target.focus?.();
230
+ }
231
+ });
232
+ }
233
+ /** Tags that can hold focus without a tabindex — the DFS twin of `FOCUSABLE_SELECTOR`. */
234
+ const FOCUSABLE_TAGS = new Set(["A", "BUTTON", "INPUT", "TEXTAREA", "SELECT"]);
235
+ /**
236
+ * First focusable descendant of `root` in document order, or `null`. Matches
237
+ * the focus trap's element set (`FOCUSABLE_SELECTOR`) but walks `children`
238
+ * recursively instead of `querySelectorAll`, so it works on hosts without
239
+ * selector support (test doubles) — document order is exactly child order.
240
+ */
241
+ export function findFirstFocusable(root) {
242
+ const children = Array.from((root.children ?? []));
243
+ for (const child of children) {
244
+ const tag = child.tagName?.toUpperCase() ?? "";
245
+ if (FOCUSABLE_TAGS.has(tag) || child.getAttribute?.("tabindex") != null)
246
+ return child;
247
+ const nested = findFirstFocusable(child);
248
+ if (nested)
249
+ return nested;
250
+ }
251
+ return null;
252
+ }
253
+ /**
254
+ * Dialog auto-focus on mount (WAI-ARIA APG "Dialog (Modal)"): focus the
255
+ * dialog's first focusable descendant, else the dialog itself (granted
256
+ * programmatic focusability by `focusRouteTarget`). Returns the element that
257
+ * held focus before the dialog opened, so the caller can restore it on close.
258
+ */
259
+ export function focusDialogOnOpen(dialog) {
260
+ const doc = dialog.ownerDocument ?? (typeof document !== "undefined" ? document : null);
261
+ const previous = (doc?.activeElement ?? null);
262
+ focusRouteTarget(findFirstFocusable(dialog) ?? dialog);
263
+ return previous;
264
+ }
265
+ /**
266
+ * Restore focus to the element that had it before a dialog opened. Skipped
267
+ * when the opener is gone: no longer connected to the document, or living
268
+ * inside the (now closed) dialog itself.
269
+ */
270
+ export function restoreDialogFocus(dialog, opener) {
271
+ if (!opener)
272
+ return;
273
+ if (dialog === opener || dialog.contains?.(opener))
274
+ return;
275
+ if (opener.isConnected === false)
276
+ return;
277
+ opener.focus?.();
278
+ }
279
+ /**
280
+ * Escape-to-close for a dialog that declares an `onClose` action: Escape
281
+ * dispatches the action; the actual close (state flip, unmount) stays with
282
+ * the app. A dialog without `onClose` gets no Escape behaviour — the renderer
283
+ * cannot know how to close it. Idempotent across re-renders, guarded by a
284
+ * dataset flag.
285
+ */
286
+ export function installDialogEscape(dialog, onCloseValue) {
287
+ if (dialog.dataset?.hypenDialogEsc)
288
+ return;
289
+ if (dialog.dataset)
290
+ dialog.dataset.hypenDialogEsc = "1";
291
+ dialog.addEventListener("keydown", (event) => {
292
+ if (event.key !== "Escape")
293
+ return;
294
+ event.preventDefault?.();
295
+ triggerElementAction(dialog, onCloseValue);
296
+ });
297
+ }
298
+ //# sourceMappingURL=operability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operability.js","sourceRoot":"","sources":["../../src/dom/operability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,kEAAkE;AAClE,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;AAEpD;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAoB,EAAE,WAAoB;IAChF,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,EAAE,WAAW,EAAE,CAAC;IAC3C,IAAI,GAAG,IAAI,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO;IAE/C,sEAAsE;IACtE,yEAAyE;IACzE,uEAAuE;IACvE,OAAO,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IAE3C,uCAAuC;IACvC,IAAI,OAAO,CAAC,OAAO,EAAE,QAAQ;QAAE,OAAO;IACtC,IAAI,OAAO,CAAC,OAAO;QAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,GAAG,GAAG,CAAC;IAEpD,wEAAwE;IACxE,OAAO,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IAErB,OAAO,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAY,EAAE,EAAE;QACnD,MAAM,GAAG,GAAI,KAAuB,CAAC,GAAG,CAAC;QACzC,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,UAAU,EAAE,CAAC;YACzD,yCAAyC;YACzC,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;YACzB,oBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,kEAAkE;AAClE,MAAM,kBAAkB,GAAG,2CAA2C,CAAC;AAEvE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAI,SAAc,EAAE,MAAgB,EAAE,KAAc;IAC/E,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAE,CAAC;IAC5B,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;IAC9C,MAAM,KAAK,GAAG,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAE9D,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IAC9C,IAAI,KAAK;QAAE,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,CAAE,CAAC;IAC7D,OAAO,KAAK,KAAK,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,CAAE,CAAC;AACxE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAI,IAAS,EAAE,MAAgB,EAAE,GAAW;IACzE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;IACvB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;IACpC,IAAI,GAAG,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IACjC,IAAI,GAAG,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAE/B,MAAM,OAAO,GAAG,GAAG,KAAK,YAAY,IAAI,GAAG,KAAK,WAAW,CAAC;IAC5D,MAAM,QAAQ,GAAG,GAAG,KAAK,WAAW,IAAI,GAAG,KAAK,SAAS,CAAC;IAC1D,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAEvC,MAAM,KAAK,GAAG,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/B,IAAI,OAAO;QAAE,OAAO,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAE,CAAC;IACzE,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAE,CAAC;AAC/C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAU,EACV,MAAgB,EAChB,KAAa,EACb,MAA2B;IAE3B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1D,MAAM,CAAC,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IAC9B,MAAM,KAAK,GAAG,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1D,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC;QAChD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAE,CAAC;QAClE,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;IACnE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,oEAAoE;AACpE,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAE/B;;;;;;;;;;;;GAYG;AACH,SAAS,mBAAmB,CAAC,SAAsB,EAAE,YAAoB;IACvE,IAAI,SAAS,CAAC,OAAO,EAAE,WAAW;QAAE,OAAO;IAC3C,IAAI,SAAS,CAAC,OAAO;QAAE,SAAS,CAAC,OAAO,CAAC,WAAW,GAAG,GAAG,CAAC;IAE3D,MAAM,OAAO,GAAG,GAAkB,EAAE,CAClC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAA6B,CAAC;IAEnF,MAAM,IAAI,GAAG,CAAC,MAAmB,EAAE,KAAoB,EAAQ,EAAE;QAC/D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,QAAQ,GAAG,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3C,CAAC;QACD,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;IACnB,CAAC,CAAC;IAEF,IAAI,SAAS,GAAG,EAAE,CAAC;IACnB,IAAI,WAAW,GAAG,CAAC,CAAC;IAEpB,SAAS,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAY,EAAE,EAAE;QACrD,MAAM,QAAQ,GAAG,KAAsB,CAAC;QACxC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC;QACzB,MAAM,KAAK,GAAG,OAAO,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,CAAC,SAAS,CAAC,aAAa,EAAE,aAAa,IAAI,IAAI,CAAuB,CAAC;QACvF,MAAM,MAAM,GAAG,OAAO,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QAEnE,IAAI,MAAM,GAAG,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;QAEjD,IACE,CAAC,MAAM;YACP,GAAG,CAAC,MAAM,KAAK,CAAC;YAChB,GAAG,KAAK,GAAG;YACX,CAAC,QAAQ,CAAC,OAAO;YACjB,CAAC,QAAQ,CAAC,OAAO;YACjB,CAAC,QAAQ,CAAC,MAAM,EAChB,CAAC;YACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACvB,IAAI,GAAG,GAAG,WAAW,GAAG,kBAAkB;gBAAE,SAAS,GAAG,EAAE,CAAC;YAC3D,WAAW,GAAG,GAAG,CAAC;YAClB,SAAS,IAAI,GAAG,CAAC;YACjB,MAAM,GAAG,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;QAC1F,CAAC;QAED,IAAI,MAAM,EAAE,CAAC;YACX,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;YACzB,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,yEAAyE;IACzE,uEAAuE;IACvE,SAAS,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAY,EAAE,EAAE;QACrD,MAAM,KAAK,GAAG,OAAO,EAAE,CAAC;QACxB,MAAM,OAAO,GAAI,KAAoB,CAAC,MAA4B,CAAC;QACnE,IAAI,OAAO,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACvC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IAAI,CAAC,QAAQ,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5C,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAsB;IACtD,mBAAmB,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAsB;IACtD,IAAI,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,QAAQ;QAAE,OAAO;IAC1D,mBAAmB,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,SAAsB;IAClD,IAAI,SAAS,CAAC,OAAO,EAAE,SAAS;QAAE,OAAO;IACzC,IAAI,SAAS,CAAC,OAAO;QAAE,SAAS,CAAC,OAAO,CAAC,SAAS,GAAG,GAAG,CAAC;IAEzD,SAAS,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAY,EAAE,EAAE;QACrD,IAAK,KAAuB,CAAC,GAAG,KAAK,KAAK;YAAE,OAAO;QACnD,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAC1B,SAAS,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,CACnB,CAAC;QAC9B,MAAM,MAAM,GAAG,CAAC,SAAS,CAAC,aAAa,EAAE,aAAa,IAAI,IAAI,CAAuB,CAAC;QACtF,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,EAAE,MAAM,EAAG,KAAuB,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;QAC5F,IAAI,MAAM,EAAE,CAAC;YACX,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;YACzB,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,0FAA0F;AAC1F,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC;AAE/E;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAiB;IAClD,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAsC,CAAC,CAAC;IACxF,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAC/C,IAAI,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC,UAAU,CAAC,IAAI,IAAI;YAAE,OAAO,KAAK,CAAC;QACtF,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;IAC5B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAmB;IACnD,MAAM,GAAG,GAAG,MAAM,CAAC,aAAa,IAAI,CAAC,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACxF,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE,aAAa,IAAI,IAAI,CAAuB,CAAC;IACpE,gBAAgB,CAAC,kBAAkB,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,CAAC;IACvD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAmB,EAAE,MAA0B;IAChF,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC;QAAE,OAAO;IAC3D,IAAK,MAAoC,CAAC,WAAW,KAAK,KAAK;QAAE,OAAO;IACxE,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;AACnB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAmB,EAAE,YAAqB;IAC5E,IAAI,MAAM,CAAC,OAAO,EAAE,cAAc;QAAE,OAAO;IAC3C,IAAI,MAAM,CAAC,OAAO;QAAE,MAAM,CAAC,OAAO,CAAC,cAAc,GAAG,GAAG,CAAC;IAExD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAY,EAAE,EAAE;QAClD,IAAK,KAAuB,CAAC,GAAG,KAAK,QAAQ;YAAE,OAAO;QACtD,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;QACzB,oBAAoB,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -12,6 +12,17 @@ import { type DebugConfig } from "./debug.js";
12
12
  interface IEngine {
13
13
  dispatchAction(name: string, payload?: any): void;
14
14
  }
15
+ export interface DOMRendererOptions {
16
+ /**
17
+ * Route-change focus contract (route-focus.ts). `"auto"` (default) moves
18
+ * focus to the incoming route's first heading / `main` landmark / subtree
19
+ * root on every navigation. `"off"` skips `focusIncomingRoute` entirely —
20
+ * the escape hatch for authors who own focus management themselves (custom
21
+ * transition choreography, embedded shells) and would otherwise fight the
22
+ * renderer for focus on every navigation.
23
+ */
24
+ routeFocus?: "auto" | "off";
25
+ }
15
26
  export declare class DOMRenderer {
16
27
  private container;
17
28
  private nodes;
@@ -24,12 +35,45 @@ export declare class DOMRenderer {
24
35
  private globalContext;
25
36
  private componentInstances;
26
37
  private debugTracker;
38
+ /**
39
+ * Text elements whose template contains an `@{...}` binding, with the
40
+ * template pre-compiled into segments. Only these nodes are visited on
41
+ * state changes — static text never re-interpolates.
42
+ */
43
+ private textBindings;
27
44
  private canvasRenderers;
28
45
  private canvasSubtreeMap;
46
+ private canvasNodeChildren;
47
+ private canvasNodeParent;
29
48
  private canvasElements;
30
49
  /** Props stashed at create-time so we can forward them to CanvasRenderer */
31
50
  private pendingCreateProps;
32
- constructor(container: HTMLElement, engine: IEngine, debugConfig?: Partial<DebugConfig>);
51
+ /**
52
+ * Focus-restore memory for the Router subtree cache, keyed by detached
53
+ * subtree root id: the element that held focus when the route was
54
+ * detached, restored on cached re-`attach`. Entries are dropped on
55
+ * `remove` (Router LRU eviction) so focus restore can never target an
56
+ * evicted NodeId — the route-focus contract (see route-focus.ts).
57
+ */
58
+ private routeFocusMemory;
59
+ /** See `DOMRendererOptions.routeFocus` — `"off"` disables `focusIncomingRoute`. */
60
+ private routeFocus;
61
+ /** Ids created with `semantics.role === "dialog"`, so insert/remove hooks can act. */
62
+ private dialogIds;
63
+ /**
64
+ * Open dialog id → the element that held focus before the dialog took it
65
+ * (the trigger), restored on dialog remove/detach when still connected.
66
+ * Presence of an entry means "already focused" — re-inserts (moves) of an
67
+ * open dialog never re-run the mount focus.
68
+ */
69
+ private dialogOpeners;
70
+ /**
71
+ * Dialog ids inserted during the current batch. Mount focus runs after the
72
+ * whole batch, because a dialog's focusable children may be inserted after
73
+ * the dialog's own insert patch.
74
+ */
75
+ private pendingDialogMounts;
76
+ constructor(container: HTMLElement, engine: IEngine, debugConfig?: Partial<DebugConfig>, options?: DOMRendererOptions);
33
77
  /**
34
78
  * Set router and global context for component composition
35
79
  */
@@ -42,11 +86,37 @@ export declare class DOMRenderer {
42
86
  * the DOMRenderer or the owning CanvasRenderer.
43
87
  */
44
88
  applyPatches(patches: Patch[]): void;
89
+ /**
90
+ * Apply the dialog focus contract to dialogs inserted by the finished
91
+ * batch: focus the first focusable descendant, else the dialog itself, and
92
+ * remember the previously-focused element (the trigger) for restore on
93
+ * close. Deferred to batch end so descendants inserted after the dialog's
94
+ * own insert patch are visible to the focus search.
95
+ */
96
+ private flushDialogMounts;
97
+ /**
98
+ * Apply the route-focus contract to a freshly-shown route subtree: restore
99
+ * the remembered focus of a cached route when it is still inside the
100
+ * subtree, otherwise land on the first heading / main landmark / the
101
+ * subtree root (see route-focus.ts for the full contract).
102
+ */
103
+ private focusIncomingRoute;
45
104
  /**
46
105
  * Determine which canvas root (if any) a patch should be routed to.
47
106
  * Returns the canvas root node ID, or undefined for DOM patches.
48
107
  */
49
108
  private getCanvasRouteTarget;
109
+ /**
110
+ * Record a node as a member of a canvas subtree, tracking its parent link
111
+ * so a later `remove` can prune the whole subtree from the routing maps.
112
+ */
113
+ private registerCanvasMember;
114
+ /**
115
+ * Drop a removed canvas node and all of its descendants from the canvas
116
+ * routing maps. The engine emits `remove` only for the subtree root, so
117
+ * descendants are walked via the tracked child links.
118
+ */
119
+ private pruneCanvasSubtree;
50
120
  /**
51
121
  * Update state and interpolate text content
52
122
  */
@@ -56,19 +126,35 @@ export declare class DOMRenderer {
56
126
  */
57
127
  private mergeComponentState;
58
128
  /**
59
- * Interpolate state values in all text elements
129
+ * Interpolate state values in all binding-bearing text elements
60
130
  */
61
131
  private interpolateAllText;
62
132
  /**
63
- * Interpolate state values in text template.
133
+ * Resolve a compiled template against the current state.
64
134
  *
65
- * Bindings use the `@{state.path}` syntax (matching the engine's parser).
135
+ * Bindings use the `@{state.path}` syntax (matching the engine's parser);
136
+ * unresolvable bindings render as their original `@{...}` text.
66
137
  */
67
- private interpolateText;
138
+ private interpolateSegments;
139
+ /**
140
+ * Keep the binding index in sync with the element's stored template:
141
+ * register text elements whose template contains an `@{...}` binding,
142
+ * drop everything else.
143
+ */
144
+ private syncTextBinding;
68
145
  /**
69
146
  * Apply a single patch.
70
147
  */
71
148
  private applyPatch;
149
+ /**
150
+ * Reactive accessibility update: re-run the create-time semantics
151
+ * translation on the live element with the node's complete re-resolved
152
+ * block. `applySemantics` is idempotent and clears attributes it set on a
153
+ * previous pass that the new block no longer produces. This is the sole
154
+ * ARIA writer for bound self-state — the SetProp that accompanies a bound
155
+ * `.expanded(@state.open)` change does not touch ARIA attributes.
156
+ */
157
+ private onSetSemantics;
72
158
  /**
73
159
  * Create a new element
74
160
  */
@@ -111,6 +197,13 @@ export declare class DOMRenderer {
111
197
  * route subtrees alive between navigations.
112
198
  */
113
199
  private onDetach;
200
+ /**
201
+ * Close-side of the dialog focus contract: every open dialog at-or-under
202
+ * `subtree` has left the document — restore focus to its remembered
203
+ * trigger (when still connected) and drop the open entry so a later
204
+ * insert/attach counts as a fresh mount.
205
+ */
206
+ private restoreDialogsWithin;
114
207
  /**
115
208
  * Remove an element from the tree
116
209
  */