kerfjs 4.2.0-beta.1 → 4.2.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.
package/dist/overlay.d.ts CHANGED
@@ -53,6 +53,19 @@ interface OverlayHandle {
53
53
  * handle. See {@link OverlayOptions}.
54
54
  */
55
55
  declare function overlay(content: OverlayContent, options?: OverlayOptions): OverlayHandle;
56
+ /**
57
+ * Wiring slots passed to a {@link ConfirmOptions.render} — spread `ok` / `cancel`
58
+ * onto your own clickable elements so `confirm()` still resolves them (they are
59
+ * `data-confirm` attribute bags). `message` is the raw message (escape it by
60
+ * interpolating through JSX).
61
+ */
62
+ interface ConfirmRenderSlots {
63
+ message: string;
64
+ /** Spread onto the confirm control. */
65
+ ok: Record<string, string>;
66
+ /** Spread onto the cancel control. */
67
+ cancel: Record<string, string>;
68
+ }
56
69
  /** Options for {@link confirm}. */
57
70
  interface ConfirmOptions {
58
71
  /** Where to append the overlay. Default `document.body`. */
@@ -67,16 +80,227 @@ interface ConfirmOptions {
67
80
  cancelText?: string;
68
81
  /** Add a `kerf-confirm--danger` class to the wrapper for destructive actions. */
69
82
  danger?: boolean;
83
+ /**
84
+ * Bring your own markup (design-system dialogs): return the full dialog body,
85
+ * spreading the provided `ok`/`cancel` wiring onto your buttons. Overrides the
86
+ * default two-button markup; `confirm()` keeps owning dismiss / focus-trap /
87
+ * focus-restore and still resolves `true`/`false` for OK/Cancel/dismissal.
88
+ */
89
+ render?: (slots: ConfirmRenderSlots) => OverlayContent;
70
90
  }
71
91
  /**
72
92
  * A promise-based `window.confirm` replacement (that global is a no-op in Tauri
73
93
  * webviews). Renders a two-button dialog and resolves `true` for OK, `false`
74
94
  * for Cancel or any dismissal (Escape / backdrop). Message + labels are
75
- * auto-escaped (rendered through the JSX runtime).
95
+ * auto-escaped (rendered through the JSX runtime). Pass `render` for your own markup.
76
96
  */
77
97
  declare function confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
98
+ /**
99
+ * Validate a single field's value. Return a non-empty error string to BLOCK
100
+ * submission (shown inline next to the field); return `undefined`/`null`/`''` to
101
+ * allow it.
102
+ */
103
+ type FieldValidator = (value: string) => string | null | undefined | void;
104
+ /** Options for {@link prompt}. */
105
+ interface PromptOptions {
106
+ /** Where to append the overlay. Default `document.body`. */
107
+ container?: Element;
108
+ /** Wrapper class. Default `'kerf-overlay'`. */
109
+ className?: string;
110
+ /** Optional heading above the message. */
111
+ title?: string;
112
+ /** Pre-filled input value. Default `''`. */
113
+ defaultValue?: string;
114
+ /** Input placeholder. */
115
+ placeholder?: string;
116
+ /** `type` attribute of the input (`'text'`, `'email'`, `'password'`, …). Default `'text'`. */
117
+ inputType?: string;
118
+ /** Confirm button label. Default `'OK'`. */
119
+ okText?: string;
120
+ /** Cancel button label. Default `'Cancel'`. */
121
+ cancelText?: string;
122
+ /** Block OK while this returns an error string; the message shows inline. */
123
+ validate?: FieldValidator;
124
+ /**
125
+ * Bring your own markup: return the full dialog body, spreading the provided
126
+ * `input` (the text field), `ok`/`cancel` (buttons), and optional `error` (the
127
+ * inline-error slot) wiring. `prompt()` still reads the input, runs `validate`,
128
+ * submits on Enter, and owns dismiss / focus. If you omit the `error` slot,
129
+ * `validate` simply re-focuses the input without an inline message.
130
+ */
131
+ render?: (slots: PromptRenderSlots) => OverlayContent;
132
+ }
133
+ /** Wiring slots for a {@link PromptOptions.render} — spread each onto your own markup. */
134
+ interface PromptRenderSlots {
135
+ message: string;
136
+ /** Spread onto your `<input>` — carries the marker, `type`, `value`, and `placeholder`. */
137
+ input: Record<string, string>;
138
+ /** Spread onto your inline-error element (optional). */
139
+ error: Record<string, string>;
140
+ /** Spread onto the confirm control. */
141
+ ok: Record<string, string>;
142
+ /** Spread onto the cancel control. */
143
+ cancel: Record<string, string>;
144
+ }
145
+ /**
146
+ * A promise-based `window.prompt` replacement (that global is a no-op in Tauri
147
+ * webviews). Renders a one-field dialog and resolves the entered **string** on OK
148
+ * (an empty string is a valid result) or `null` on Cancel / dismissal. Enter in
149
+ * the input submits. `message`, the default value, and labels are auto-escaped
150
+ * (rendered through the JSX runtime). Optional `validate` blocks OK inline. Pass
151
+ * `render` for your own markup.
152
+ */
153
+ declare function prompt(message: string, options?: PromptOptions): Promise<string | null>;
154
+ /** A single field in a {@link form}. */
155
+ interface FormField {
156
+ /** Field name — the key in the resolved record (and the input's `name`). */
157
+ name: string;
158
+ /** Label shown above the input. Defaults to `name`. */
159
+ label?: string;
160
+ /** Pre-filled value. Default `''`. */
161
+ defaultValue?: string;
162
+ /** Input placeholder. */
163
+ placeholder?: string;
164
+ /** `type` attribute of the input. Default `'text'`. */
165
+ type?: string;
166
+ /** Block OK while this returns an error string; the message shows inline for this field. */
167
+ validate?: FieldValidator;
168
+ }
169
+ /** One field's wiring in a {@link FormRenderSlots} — spread `input`/`error` onto your markup. */
170
+ interface FormRenderField {
171
+ name: string;
172
+ label: string;
173
+ /** Spread onto your `<input>` — carries the marker, `name`, `type`, `value`, `placeholder`. */
174
+ input: Record<string, string>;
175
+ /** Spread onto your inline-error element (optional). */
176
+ error: Record<string, string>;
177
+ }
178
+ /** Wiring slots for a {@link FormOptions.render}. */
179
+ interface FormRenderSlots {
180
+ fields: FormRenderField[];
181
+ /** Spread onto the confirm control. */
182
+ ok: Record<string, string>;
183
+ /** Spread onto the cancel control. */
184
+ cancel: Record<string, string>;
185
+ }
186
+ /** Options for {@link form}. */
187
+ interface FormOptions {
188
+ /** Where to append the overlay. Default `document.body`. */
189
+ container?: Element;
190
+ /** Wrapper class. Default `'kerf-overlay'`. */
191
+ className?: string;
192
+ /** Optional heading above the fields. */
193
+ title?: string;
194
+ /** Confirm button label. Default `'OK'`. */
195
+ okText?: string;
196
+ /** Cancel button label. Default `'Cancel'`. */
197
+ cancelText?: string;
198
+ /**
199
+ * Bring your own markup: return the full form body, laying out `slots.fields`
200
+ * (each with `input`/`error` wiring to spread) and the `ok`/`cancel` buttons.
201
+ * `form()` still reads each input, runs per-field `validate`, focuses the first
202
+ * invalid field, submits on Enter, and owns dismiss / focus. Omit a field's
203
+ * `error` slot to skip its inline message.
204
+ */
205
+ render?: (slots: FormRenderSlots) => OverlayContent;
206
+ }
207
+ /**
208
+ * A promise-based multi-field dialog — the two-or-three-input sibling of
209
+ * {@link prompt}. Renders one labeled input per {@link FormField} and resolves a
210
+ * `Record<name, value>` on OK (after every field's `validate` passes) or `null`
211
+ * on Cancel / dismissal. Enter in any field submits. All labels, defaults, and
212
+ * the title are auto-escaped through the JSX runtime.
213
+ */
214
+ declare function form(fields: readonly FormField[], options?: FormOptions): Promise<Record<string, string> | null>;
215
+ /** Vertical placement relative to an anchor (used by {@link popover}, {@link positionAnchored}, {@link tooltip}). */
216
+ type PopoverPlacement = 'bottom' | 'top';
217
+ /** Placement options for {@link positionAnchored} / {@link autoReposition}. */
218
+ interface AnchorPositionOptions {
219
+ /** Preferred side of the anchor; flips to the other side if it would overflow the viewport. Default `'bottom'`. */
220
+ placement?: PopoverPlacement;
221
+ /** Horizontal edge to line up with the anchor: `'start'` (left edges) or `'end'` (right edges). Default `'start'`. */
222
+ align?: 'start' | 'end';
223
+ /** Gap in px between the anchor and the element. Default `4`. */
224
+ gap?: number;
225
+ }
226
+ /**
227
+ * One-shot: position `el` relative to `anchor` — below by default, flipping above
228
+ * if it would overflow the viewport, aligned to a horizontal edge and clamped into
229
+ * view. Sets `el.style` `position: fixed`, `margin: 0`, `left`, and `top` (fixed so
230
+ * `left`/`top` are viewport coordinates, matching `getBoundingClientRect`). This is
231
+ * `popover()`'s placement core, usable on any element (an inline hint, a tooltip) —
232
+ * no overlay lifecycle. Pair with {@link autoReposition} to keep it glued while open.
233
+ */
234
+ declare function positionAnchored(el: HTMLElement, anchor: Element, options?: AnchorPositionOptions): void;
235
+ /**
236
+ * Keep `el` positioned against `anchor` (via {@link positionAnchored}) as the page
237
+ * scrolls or resizes. Positions once immediately, then re-runs on `scroll`
238
+ * (capture phase — catches scrolls in any inner container, not just `window`) and
239
+ * `resize`. Returns a disposer that removes the listeners.
240
+ */
241
+ declare function autoReposition(el: HTMLElement, anchor: Element, options?: AnchorPositionOptions): () => void;
242
+ /** Options for {@link popover}. */
243
+ interface PopoverOptions {
244
+ /** Where to append the popover wrapper. Default `document.body`. */
245
+ container?: Element;
246
+ /** Class on the wrapper. Default `'kerf-popover'`. */
247
+ className?: string;
248
+ /** Preferred side of the anchor. Flips to the other side if it would overflow the viewport. Default `'bottom'`. */
249
+ placement?: PopoverPlacement;
250
+ /** Horizontal edge to line up with the anchor: `'start'` (left edges) or `'end'` (right edges). Default `'start'`. */
251
+ align?: 'start' | 'end';
252
+ /** Gap in px between the anchor and the popover. Default `4`. */
253
+ gap?: number;
254
+ /**
255
+ * Which user actions dismiss the popover. Default `['outside']` (a click
256
+ * outside the popover, the anchor exempt). Pass `false` to close only via `close()`.
257
+ */
258
+ dismiss?: DismissTrigger | DismissTrigger[] | false;
259
+ /** Focus behavior on open. Default `false` (non-modal — leave focus alone). */
260
+ initialFocus?: string | boolean;
261
+ /** Extra elements (besides the anchor) whose clicks do NOT count as outside. */
262
+ outsideIgnore?: Element | readonly Element[];
263
+ /** Called on any user-initiated dismissal. */
264
+ onDismiss?: () => void;
265
+ }
266
+ /**
267
+ * Anchored, non-modal overlay: positions `content` relative to `anchor` (below by
268
+ * default, flipping above if it would overflow, and clamped horizontally to the
269
+ * viewport) and repositions on scroll / resize while open. A thin wrapper over
270
+ * {@link overlay} with non-modal defaults — `trap: false`, `dismiss: ['outside']`,
271
+ * and the anchor added to `outsideIgnore` so the trigger click doesn't self-close.
272
+ * Returns the same {@link OverlayHandle}; `close()` also drops the reposition
273
+ * listeners. `position: fixed` is set inline (you style everything else).
274
+ */
275
+ declare function popover(anchor: Element, content: OverlayContent, options?: PopoverOptions): OverlayHandle;
276
+ /** Content for a {@link tooltip}: text (auto-escaped), `SafeHtml`, or a render function. */
277
+ type TooltipContent = string | SafeHtml | (() => MountResult);
278
+ /** Options for {@link tooltip}. */
279
+ interface TooltipOptions extends AnchorPositionOptions {
280
+ /** Where to append the tooltip wrapper. Default `document.body`. */
281
+ container?: Element;
282
+ /** Class on the wrapper. Default `'kerf-tooltip'`. */
283
+ className?: string;
284
+ /** Delay in ms before showing after hover/focus enters. Default `400`. */
285
+ delay?: number;
286
+ /** Delay in ms before hiding after hover/focus leaves. Default `100`. */
287
+ hideDelay?: number;
288
+ /** ARIA role on the wrapper. Default `'tooltip'`. */
289
+ role?: string;
290
+ }
291
+ /**
292
+ * A hover/focus-triggered, non-modal, auto-hiding tooltip anchored to `anchor`.
293
+ * Shows after `delay` on `pointerenter`/`focus`, hides after `hideDelay` on
294
+ * `pointerleave`/`blur`, and positions itself with {@link autoReposition} (above
295
+ * the anchor by default). Unlike {@link popover} there is no click-dismiss model —
296
+ * it follows the pointer/focus. Returns a disposer that removes the anchor
297
+ * listeners and hides any shown tooltip. Structural only (kerf ships no CSS).
298
+ */
299
+ declare function tooltip(anchor: Element, content: TooltipContent, options?: TooltipOptions): () => void;
78
300
  /** Content for a {@link toast}: text, `SafeHtml`, or a render function. */
79
301
  type ToastContent = string | SafeHtml | (() => MountResult);
302
+ /** Accent variant for a {@link toast} — mapped to a `${className}--${variant}` class. */
303
+ type ToastVariant = 'info' | 'success' | 'warning';
80
304
  /** Options for {@link toast}. */
81
305
  interface ToastOptions {
82
306
  /** Where toasts stack. Default: a lazily-created `<div class="kerf-toasts">` on `document.body`. */
@@ -87,11 +311,34 @@ interface ToastOptions {
87
311
  duration?: number;
88
312
  /** ARIA role. Default `'status'`. */
89
313
  role?: string;
314
+ /**
315
+ * `'stack'` (default) shows toasts stacked in the region; `'replace'` dismisses
316
+ * the region's current toast(s) first (collapse-to-latest for a rapid sequence).
317
+ */
318
+ mode?: 'stack' | 'replace';
319
+ /** Accent variant — adds a `${className}--${variant}` class (kerf ships no CSS; you style it). */
320
+ variant?: ToastVariant;
321
+ /** Class added on the next animation frame after mount, so a CSS **entrance** transition can run. */
322
+ enterClass?: string;
323
+ /** Class added when dismissing, so CSS owns the **exit** — the node is removed `exitDuration` ms later. */
324
+ exitClass?: string;
325
+ /** ms to wait after `exitClass` is added before removing the node. Default `0`. */
326
+ exitDuration?: number;
327
+ }
328
+ /** Handle returned by {@link toast}. */
329
+ interface ToastHandle {
330
+ /** The toast element — inspect it, or run your own entrance/exit transitions. */
331
+ el: HTMLElement;
332
+ /** Dismiss it early (running the `exitClass` transition if set). Idempotent. */
333
+ dismiss(): void;
90
334
  }
91
335
  /**
92
336
  * Show a non-modal, auto-dismissing notification. Stacks in a shared body-level
93
- * region (or your `container`). Returns a `() => void` that dismisses it early.
337
+ * region (or your `container`). Returns a {@link ToastHandle} (`{ el, dismiss }`)
338
+ * so you can run entrance/exit transitions, wire an action button, or inspect the
339
+ * node. `mode: 'replace'` collapses a rapid sequence to the latest; `variant`
340
+ * adds an accent class; `enterClass`/`exitClass` let CSS own the animation.
94
341
  */
95
- declare function toast(content: ToastContent, options?: ToastOptions): () => void;
342
+ declare function toast(content: ToastContent, options?: ToastOptions): ToastHandle;
96
343
 
97
- export { type ConfirmOptions, type DismissTrigger, type OverlayContent, type OverlayHandle, type OverlayOptions, type ToastContent, type ToastOptions, confirm, overlay, toast };
344
+ export { type AnchorPositionOptions, type ConfirmOptions, type ConfirmRenderSlots, type DismissTrigger, type FieldValidator, type FormField, type FormOptions, type FormRenderField, type FormRenderSlots, type OverlayContent, type OverlayHandle, type OverlayOptions, type PopoverOptions, type PopoverPlacement, type PromptOptions, type PromptRenderSlots, type ToastContent, type ToastHandle, type ToastOptions, type ToastVariant, type TooltipContent, type TooltipOptions, autoReposition, confirm, form, overlay, popover, positionAnchored, prompt, toast, tooltip };
package/dist/overlay.js CHANGED
@@ -1,5 +1,5 @@
1
- import { delegate } from './chunk-KEZTD6H4.js';
2
1
  import { mount } from './chunk-4MY2656S.js';
2
+ import { delegate } from './chunk-KEZTD6H4.js';
3
3
  import './chunk-QIP723L4.js';
4
4
  import './chunk-YHH7OUFA.js';
5
5
  import { jsx } from './chunk-FSAQR6IU.js';
@@ -125,9 +125,10 @@ function confirm(message, options = {}) {
125
125
  title,
126
126
  okText = "OK",
127
127
  cancelText = "Cancel",
128
- danger = false
128
+ danger = false,
129
+ render
129
130
  } = options;
130
- const body = jsx("div", {
131
+ const body = render !== void 0 ? render({ message, ok: { "data-confirm": "ok" }, cancel: { "data-confirm": "cancel" } }) : jsx("div", {
131
132
  class: "kerf-confirm",
132
133
  children: [
133
134
  title !== void 0 ? jsx("h2", { class: "kerf-confirm__title", children: title }) : "",
@@ -150,7 +151,7 @@ function confirm(message, options = {}) {
150
151
  container,
151
152
  className: danger ? `${className} kerf-confirm--danger` : className,
152
153
  dismiss: ["escape", "backdrop"],
153
- initialFocus: ".kerf-confirm__ok",
154
+ initialFocus: '[data-confirm="ok"]',
154
155
  trap: true
155
156
  });
156
157
  delegate(handle.el, "click", "[data-confirm]", (_event, el) => {
@@ -158,6 +159,295 @@ function confirm(message, options = {}) {
158
159
  });
159
160
  return handle.result.then((value) => value === true);
160
161
  }
162
+ function prompt(message, options = {}) {
163
+ const {
164
+ container,
165
+ className = "kerf-overlay",
166
+ title,
167
+ defaultValue = "",
168
+ placeholder,
169
+ inputType = "text",
170
+ okText = "OK",
171
+ cancelText = "Cancel",
172
+ validate,
173
+ render
174
+ } = options;
175
+ const inputAttrs = {
176
+ "data-prompt-input": "",
177
+ type: inputType,
178
+ value: defaultValue,
179
+ ...placeholder !== void 0 ? { placeholder } : {}
180
+ };
181
+ const body = render !== void 0 ? render({
182
+ message,
183
+ input: inputAttrs,
184
+ error: { "data-prompt-error": "" },
185
+ ok: { "data-prompt": "ok" },
186
+ cancel: { "data-prompt": "cancel" }
187
+ }) : jsx("div", {
188
+ class: "kerf-prompt",
189
+ children: [
190
+ title !== void 0 ? jsx("h2", { class: "kerf-prompt__title", children: title }) : "",
191
+ jsx("label", { class: "kerf-prompt__message", children: message }),
192
+ jsx("input", { class: "kerf-prompt__input", ...inputAttrs }),
193
+ jsx("p", { class: "kerf-prompt__error", "data-prompt-error": "", children: "" }),
194
+ jsx("div", {
195
+ class: "kerf-prompt__actions",
196
+ children: [
197
+ jsx("button", { type: "button", "data-prompt": "cancel", children: cancelText }),
198
+ jsx("button", {
199
+ type: "button",
200
+ "data-prompt": "ok",
201
+ class: "kerf-prompt__ok",
202
+ children: okText
203
+ })
204
+ ]
205
+ })
206
+ ]
207
+ });
208
+ const handle = overlay(body, {
209
+ container,
210
+ className,
211
+ dismiss: ["escape", "backdrop"],
212
+ initialFocus: "[data-prompt-input]",
213
+ trap: true
214
+ });
215
+ const input = handle.el.querySelector("[data-prompt-input]");
216
+ const errorEl = handle.el.querySelector("[data-prompt-error]");
217
+ if (errorEl !== null) errorEl.hidden = true;
218
+ function attemptOk() {
219
+ const value = input.value;
220
+ const error = validate?.(value);
221
+ if (typeof error === "string" && error.length > 0) {
222
+ if (errorEl !== null) {
223
+ errorEl.textContent = error;
224
+ errorEl.hidden = false;
225
+ }
226
+ input.focus();
227
+ return;
228
+ }
229
+ handle.close(value);
230
+ }
231
+ delegate(handle.el, "click", "[data-prompt]", (_event, el) => {
232
+ if (el.getAttribute("data-prompt") === "ok") attemptOk();
233
+ else handle.close(null);
234
+ });
235
+ handle.el.addEventListener("keydown", (event) => {
236
+ if (event.key === "Enter" && event.target === input) {
237
+ event.preventDefault();
238
+ attemptOk();
239
+ }
240
+ });
241
+ return handle.result.then((value) => typeof value === "string" ? value : null);
242
+ }
243
+ function form(fields, options = {}) {
244
+ const { container, className = "kerf-overlay", title, okText = "OK", cancelText = "Cancel", render } = options;
245
+ const fieldAttrs = (field) => ({
246
+ "data-field": field.name,
247
+ name: field.name,
248
+ type: field.type ?? "text",
249
+ value: field.defaultValue ?? "",
250
+ ...field.placeholder !== void 0 ? { placeholder: field.placeholder } : {}
251
+ });
252
+ const body = render !== void 0 ? render({
253
+ fields: fields.map((field) => ({
254
+ name: field.name,
255
+ label: field.label ?? field.name,
256
+ input: fieldAttrs(field),
257
+ error: { "data-field-error": field.name }
258
+ })),
259
+ ok: { "data-form": "ok" },
260
+ cancel: { "data-form": "cancel" }
261
+ }) : jsx("div", {
262
+ class: "kerf-form",
263
+ children: [
264
+ title !== void 0 ? jsx("h2", { class: "kerf-form__title", children: title }) : "",
265
+ ...fields.map(
266
+ (field) => jsx("div", {
267
+ class: "kerf-form__field",
268
+ children: [
269
+ jsx("label", { class: "kerf-form__label", children: field.label ?? field.name }),
270
+ jsx("input", { class: "kerf-form__input", ...fieldAttrs(field) }),
271
+ jsx("p", { class: "kerf-form__error", "data-field-error": field.name, children: "" })
272
+ ]
273
+ })
274
+ ),
275
+ jsx("div", {
276
+ class: "kerf-form__actions",
277
+ children: [
278
+ jsx("button", { type: "button", "data-form": "cancel", children: cancelText }),
279
+ jsx("button", {
280
+ type: "button",
281
+ "data-form": "ok",
282
+ class: "kerf-form__ok",
283
+ children: okText
284
+ })
285
+ ]
286
+ })
287
+ ]
288
+ });
289
+ const handle = overlay(body, {
290
+ container,
291
+ className,
292
+ dismiss: ["escape", "backdrop"],
293
+ initialFocus: "[data-field]",
294
+ trap: true
295
+ });
296
+ const byAttr = (attr, name) => Array.from(handle.el.querySelectorAll(`[${attr}]`)).find(
297
+ (el) => el.getAttribute(attr) === name
298
+ );
299
+ const errorFor = (name) => Array.from(handle.el.querySelectorAll("[data-field-error]")).find(
300
+ (el) => el.getAttribute("data-field-error") === name
301
+ ) ?? null;
302
+ for (const field of fields) {
303
+ const errorEl = errorFor(field.name);
304
+ if (errorEl !== null) errorEl.hidden = true;
305
+ }
306
+ function attemptOk() {
307
+ const record = {};
308
+ let firstInvalid = null;
309
+ for (const field of fields) {
310
+ const el = byAttr("data-field", field.name);
311
+ const value = el.value;
312
+ record[field.name] = value;
313
+ const error = field.validate?.(value);
314
+ const errorEl = errorFor(field.name);
315
+ if (typeof error === "string" && error.length > 0) {
316
+ if (errorEl !== null) {
317
+ errorEl.textContent = error;
318
+ errorEl.hidden = false;
319
+ }
320
+ if (firstInvalid === null) firstInvalid = el;
321
+ } else if (errorEl !== null) {
322
+ errorEl.hidden = true;
323
+ }
324
+ }
325
+ if (firstInvalid !== null) {
326
+ firstInvalid.focus();
327
+ return;
328
+ }
329
+ handle.close(record);
330
+ }
331
+ delegate(handle.el, "click", "[data-form]", (_event, el) => {
332
+ if (el.getAttribute("data-form") === "ok") attemptOk();
333
+ else handle.close(null);
334
+ });
335
+ handle.el.addEventListener("keydown", (event) => {
336
+ if (event.key === "Enter" && event.target?.matches("[data-field]")) {
337
+ event.preventDefault();
338
+ attemptOk();
339
+ }
340
+ });
341
+ return handle.result.then(
342
+ (value) => value !== null && typeof value === "object" ? value : null
343
+ );
344
+ }
345
+ function positionAnchored(el, anchor, options = {}) {
346
+ const { placement = "bottom", align = "start", gap = 4 } = options;
347
+ const a = anchor.getBoundingClientRect();
348
+ const p = el.getBoundingClientRect();
349
+ const vw = window.innerWidth;
350
+ const vh = window.innerHeight;
351
+ const belowTop = a.bottom + gap;
352
+ const aboveTop = a.top - gap - p.height;
353
+ let below = placement !== "top";
354
+ if (below && belowTop + p.height > vh && aboveTop >= 0) below = false;
355
+ else if (!below && aboveTop < 0 && belowTop + p.height <= vh) below = true;
356
+ let left = align === "end" ? a.right - p.width : a.left;
357
+ left = Math.max(0, Math.min(left, vw - p.width));
358
+ el.style.position = "fixed";
359
+ el.style.margin = "0";
360
+ el.style.left = `${left}px`;
361
+ el.style.top = `${below ? belowTop : aboveTop}px`;
362
+ }
363
+ function autoReposition(el, anchor, options = {}) {
364
+ const reposition = () => positionAnchored(el, anchor, options);
365
+ reposition();
366
+ window.addEventListener("scroll", reposition, true);
367
+ window.addEventListener("resize", reposition);
368
+ return () => {
369
+ window.removeEventListener("scroll", reposition, true);
370
+ window.removeEventListener("resize", reposition);
371
+ };
372
+ }
373
+ function popover(anchor, content, options = {}) {
374
+ const {
375
+ container,
376
+ className = "kerf-popover",
377
+ placement = "bottom",
378
+ align = "start",
379
+ gap = 4,
380
+ dismiss = ["outside"],
381
+ initialFocus = false,
382
+ outsideIgnore,
383
+ onDismiss
384
+ } = options;
385
+ const extraIgnore = outsideIgnore === void 0 ? [] : Array.isArray(outsideIgnore) ? [...outsideIgnore] : [outsideIgnore];
386
+ const handle = overlay(content, {
387
+ container,
388
+ className,
389
+ dismiss,
390
+ trap: false,
391
+ initialFocus,
392
+ onDismiss,
393
+ outsideIgnore: [anchor, ...extraIgnore]
394
+ });
395
+ const stopReposition = autoReposition(handle.el, anchor, { placement, align, gap });
396
+ void handle.result.then(stopReposition);
397
+ return handle;
398
+ }
399
+ function tooltip(anchor, content, options = {}) {
400
+ const {
401
+ container,
402
+ className = "kerf-tooltip",
403
+ delay = 400,
404
+ hideDelay = 100,
405
+ role = "tooltip",
406
+ placement = "top",
407
+ align = "start",
408
+ gap = 4
409
+ } = options;
410
+ const body = typeof content === "function" ? content : typeof content === "string" ? jsx("span", { class: `${className}__text`, children: content }) : content;
411
+ const timers = {};
412
+ let current;
413
+ function show() {
414
+ const handle = overlay(body, { container, className, dismiss: false, trap: false, initialFocus: false });
415
+ handle.el.setAttribute("role", role);
416
+ const stop = autoReposition(handle.el, anchor, { placement, align, gap });
417
+ current = { handle, stop };
418
+ }
419
+ function hide() {
420
+ if (current === void 0) return;
421
+ current.stop();
422
+ current.handle.close();
423
+ current = void 0;
424
+ }
425
+ const onEnter = () => {
426
+ if (timers.hide !== void 0) clearTimeout(timers.hide);
427
+ if (current !== void 0) return;
428
+ if (timers.show !== void 0) clearTimeout(timers.show);
429
+ timers.show = setTimeout(show, delay);
430
+ };
431
+ const onLeave = () => {
432
+ if (timers.show !== void 0) clearTimeout(timers.show);
433
+ if (current === void 0) return;
434
+ timers.hide = setTimeout(hide, hideDelay);
435
+ };
436
+ anchor.addEventListener("pointerenter", onEnter);
437
+ anchor.addEventListener("pointerleave", onLeave);
438
+ anchor.addEventListener("focus", onEnter);
439
+ anchor.addEventListener("blur", onLeave);
440
+ return () => {
441
+ anchor.removeEventListener("pointerenter", onEnter);
442
+ anchor.removeEventListener("pointerleave", onLeave);
443
+ anchor.removeEventListener("focus", onEnter);
444
+ anchor.removeEventListener("blur", onLeave);
445
+ if (timers.show !== void 0) clearTimeout(timers.show);
446
+ if (timers.hide !== void 0) clearTimeout(timers.hide);
447
+ hide();
448
+ };
449
+ }
450
+ var TOAST_SET = /* @__PURE__ */ Symbol("kerf.toasts");
161
451
  function toastRegion(container) {
162
452
  if (container !== void 0) return container;
163
453
  const existing = document.querySelector(".kerf-toasts");
@@ -169,27 +459,53 @@ function toastRegion(container) {
169
459
  return region;
170
460
  }
171
461
  function toast(content, options = {}) {
172
- const { container, className = "kerf-toast", duration = 4e3, role = "status" } = options;
462
+ const {
463
+ container,
464
+ className = "kerf-toast",
465
+ duration = 4e3,
466
+ role = "status",
467
+ mode = "stack",
468
+ variant,
469
+ enterClass,
470
+ exitClass,
471
+ exitDuration = 0
472
+ } = options;
473
+ const region = toastRegion(container);
474
+ const active = region[TOAST_SET] ??= /* @__PURE__ */ new Set();
475
+ if (mode === "replace") for (const d of [...active]) d();
173
476
  const el = document.createElement("div");
174
477
  el.className = className;
478
+ if (variant !== void 0) el.classList.add(`${className}--${variant}`);
175
479
  el.setAttribute("role", role);
176
- toastRegion(container).appendChild(el);
480
+ region.appendChild(el);
177
481
  const disposeMount = mount(el, typeof content === "function" ? content : () => content);
178
- const state = {
179
- dismissed: false,
180
- timer: void 0
482
+ const state = { dismissed: false, timer: void 0 };
483
+ if (enterClass !== void 0) {
484
+ globalThis.requestAnimationFrame(() => {
485
+ if (!state.dismissed) el.classList.add(enterClass);
486
+ });
487
+ }
488
+ const remove = () => {
489
+ disposeMount();
490
+ el.remove();
491
+ active.delete(dismiss);
181
492
  };
182
493
  function dismiss() {
183
494
  if (state.dismissed) return;
184
495
  state.dismissed = true;
185
496
  if (state.timer !== void 0) clearTimeout(state.timer);
186
- disposeMount();
187
- el.remove();
497
+ if (exitClass !== void 0) {
498
+ el.classList.add(exitClass);
499
+ setTimeout(remove, exitDuration);
500
+ } else {
501
+ remove();
502
+ }
188
503
  }
504
+ active.add(dismiss);
189
505
  if (duration > 0) state.timer = setTimeout(dismiss, duration);
190
- return dismiss;
506
+ return { el, dismiss };
191
507
  }
192
508
 
193
- export { confirm, overlay, toast };
509
+ export { autoReposition, confirm, form, overlay, popover, positionAnchored, prompt, toast, tooltip };
194
510
  //# sourceMappingURL=overlay.js.map
195
511
  //# sourceMappingURL=overlay.js.map