kerfjs 5.0.0-beta.7 → 5.0.0-beta.71

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 (83) hide show
  1. package/CHANGELOG.md +2488 -117
  2. package/README.md +96 -37
  3. package/ai/cursorrules +110 -61
  4. package/ai/manifest.json +16 -7
  5. package/ai/skill.md +120 -75
  6. package/dist/actions.d.ts +1 -1
  7. package/dist/actions.js +10 -6
  8. package/dist/actions.js.map +1 -1
  9. package/dist/array-signal.js +1 -1
  10. package/dist/async.js +22 -19
  11. package/dist/async.js.map +1 -1
  12. package/dist/attach.js +1 -66
  13. package/dist/attach.js.map +1 -1
  14. package/dist/{chunk-HW7KSM2Y.js → chunk-3C6F6NM4.js} +1 -1
  15. package/dist/{chunk-HW7KSM2Y.js.map → chunk-3C6F6NM4.js.map} +1 -1
  16. package/dist/{chunk-GY4XV2UV.js → chunk-4TX6YJDM.js} +10 -3
  17. package/dist/chunk-4TX6YJDM.js.map +1 -0
  18. package/dist/{chunk-QIP723L4.js → chunk-5WRGJZV6.js} +3 -2
  19. package/dist/chunk-5WRGJZV6.js.map +1 -0
  20. package/dist/{chunk-KZJXHFIB.js → chunk-BI4HGICV.js} +3 -3
  21. package/dist/chunk-BI4HGICV.js.map +1 -0
  22. package/dist/chunk-FM6N2B4N.js +70 -0
  23. package/dist/chunk-FM6N2B4N.js.map +1 -0
  24. package/dist/{chunk-LVH3GC6B.js → chunk-H7WTRM6F.js} +3 -3
  25. package/dist/chunk-H7WTRM6F.js.map +1 -0
  26. package/dist/{chunk-U6FK33SG.js → chunk-IHMDBBUJ.js} +2 -2
  27. package/dist/chunk-IHMDBBUJ.js.map +1 -0
  28. package/dist/chunk-JN7EJM3B.js +86 -0
  29. package/dist/chunk-JN7EJM3B.js.map +1 -0
  30. package/dist/{chunk-UZJ6I4T6.js → chunk-MK42GLPV.js} +1 -1
  31. package/dist/chunk-MK42GLPV.js.map +1 -0
  32. package/dist/{chunk-SVATPF5R.js → chunk-TLL3KXT6.js} +249 -160
  33. package/dist/chunk-TLL3KXT6.js.map +1 -0
  34. package/dist/{chunk-ZDCJZCNO.js → chunk-UGNPDLNI.js} +78 -45
  35. package/dist/chunk-UGNPDLNI.js.map +1 -0
  36. package/dist/{chunk-V46JKE44.js → chunk-WKIPLNVO.js} +1 -1
  37. package/dist/chunk-WKIPLNVO.js.map +1 -0
  38. package/dist/{delegate-CL9VTZFb.d.ts → delegate--CrfgYtC.d.ts} +0 -34
  39. package/dist/dev.d.ts +31 -13
  40. package/dist/dev.js +101 -35
  41. package/dist/dev.js.map +1 -1
  42. package/dist/html.js +5 -4
  43. package/dist/html.js.map +1 -1
  44. package/dist/index.d.ts +14 -9
  45. package/dist/index.js +29 -20
  46. package/dist/index.js.map +1 -1
  47. package/dist/jsx-runtime.d.ts +13 -5
  48. package/dist/jsx-runtime.js +1 -1
  49. package/dist/list.d.ts +7 -3
  50. package/dist/list.js +126 -28
  51. package/dist/list.js.map +1 -1
  52. package/dist/overlay.d.ts +25 -28
  53. package/dist/overlay.js +580 -275
  54. package/dist/overlay.js.map +1 -1
  55. package/dist/remount.js +2 -2
  56. package/dist/router.d.ts +13 -4
  57. package/dist/router.js +37 -10
  58. package/dist/router.js.map +1 -1
  59. package/dist/scope.d.ts +35 -8
  60. package/dist/scope.js +61 -30
  61. package/dist/scope.js.map +1 -1
  62. package/dist/testing.js +1 -1
  63. package/dist/timing.d.ts +3 -2
  64. package/dist/timing.js +1 -1
  65. package/dist/timing.js.map +1 -1
  66. package/llms.txt +11 -4
  67. package/package.json +32 -7
  68. package/setup/cli.mjs +90 -0
  69. package/setup/index.d.mts +37 -0
  70. package/setup/index.mjs +1347 -0
  71. package/setup/jsonc.mjs +201 -0
  72. package/setup/state.schema.json +56 -0
  73. package/dist/chunk-GY4XV2UV.js.map +0 -1
  74. package/dist/chunk-KPXIOG2C.js +0 -54
  75. package/dist/chunk-KPXIOG2C.js.map +0 -1
  76. package/dist/chunk-KZJXHFIB.js.map +0 -1
  77. package/dist/chunk-LVH3GC6B.js.map +0 -1
  78. package/dist/chunk-QIP723L4.js.map +0 -1
  79. package/dist/chunk-SVATPF5R.js.map +0 -1
  80. package/dist/chunk-U6FK33SG.js.map +0 -1
  81. package/dist/chunk-UZJ6I4T6.js.map +0 -1
  82. package/dist/chunk-V46JKE44.js.map +0 -1
  83. package/dist/chunk-ZDCJZCNO.js.map +0 -1
package/dist/overlay.js CHANGED
@@ -1,11 +1,12 @@
1
- import { mount } from './chunk-SVATPF5R.js';
2
- import { delegate } from './chunk-KPXIOG2C.js';
3
-
4
-
5
- import { jsx, escapeHtml } from './chunk-ZDCJZCNO.js';
1
+ import { attach } from './chunk-FM6N2B4N.js';
2
+ import { mount } from './chunk-TLL3KXT6.js';
3
+ import { delegate } from './chunk-JN7EJM3B.js';
6
4
 
7
5
 
6
+ import { jsx, escapeHtml } from './chunk-UGNPDLNI.js';
8
7
 
8
+ import { OVERLAY_HOST_SELECTOR } from './chunk-4TX6YJDM.js';
9
+ import { devHooks } from './chunk-3C6F6NM4.js';
9
10
 
10
11
  // src/overlay-position.ts
11
12
  function positionAnchored(el, anchor, options = {}) {
@@ -19,12 +20,25 @@ function positionAnchored(el, anchor, options = {}) {
19
20
  const belowTop = anchorRect.bottom + gap;
20
21
  const aboveTop = anchorRect.top - gap - elementRect.height;
21
22
  let below = placement !== "top";
22
- if (below && belowTop + elementRect.height > viewportHeight && aboveTop >= 0) below = false;
23
- else if (!below && aboveTop < 0 && belowTop + elementRect.height <= viewportHeight) below = true;
23
+ if (below && belowTop + elementRect.height > viewportHeight && aboveTop >= 0)
24
+ below = false;
25
+ else if (!below && aboveTop < 0 && belowTop + elementRect.height <= viewportHeight)
26
+ below = true;
24
27
  let left = align === "end" ? anchorRect.right - elementRect.width : anchorRect.left;
25
28
  left = Math.max(0, Math.min(left, viewportWidth - elementRect.width));
26
- el.style.left = `${left}px`;
27
- el.style.top = `${below ? belowTop : aboveTop}px`;
29
+ const [originX, originY, scaleX, scaleY] = fixedOrigin(el);
30
+ el.style.left = `${(left - originX) / scaleX}px`;
31
+ el.style.top = `${((below ? belowTop : aboveTop) - originY) / scaleY}px`;
32
+ }
33
+ function fixedOrigin(el) {
34
+ const parent = el.parentNode;
35
+ if (!parent) return [0, 0, 1, 1];
36
+ const probe = el.ownerDocument.createElement("div");
37
+ probe.style.cssText = "position:fixed;left:0;top:0;width:1px;height:1px;margin:0;padding:0;border:0";
38
+ parent.insertBefore(probe, el);
39
+ const rect = probe.getBoundingClientRect();
40
+ probe.remove();
41
+ return [rect.left, rect.top, rect.width || 1, rect.height || 1];
28
42
  }
29
43
  function autoReposition(el, anchor, options = {}) {
30
44
  const reposition = () => positionAnchored(el, anchor, options);
@@ -40,9 +54,9 @@ function autoReposition(el, anchor, options = {}) {
40
54
  // src/overlay-core.ts
41
55
  var FOCUSABLE = 'a[href],area[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),iframe,[tabindex]:not([tabindex="-1"]),[contenteditable="true"]';
42
56
  function focusable(root) {
43
- const items = Array.from(root.querySelectorAll(FOCUSABLE)).filter(
44
- (el) => !el.hasAttribute("hidden") && el.tabIndex >= 0
45
- );
57
+ const items = Array.from(
58
+ root.querySelectorAll(FOCUSABLE)
59
+ ).filter((el) => !el.hasAttribute("hidden") && el.tabIndex >= 0);
46
60
  for (const el of items) if (!el.hasAttribute("tabindex")) el.tabIndex = 0;
47
61
  return items;
48
62
  }
@@ -52,10 +66,78 @@ function supportsDialog() {
52
66
  function supportsPopover() {
53
67
  return typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.showPopover === "function";
54
68
  }
55
- var FALLBACK_OVERLAY_STACK = /* @__PURE__ */ Symbol("kerf.fallbackOverlayStack");
56
- function fallbackOverlayStack(wrapper) {
57
- const document2 = wrapper.ownerDocument;
58
- return document2[FALLBACK_OVERLAY_STACK] ??= [];
69
+ var OVERLAY_ARBITRATION = /* @__PURE__ */ Symbol("kerf.overlayArbitration");
70
+ function overlayArbitration(doc) {
71
+ const arbitrated = doc;
72
+ return arbitrated[OVERLAY_ARBITRATION] ??= {
73
+ stack: [],
74
+ handled: /* @__PURE__ */ new WeakSet(),
75
+ // KF-AHY6H4: an app-owned modal `<dialog>` opened outside kerf would leave
76
+ // the kerf surfaces already showing inert beneath it. A dialog fires
77
+ // `toggle` after it opens (it does not bubble, so listen in the capture
78
+ // phase); re-host above it exactly as kerf does for its own dialogs, which
79
+ // are skipped here because `overlay()` already re-hosted synchronously.
80
+ onToggle: (event) => {
81
+ const dialog = event.target;
82
+ const arbitration = arbitrated[OVERLAY_ARBITRATION];
83
+ if (dialog.tagName === "DIALOG" && !arbitration.stack.some(({ el }) => el === dialog) && isOpenModal(dialog, arbitration))
84
+ rehostAbove(arbitration, dialog);
85
+ }
86
+ };
87
+ }
88
+ function rehostAbove(arbitration, dialog) {
89
+ for (const { el, liftAbove } of arbitration.stack)
90
+ if (el !== dialog && el.isConnected && !dialog.contains(el)) liftAbove?.();
91
+ }
92
+ function isOpenModal(dialog, arbitration) {
93
+ if (!dialog.isConnected || dialog.open !== true)
94
+ return false;
95
+ if (arbitration.stack.some(({ el, modal }) => modal && el === dialog))
96
+ return true;
97
+ try {
98
+ return dialog.matches(":modal");
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+ function modalDialogOpen(doc, arbitration) {
104
+ return arbitration.stack.some(({ el }) => isOpenModal(el, arbitration)) || Array.from(doc.querySelectorAll("dialog[open]")).some(
105
+ (dialog) => isOpenModal(dialog, arbitration)
106
+ );
107
+ }
108
+ function overlayHost(anchor) {
109
+ const arbitration = overlayArbitration(document);
110
+ const dialog = anchor.closest("dialog");
111
+ if (dialog === null || !isOpenModal(dialog, arbitration)) return void 0;
112
+ let host = Array.from(dialog.querySelectorAll(OVERLAY_HOST_SELECTOR)).find(
113
+ (el) => el.closest("dialog") === dialog
114
+ );
115
+ if (host === void 0 && arbitration.stack.some(({ el }) => el === dialog)) {
116
+ dialog.insertAdjacentHTML(
117
+ "beforeend",
118
+ '<div data-kerf-overlay-host data-morph-skip data-morph-preserve style="display:contents"></div>'
119
+ );
120
+ host = dialog.lastElementChild;
121
+ }
122
+ return host;
123
+ }
124
+ function ownsInput(stack, self, kind) {
125
+ for (let i = stack.indexOf(self) + 1; i < stack.length; i++) {
126
+ const above = stack[i];
127
+ if (above.modal || kind !== "trap" && above.triggers.includes(kind))
128
+ return false;
129
+ }
130
+ return true;
131
+ }
132
+ function assertInitialFocusSelector(selector) {
133
+ try {
134
+ document.createDocumentFragment().querySelector(selector);
135
+ } catch (cause) {
136
+ throw new Error(
137
+ `overlay(): invalid initialFocus selector ${JSON.stringify(selector)}`,
138
+ { cause }
139
+ );
140
+ }
59
141
  }
60
142
  function overlay(content, options = {}) {
61
143
  const {
@@ -69,10 +151,15 @@ function overlay(content, options = {}) {
69
151
  outsideIgnore,
70
152
  native = false
71
153
  } = options;
154
+ if (typeof initialFocus === "string")
155
+ assertInitialFocusSelector(initialFocus);
72
156
  const triggers = dismiss === false ? [] : Array.isArray(dismiss) ? dismiss : [dismiss];
73
- const restoreTo = document.activeElement;
74
- const useDialog = native && trap && supportsDialog();
75
- const usePopover = native && !trap && supportsPopover();
157
+ const arbitration = overlayArbitration(document);
158
+ const containerDialog = container.closest("dialog");
159
+ const liftOverModal = !native && !(containerDialog !== null && isOpenModal(containerDialog, arbitration)) && modalDialogOpen(document, arbitration);
160
+ const topLayer = native || liftOverModal;
161
+ const useDialog = topLayer && trap && supportsDialog();
162
+ const usePopover = topLayer && !trap && supportsPopover();
76
163
  const wrapper = useDialog ? document.createElement("dialog") : document.createElement("div");
77
164
  wrapper.className = className;
78
165
  if (usePopover) wrapper.setAttribute("popover", "manual");
@@ -80,122 +167,224 @@ function overlay(content, options = {}) {
80
167
  wrapper.setAttribute("role", role);
81
168
  wrapper.setAttribute("aria-modal", "true");
82
169
  }
83
- container.appendChild(wrapper);
84
- const fallback = !useDialog && !usePopover;
85
- const fallbackStack = fallback ? fallbackOverlayStack(wrapper) : void 0;
86
- const isTopmostFallback = () => fallbackStack === void 0 || fallbackStack[fallbackStack.length - 1] === wrapper;
87
- const disposeMount = mount(wrapper, typeof content === "function" ? content : () => content);
88
- fallbackStack?.push(wrapper);
89
- let nativeOpened = false;
90
- if (useDialog) {
91
- wrapper.showModal();
92
- nativeOpened = true;
93
- } else if (usePopover) {
94
- wrapper.showPopover();
95
- wrapper.style.inset = "auto";
96
- nativeOpened = true;
97
- }
170
+ const surface = {
171
+ el: wrapper,
172
+ triggers,
173
+ modal: trap,
174
+ restoreTo: document.activeElement
175
+ };
176
+ const owns = (kind, event) => !(event !== void 0 && arbitration.handled.has(event)) && ownsInput(arbitration.stack, surface, kind);
98
177
  const removers = [];
99
178
  const resultBox = {};
100
179
  const result = new Promise((resolve) => {
101
180
  resultBox.resolve = resolve;
102
181
  });
103
182
  const state = { closed: false };
183
+ let disposeMount;
184
+ let nativeOpened = false;
185
+ if (!trap)
186
+ surface.liftAbove = () => {
187
+ if (!supportsPopover()) {
188
+ devHooks.overlayBlockedByModal?.("hidden");
189
+ return;
190
+ }
191
+ const el = wrapper;
192
+ if (nativeOpened) el.hidePopover();
193
+ else {
194
+ wrapper.setAttribute("popover", "manual");
195
+ wrapper.style.inset = "auto";
196
+ }
197
+ el.showPopover();
198
+ nativeOpened = true;
199
+ if (wrapper.querySelector(FOCUSABLE) !== null)
200
+ devHooks.overlayBlockedByModal?.("inert");
201
+ };
104
202
  function close(value) {
105
203
  if (state.closed) return;
106
204
  state.closed = true;
107
- if (fallbackStack !== void 0) {
108
- const stackIndex = fallbackStack.indexOf(wrapper);
109
- if (stackIndex !== -1) fallbackStack.splice(stackIndex, 1);
110
- }
205
+ const { stack } = arbitration;
206
+ const stackIndex = stack.indexOf(surface);
207
+ if (stackIndex !== -1) stack.splice(stackIndex, 1);
208
+ if (stack.length === 0)
209
+ document.removeEventListener("toggle", arbitration.onToggle, true);
210
+ for (const other of stack)
211
+ if (other.restoreTo !== null && wrapper.contains(other.restoreTo))
212
+ other.restoreTo = surface.restoreTo;
213
+ const active = document.activeElement;
111
214
  for (const remove of removers) remove();
112
- disposeMount();
215
+ disposeMount?.();
113
216
  if (nativeOpened) {
114
217
  nativeOpened = false;
115
218
  if (useDialog) wrapper.close();
116
219
  else wrapper.hidePopover();
117
220
  }
118
221
  wrapper.remove();
119
- if (restoreTo instanceof HTMLElement && restoreTo.isConnected) restoreTo.focus();
222
+ restoreFocus(active);
120
223
  resultBox.resolve?.(value);
121
224
  }
225
+ function restoreFocus(active) {
226
+ if (active instanceof HTMLElement && active.isConnected && arbitration.stack.some((other) => other.el.contains(active))) {
227
+ if (document.activeElement !== active) active.focus();
228
+ return;
229
+ }
230
+ const { restoreTo } = surface;
231
+ if (restoreTo instanceof HTMLElement && restoreTo.isConnected)
232
+ restoreTo.focus();
233
+ }
122
234
  function userDismiss() {
123
235
  onDismiss?.();
124
236
  close();
125
237
  }
126
- const wantEscape = triggers.includes("escape");
127
- if (useDialog) {
128
- const onCancel = (event) => {
129
- event.preventDefault();
130
- if (wantEscape) userDismiss();
131
- };
132
- wrapper.addEventListener("cancel", onCancel);
133
- removers.push(() => wrapper.removeEventListener("cancel", onCancel));
134
- } else if (wantEscape || trap) {
135
- const onKeydown = (event) => {
136
- if (!isTopmostFallback()) return;
137
- if (wantEscape && event.key === "Escape") {
138
- event.stopPropagation();
139
- userDismiss();
140
- return;
141
- }
142
- if (trap && event.key === "Tab") {
143
- const items = focusable(wrapper);
144
- if (items.length === 0) {
238
+ try {
239
+ container.appendChild(wrapper);
240
+ disposeMount = mount(
241
+ wrapper,
242
+ typeof content === "function" ? content : () => content
243
+ );
244
+ if (arbitration.stack.push(surface) === 1)
245
+ document.addEventListener("toggle", arbitration.onToggle, true);
246
+ if (useDialog) {
247
+ wrapper.showModal();
248
+ nativeOpened = true;
249
+ rehostAbove(arbitration, wrapper);
250
+ } else if (usePopover) {
251
+ wrapper.showPopover();
252
+ nativeOpened = true;
253
+ wrapper.style.inset = "auto";
254
+ }
255
+ installDismissal();
256
+ if (container.closest(OVERLAY_HOST_SELECTOR) !== null) {
257
+ makeTabReachable();
258
+ removers.push(attach(wrapper, () => close));
259
+ }
260
+ applyInitialFocus();
261
+ } catch (error) {
262
+ close();
263
+ throw error;
264
+ }
265
+ if (liftOverModal) {
266
+ if (!useDialog && !usePopover) devHooks.overlayBlockedByModal?.("hidden");
267
+ else if (usePopover && wrapper.querySelector(FOCUSABLE) !== null)
268
+ devHooks.overlayBlockedByModal?.("inert");
269
+ }
270
+ return { el: wrapper, close, result };
271
+ function installDismissal() {
272
+ const wantEscape = triggers.includes("escape");
273
+ if (useDialog) {
274
+ const onCancel = (event) => {
275
+ event.preventDefault();
276
+ if (wantEscape && owns("escape")) userDismiss();
277
+ };
278
+ wrapper.addEventListener("cancel", onCancel);
279
+ removers.push(() => wrapper.removeEventListener("cancel", onCancel));
280
+ } else if (wantEscape || trap) {
281
+ const onKeydown = (event) => {
282
+ if (wantEscape && event.key === "Escape") {
283
+ if (!owns("escape", event)) return;
284
+ event.stopPropagation();
145
285
  event.preventDefault();
286
+ arbitration.handled.add(event);
287
+ userDismiss();
146
288
  return;
147
289
  }
148
- const first = items[0];
149
- const last = items[items.length - 1];
150
- const active = document.activeElement;
151
- const outside = !wrapper.contains(active);
152
- if (event.shiftKey && (active === first || outside)) {
153
- event.preventDefault();
154
- last.focus();
155
- } else if (!event.shiftKey && (active === last || outside)) {
156
- event.preventDefault();
157
- first.focus();
290
+ if (trap && event.key === "Tab" && owns("trap")) {
291
+ const items = focusable(wrapper);
292
+ if (items.length === 0) {
293
+ event.preventDefault();
294
+ return;
295
+ }
296
+ const first = items[0];
297
+ const last = items[items.length - 1];
298
+ const active = document.activeElement;
299
+ const outside = !wrapper.contains(active);
300
+ if (event.shiftKey && (active === first || outside)) {
301
+ event.preventDefault();
302
+ last.focus();
303
+ } else if (!event.shiftKey && (active === last || outside)) {
304
+ event.preventDefault();
305
+ first.focus();
306
+ }
158
307
  }
159
- }
160
- };
161
- document.addEventListener("keydown", onKeydown, true);
162
- removers.push(() => document.removeEventListener("keydown", onKeydown, true));
163
- }
164
- if (triggers.includes("backdrop")) {
165
- const onClick = (event) => {
166
- if (!isTopmostFallback()) return;
167
- if (event.target === wrapper) userDismiss();
168
- };
169
- wrapper.addEventListener("click", onClick);
170
- removers.push(() => wrapper.removeEventListener("click", onClick));
308
+ };
309
+ document.addEventListener("keydown", onKeydown, true);
310
+ removers.push(
311
+ () => document.removeEventListener("keydown", onKeydown, true)
312
+ );
313
+ }
314
+ if (triggers.includes("backdrop")) {
315
+ const onClick = (event) => {
316
+ if (event.target !== wrapper || !owns("backdrop", event)) return;
317
+ arbitration.handled.add(event);
318
+ userDismiss();
319
+ };
320
+ wrapper.addEventListener("click", onClick);
321
+ removers.push(() => wrapper.removeEventListener("click", onClick));
322
+ }
323
+ if (triggers.includes("outside")) {
324
+ const ignore = outsideIgnore === void 0 ? [] : Array.isArray(outsideIgnore) ? outsideIgnore : [outsideIgnore];
325
+ const onDocClick = (event) => {
326
+ const target = event.target;
327
+ if (target === null) return;
328
+ if (wrapper.contains(target)) return;
329
+ if (ignore.some((el) => el === target || el.contains(target))) return;
330
+ if (!owns("outside", event)) return;
331
+ arbitration.handled.add(event);
332
+ userDismiss();
333
+ };
334
+ document.addEventListener("click", onDocClick, true);
335
+ removers.push(
336
+ () => document.removeEventListener("click", onDocClick, true)
337
+ );
338
+ }
171
339
  }
172
- if (triggers.includes("outside")) {
173
- const ignore = outsideIgnore === void 0 ? [] : Array.isArray(outsideIgnore) ? outsideIgnore : [outsideIgnore];
174
- const onDocClick = (event) => {
175
- if (!isTopmostFallback()) return;
176
- const target = event.target;
177
- if (target === null) return;
178
- if (wrapper.contains(target)) return;
179
- if (ignore.some((el) => el === target || el.contains(target))) return;
180
- userDismiss();
340
+ function makeTabReachable() {
341
+ focusable(wrapper);
342
+ const onTab = (event) => {
343
+ if (event.key === "Tab") focusable(wrapper);
181
344
  };
182
- document.addEventListener("click", onDocClick, true);
183
- removers.push(() => document.removeEventListener("click", onDocClick, true));
345
+ document.addEventListener("keydown", onTab, true);
346
+ removers.push(() => document.removeEventListener("keydown", onTab, true));
184
347
  }
185
- if (initialFocus !== false) {
186
- if (typeof initialFocus === "string") {
187
- wrapper.querySelector(initialFocus)?.focus();
188
- } else {
189
- const first = focusable(wrapper)[0];
190
- if (first !== void 0) {
191
- first.focus();
348
+ function applyInitialFocus() {
349
+ if (initialFocus !== false) {
350
+ if (typeof initialFocus === "string") {
351
+ wrapper.querySelector(initialFocus)?.focus();
192
352
  } else {
193
- wrapper.tabIndex = -1;
194
- wrapper.focus();
353
+ const first = focusable(wrapper)[0];
354
+ if (first !== void 0) {
355
+ first.focus();
356
+ } else {
357
+ wrapper.tabIndex = -1;
358
+ wrapper.focus();
359
+ }
195
360
  }
196
361
  }
197
362
  }
198
- return { el: wrapper, close, result };
363
+ }
364
+ function wireDialog(handle, wire, settle) {
365
+ const disposers = [];
366
+ const failure = [];
367
+ try {
368
+ wire(
369
+ (dispose) => void disposers.push(dispose),
370
+ (action) => () => {
371
+ try {
372
+ action();
373
+ } catch (error) {
374
+ failure[0] = error;
375
+ handle.close(failure);
376
+ }
377
+ }
378
+ );
379
+ } catch (error) {
380
+ disposers.forEach((dispose) => dispose());
381
+ handle.close();
382
+ throw error;
383
+ }
384
+ return handle.result.then((value) => {
385
+ if (value === failure) throw failure[0];
386
+ return settle(value);
387
+ });
199
388
  }
200
389
  function popover(anchor, content, options = {}) {
201
390
  const {
@@ -212,7 +401,7 @@ function popover(anchor, content, options = {}) {
212
401
  } = options;
213
402
  const extraIgnore = outsideIgnore === void 0 ? [] : Array.isArray(outsideIgnore) ? [...outsideIgnore] : [outsideIgnore];
214
403
  const handle = overlay(content, {
215
- container,
404
+ container: container ?? overlayHost(anchor),
216
405
  className,
217
406
  dismiss,
218
407
  trap: false,
@@ -221,8 +410,19 @@ function popover(anchor, content, options = {}) {
221
410
  outsideIgnore: [anchor, ...extraIgnore],
222
411
  native
223
412
  });
224
- const stopReposition = autoReposition(handle.el, anchor, { placement, align, gap });
413
+ let stopReposition;
414
+ try {
415
+ stopReposition = autoReposition(handle.el, anchor, {
416
+ placement,
417
+ align,
418
+ gap
419
+ });
420
+ } catch (error) {
421
+ handle.close();
422
+ throw error;
423
+ }
225
424
  void handle.result.then(stopReposition);
425
+ void handle.result.then(attach(anchor, () => handle.close));
226
426
  return handle;
227
427
  }
228
428
  function tooltip(anchor, content, options = {}) {
@@ -242,16 +442,40 @@ function tooltip(anchor, content, options = {}) {
242
442
  let current;
243
443
  let presence = 0;
244
444
  function show() {
245
- const handle = overlay(body, { container, className, dismiss: false, trap: false, initialFocus: false, native });
445
+ if (!anchor.isConnected) return;
446
+ const handle = overlay(body, {
447
+ container: container ?? overlayHost(anchor),
448
+ className,
449
+ dismiss: false,
450
+ trap: false,
451
+ initialFocus: false,
452
+ native
453
+ });
246
454
  handle.el.setAttribute("role", role);
247
- const stop = autoReposition(handle.el, anchor, { placement, align, gap });
248
- current = { handle, stop };
455
+ let stop;
456
+ try {
457
+ stop = autoReposition(handle.el, anchor, { placement, align, gap });
458
+ } catch (error) {
459
+ handle.close();
460
+ throw error;
461
+ }
462
+ const unwatch = attach(anchor, () => () => {
463
+ presence = 0;
464
+ if (timers.hide !== void 0) clearTimeout(timers.hide);
465
+ hide();
466
+ });
467
+ current = { handle, stop, unwatch };
468
+ void handle.result.then(() => {
469
+ if (current?.handle === handle) hide();
470
+ });
249
471
  }
250
472
  function hide() {
251
- if (current === void 0) return;
252
- current.stop();
253
- current.handle.close();
473
+ const shown = current;
474
+ if (shown === void 0) return;
254
475
  current = void 0;
476
+ shown.unwatch();
477
+ shown.stop();
478
+ shown.handle.close();
255
479
  }
256
480
  const onEnter = (event) => {
257
481
  presence |= event.type === "focus" ? 2 : 1;
@@ -284,7 +508,14 @@ function tooltip(anchor, content, options = {}) {
284
508
 
285
509
  // src/overlay-choice.ts
286
510
  function choice(message, actions, options = {}) {
287
- const { container, className = "kerf-overlay", title, defaultValue, native = false, render } = options;
511
+ const {
512
+ container,
513
+ className = "kerf-overlay",
514
+ title,
515
+ defaultValue,
516
+ native = false,
517
+ render
518
+ } = options;
288
519
  const hasDefault = "defaultValue" in options;
289
520
  const actionAttrs = actions.map((_, i) => ({ "data-choice": String(i) }));
290
521
  const body = render !== void 0 ? render({ message, actions: actionAttrs }) : jsx("div", {
@@ -317,20 +548,28 @@ function choice(message, actions, options = {}) {
317
548
  trap: true,
318
549
  native
319
550
  });
320
- delegate(handle.el, "click", "[data-choice]", (_event, el) => {
321
- resolveChoice(actions[Number(el.getAttribute("data-choice"))].value);
322
- handle.close();
323
- });
324
- if (hasDefault) {
325
- handle.el.addEventListener("keydown", (event) => {
326
- if (event.key === "Enter") {
327
- event.preventDefault();
328
- resolveChoice(defaultValue);
329
- handle.close();
551
+ void wireDialog(
552
+ handle,
553
+ (track) => {
554
+ track(
555
+ delegate(handle.el, "click", "[data-choice]", (_event, el) => {
556
+ resolveChoice(actions[Number(el.getAttribute("data-choice"))].value);
557
+ handle.close();
558
+ })
559
+ );
560
+ if (hasDefault) {
561
+ const onKeydown = (event) => {
562
+ if (event.key === "Enter") {
563
+ event.preventDefault();
564
+ resolveChoice(defaultValue);
565
+ handle.close();
566
+ }
567
+ };
568
+ handle.el.addEventListener("keydown", onKeydown);
330
569
  }
331
- });
332
- }
333
- void handle.result.then(() => resolveChoice(null));
570
+ },
571
+ () => resolveChoice(null)
572
+ );
334
573
  return result;
335
574
  }
336
575
 
@@ -346,7 +585,11 @@ function confirm(message, options = {}) {
346
585
  native = false,
347
586
  render
348
587
  } = options;
349
- const body = render !== void 0 ? render({ message, ok: { "data-confirm": "ok" }, cancel: { "data-confirm": "cancel" } }) : jsx("div", {
588
+ const body = render !== void 0 ? render({
589
+ message,
590
+ ok: { "data-confirm": "ok" },
591
+ cancel: { "data-confirm": "cancel" }
592
+ }) : jsx("div", {
350
593
  class: "kerf-confirm",
351
594
  children: [
352
595
  title !== void 0 ? jsx("h2", { class: "kerf-confirm__title", children: title }) : "",
@@ -354,7 +597,11 @@ function confirm(message, options = {}) {
354
597
  jsx("div", {
355
598
  class: "kerf-confirm__actions",
356
599
  children: [
357
- jsx("button", { type: "button", "data-confirm": "cancel", children: cancelText }),
600
+ jsx("button", {
601
+ type: "button",
602
+ "data-confirm": "cancel",
603
+ children: cancelText
604
+ }),
358
605
  jsx("button", {
359
606
  type: "button",
360
607
  "data-confirm": "ok",
@@ -373,61 +620,75 @@ function confirm(message, options = {}) {
373
620
  trap: true,
374
621
  native
375
622
  });
376
- delegate(handle.el, "click", "[data-confirm]", (_event, el) => {
377
- handle.close(el.getAttribute("data-confirm") === "ok");
378
- });
379
- return handle.result.then((value) => value === true);
623
+ return wireDialog(
624
+ handle,
625
+ () => {
626
+ delegate(handle.el, "click", "[data-confirm]", (_event, el) => {
627
+ handle.close(el.getAttribute("data-confirm") === "ok");
628
+ });
629
+ },
630
+ (value) => value === true
631
+ );
380
632
  }
381
633
 
382
- // src/overlay-form.ts
383
- function form(fields, options = {}) {
634
+ // src/overlay-prompt.ts
635
+ function runValidator(validate, value, owner) {
636
+ const result = validate?.(value);
637
+ if (typeof result?.then === "function") {
638
+ result.then(void 0, () => void 0);
639
+ throw new TypeError(`${owner}: validate returned a promise.`);
640
+ }
641
+ return typeof result === "string" && result.length > 0 ? result : void 0;
642
+ }
643
+ function prompt(message, options = {}) {
384
644
  const {
385
645
  container,
386
646
  className = "kerf-overlay",
387
647
  title,
648
+ defaultValue = "",
649
+ placeholder,
650
+ inputType = "text",
388
651
  okText = "OK",
389
652
  cancelText = "Cancel",
653
+ validate,
390
654
  native = false,
391
655
  render
392
656
  } = options;
393
- const fieldAttrs = (field) => ({
394
- "data-field": field.name,
395
- name: field.name,
396
- type: field.type ?? "text",
397
- value: field.defaultValue ?? "",
398
- ...field.placeholder !== void 0 ? { placeholder: field.placeholder } : {}
399
- });
657
+ const inputAttrs = {
658
+ "data-prompt-input": "",
659
+ type: inputType,
660
+ value: defaultValue,
661
+ ...placeholder !== void 0 ? { placeholder } : {}
662
+ };
400
663
  const body = render !== void 0 ? render({
401
- fields: fields.map((field) => ({
402
- name: field.name,
403
- label: field.label ?? field.name,
404
- input: fieldAttrs(field),
405
- error: { "data-field-error": field.name }
406
- })),
407
- ok: { "data-form": "ok" },
408
- cancel: { "data-form": "cancel" }
664
+ message,
665
+ input: inputAttrs,
666
+ error: { "data-prompt-error": "" },
667
+ ok: { "data-prompt": "ok" },
668
+ cancel: { "data-prompt": "cancel" }
409
669
  }) : jsx("div", {
410
- class: "kerf-form",
670
+ class: "kerf-prompt",
411
671
  children: [
412
- title !== void 0 ? jsx("h2", { class: "kerf-form__title", children: title }) : "",
413
- ...fields.map(
414
- (field) => jsx("div", {
415
- class: "kerf-form__field",
416
- children: [
417
- jsx("label", { class: "kerf-form__label", children: field.label ?? field.name }),
418
- jsx("input", { class: "kerf-form__input", ...fieldAttrs(field) }),
419
- jsx("p", { class: "kerf-form__error", "data-field-error": field.name, children: "" })
420
- ]
421
- })
422
- ),
672
+ title !== void 0 ? jsx("h2", { class: "kerf-prompt__title", children: title }) : "",
673
+ jsx("label", { class: "kerf-prompt__message", children: message }),
674
+ jsx("input", { class: "kerf-prompt__input", ...inputAttrs }),
675
+ jsx("p", {
676
+ class: "kerf-prompt__error",
677
+ "data-prompt-error": "",
678
+ children: ""
679
+ }),
423
680
  jsx("div", {
424
- class: "kerf-form__actions",
681
+ class: "kerf-prompt__actions",
425
682
  children: [
426
- jsx("button", { type: "button", "data-form": "cancel", children: cancelText }),
427
683
  jsx("button", {
428
684
  type: "button",
429
- "data-form": "ok",
430
- class: "kerf-form__ok",
685
+ "data-prompt": "cancel",
686
+ children: cancelText
687
+ }),
688
+ jsx("button", {
689
+ type: "button",
690
+ "data-prompt": "ok",
691
+ class: "kerf-prompt__ok",
431
692
  children: okText
432
693
  })
433
694
  ]
@@ -438,108 +699,120 @@ function form(fields, options = {}) {
438
699
  container,
439
700
  className,
440
701
  dismiss: ["escape", "backdrop"],
441
- initialFocus: "[data-field]",
702
+ initialFocus: "[data-prompt-input]",
442
703
  trap: true,
443
704
  native
444
705
  });
445
- const errorFor = (name) => Array.from(handle.el.querySelectorAll("[data-field-error]")).find(
446
- (el) => el.getAttribute("data-field-error") === name
447
- ) ?? null;
448
- const inputFor = (name) => {
449
- const input = Array.from(handle.el.querySelectorAll("input[data-field]")).find(
450
- (el) => el.getAttribute("data-field") === name
451
- );
452
- if (input !== void 0) return input;
453
- handle.close(null);
454
- throw new Error(`form(): render missing <input data-field="${name}">.`);
455
- };
456
- for (const field of fields) {
457
- inputFor(field.name);
458
- const errorEl = errorFor(field.name);
459
- if (errorEl !== null) errorEl.hidden = true;
460
- }
461
- function attemptOk() {
462
- const record = {};
463
- let firstInvalid = null;
464
- for (const field of fields) {
465
- const el = inputFor(field.name);
466
- const value = el.value;
467
- record[field.name] = value;
468
- const error = field.validate?.(value);
469
- const errorEl = errorFor(field.name);
470
- if (typeof error === "string" && error.length > 0) {
471
- if (errorEl !== null) {
472
- errorEl.textContent = error;
473
- errorEl.hidden = false;
706
+ return wireDialog(
707
+ handle,
708
+ (track, guard) => {
709
+ const inputFor = (problem = "render missing") => {
710
+ const input = handle.el.querySelector(
711
+ "input[data-prompt-input]"
712
+ );
713
+ if (input !== null) return input;
714
+ throw new Error(`prompt(): ${problem} <input data-prompt-input>.`);
715
+ };
716
+ inputFor();
717
+ const errorEl = handle.el.querySelector(
718
+ "[data-prompt-error]"
719
+ );
720
+ if (errorEl !== null) errorEl.hidden = true;
721
+ const attemptOk = guard(() => {
722
+ const input = inputFor("removed after open:");
723
+ const value = input.value;
724
+ const error = runValidator(validate, value, "prompt()");
725
+ if (error !== void 0) {
726
+ if (errorEl !== null) {
727
+ errorEl.textContent = error;
728
+ errorEl.hidden = false;
729
+ }
730
+ input.focus();
731
+ return;
474
732
  }
475
- if (firstInvalid === null) firstInvalid = el;
476
- } else if (errorEl !== null) {
477
- errorEl.hidden = true;
478
- }
479
- }
480
- if (firstInvalid !== null) {
481
- firstInvalid.focus();
482
- return;
483
- }
484
- handle.close(record);
485
- }
486
- delegate(handle.el, "click", "[data-form]", (_event, el) => {
487
- if (el.getAttribute("data-form") === "ok") attemptOk();
488
- else handle.close(null);
489
- });
490
- handle.el.addEventListener("keydown", (event) => {
491
- if (event.key === "Enter" && event.target?.matches("[data-field]")) {
492
- event.preventDefault();
493
- attemptOk();
494
- }
495
- });
496
- return handle.result.then(
497
- (value) => value !== null && typeof value === "object" ? value : null
733
+ handle.close(value);
734
+ });
735
+ track(
736
+ delegate(handle.el, "click", "[data-prompt]", (_event, el) => {
737
+ if (el.getAttribute("data-prompt") === "ok") attemptOk();
738
+ else handle.close(null);
739
+ })
740
+ );
741
+ const onKeydown = (event) => {
742
+ if (event.key === "Enter" && event.target?.matches("input[data-prompt-input]")) {
743
+ event.preventDefault();
744
+ attemptOk();
745
+ }
746
+ };
747
+ handle.el.addEventListener("keydown", onKeydown);
748
+ },
749
+ (value) => typeof value === "string" ? value : null
498
750
  );
499
751
  }
500
752
 
501
- // src/overlay-prompt.ts
502
- function prompt(message, options = {}) {
753
+ // src/overlay-form.ts
754
+ function form(fields, options = {}) {
503
755
  const {
504
756
  container,
505
757
  className = "kerf-overlay",
506
758
  title,
507
- defaultValue = "",
508
- placeholder,
509
- inputType = "text",
510
759
  okText = "OK",
511
760
  cancelText = "Cancel",
512
- validate,
513
761
  native = false,
514
762
  render
515
763
  } = options;
516
- const inputAttrs = {
517
- "data-prompt-input": "",
518
- type: inputType,
519
- value: defaultValue,
520
- ...placeholder !== void 0 ? { placeholder } : {}
521
- };
764
+ const fieldAttrs = (field) => ({
765
+ "data-field": field.name,
766
+ name: field.name,
767
+ type: field.type ?? "text",
768
+ value: field.defaultValue ?? "",
769
+ ...field.placeholder !== void 0 ? { placeholder: field.placeholder } : {}
770
+ });
522
771
  const body = render !== void 0 ? render({
523
- message,
524
- input: inputAttrs,
525
- error: { "data-prompt-error": "" },
526
- ok: { "data-prompt": "ok" },
527
- cancel: { "data-prompt": "cancel" }
772
+ fields: fields.map((field) => ({
773
+ name: field.name,
774
+ label: field.label ?? field.name,
775
+ input: fieldAttrs(field),
776
+ error: { "data-field-error": field.name }
777
+ })),
778
+ ok: { "data-form": "ok" },
779
+ cancel: { "data-form": "cancel" }
528
780
  }) : jsx("div", {
529
- class: "kerf-prompt",
781
+ class: "kerf-form",
530
782
  children: [
531
- title !== void 0 ? jsx("h2", { class: "kerf-prompt__title", children: title }) : "",
532
- jsx("label", { class: "kerf-prompt__message", children: message }),
533
- jsx("input", { class: "kerf-prompt__input", ...inputAttrs }),
534
- jsx("p", { class: "kerf-prompt__error", "data-prompt-error": "", children: "" }),
783
+ title !== void 0 ? jsx("h2", { class: "kerf-form__title", children: title }) : "",
784
+ ...fields.map(
785
+ (field) => jsx("div", {
786
+ class: "kerf-form__field",
787
+ children: [
788
+ jsx("label", {
789
+ class: "kerf-form__label",
790
+ children: field.label ?? field.name
791
+ }),
792
+ jsx("input", {
793
+ class: "kerf-form__input",
794
+ ...fieldAttrs(field)
795
+ }),
796
+ jsx("p", {
797
+ class: "kerf-form__error",
798
+ "data-field-error": field.name,
799
+ children: ""
800
+ })
801
+ ]
802
+ })
803
+ ),
535
804
  jsx("div", {
536
- class: "kerf-prompt__actions",
805
+ class: "kerf-form__actions",
537
806
  children: [
538
- jsx("button", { type: "button", "data-prompt": "cancel", children: cancelText }),
539
807
  jsx("button", {
540
808
  type: "button",
541
- "data-prompt": "ok",
542
- class: "kerf-prompt__ok",
809
+ "data-form": "cancel",
810
+ children: cancelText
811
+ }),
812
+ jsx("button", {
813
+ type: "button",
814
+ "data-form": "ok",
815
+ class: "kerf-form__ok",
543
816
  children: okText
544
817
  })
545
818
  ]
@@ -550,42 +823,69 @@ function prompt(message, options = {}) {
550
823
  container,
551
824
  className,
552
825
  dismiss: ["escape", "backdrop"],
553
- initialFocus: "[data-prompt-input]",
826
+ initialFocus: "[data-field]",
554
827
  trap: true,
555
828
  native
556
829
  });
557
- const input = handle.el.querySelector("input[data-prompt-input]");
558
- if (input === null) {
559
- handle.close(null);
560
- throw new Error("prompt(): render missing <input data-prompt-input>.");
561
- }
562
- const promptInput = input;
563
- const errorEl = handle.el.querySelector("[data-prompt-error]");
564
- if (errorEl !== null) errorEl.hidden = true;
565
- function attemptOk() {
566
- const value = promptInput.value;
567
- const error = validate?.(value);
568
- if (typeof error === "string" && error.length > 0) {
569
- if (errorEl !== null) {
570
- errorEl.textContent = error;
571
- errorEl.hidden = false;
830
+ return wireDialog(
831
+ handle,
832
+ (track, guard) => {
833
+ const errorFor = (name) => Array.from(
834
+ handle.el.querySelectorAll("[data-field-error]")
835
+ ).find((el) => el.getAttribute("data-field-error") === name) ?? null;
836
+ const inputFor = (name, problem = "render missing") => {
837
+ const input = Array.from(
838
+ handle.el.querySelectorAll("input[data-field]")
839
+ ).find((el) => el.getAttribute("data-field") === name);
840
+ if (input !== void 0) return input;
841
+ throw new Error(`form(): ${problem} <input data-field="${name}">.`);
842
+ };
843
+ for (const field of fields) {
844
+ inputFor(field.name);
845
+ const errorEl = errorFor(field.name);
846
+ if (errorEl !== null) errorEl.hidden = true;
572
847
  }
573
- promptInput.focus();
574
- return;
575
- }
576
- handle.close(value);
577
- }
578
- delegate(handle.el, "click", "[data-prompt]", (_event, el) => {
579
- if (el.getAttribute("data-prompt") === "ok") attemptOk();
580
- else handle.close(null);
581
- });
582
- handle.el.addEventListener("keydown", (event) => {
583
- if (event.key === "Enter" && event.target === promptInput) {
584
- event.preventDefault();
585
- attemptOk();
586
- }
587
- });
588
- return handle.result.then((value) => typeof value === "string" ? value : null);
848
+ const attemptOk = guard(() => {
849
+ const record = {};
850
+ let firstInvalid = null;
851
+ for (const field of fields) {
852
+ const el = inputFor(field.name, "removed after open:");
853
+ const value = el.value;
854
+ record[field.name] = value;
855
+ const error = runValidator(field.validate, value, "form()");
856
+ const errorEl = errorFor(field.name);
857
+ if (error !== void 0) {
858
+ if (errorEl !== null) {
859
+ errorEl.textContent = error;
860
+ errorEl.hidden = false;
861
+ }
862
+ if (firstInvalid === null) firstInvalid = el;
863
+ } else if (errorEl !== null) {
864
+ errorEl.hidden = true;
865
+ }
866
+ }
867
+ if (firstInvalid !== null) {
868
+ firstInvalid.focus();
869
+ return;
870
+ }
871
+ handle.close(record);
872
+ });
873
+ track(
874
+ delegate(handle.el, "click", "[data-form]", (_event, el) => {
875
+ if (el.getAttribute("data-form") === "ok") attemptOk();
876
+ else handle.close(null);
877
+ })
878
+ );
879
+ const onKeydown = (event) => {
880
+ if (event.key === "Enter" && event.target?.matches("[data-field]")) {
881
+ event.preventDefault();
882
+ attemptOk();
883
+ }
884
+ };
885
+ handle.el.addEventListener("keydown", onKeydown);
886
+ },
887
+ (value) => value !== null && typeof value === "object" ? value : null
888
+ );
589
889
  }
590
890
 
591
891
  // src/overlay-toast.ts
@@ -628,7 +928,12 @@ function toast(content, options = {}) {
628
928
  region.appendChild(el);
629
929
  const render = typeof content === "function" ? content : () => typeof content === "string" ? escapeHtml(content) : content;
630
930
  const disposeMount = mount(el, render);
631
- const state = { dismissed: false, removed: false, timer: void 0, exitTimer: void 0 };
931
+ const state = {
932
+ dismissed: false,
933
+ removed: false,
934
+ timer: void 0,
935
+ exitTimer: void 0
936
+ };
632
937
  if (enterClass !== void 0) {
633
938
  globalThis.requestAnimationFrame(() => {
634
939
  if (!state.dismissed) el.classList.add(enterClass);