kerfjs 4.4.0 → 5.0.0-beta.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 (73) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/LICENSE +1 -1
  3. package/README.md +34 -35
  4. package/ai/cursorrules +46 -1
  5. package/ai/manifest.json +62 -5
  6. package/ai/skill.md +53 -2
  7. package/dist/actions.d.ts +1 -1
  8. package/dist/actions.js +4 -4
  9. package/dist/actions.js.map +1 -1
  10. package/dist/array-signal.js +5 -5
  11. package/dist/async.js +17 -10
  12. package/dist/async.js.map +1 -1
  13. package/dist/attach.d.ts +11 -8
  14. package/dist/attach.js +52 -4
  15. package/dist/attach.js.map +1 -1
  16. package/dist/chunk-GY4XV2UV.js +1 -1
  17. package/dist/{chunk-VVDJLWMP.js → chunk-HW7KSM2Y.js} +2 -2
  18. package/dist/chunk-HW7KSM2Y.js.map +1 -0
  19. package/dist/{chunk-KEZTD6H4.js → chunk-KPXIOG2C.js} +3 -3
  20. package/dist/{chunk-KEZTD6H4.js.map → chunk-KPXIOG2C.js.map} +1 -1
  21. package/dist/{chunk-SAYPJ6XR.js → chunk-KZJXHFIB.js} +10 -6
  22. package/dist/chunk-KZJXHFIB.js.map +1 -0
  23. package/dist/{chunk-MRYM3O3V.js → chunk-LVH3GC6B.js} +11 -8
  24. package/dist/chunk-LVH3GC6B.js.map +1 -0
  25. package/dist/chunk-QIP723L4.js +1 -1
  26. package/dist/{chunk-SRWQKB33.js → chunk-SVATPF5R.js} +97 -74
  27. package/dist/chunk-SVATPF5R.js.map +1 -0
  28. package/dist/{chunk-3APBEVHF.js → chunk-U6FK33SG.js} +3 -3
  29. package/dist/{chunk-3APBEVHF.js.map → chunk-U6FK33SG.js.map} +1 -1
  30. package/dist/{chunk-U32TFTGZ.js → chunk-UZJ6I4T6.js} +3 -3
  31. package/dist/chunk-UZJ6I4T6.js.map +1 -0
  32. package/dist/{chunk-YHH7OUFA.js → chunk-V46JKE44.js} +3 -3
  33. package/dist/chunk-V46JKE44.js.map +1 -0
  34. package/dist/{chunk-SUPUPSBE.js → chunk-ZDCJZCNO.js} +9 -8
  35. package/dist/chunk-ZDCJZCNO.js.map +1 -0
  36. package/dist/dev.d.ts +9 -6
  37. package/dist/dev.js +5 -5
  38. package/dist/dev.js.map +1 -1
  39. package/dist/html.d.ts +1 -1
  40. package/dist/html.js +5 -5
  41. package/dist/html.js.map +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.js +11 -11
  44. package/dist/jsx-runtime.js +5 -5
  45. package/dist/list.d.ts +1 -1
  46. package/dist/list.js +273 -211
  47. package/dist/list.js.map +1 -1
  48. package/dist/overlay.d.ts +190 -280
  49. package/dist/overlay.js +401 -361
  50. package/dist/overlay.js.map +1 -1
  51. package/dist/remount.d.ts +5 -3
  52. package/dist/remount.js +29 -10
  53. package/dist/remount.js.map +1 -1
  54. package/dist/router.d.ts +1 -1
  55. package/dist/router.js +44 -22
  56. package/dist/router.js.map +1 -1
  57. package/dist/scope.d.ts +4 -3
  58. package/dist/scope.js +10 -10
  59. package/dist/scope.js.map +1 -1
  60. package/dist/testing.js +4 -4
  61. package/dist/timing.d.ts +3 -2
  62. package/dist/timing.js +5 -5
  63. package/dist/timing.js.map +1 -1
  64. package/llms.txt +7 -4
  65. package/package.json +12 -10
  66. package/dist/chunk-MRYM3O3V.js.map +0 -1
  67. package/dist/chunk-SAYPJ6XR.js.map +0 -1
  68. package/dist/chunk-SRWQKB33.js.map +0 -1
  69. package/dist/chunk-SUPUPSBE.js.map +0 -1
  70. package/dist/chunk-U32TFTGZ.js.map +0 -1
  71. package/dist/chunk-VVDJLWMP.js.map +0 -1
  72. package/dist/chunk-YHH7OUFA.js.map +0 -1
  73. /package/dist/{attrSelector-Cmu2ZoGO.d.ts → attr-Cmu2ZoGO.d.ts} +0 -0
package/dist/overlay.js CHANGED
@@ -1,28 +1,28 @@
1
- import { mount } from './chunk-SRWQKB33.js';
2
- import { delegate } from './chunk-KEZTD6H4.js';
3
- import './chunk-QIP723L4.js';
4
- import './chunk-YHH7OUFA.js';
5
- import { jsx } from './chunk-SUPUPSBE.js';
6
- import './chunk-3APBEVHF.js';
7
- import './chunk-GY4XV2UV.js';
8
- import './chunk-VVDJLWMP.js';
1
+ import { mount } from './chunk-SVATPF5R.js';
2
+ import { delegate } from './chunk-KPXIOG2C.js';
3
+
4
+
5
+ import { jsx, escapeHtml } from './chunk-ZDCJZCNO.js';
6
+
7
+
8
+
9
9
 
10
10
  // src/overlay-position.ts
11
11
  function positionAnchored(el, anchor, options = {}) {
12
12
  const { placement = "bottom", align = "start", gap = 4 } = options;
13
13
  el.style.position = "fixed";
14
14
  el.style.margin = "0";
15
- const a = anchor.getBoundingClientRect();
16
- const p = el.getBoundingClientRect();
17
- const vw = window.innerWidth;
18
- const vh = window.innerHeight;
19
- const belowTop = a.bottom + gap;
20
- const aboveTop = a.top - gap - p.height;
15
+ const anchorRect = anchor.getBoundingClientRect();
16
+ const elementRect = el.getBoundingClientRect();
17
+ const viewportWidth = window.innerWidth;
18
+ const viewportHeight = window.innerHeight;
19
+ const belowTop = anchorRect.bottom + gap;
20
+ const aboveTop = anchorRect.top - gap - elementRect.height;
21
21
  let below = placement !== "top";
22
- if (below && belowTop + p.height > vh && aboveTop >= 0) below = false;
23
- else if (!below && aboveTop < 0 && belowTop + p.height <= vh) below = true;
24
- let left = align === "end" ? a.right - p.width : a.left;
25
- left = Math.max(0, Math.min(left, vw - p.width));
22
+ if (below && belowTop + elementRect.height > viewportHeight && aboveTop >= 0) below = false;
23
+ else if (!below && aboveTop < 0 && belowTop + elementRect.height <= viewportHeight) below = true;
24
+ let left = align === "end" ? anchorRect.right - elementRect.width : anchorRect.left;
25
+ left = Math.max(0, Math.min(left, viewportWidth - elementRect.width));
26
26
  el.style.left = `${left}px`;
27
27
  el.style.top = `${below ? belowTop : aboveTop}px`;
28
28
  }
@@ -37,91 +37,328 @@ function autoReposition(el, anchor, options = {}) {
37
37
  };
38
38
  }
39
39
 
40
- // src/overlay-dialogs.ts
41
- function confirm(message, options = {}) {
40
+ // src/overlay-core.ts
41
+ 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
+ function focusable(root) {
43
+ const items = Array.from(root.querySelectorAll(FOCUSABLE)).filter(
44
+ (el) => !el.hasAttribute("hidden") && el.tabIndex >= 0
45
+ );
46
+ for (const el of items) if (!el.hasAttribute("tabindex")) el.tabIndex = 0;
47
+ return items;
48
+ }
49
+ function supportsDialog() {
50
+ return typeof HTMLDialogElement !== "undefined" && typeof HTMLDialogElement.prototype.showModal === "function";
51
+ }
52
+ function supportsPopover() {
53
+ return typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.showPopover === "function";
54
+ }
55
+ var FALLBACK_OVERLAY_STACK = /* @__PURE__ */ Symbol("kerf.fallbackOverlayStack");
56
+ function fallbackOverlayStack(wrapper) {
57
+ const document2 = wrapper.ownerDocument;
58
+ return document2[FALLBACK_OVERLAY_STACK] ??= [];
59
+ }
60
+ function overlay(content, options = {}) {
42
61
  const {
43
- container,
62
+ container = document.body,
44
63
  className = "kerf-overlay",
45
- title,
46
- okText = "OK",
47
- cancelText = "Cancel",
48
- danger = false,
49
- native = false,
50
- render
64
+ dismiss = ["escape", "backdrop"],
65
+ initialFocus = true,
66
+ trap = true,
67
+ role = "dialog",
68
+ onDismiss,
69
+ outsideIgnore,
70
+ native = false
51
71
  } = options;
52
- const body = render !== void 0 ? render({ message, ok: { "data-confirm": "ok" }, cancel: { "data-confirm": "cancel" } }) : jsx("div", {
53
- class: "kerf-confirm",
72
+ 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();
76
+ const wrapper = useDialog ? document.createElement("dialog") : document.createElement("div");
77
+ wrapper.className = className;
78
+ if (usePopover) wrapper.setAttribute("popover", "manual");
79
+ if (trap && !useDialog) {
80
+ wrapper.setAttribute("role", role);
81
+ wrapper.setAttribute("aria-modal", "true");
82
+ }
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
+ }
98
+ const removers = [];
99
+ const resultBox = {};
100
+ const result = new Promise((resolve) => {
101
+ resultBox.resolve = resolve;
102
+ });
103
+ const state = { closed: false };
104
+ function close(value) {
105
+ if (state.closed) return;
106
+ state.closed = true;
107
+ if (fallbackStack !== void 0) {
108
+ const stackIndex = fallbackStack.indexOf(wrapper);
109
+ if (stackIndex !== -1) fallbackStack.splice(stackIndex, 1);
110
+ }
111
+ for (const remove of removers) remove();
112
+ disposeMount();
113
+ if (nativeOpened) {
114
+ nativeOpened = false;
115
+ if (useDialog) wrapper.close();
116
+ else wrapper.hidePopover();
117
+ }
118
+ wrapper.remove();
119
+ if (restoreTo instanceof HTMLElement && restoreTo.isConnected) restoreTo.focus();
120
+ resultBox.resolve?.(value);
121
+ }
122
+ function userDismiss() {
123
+ onDismiss?.();
124
+ close();
125
+ }
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) {
145
+ event.preventDefault();
146
+ return;
147
+ }
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();
158
+ }
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));
171
+ }
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();
181
+ };
182
+ document.addEventListener("click", onDocClick, true);
183
+ removers.push(() => document.removeEventListener("click", onDocClick, true));
184
+ }
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();
192
+ } else {
193
+ wrapper.tabIndex = -1;
194
+ wrapper.focus();
195
+ }
196
+ }
197
+ }
198
+ return { el: wrapper, close, result };
199
+ }
200
+ function popover(anchor, content, options = {}) {
201
+ const {
202
+ container,
203
+ className = "kerf-popover",
204
+ placement = "bottom",
205
+ align = "start",
206
+ gap = 4,
207
+ dismiss = ["outside"],
208
+ initialFocus = false,
209
+ outsideIgnore,
210
+ onDismiss,
211
+ native = false
212
+ } = options;
213
+ const extraIgnore = outsideIgnore === void 0 ? [] : Array.isArray(outsideIgnore) ? [...outsideIgnore] : [outsideIgnore];
214
+ const handle = overlay(content, {
215
+ container,
216
+ className,
217
+ dismiss,
218
+ trap: false,
219
+ initialFocus,
220
+ onDismiss,
221
+ outsideIgnore: [anchor, ...extraIgnore],
222
+ native
223
+ });
224
+ const stopReposition = autoReposition(handle.el, anchor, { placement, align, gap });
225
+ void handle.result.then(stopReposition);
226
+ return handle;
227
+ }
228
+ function tooltip(anchor, content, options = {}) {
229
+ const {
230
+ container,
231
+ className = "kerf-tooltip",
232
+ delay = 400,
233
+ hideDelay = 100,
234
+ role = "tooltip",
235
+ placement = "top",
236
+ align = "start",
237
+ gap = 4,
238
+ native = false
239
+ } = options;
240
+ const body = typeof content === "function" ? content : typeof content === "string" ? jsx("span", { class: `${className}__text`, children: content }) : content;
241
+ const timers = {};
242
+ let current;
243
+ let presence = 0;
244
+ function show() {
245
+ const handle = overlay(body, { container, className, dismiss: false, trap: false, initialFocus: false, native });
246
+ handle.el.setAttribute("role", role);
247
+ const stop = autoReposition(handle.el, anchor, { placement, align, gap });
248
+ current = { handle, stop };
249
+ }
250
+ function hide() {
251
+ if (current === void 0) return;
252
+ current.stop();
253
+ current.handle.close();
254
+ current = void 0;
255
+ }
256
+ const onEnter = (event) => {
257
+ presence |= event.type === "focus" ? 2 : 1;
258
+ if (timers.hide !== void 0) clearTimeout(timers.hide);
259
+ if (current !== void 0) return;
260
+ if (timers.show !== void 0) clearTimeout(timers.show);
261
+ timers.show = setTimeout(show, delay);
262
+ };
263
+ const onLeave = (event) => {
264
+ presence &= event.type === "blur" ? -3 : -2;
265
+ if (presence) return;
266
+ if (timers.show !== void 0) clearTimeout(timers.show);
267
+ if (current === void 0) return;
268
+ timers.hide = setTimeout(hide, hideDelay);
269
+ };
270
+ anchor.addEventListener("pointerenter", onEnter);
271
+ anchor.addEventListener("pointerleave", onLeave);
272
+ anchor.addEventListener("focus", onEnter);
273
+ anchor.addEventListener("blur", onLeave);
274
+ return () => {
275
+ anchor.removeEventListener("pointerenter", onEnter);
276
+ anchor.removeEventListener("pointerleave", onLeave);
277
+ anchor.removeEventListener("focus", onEnter);
278
+ anchor.removeEventListener("blur", onLeave);
279
+ if (timers.show !== void 0) clearTimeout(timers.show);
280
+ if (timers.hide !== void 0) clearTimeout(timers.hide);
281
+ hide();
282
+ };
283
+ }
284
+
285
+ // src/overlay-choice.ts
286
+ function choice(message, actions, options = {}) {
287
+ const { container, className = "kerf-overlay", title, defaultValue, native = false, render } = options;
288
+ const hasDefault = "defaultValue" in options;
289
+ const actionAttrs = actions.map((_, i) => ({ "data-choice": String(i) }));
290
+ const body = render !== void 0 ? render({ message, actions: actionAttrs }) : jsx("div", {
291
+ class: "kerf-choice",
54
292
  children: [
55
- title !== void 0 ? jsx("h2", { class: "kerf-confirm__title", children: title }) : "",
56
- jsx("p", { class: "kerf-confirm__message", children: message }),
293
+ title !== void 0 ? jsx("h2", { class: "kerf-choice__title", children: title }) : "",
294
+ jsx("p", { class: "kerf-choice__message", children: message }),
57
295
  jsx("div", {
58
- class: "kerf-confirm__actions",
59
- children: [
60
- jsx("button", { type: "button", "data-confirm": "cancel", children: cancelText }),
61
- jsx("button", {
296
+ class: "kerf-choice__actions",
297
+ children: actions.map(
298
+ (action, i) => jsx("button", {
62
299
  type: "button",
63
- "data-confirm": "ok",
64
- class: "kerf-confirm__ok",
65
- children: okText
300
+ class: action.className !== void 0 ? `kerf-choice__action ${action.className}` : "kerf-choice__action",
301
+ ...actionAttrs[i],
302
+ children: action.label
66
303
  })
67
- ]
304
+ )
68
305
  })
69
306
  ]
70
307
  });
308
+ let resolveChoice;
309
+ const result = new Promise((resolve) => {
310
+ resolveChoice = resolve;
311
+ });
71
312
  const handle = overlay(body, {
72
313
  container,
73
- className: danger ? `${className} kerf-confirm--danger` : className,
314
+ className,
74
315
  dismiss: ["escape", "backdrop"],
75
- initialFocus: '[data-confirm="ok"]',
316
+ initialFocus: "[data-choice]",
76
317
  trap: true,
77
318
  native
78
319
  });
79
- delegate(handle.el, "click", "[data-confirm]", (_event, el) => {
80
- handle.close(el.getAttribute("data-confirm") === "ok");
320
+ delegate(handle.el, "click", "[data-choice]", (_event, el) => {
321
+ resolveChoice(actions[Number(el.getAttribute("data-choice"))].value);
322
+ handle.close();
81
323
  });
82
- return handle.result.then((value) => value === true);
324
+ if (hasDefault) {
325
+ handle.el.addEventListener("keydown", (event) => {
326
+ if (event.key === "Enter") {
327
+ event.preventDefault();
328
+ resolveChoice(defaultValue);
329
+ handle.close();
330
+ }
331
+ });
332
+ }
333
+ void handle.result.then(() => resolveChoice(null));
334
+ return result;
83
335
  }
84
- function prompt(message, options = {}) {
336
+
337
+ // src/overlay-confirm.ts
338
+ function confirm(message, options = {}) {
85
339
  const {
86
340
  container,
87
341
  className = "kerf-overlay",
88
342
  title,
89
- defaultValue = "",
90
- placeholder,
91
- inputType = "text",
92
343
  okText = "OK",
93
344
  cancelText = "Cancel",
94
- validate,
345
+ danger = false,
95
346
  native = false,
96
347
  render
97
348
  } = options;
98
- const inputAttrs = {
99
- "data-prompt-input": "",
100
- type: inputType,
101
- value: defaultValue,
102
- ...placeholder !== void 0 ? { placeholder } : {}
103
- };
104
- const body = render !== void 0 ? render({
105
- message,
106
- input: inputAttrs,
107
- error: { "data-prompt-error": "" },
108
- ok: { "data-prompt": "ok" },
109
- cancel: { "data-prompt": "cancel" }
110
- }) : jsx("div", {
111
- class: "kerf-prompt",
349
+ const body = render !== void 0 ? render({ message, ok: { "data-confirm": "ok" }, cancel: { "data-confirm": "cancel" } }) : jsx("div", {
350
+ class: "kerf-confirm",
112
351
  children: [
113
- title !== void 0 ? jsx("h2", { class: "kerf-prompt__title", children: title }) : "",
114
- jsx("label", { class: "kerf-prompt__message", children: message }),
115
- jsx("input", { class: "kerf-prompt__input", ...inputAttrs }),
116
- jsx("p", { class: "kerf-prompt__error", "data-prompt-error": "", children: "" }),
352
+ title !== void 0 ? jsx("h2", { class: "kerf-confirm__title", children: title }) : "",
353
+ jsx("p", { class: "kerf-confirm__message", children: message }),
117
354
  jsx("div", {
118
- class: "kerf-prompt__actions",
355
+ class: "kerf-confirm__actions",
119
356
  children: [
120
- jsx("button", { type: "button", "data-prompt": "cancel", children: cancelText }),
357
+ jsx("button", { type: "button", "data-confirm": "cancel", children: cancelText }),
121
358
  jsx("button", {
122
359
  type: "button",
123
- "data-prompt": "ok",
124
- class: "kerf-prompt__ok",
360
+ "data-confirm": "ok",
361
+ class: "kerf-confirm__ok",
125
362
  children: okText
126
363
  })
127
364
  ]
@@ -130,40 +367,19 @@ function prompt(message, options = {}) {
130
367
  });
131
368
  const handle = overlay(body, {
132
369
  container,
133
- className,
370
+ className: danger ? `${className} kerf-confirm--danger` : className,
134
371
  dismiss: ["escape", "backdrop"],
135
- initialFocus: "[data-prompt-input]",
372
+ initialFocus: '[data-confirm="ok"]',
136
373
  trap: true,
137
374
  native
138
375
  });
139
- const input = handle.el.querySelector("[data-prompt-input]");
140
- const errorEl = handle.el.querySelector("[data-prompt-error]");
141
- if (errorEl !== null) errorEl.hidden = true;
142
- function attemptOk() {
143
- const value = input.value;
144
- const error = validate?.(value);
145
- if (typeof error === "string" && error.length > 0) {
146
- if (errorEl !== null) {
147
- errorEl.textContent = error;
148
- errorEl.hidden = false;
149
- }
150
- input.focus();
151
- return;
152
- }
153
- handle.close(value);
154
- }
155
- delegate(handle.el, "click", "[data-prompt]", (_event, el) => {
156
- if (el.getAttribute("data-prompt") === "ok") attemptOk();
157
- else handle.close(null);
158
- });
159
- handle.el.addEventListener("keydown", (event) => {
160
- if (event.key === "Enter" && event.target === input) {
161
- event.preventDefault();
162
- attemptOk();
163
- }
376
+ delegate(handle.el, "click", "[data-confirm]", (_event, el) => {
377
+ handle.close(el.getAttribute("data-confirm") === "ok");
164
378
  });
165
- return handle.result.then((value) => typeof value === "string" ? value : null);
379
+ return handle.result.then((value) => value === true);
166
380
  }
381
+
382
+ // src/overlay-form.ts
167
383
  function form(fields, options = {}) {
168
384
  const {
169
385
  container,
@@ -226,13 +442,19 @@ function form(fields, options = {}) {
226
442
  trap: true,
227
443
  native
228
444
  });
229
- const byAttr = (attr, name) => Array.from(handle.el.querySelectorAll(`[${attr}]`)).find(
230
- (el) => el.getAttribute(attr) === name
231
- );
232
445
  const errorFor = (name) => Array.from(handle.el.querySelectorAll("[data-field-error]")).find(
233
446
  (el) => el.getAttribute("data-field-error") === name
234
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
+ };
235
456
  for (const field of fields) {
457
+ inputFor(field.name);
236
458
  const errorEl = errorFor(field.name);
237
459
  if (errorEl !== null) errorEl.hidden = true;
238
460
  }
@@ -240,7 +462,7 @@ function form(fields, options = {}) {
240
462
  const record = {};
241
463
  let firstInvalid = null;
242
464
  for (const field of fields) {
243
- const el = byAttr("data-field", field.name);
465
+ const el = inputFor(field.name);
244
466
  const value = el.value;
245
467
  record[field.name] = value;
246
468
  const error = field.validate?.(value);
@@ -275,55 +497,95 @@ function form(fields, options = {}) {
275
497
  (value) => value !== null && typeof value === "object" ? value : null
276
498
  );
277
499
  }
278
- function choice(message, actions, options = {}) {
279
- const { container, className = "kerf-overlay", title, defaultValue, native = false, render } = options;
280
- const hasDefault = "defaultValue" in options;
281
- const actionAttrs = actions.map((_, i) => ({ "data-choice": String(i) }));
282
- const body = render !== void 0 ? render({ message, actions: actionAttrs }) : jsx("div", {
283
- class: "kerf-choice",
500
+
501
+ // src/overlay-prompt.ts
502
+ function prompt(message, options = {}) {
503
+ const {
504
+ container,
505
+ className = "kerf-overlay",
506
+ title,
507
+ defaultValue = "",
508
+ placeholder,
509
+ inputType = "text",
510
+ okText = "OK",
511
+ cancelText = "Cancel",
512
+ validate,
513
+ native = false,
514
+ render
515
+ } = options;
516
+ const inputAttrs = {
517
+ "data-prompt-input": "",
518
+ type: inputType,
519
+ value: defaultValue,
520
+ ...placeholder !== void 0 ? { placeholder } : {}
521
+ };
522
+ 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" }
528
+ }) : jsx("div", {
529
+ class: "kerf-prompt",
284
530
  children: [
285
- title !== void 0 ? jsx("h2", { class: "kerf-choice__title", children: title }) : "",
286
- jsx("p", { class: "kerf-choice__message", children: message }),
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: "" }),
287
535
  jsx("div", {
288
- class: "kerf-choice__actions",
289
- children: actions.map(
290
- (action, i) => jsx("button", {
536
+ class: "kerf-prompt__actions",
537
+ children: [
538
+ jsx("button", { type: "button", "data-prompt": "cancel", children: cancelText }),
539
+ jsx("button", {
291
540
  type: "button",
292
- class: action.className !== void 0 ? `kerf-choice__action ${action.className}` : "kerf-choice__action",
293
- ...actionAttrs[i],
294
- children: action.label
541
+ "data-prompt": "ok",
542
+ class: "kerf-prompt__ok",
543
+ children: okText
295
544
  })
296
- )
545
+ ]
297
546
  })
298
547
  ]
299
548
  });
300
- let resolveChoice;
301
- const result = new Promise((resolve) => {
302
- resolveChoice = resolve;
303
- });
304
549
  const handle = overlay(body, {
305
550
  container,
306
551
  className,
307
552
  dismiss: ["escape", "backdrop"],
308
- initialFocus: "[data-choice]",
553
+ initialFocus: "[data-prompt-input]",
309
554
  trap: true,
310
555
  native
311
556
  });
312
- delegate(handle.el, "click", "[data-choice]", (_event, el) => {
313
- resolveChoice(actions[Number(el.getAttribute("data-choice"))].value);
314
- handle.close();
315
- });
316
- if (hasDefault) {
317
- handle.el.addEventListener("keydown", (event) => {
318
- if (event.key === "Enter") {
319
- event.preventDefault();
320
- resolveChoice(defaultValue);
321
- handle.close();
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;
322
572
  }
323
- });
573
+ promptInput.focus();
574
+ return;
575
+ }
576
+ handle.close(value);
324
577
  }
325
- void handle.result.then(() => resolveChoice(null));
326
- return result;
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);
327
589
  }
328
590
 
329
591
  // src/overlay-toast.ts
@@ -364,7 +626,8 @@ function toast(content, options = {}) {
364
626
  if (variant !== void 0) el.classList.add(`${className}--${variant}`);
365
627
  el.setAttribute("role", role);
366
628
  region.appendChild(el);
367
- const disposeMount = mount(el, typeof content === "function" ? content : () => content);
629
+ const render = typeof content === "function" ? content : () => typeof content === "string" ? escapeHtml(content) : content;
630
+ const disposeMount = mount(el, render);
368
631
  const state = { dismissed: false, removed: false, timer: void 0, exitTimer: void 0 };
369
632
  if (enterClass !== void 0) {
370
633
  globalThis.requestAnimationFrame(() => {
@@ -406,229 +669,6 @@ function toast(content, options = {}) {
406
669
  return { el, dismiss };
407
670
  }
408
671
 
409
- // src/overlay.ts
410
- 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"]';
411
- function focusable(root) {
412
- return Array.from(root.querySelectorAll(FOCUSABLE)).filter(
413
- (el) => !el.hasAttribute("hidden")
414
- );
415
- }
416
- function supportsDialog() {
417
- return typeof HTMLDialogElement !== "undefined" && typeof HTMLDialogElement.prototype.showModal === "function";
418
- }
419
- function supportsPopover() {
420
- return typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.showPopover === "function";
421
- }
422
- function overlay(content, options = {}) {
423
- const {
424
- container = document.body,
425
- className = "kerf-overlay",
426
- dismiss = ["escape", "backdrop"],
427
- initialFocus = true,
428
- trap = true,
429
- role = "dialog",
430
- onDismiss,
431
- outsideIgnore,
432
- native = false
433
- } = options;
434
- const triggers = dismiss === false ? [] : Array.isArray(dismiss) ? dismiss : [dismiss];
435
- const restoreTo = document.activeElement;
436
- const useDialog = native && trap && supportsDialog();
437
- const usePopover = native && !trap && supportsPopover();
438
- const wrapper = useDialog ? document.createElement("dialog") : document.createElement("div");
439
- wrapper.className = className;
440
- if (usePopover) wrapper.setAttribute("popover", "manual");
441
- if (trap && !useDialog) {
442
- wrapper.setAttribute("role", role);
443
- wrapper.setAttribute("aria-modal", "true");
444
- }
445
- container.appendChild(wrapper);
446
- const disposeMount = mount(wrapper, typeof content === "function" ? content : () => content);
447
- let nativeOpened = false;
448
- if (useDialog) {
449
- wrapper.showModal();
450
- nativeOpened = true;
451
- } else if (usePopover) {
452
- wrapper.showPopover();
453
- wrapper.style.inset = "auto";
454
- nativeOpened = true;
455
- }
456
- const removers = [];
457
- const resultBox = {};
458
- const result = new Promise((resolve) => {
459
- resultBox.resolve = resolve;
460
- });
461
- const state = { closed: false };
462
- function close(value) {
463
- if (state.closed) return;
464
- state.closed = true;
465
- for (const remove of removers) remove();
466
- disposeMount();
467
- if (nativeOpened) {
468
- nativeOpened = false;
469
- if (useDialog) wrapper.close();
470
- else wrapper.hidePopover();
471
- }
472
- wrapper.remove();
473
- if (restoreTo instanceof HTMLElement && restoreTo.isConnected) restoreTo.focus();
474
- resultBox.resolve?.(value);
475
- }
476
- function userDismiss() {
477
- onDismiss?.();
478
- close();
479
- }
480
- const wantEscape = triggers.includes("escape");
481
- if (useDialog) {
482
- const onCancel = (event) => {
483
- event.preventDefault();
484
- if (wantEscape) userDismiss();
485
- };
486
- wrapper.addEventListener("cancel", onCancel);
487
- removers.push(() => wrapper.removeEventListener("cancel", onCancel));
488
- } else if (wantEscape || trap) {
489
- const onKeydown = (event) => {
490
- if (wantEscape && event.key === "Escape") {
491
- event.stopPropagation();
492
- userDismiss();
493
- return;
494
- }
495
- if (trap && event.key === "Tab") {
496
- const items = focusable(wrapper);
497
- if (items.length === 0) {
498
- event.preventDefault();
499
- return;
500
- }
501
- const first = items[0];
502
- const last = items[items.length - 1];
503
- const active = document.activeElement;
504
- const outside = !wrapper.contains(active);
505
- if (event.shiftKey && (active === first || outside)) {
506
- event.preventDefault();
507
- last.focus();
508
- } else if (!event.shiftKey && (active === last || outside)) {
509
- event.preventDefault();
510
- first.focus();
511
- }
512
- }
513
- };
514
- document.addEventListener("keydown", onKeydown, true);
515
- removers.push(() => document.removeEventListener("keydown", onKeydown, true));
516
- }
517
- if (triggers.includes("backdrop")) {
518
- const onClick = (event) => {
519
- if (event.target === wrapper) userDismiss();
520
- };
521
- wrapper.addEventListener("click", onClick);
522
- removers.push(() => wrapper.removeEventListener("click", onClick));
523
- }
524
- if (triggers.includes("outside")) {
525
- const ignore = outsideIgnore === void 0 ? [] : Array.isArray(outsideIgnore) ? outsideIgnore : [outsideIgnore];
526
- const onDocClick = (event) => {
527
- const target = event.target;
528
- if (target === null) return;
529
- if (wrapper.contains(target)) return;
530
- if (ignore.some((el) => el === target || el.contains(target))) return;
531
- userDismiss();
532
- };
533
- document.addEventListener("click", onDocClick, true);
534
- removers.push(() => document.removeEventListener("click", onDocClick, true));
535
- }
536
- if (initialFocus !== false) {
537
- if (typeof initialFocus === "string") {
538
- wrapper.querySelector(initialFocus)?.focus();
539
- } else {
540
- const first = focusable(wrapper)[0];
541
- if (first !== void 0) {
542
- first.focus();
543
- } else {
544
- wrapper.tabIndex = -1;
545
- wrapper.focus();
546
- }
547
- }
548
- }
549
- return { el: wrapper, close, result };
550
- }
551
- function popover(anchor, content, options = {}) {
552
- const {
553
- container,
554
- className = "kerf-popover",
555
- placement = "bottom",
556
- align = "start",
557
- gap = 4,
558
- dismiss = ["outside"],
559
- initialFocus = false,
560
- outsideIgnore,
561
- onDismiss,
562
- native = false
563
- } = options;
564
- const extraIgnore = outsideIgnore === void 0 ? [] : Array.isArray(outsideIgnore) ? [...outsideIgnore] : [outsideIgnore];
565
- const handle = overlay(content, {
566
- container,
567
- className,
568
- dismiss,
569
- trap: false,
570
- initialFocus,
571
- onDismiss,
572
- outsideIgnore: [anchor, ...extraIgnore],
573
- native
574
- });
575
- const stopReposition = autoReposition(handle.el, anchor, { placement, align, gap });
576
- void handle.result.then(stopReposition);
577
- return handle;
578
- }
579
- function tooltip(anchor, content, options = {}) {
580
- const {
581
- container,
582
- className = "kerf-tooltip",
583
- delay = 400,
584
- hideDelay = 100,
585
- role = "tooltip",
586
- placement = "top",
587
- align = "start",
588
- gap = 4,
589
- native = false
590
- } = options;
591
- const body = typeof content === "function" ? content : typeof content === "string" ? jsx("span", { class: `${className}__text`, children: content }) : content;
592
- const timers = {};
593
- let current;
594
- function show() {
595
- const handle = overlay(body, { container, className, dismiss: false, trap: false, initialFocus: false, native });
596
- handle.el.setAttribute("role", role);
597
- const stop = autoReposition(handle.el, anchor, { placement, align, gap });
598
- current = { handle, stop };
599
- }
600
- function hide() {
601
- if (current === void 0) return;
602
- current.stop();
603
- current.handle.close();
604
- current = void 0;
605
- }
606
- const onEnter = () => {
607
- if (timers.hide !== void 0) clearTimeout(timers.hide);
608
- if (current !== void 0) return;
609
- if (timers.show !== void 0) clearTimeout(timers.show);
610
- timers.show = setTimeout(show, delay);
611
- };
612
- const onLeave = () => {
613
- if (timers.show !== void 0) clearTimeout(timers.show);
614
- if (current === void 0) return;
615
- timers.hide = setTimeout(hide, hideDelay);
616
- };
617
- anchor.addEventListener("pointerenter", onEnter);
618
- anchor.addEventListener("pointerleave", onLeave);
619
- anchor.addEventListener("focus", onEnter);
620
- anchor.addEventListener("blur", onLeave);
621
- return () => {
622
- anchor.removeEventListener("pointerenter", onEnter);
623
- anchor.removeEventListener("pointerleave", onLeave);
624
- anchor.removeEventListener("focus", onEnter);
625
- anchor.removeEventListener("blur", onLeave);
626
- if (timers.show !== void 0) clearTimeout(timers.show);
627
- if (timers.hide !== void 0) clearTimeout(timers.hide);
628
- hide();
629
- };
630
- }
631
-
632
672
  export { autoReposition, choice, confirm, form, overlay, popover, positionAnchored, prompt, toast, tooltip };
633
- //# sourceMappingURL=overlay.js.map
673
+
634
674
  //# sourceMappingURL=overlay.js.map