@versini/ui-dialog 9.0.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,16 +1,14 @@
1
1
  /*!
2
- @versini/ui-dialog v9.0.0
3
- © 2025 gizmette.com
2
+ @versini/ui-dialog v10.0.0
3
+ © 2026 gizmette.com
4
4
  */
5
5
 
6
- import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { useCallback, useEffect, useId, useRef, useState } from "react";
8
8
  import clsx from "clsx";
9
- import { createPortal } from "react-dom";
10
9
 
11
- ;// CONCATENATED MODULE: ./src/common/constants.ts
12
10
  const MESSAGEBOX_CLASSNAME = "av-messagebox";
13
- const PANEL_CLASSNAME = "av-panel";
11
+ const DIALOG_CLASSNAME = "av-dialog";
14
12
  const TYPE_PANEL = "panel";
15
13
  const TYPE_MESSAGEBOX = "messagebox";
16
14
  const ANIMATION_SLIDE = "slide";
@@ -20,543 +18,131 @@ const MEDIUM = "medium";
20
18
  const LARGE = "large";
21
19
  const NONE = "none";
22
20
 
23
- ;// CONCATENATED MODULE: external "react/jsx-runtime"
24
21
 
25
- ;// CONCATENATED MODULE: external "react"
26
22
 
27
- ;// CONCATENATED MODULE: external "clsx"
28
-
29
- ;// CONCATENATED MODULE: external "react-dom"
30
-
31
- ;// CONCATENATED MODULE: ./src/components/Panel/dialogStackManager.ts
32
- /**
33
- * Dialog Stack Manager
34
- *
35
- * Implements the W3C WAI-ARIA APG pattern for managing nested modal dialogs.
36
- * This module maintains a stack of open dialogs and coordinates focus event
37
- * listeners between them.
38
- *
39
- * Key features:
40
- * - Only the topmost dialog has active focus listeners
41
- * - When a nested dialog opens, parent listeners are suspended
42
- * - When a dialog closes, parent listeners are restored
43
- * - Programmatic focus changes are flagged to prevent event interference
44
- *
45
- * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/
46
- */ /**
47
- * Stack of currently open dialogs. The last item is the topmost dialog.
48
- */ const openDialogStack = [];
49
- /**
50
- * Flag to ignore focus changes during programmatic focus operations.
51
- * When true, focus event handlers should return early to prevent interference.
52
- *
53
- * This is critical for nested dialogs: when a child dialog closes and
54
- * programmatically returns focus to its trigger element inside the parent,
55
- * we don't want the parent's focusin handler to interfere.
56
- */ let ignoreFocusChanges = false;
57
- /**
58
- * Get whether focus changes should be ignored.
59
- * Focus event handlers should check this and return early if true.
60
- */ function shouldIgnoreFocusChanges() {
61
- return ignoreFocusChanges;
62
- }
63
- /**
64
- * Execute a function while ignoring focus change events.
65
- * Use this when programmatically moving focus to prevent event handlers
66
- * from interfering.
67
- *
68
- * @param fn - Function to execute (typically contains focus() calls)
69
- */ function withIgnoredFocusChanges(fn) {
70
- ignoreFocusChanges = true;
71
- try {
72
- fn();
73
- } finally{
74
- // Use setTimeout to ensure the flag stays true through any
75
- // microtasks that might fire focus events
76
- setTimeout(()=>{
77
- ignoreFocusChanges = false;
78
- }, 0);
79
- }
80
- }
81
23
  /**
82
- * Register a dialog in the stack when it opens.
83
- * If there's a parent dialog, its listeners are suspended.
84
- *
85
- * @param entry - The dialog entry to register
86
- */ function registerDialog(entry) {
87
- // If there's already a dialog open, suspend its listeners
88
- if (openDialogStack.length > 0) {
89
- const currentTop = openDialogStack[openDialogStack.length - 1];
90
- currentTop.removeListeners();
91
- }
92
- // Add the new dialog to the stack and activate its listeners
93
- openDialogStack.push(entry);
94
- entry.addListeners();
95
- }
24
+ * Reference-counted scroll lock for document.documentElement.
25
+ * Supports nested dialogs: overflow is only restored when the last lock is released.
26
+ */ let scrollLockCount = 0;
27
+ let previousOverflow = "";
96
28
  /**
97
- * Unregister a dialog from the stack when it closes.
98
- * If there's a parent dialog, its listeners are restored.
99
- *
100
- * @param dialogRef - Reference to the dialog element being closed
101
- */ function unregisterDialog(dialogRef) {
102
- const index = openDialogStack.findIndex((entry)=>entry.dialogRef === dialogRef);
103
- /* c8 ignore next 3 - defensive check */ if (index === -1) {
104
- return;
105
- }
106
- // Remove listeners from the dialog being closed
107
- const [removedEntry] = openDialogStack.splice(index, 1);
108
- removedEntry.removeListeners();
109
- // If there's a parent dialog, restore its listeners
110
- if (openDialogStack.length > 0) {
111
- const newTop = openDialogStack[openDialogStack.length - 1];
112
- newTop.addListeners();
29
+ * Acquire a scroll lock. Call the returned function to release.
30
+ * Multiple calls stack; overflow is restored only when all locks are released.
31
+ */ function acquireScrollLock() {
32
+ scrollLockCount += 1;
33
+ if (scrollLockCount === 1) {
34
+ previousOverflow = document.documentElement.style.overflow;
35
+ document.documentElement.style.overflow = "hidden";
113
36
  }
114
- }
115
- /**
116
- * Get the current number of open dialogs.
117
- * Useful for debugging and testing.
118
- */ function getOpenDialogCount() {
119
- return openDialogStack.length;
120
- }
121
- /**
122
- * Check if a given dialog is the topmost (current) dialog.
123
- *
124
- * @param dialogRef - Reference to the dialog element to check
125
- */ function isTopmostDialog(dialogRef) {
126
- if (openDialogStack.length === 0) {
127
- return false;
128
- }
129
- return openDialogStack[openDialogStack.length - 1].dialogRef === dialogRef;
130
- }
131
- /**
132
- * Reset the stack (for testing purposes only).
133
- * @internal
134
- */ function _resetStackForTesting() {
135
- while(openDialogStack.length > 0){
136
- openDialogStack.pop();
137
- }
138
- ignoreFocusChanges = false;
139
- }
140
-
141
- ;// CONCATENATED MODULE: ./src/components/Panel/PanelPortal.tsx
142
-
143
-
144
-
145
-
146
-
147
-
148
- /**
149
- * Selector for all focusable elements within a container. Based on W3C WAI-ARIA
150
- * practices for dialog focus management.
151
- */ const FOCUSABLE_SELECTOR = [
152
- 'a[href]:not([disabled]):not([tabindex="-1"])',
153
- 'button:not([disabled]):not([tabindex="-1"])',
154
- 'textarea:not([disabled]):not([tabindex="-1"])',
155
- 'input:not([disabled]):not([tabindex="-1"])',
156
- 'select:not([disabled]):not([tabindex="-1"])',
157
- '[tabindex]:not([tabindex="-1"]):not([disabled])',
158
- 'audio[controls]:not([tabindex="-1"])',
159
- 'video[controls]:not([tabindex="-1"])',
160
- 'details:not([tabindex="-1"])'
161
- ].join(", ");
162
- /**
163
- * Portal component for rendering the Panel as a modal dialog using the native
164
- * HTML <dialog> element with showModal(). This provides:
165
- * - Native focus trapping (works correctly on iPad with physical keyboard)
166
- * - Native ESC key handling via cancel event
167
- * - Native backdrop via ::backdrop pseudo-element
168
- * - Native inert background (no need for manual scroll lock)
169
- * - Top layer rendering (no need for createPortal)
170
- *
171
- * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
172
- * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
173
- *
174
- */ function PanelPortal({ open, onClose, children, className, style, title, initialFocus = 0, kind = /* inlined export .TYPE_PANEL */ ("panel") }) {
175
- const labelId = useId();
176
- const descriptionId = useId();
177
- const dialogRef = useRef(null);
178
- const previouslyFocusedRef = useRef(null);
179
- /**
180
- * Get all focusable elements within the dialog. Excludes focus sentinel
181
- * elements used for circular focus trapping.
182
- */ const getFocusableElements = useCallback(()=>{
183
- /* v8 ignore start - defensive check, dialogRef is always set when open */ if (!dialogRef.current) {
184
- return [];
185
- }
186
- /* v8 ignore stop */ const elements = dialogRef.current.querySelectorAll(FOCUSABLE_SELECTOR);
187
- return Array.from(elements).filter((el)=>el.offsetParent !== null && // Filter out hidden elements
188
- !el.hasAttribute("data-focus-sentinel"));
189
- }, []);
190
- /**
191
- * Focus a specific element by index, or the element referenced by a ref.
192
- */ /* v8 ignore start - focus element logic with multiple branches */ const focusElement = useCallback((target)=>{
193
- if (typeof target === "number") {
194
- if (target === -1) {
195
- // -1 means don't focus anything.
196
- return;
197
- }
198
- const focusableElements = getFocusableElements();
199
- if (focusableElements.length > 0) {
200
- const index = Math.min(target, focusableElements.length - 1);
201
- focusableElements[index]?.focus();
202
- }
203
- } else if (target?.current) {
204
- target.current.focus();
205
- }
206
- }, [
207
- getFocusableElements
208
- ]);
209
- /* v8 ignore stop */ /**
210
- * Handle the native cancel event (fired when ESC is pressed). This replaces
211
- * the custom keydown handler since the native dialog handles ESC
212
- * automatically.
213
- */ const handleCancel = useCallback((event)=>{
214
- // Prevent the default close behavior so we can control it via onClose.
215
- event.preventDefault();
216
- onClose();
217
- }, [
218
- onClose
219
- ]);
220
- /**
221
- * Handle Tab key to implement circular focus trapping. Native dialog focus
222
- * trapping may not be circular in all browsers, so we manually wrap focus from
223
- * last to first element (and vice versa). Uses document-level event listener
224
- * for better iPad physical keyboard support.
225
- *
226
- * IMPORTANT: On iPad Safari with a physical keyboard, the Tab key does not
227
- * automatically navigate between focusable elements. We must manually handle
228
- * ALL Tab key presses, not just wrapping cases.
229
- *
230
- */ const handleKeyDown = useCallback((event)=>{
231
- if (event.key !== "Tab" || !dialogRef.current) {
232
- return;
233
- }
234
- const focusableElements = getFocusableElements();
235
- /* v8 ignore start - edge case: dialog with no focusable elements */ if (focusableElements.length === 0) {
236
- event.preventDefault();
237
- return;
238
- }
239
- /* v8 ignore stop */ const firstElement = focusableElements[0];
240
- const lastElement = focusableElements[focusableElements.length - 1];
241
- const activeElement = document.activeElement;
242
- // Find the current index of the focused element.
243
- const currentIndex = focusableElements.indexOf(activeElement);
244
- /**
245
- * Always prevent default and manually handle focus navigation. This is
246
- * required for iPad Safari with physical keyboard, where Tab key doesn't
247
- * automatically navigate between focusable elements.
248
- */ event.preventDefault();
249
- if (event.shiftKey) {
250
- // Shift+Tab: move to previous element, wrap to last if on first.
251
- if (activeElement === firstElement || currentIndex <= 0) {
252
- lastElement?.focus();
253
- } else {
254
- focusableElements[currentIndex - 1]?.focus();
255
- }
256
- } else {
257
- // Tab: move to next element, wrap to first if on last.
258
- if (activeElement === lastElement || currentIndex >= focusableElements.length - 1) {
259
- firstElement?.focus();
260
- } else {
261
- focusableElements[currentIndex + 1]?.focus();
262
- }
263
- }
264
- }, [
265
- getFocusableElements
266
- ]);
267
- /**
268
- * Handle focus events to ensure focus stays within the dialog. This catches
269
- * focus that escapes via Tab key on iPad Safari or other means.
270
- *
271
- * Uses the dialog stack manager's ignore flag to prevent interference during
272
- * programmatic focus operations (e.g., when a nested dialog closes and returns
273
- * focus to its trigger element).
274
- *
275
- */ /* v8 ignore start - focus escape handling for iPad Safari, hard to test in jsdom */ const handleFocusIn = useCallback((event)=>{
276
- // Ignore focus changes triggered by programmatic focus operations.
277
- if (shouldIgnoreFocusChanges()) {
278
- return;
279
- }
280
- if (!dialogRef.current || dialogRef.current.contains(event.target)) {
281
- return;
282
- }
283
- // Focus escaped the dialog, bring it back.
284
- const focusableElements = getFocusableElements();
285
- if (focusableElements.length > 0) {
286
- focusableElements[0]?.focus();
287
- }
288
- }, [
289
- getFocusableElements
290
- ]);
291
- /* v8 ignore stop */ /**
292
- * Handle clicks on the backdrop (the area outside the dialog content). Native
293
- * dialog doesn't provide backdrop click handling, so we use the dialog
294
- * element's click event and check if the click target is the dialog itself
295
- * (not a child element).
296
- */ /* v8 ignore start - backdrop clicks are disabled by design in current implementation */ const handleDialogClick = useCallback((_event)=>{
297
- /**
298
- * If the click is directly on the dialog element (the backdrop area), not on
299
- * any child element, then close the dialog. Currently disabled -
300
- * outsidePress is false by design. if (_event.target === dialogRef.current)
301
- * { onClose(); }
302
- */ }, []);
303
- /* v8 ignore stop */ /**
304
- * Focus sentinel handler - when a sentinel element receives focus, redirect
305
- * focus to the appropriate element inside the dialog. This handles iPad
306
- * Safari's Tab key behavior which can bypass event listeners.
307
- */ const handleSentinelFocus = useCallback((position)=>{
308
- const focusableElements = getFocusableElements();
309
- /* v8 ignore start - edge case: dialog with no focusable elements */ if (focusableElements.length === 0) {
310
- return;
37
+ return ()=>{
38
+ scrollLockCount -= 1;
39
+ if (scrollLockCount === 0) {
40
+ document.documentElement.style.overflow = previousOverflow;
311
41
  }
312
- /* v8 ignore stop */ if (position === "start") {
313
- // Focus came from the end, wrap to last element.
314
- focusableElements[focusableElements.length - 1]?.focus();
315
- } else {
316
- // Focus came from the start, wrap to first element.
317
- focusableElements[0]?.focus();
318
- }
319
- }, [
320
- getFocusableElements
321
- ]);
322
- /**
323
- * Effect to show/hide the dialog and manage focus. Uses the dialog stack
324
- * manager to coordinate listeners between nested dialogs.
325
- */ /* v8 ignore start - complex dialog lifecycle management */ useEffect(()=>{
326
- const dialog = dialogRef.current;
327
- /* v8 ignore start - defensive check */ if (!dialog) {
328
- return;
329
- }
330
- /* v8 ignore stop */ // Store the currently focused element to restore later.
331
- previouslyFocusedRef.current = document.activeElement;
332
- // Show the dialog as a modal.
333
- if (!dialog.open) {
334
- dialog.showModal();
335
- }
336
- /**
337
- * Add cancel event listener for ESC key (always needed, not managed by
338
- * stack).
339
- */ dialog.addEventListener("cancel", handleCancel);
340
- /**
341
- * Define listener management functions for the stack manager. These will be
342
- * called when this dialog becomes/stops being the topmost dialog.
343
- */ const addListeners = ()=>{
344
- document.addEventListener("keydown", handleKeyDown);
345
- document.addEventListener("focusin", handleFocusIn);
346
- };
347
- const removeListeners = ()=>{
348
- document.removeEventListener("keydown", handleKeyDown);
349
- document.removeEventListener("focusin", handleFocusIn);
350
- };
351
- /**
352
- * Register this dialog with the stack manager. This will suspend parent
353
- * dialog listeners if any exist.
354
- */ registerDialog({
355
- dialogRef: dialog,
356
- addListeners,
357
- removeListeners
358
- });
359
- /**
360
- * Set initial focus after a small delay to ensure the DOM is ready. This
361
- * works around React's autoFocus prop not working with native dialog.
362
- */ const focusTimer = setTimeout(()=>{
363
- focusElement(initialFocus);
364
- }, 0);
365
- // Capture the previously focused element for restoration in cleanup.
366
- const previouslyFocused = previouslyFocusedRef.current;
367
- return ()=>{
368
- clearTimeout(focusTimer);
369
- dialog.removeEventListener("cancel", handleCancel);
370
- /**
371
- * Unregister from the stack manager. This will restore parent dialog
372
- * listeners if any exist.
373
- */ unregisterDialog(dialog);
374
- // Close the dialog if it's still open.
375
- if (dialog.open) {
376
- dialog.close();
377
- }
378
- /**
379
- * Restore focus to the previously focused element if it's still in the DOM.
380
- * Use withIgnoredFocusChanges to prevent parent dialog's handleFocusIn from
381
- * interfering with focus restoration.
382
- */ if (previouslyFocused?.isConnected) {
383
- withIgnoredFocusChanges(()=>{
384
- if (previouslyFocused.isConnected) {
385
- previouslyFocused.focus();
386
- }
387
- });
388
- }
389
- };
390
- }, [
391
- handleCancel,
392
- handleKeyDown,
393
- handleFocusIn,
394
- initialFocus,
395
- focusElement
396
- ]);
397
- /* v8 ignore stop */ /* v8 ignore start - early return when panel is closed */ if (!open) {
398
- return null;
399
- }
400
- /* v8 ignore stop */ const isMessageBox = kind === TYPE_MESSAGEBOX;
401
- const dialogClass = clsx(/**
402
- * Center the dialog on screen with fixed positioning. Native dialog uses
403
- * position: fixed by default.
404
- * - Panel on mobile: inset-0 + no margin for full screen.
405
- * - Panel on desktop: inset-x-0 + top/bottom auto for vertical centering.
406
- * - MessageBox: Always centered (both mobile and desktop) since it doesn't
407
- * take full screen.
408
- */ "fixed max-h-none max-w-none p-0", {
409
- // Panel: full screen on mobile, centered on desktop
410
- "inset-0 m-0 sm:inset-auto sm:inset-x-0 sm:top-1/2 sm:-translate-y-1/2 sm:mx-auto": !isMessageBox,
411
- // MessageBox: always centered at all breakpoints
412
- "inset-auto inset-x-0 top-1/2 -translate-y-1/2 mx-auto z-100": isMessageBox
413
- }, /**
414
- * Backdrop styling via Tailwind's backdrop: variant for ::backdrop
415
- * pseudo-element. Full black on mobile, 80% opacity on desktop (matches
416
- * original overlay).
417
- */ "backdrop:bg-black sm:backdrop:bg-black/80", className);
418
- return /*#__PURE__*/ createPortal(/*#__PURE__*/ jsxs("dialog", {
419
- ref: dialogRef,
420
- "aria-labelledby": labelId,
421
- "aria-describedby": descriptionId,
422
- className: dialogClass,
423
- style: style,
424
- onClick: handleDialogClick,
425
- children: [
426
- /*#__PURE__*/ jsx("span", {
427
- tabIndex: 0,
428
- onFocus: ()=>handleSentinelFocus("start"),
429
- "data-focus-sentinel": "start",
430
- style: {
431
- position: "absolute",
432
- width: 1,
433
- height: 1,
434
- padding: 0,
435
- margin: -1,
436
- overflow: "hidden",
437
- clip: "rect(0, 0, 0, 0)",
438
- whiteSpace: "nowrap",
439
- border: 0
440
- },
441
- "aria-hidden": "true"
442
- }),
443
- /*#__PURE__*/ jsx("span", {
444
- id: labelId,
445
- className: "sr-only",
446
- children: title
447
- }),
448
- children,
449
- /*#__PURE__*/ jsx("span", {
450
- tabIndex: 0,
451
- onFocus: ()=>handleSentinelFocus("end"),
452
- "data-focus-sentinel": "end",
453
- style: {
454
- position: "absolute",
455
- width: 1,
456
- height: 1,
457
- padding: 0,
458
- margin: -1,
459
- overflow: "hidden",
460
- clip: "rect(0, 0, 0, 0)",
461
- whiteSpace: "nowrap",
462
- border: 0
463
- },
464
- "aria-hidden": "true"
465
- })
466
- ]
467
- }), document.body);
42
+ };
468
43
  }
469
44
 
470
- ;// CONCATENATED MODULE: ./src/components/Panel/utilities.ts
45
+
471
46
 
472
47
 
473
48
  const getFooterAndHeaderCommonClasses = ({ blurEffect })=>{
474
49
  return clsx("absolute left-0 right-0 z-20 backdrop-brightness-50", {
475
- "backdrop-blur-sm": blurEffect === /* inlined export .SMALL */ ("small"),
476
- "backdrop-blur-md": blurEffect === /* inlined export .MEDIUM */ ("medium"),
477
- "backdrop-blur-lg": blurEffect === /* inlined export .LARGE */ ("large"),
478
- "bg-surface-darker": blurEffect === /* inlined export .NONE */ ("none")
50
+ "backdrop-blur-sm": blurEffect === (/* inlined export .SMALL */"small"),
51
+ "backdrop-blur-md": blurEffect === (/* inlined export .MEDIUM */"medium"),
52
+ "backdrop-blur-lg": blurEffect === (/* inlined export .LARGE */"large"),
53
+ "bg-surface-darker": blurEffect === (/* inlined export .NONE */"none")
479
54
  });
480
55
  };
481
- const getPanelClassName = ({ className, kind, borderMode, animation, maxWidth = /* inlined export .MEDIUM */ ("medium"), maxHeight, blurEffect = /* inlined export .NONE */ ("none"), hasFooter })=>{
482
- const effectiveMaxHeight = maxHeight ?? (kind === /* inlined export .TYPE_PANEL */ ("panel") ? /* inlined export .LARGE */ ("large") : /* inlined export .SMALL */ ("small"));
56
+ const getDialogClassName = ({ className, kind, borderMode, animation, maxWidth = (/* inlined export .MEDIUM */"medium"), maxHeight, blurEffect = (/* inlined export .NONE */"none"), hasFooter })=>{
57
+ const effectiveMaxHeight = maxHeight ?? (kind === (/* inlined export .TYPE_PANEL */"panel") ? (/* inlined export .LARGE */"large") : (/* inlined export .SMALL */"small"));
483
58
  return {
484
- outerWrapper: clsx("prose prose-lighter flex flex-col bg-surface-dark overflow-hidden", {
59
+ outerWrapper: clsx("plume plume-lighter flex flex-col bg-surface-dark overflow-hidden", "p-0", {
485
60
  "duration-200 ease-out": animation,
486
61
  /**
487
62
  * Panel styles
488
- */ [`${PANEL_CLASSNAME} sm:rounded-3xl sm:border`]: kind === /* inlined export .TYPE_PANEL */ ("panel"),
63
+ * border-0 resets the native <dialog> border on mobile;
64
+ * sm:border restores it on desktop.
65
+ */ [`${DIALOG_CLASSNAME} border-0 sm:rounded-3xl sm:border m-auto! sm:h-fit backdrop:bg-surface-darkest/80`]: kind === (/* inlined export .TYPE_PANEL */"panel"),
489
66
  /**
490
- * Widths and max widths for Panel when no className is provided
491
- */ ["w-full sm:w-[95%] md:max-w-2xl"]: kind === /* inlined export .TYPE_PANEL */ ("panel") && !className && maxWidth === /* inlined export .SMALL */ ("small"),
492
- ["w-full sm:w-[95%] md:max-w-3xl"]: kind === /* inlined export .TYPE_PANEL */ ("panel") && !className && maxWidth === /* inlined export .MEDIUM */ ("medium"),
493
- ["w-full sm:w-[95%] md:max-w-4xl"]: kind === /* inlined export .TYPE_PANEL */ ("panel") && !className && maxWidth === /* inlined export .LARGE */ ("large"),
67
+ * Widths and max widths for Panel
68
+ */ ["w-full sm:w-[95%] md:max-w-2xl"]: kind === (/* inlined export .TYPE_PANEL */"panel") && maxWidth === (/* inlined export .SMALL */"small"),
69
+ ["w-full sm:w-[95%] md:max-w-3xl"]: kind === (/* inlined export .TYPE_PANEL */"panel") && maxWidth === (/* inlined export .MEDIUM */"medium"),
70
+ ["w-full sm:w-[95%] md:max-w-4xl"]: kind === (/* inlined export .TYPE_PANEL */"panel") && maxWidth === (/* inlined export .LARGE */"large"),
494
71
  /**
495
72
  * Heights and max heights for Panel
496
- * Mobile: full height (h-full works with inset-0), Desktop: shrink-to-fit with max-height constraint
497
- */ "h-full sm:h-auto min-h-40 sm:min-h-0 max-h-full sm:max-h-[40vh]": kind === /* inlined export .TYPE_PANEL */ ("panel") && effectiveMaxHeight === /* inlined export .SMALL */ ("small"),
498
- "h-full sm:h-auto min-h-40 sm:min-h-0 max-h-full sm:max-h-[60vh]": kind === /* inlined export .TYPE_PANEL */ ("panel") && effectiveMaxHeight === /* inlined export .MEDIUM */ ("medium"),
499
- "h-full sm:h-auto min-h-40 sm:min-h-0 max-h-full sm:max-h-[95vh]": kind === /* inlined export .TYPE_PANEL */ ("panel") && effectiveMaxHeight === /* inlined export .LARGE */ ("large"),
73
+ * Mobile: full viewport height
74
+ * Desktop: h-auto (overridden to fit-content by the injected
75
+ * stylesheet in Dialog.tsx, required for native <dialog> with
76
+ * position:fixed + inset:0 + margin:auto), clamped by max-height.
77
+ */ "h-dvh sm:h-auto min-h-40 sm:max-h-[40dvh]": kind === (/* inlined export .TYPE_PANEL */"panel") && effectiveMaxHeight === (/* inlined export .SMALL */"small"),
78
+ "h-dvh sm:h-auto min-h-40 sm:max-h-[60dvh]": kind === (/* inlined export .TYPE_PANEL */"panel") && effectiveMaxHeight === (/* inlined export .MEDIUM */"medium"),
79
+ "h-dvh sm:h-auto min-h-40 sm:max-h-[95dvh]": kind === (/* inlined export .TYPE_PANEL */"panel") && effectiveMaxHeight === (/* inlined export .LARGE */"large"),
500
80
  /**
501
81
  * Panel border colors
502
- */ "sm:border-border-dark": borderMode === "dark" && kind === /* inlined export .TYPE_PANEL */ ("panel"),
503
- "sm:border-border-accent": borderMode === "light" && kind === /* inlined export .TYPE_PANEL */ ("panel"),
82
+ */ "sm:border-border-dark": borderMode === "dark" && kind === (/* inlined export .TYPE_PANEL */"panel"),
83
+ "sm:border-border-accent": borderMode === "light" && kind === (/* inlined export .TYPE_PANEL */"panel"),
84
+ "sm:border-border-medium": borderMode === "medium" && kind === (/* inlined export .TYPE_PANEL */"panel"),
504
85
  /**
505
86
  * Messagebox styles
506
- */ [`${MESSAGEBOX_CLASSNAME} rounded-3xl border`]: kind === TYPE_MESSAGEBOX,
87
+ */ [`${MESSAGEBOX_CLASSNAME} rounded-3xl border m-auto! backdrop:bg-surface-darkest/80`]: kind === TYPE_MESSAGEBOX,
507
88
  /**
508
- * Widths and max widths for Messagebox when no className is provided
509
- */ ["w-[95%] sm:w-[50%] md:max-w-2xl"]: kind === TYPE_MESSAGEBOX && !className,
89
+ * Widths and max widths for Messagebox
90
+ */ ["w-[95%] sm:w-[50%] md:max-w-2xl"]: kind === TYPE_MESSAGEBOX,
510
91
  /**
511
92
  * Heights and max heights for Messagebox
512
- */ "h-64": kind === TYPE_MESSAGEBOX && effectiveMaxHeight === /* inlined export .SMALL */ ("small"),
513
- "h-80": kind === TYPE_MESSAGEBOX && effectiveMaxHeight === /* inlined export .MEDIUM */ ("medium"),
514
- "h-96": kind === TYPE_MESSAGEBOX && effectiveMaxHeight === /* inlined export .LARGE */ ("large"),
93
+ */ "h-64": kind === TYPE_MESSAGEBOX && effectiveMaxHeight === (/* inlined export .SMALL */"small"),
94
+ "h-80": kind === TYPE_MESSAGEBOX && effectiveMaxHeight === (/* inlined export .MEDIUM */"medium"),
95
+ "h-96": kind === TYPE_MESSAGEBOX && effectiveMaxHeight === (/* inlined export .LARGE */"large"),
515
96
  /**
516
97
  * Messagebox border colors
517
98
  */ "border-border-dark": borderMode === "dark" && kind === TYPE_MESSAGEBOX,
518
99
  "border-border-accent": borderMode === "light" && kind === TYPE_MESSAGEBOX,
519
- [`${className}`]: !!className
520
- }),
521
- innerWrapper: "content flex flex-col rounded-[inherit] relative min-h-full isolate",
100
+ "border-border-medium": borderMode === "medium" && kind === TYPE_MESSAGEBOX
101
+ }, className),
102
+ innerWrapper: "content flex flex-col rounded-[inherit] relative flex-1 min-h-0",
522
103
  scrollableContent: clsx("flex-1 overflow-y-auto overflow-x-hidden", "pt-12", {
523
- "pb-12": hasFooter
104
+ "pb-14": hasFooter
524
105
  }),
525
106
  footer: clsx(getFooterAndHeaderCommonClasses({
526
107
  blurEffect
527
- }), "p-2 bottom-0", "sm:min-h-auto h-12", {
528
- "min-h-20": hasFooter && kind === /* inlined export .TYPE_PANEL */ ("panel"),
529
- "sm:rounded-b-3xl": kind === /* inlined export .TYPE_PANEL */ ("panel"),
108
+ }), "bottom-0", "sm:min-h-auto h-12", {
109
+ "px-2 pt-4 pb-8 sm:pt-2 sm:pb-2": kind === (/* inlined export .TYPE_PANEL */"panel"),
110
+ "p-2": kind === TYPE_MESSAGEBOX,
111
+ "min-h-20": hasFooter && kind === (/* inlined export .TYPE_PANEL */"panel"),
112
+ "sm:rounded-b-3xl": kind === (/* inlined export .TYPE_PANEL */"panel"),
530
113
  "rounded-b-3xl": kind === TYPE_MESSAGEBOX
531
114
  }),
532
115
  header: clsx("flex flex-row-reverse items-center justify-between h-12", getFooterAndHeaderCommonClasses({
533
116
  blurEffect
534
117
  }), "top-0", {
535
- "sm:rounded-t-3xl": kind === /* inlined export .TYPE_PANEL */ ("panel"),
118
+ "sm:rounded-t-3xl": kind === (/* inlined export .TYPE_PANEL */"panel"),
536
119
  "rounded-t-3xl": kind === TYPE_MESSAGEBOX
537
120
  }),
538
121
  title: "mb-0 pt-2 pl-4 pr-2 pb-2",
539
122
  closeWrapper: "pr-[18px]",
540
- closeButton: clsx("flex items-center justify-center", "size-3", "p-1", "rounded-full", "border", "border-transparent", "text-[rgba(255,96,92,1)]", "bg-[rgba(255,96,92,1)]", "shadow-[0_0_0_0.5px_red]", "focus:outline", "focus:outline-2", "focus:outline-offset-2", "focus:outline-focus-light", "hover:text-copy-dark", "focus:text-copy-dark", "active:bg-[#ba504a]", // Extended touch target using pseudo-element
123
+ closeButton: clsx("flex items-center justify-center", "size-3", "p-1", "rounded-full", "border", "border-transparent", "cursor-pointer", "text-[rgba(255,96,92,1)]", "bg-[rgba(255,96,92,1)]", "shadow-[0_0_0_0.5px_red]", "focus:outline", "focus:outline-2", "focus:outline-offset-2", "focus:outline-focus-light", "hover:text-copy-dark", "focus:text-copy-dark", "active:bg-[#ba504a]", // Extended touch target using pseudo-element
541
124
  "relative", "before:content-['']", "before:absolute", "before:-top-4", "before:-right-4", "before:-bottom-4", "before:-left-4"),
542
125
  content: "p-4 rounded-3xl"
543
126
  };
544
127
  };
545
128
 
546
- ;// CONCATENATED MODULE: ./src/components/Panel/Panel.tsx
547
129
 
548
130
 
549
131
 
550
132
 
551
133
 
552
- const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "light", kind = /* inlined export .TYPE_PANEL */ ("panel"), className, animation = false, animationType = /* inlined export .ANIMATION_SLIDE */ ("slide"), maxWidth = /* inlined export .MEDIUM */ ("medium"), maxHeight, blurEffect = /* inlined export .NONE */ ("none"), initialFocus })=>{
134
+ const TABBABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
135
+ const Dialog = ({ open, onOpenChange, title, children, footer, borderMode = "light", kind = (/* inlined export .TYPE_PANEL */"panel"), className, animation = false, animationType = (/* inlined export .ANIMATION_SLIDE */"slide"), maxWidth = (/* inlined export .MEDIUM */"medium"), maxHeight, blurEffect = (/* inlined export .NONE */"none"), initialFocus })=>{
136
+ const dialogRef = useRef(null);
553
137
  const originalTitleRef = useRef("");
554
- /* v8 ignore start */ const [animationStyles, setAnimationStyles] = useState(!animation ? {} : animationType === /* inlined export .ANIMATION_FADE */ ("fade") ? {
138
+ const titleId = useId();
139
+ const descriptionId = useId();
140
+ /* v8 ignore start */ const [animationStyles, setAnimationStyles] = useState(!animation ? {} : animationType === (/* inlined export .ANIMATION_FADE */"fade") ? {
555
141
  opacity: 0
556
142
  } : {
557
143
  transform: "translateY(-100vh)"
558
144
  });
559
- /* v8 ignore stop */ const panelClassName = getPanelClassName({
145
+ /* v8 ignore stop */ const dialogClassName = getDialogClassName({
560
146
  className,
561
147
  kind,
562
148
  borderMode,
@@ -566,16 +152,14 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
566
152
  blurEffect,
567
153
  hasFooter: Boolean(footer)
568
154
  });
569
- /**
570
- * Handle close button click.
571
- */ const handleClose = useCallback(()=>{
155
+ const handleClose = useCallback(()=>{
572
156
  onOpenChange(false);
573
157
  }, [
574
158
  onOpenChange
575
159
  ]);
576
160
  /**
577
- * If the panel is opened, set the document title to the panel's title. If it's
578
- * closed, restore the original document.title.
161
+ * If the dialog is opened, set the document title to the dialog's title.
162
+ * If it's closed, restore the original document.title.
579
163
  */ /* v8 ignore start - document title manipulation */ useEffect(()=>{
580
164
  if (open) {
581
165
  originalTitleRef.current = document.title;
@@ -597,16 +181,16 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
597
181
  return;
598
182
  }
599
183
  if (open) {
600
- setAnimationStyles(!animation ? {} : animationType === /* inlined export .ANIMATION_FADE */ ("fade") ? {
184
+ setAnimationStyles(!animation ? {} : animationType === (/* inlined export .ANIMATION_FADE */"fade") ? {
601
185
  opacity: 0
602
186
  } : {
603
- transform: "translateY(-666vh)"
187
+ transform: "translateY(-100vh)"
604
188
  });
605
189
  /**
606
190
  * Small delay to ensure the opening state is applied after the component is
607
191
  * rendered.
608
192
  */ const timer = setTimeout(()=>{
609
- setAnimationStyles(!animation ? {} : animationType === /* inlined export .ANIMATION_FADE */ ("fade") ? {
193
+ setAnimationStyles(!animation ? {} : animationType === (/* inlined export .ANIMATION_FADE */"fade") ? {
610
194
  opacity: 1
611
195
  } : {
612
196
  transform: "translateY(0)"
@@ -619,75 +203,167 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
619
203
  animation,
620
204
  animationType
621
205
  ]);
622
- /* v8 ignore stop */ return /*#__PURE__*/ jsx(PanelPortal, {
623
- open: open,
624
- onClose: handleClose,
625
- className: panelClassName.outerWrapper,
626
- style: animationStyles,
627
- title: title,
628
- initialFocus: initialFocus,
629
- kind: kind,
630
- children: /*#__PURE__*/ jsxs("div", {
631
- className: panelClassName.innerWrapper,
632
- children: [
633
- /*#__PURE__*/ jsxs("div", {
634
- className: panelClassName.header,
635
- children: [
636
- /*#__PURE__*/ jsx("div", {
637
- className: panelClassName.closeWrapper,
638
- children: /*#__PURE__*/ jsx("button", {
639
- className: panelClassName.closeButton,
640
- type: "button",
641
- "aria-label": "Close",
642
- onClick: handleClose,
643
- children: /*#__PURE__*/ jsx("span", {
644
- children: /*#__PURE__*/ jsx("svg", {
645
- xmlns: "http://www.w3.org/2000/svg",
646
- className: "size-3",
647
- viewBox: "0 0 384 512",
648
- fill: "currentColor",
649
- role: "img",
650
- "aria-hidden": "true",
651
- focusable: "false",
652
- children: /*#__PURE__*/ jsx("path", {
653
- d: "M297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256l105.3-105.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3z",
654
- opacity: "1"
206
+ /* v8 ignore stop */ /**
207
+ * Show/close the native dialog and manage body scroll locking.
208
+ *
209
+ * Uses a reference-counted scroll lock so nested dialogs work correctly:
210
+ * overflow is only restored when the last open dialog closes.
211
+ *
212
+ * The `open` dependency is required because the Dialog component stays
213
+ * mounted while the `<dialog>` element is conditionally rendered via
214
+ * `{open && <dialog>}`. When `open` is false the ref is null and this
215
+ * effect is a no-op. When `open` becomes true, the `<dialog>` element
216
+ * is added to the DOM, the ref is populated, and this effect re-runs
217
+ * to call `showModal()`.
218
+ */ // biome-ignore lint/correctness/useExhaustiveDependencies: open triggers re-evaluation when <dialog> mounts/unmounts
219
+ useEffect(()=>{
220
+ const dialog = dialogRef.current;
221
+ /* v8 ignore start - ref is null when open is false */ if (!dialog) {
222
+ return;
223
+ }
224
+ /* v8 ignore stop */ /* v8 ignore start */ if (typeof dialog.showModal === "function") {
225
+ try {
226
+ dialog.showModal();
227
+ } catch {
228
+ /* showModal can throw if already open */ }
229
+ }
230
+ const releaseScrollLock = acquireScrollLock();
231
+ /* v8 ignore stop */ return ()=>{
232
+ /* v8 ignore start */ releaseScrollLock();
233
+ if (dialog.open) {
234
+ dialog.close();
235
+ }
236
+ /* v8 ignore stop */ };
237
+ }, [
238
+ open
239
+ ]);
240
+ /**
241
+ * Handle ESC key via the `cancel` event. The native `<dialog>` fires
242
+ * `cancel` before closing when ESC is pressed. We prevent the default
243
+ * behavior (which would natively close the dialog and cause a race
244
+ * condition with React's conditional rendering) and instead let React
245
+ * control the close by calling onOpenChange(false).
246
+ */ // biome-ignore lint/correctness/useExhaustiveDependencies: open triggers re-evaluation when <dialog> mounts/unmounts
247
+ useEffect(()=>{
248
+ const dialog = dialogRef.current;
249
+ /* v8 ignore start - ref is null when open is false */ if (!dialog) {
250
+ return;
251
+ }
252
+ /* v8 ignore stop */ const handleCancel = (e)=>{
253
+ e.preventDefault();
254
+ handleClose();
255
+ };
256
+ dialog.addEventListener("cancel", handleCancel);
257
+ return ()=>{
258
+ dialog.removeEventListener("cancel", handleCancel);
259
+ };
260
+ }, [
261
+ open,
262
+ handleClose
263
+ ]);
264
+ /**
265
+ * Manage initial focus when the dialog opens.
266
+ */ // biome-ignore lint/correctness/useExhaustiveDependencies: open triggers re-evaluation when <dialog> mounts/unmounts
267
+ useEffect(()=>{
268
+ const dialog = dialogRef.current;
269
+ /* v8 ignore start - ref is null when open is false */ if (!dialog) {
270
+ return;
271
+ }
272
+ /* v8 ignore stop */ const frameId = requestAnimationFrame(()=>{
273
+ if (initialFocus === -1) {
274
+ dialog.focus();
275
+ return;
276
+ }
277
+ if (initialFocus !== undefined && typeof initialFocus !== "number") {
278
+ initialFocus.current?.focus();
279
+ return;
280
+ }
281
+ const focusIndex = initialFocus ?? 0;
282
+ const tabbable = dialog.querySelectorAll(TABBABLE_SELECTOR);
283
+ const target = tabbable[focusIndex];
284
+ if (target) {
285
+ target.focus();
286
+ }
287
+ });
288
+ return ()=>cancelAnimationFrame(frameId);
289
+ }, [
290
+ open,
291
+ initialFocus
292
+ ]);
293
+ return /*#__PURE__*/ jsx(Fragment, {
294
+ children: open && /*#__PURE__*/ jsx("dialog", {
295
+ ref: dialogRef,
296
+ className: dialogClassName.outerWrapper,
297
+ "aria-labelledby": titleId,
298
+ "aria-describedby": descriptionId,
299
+ tabIndex: -1,
300
+ style: {
301
+ ...animationStyles
302
+ },
303
+ children: /*#__PURE__*/ jsxs("div", {
304
+ className: dialogClassName.innerWrapper,
305
+ id: descriptionId,
306
+ children: [
307
+ /*#__PURE__*/ jsxs("div", {
308
+ className: dialogClassName.header,
309
+ children: [
310
+ /*#__PURE__*/ jsx("div", {
311
+ className: dialogClassName.closeWrapper,
312
+ children: /*#__PURE__*/ jsx("button", {
313
+ className: dialogClassName.closeButton,
314
+ type: "button",
315
+ "aria-label": "Close",
316
+ onClick: handleClose,
317
+ children: /*#__PURE__*/ jsx("span", {
318
+ children: /*#__PURE__*/ jsx("svg", {
319
+ xmlns: "http://www.w3.org/2000/svg",
320
+ className: "size-3",
321
+ viewBox: "0 0 384 512",
322
+ fill: "currentColor",
323
+ role: "img",
324
+ "aria-hidden": "true",
325
+ focusable: "false",
326
+ children: /*#__PURE__*/ jsx("path", {
327
+ d: "M297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256l105.3-105.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3z",
328
+ opacity: "1"
329
+ })
655
330
  })
656
331
  })
657
332
  })
333
+ }),
334
+ /*#__PURE__*/ jsx("h1", {
335
+ className: dialogClassName.title,
336
+ id: titleId,
337
+ children: title
658
338
  })
659
- }),
660
- /*#__PURE__*/ jsx("h1", {
661
- className: panelClassName.title,
662
- children: title
339
+ ]
340
+ }),
341
+ /*#__PURE__*/ jsx("div", {
342
+ className: dialogClassName.scrollableContent,
343
+ children: /*#__PURE__*/ jsx("div", {
344
+ className: dialogClassName.content,
345
+ children: children
663
346
  })
664
- ]
665
- }),
666
- /*#__PURE__*/ jsx("div", {
667
- className: panelClassName.scrollableContent,
668
- children: /*#__PURE__*/ jsx("div", {
669
- className: panelClassName.content,
670
- children: children
347
+ }),
348
+ footer && /*#__PURE__*/ jsx("div", {
349
+ className: dialogClassName.footer,
350
+ children: footer
671
351
  })
672
- }),
673
- footer && /*#__PURE__*/ jsx("div", {
674
- className: panelClassName.footer,
675
- children: footer
676
- })
677
- ]
352
+ ]
353
+ })
678
354
  })
679
355
  });
680
356
  };
681
357
 
682
- ;// CONCATENATED MODULE: ./src/components/index.ts
358
+ // @v8-ignore-start
683
359
 
684
360
 
685
361
 
686
- var __webpack_exports__ANIMATION_FADE = /* inlined export .ANIMATION_FADE */ ("fade");
687
- var __webpack_exports__ANIMATION_SLIDE = /* inlined export .ANIMATION_SLIDE */ ("slide");
688
- var __webpack_exports__LARGE = /* inlined export .LARGE */ ("large");
689
- var __webpack_exports__MEDIUM = /* inlined export .MEDIUM */ ("medium");
690
- var __webpack_exports__NONE = /* inlined export .NONE */ ("none");
691
- var __webpack_exports__SMALL = /* inlined export .SMALL */ ("small");
692
- var __webpack_exports__TYPE_PANEL = /* inlined export .TYPE_PANEL */ ("panel");
693
- export { MESSAGEBOX_CLASSNAME, PANEL_CLASSNAME, Panel, TYPE_MESSAGEBOX, __webpack_exports__ANIMATION_FADE as ANIMATION_FADE, __webpack_exports__ANIMATION_SLIDE as ANIMATION_SLIDE, __webpack_exports__LARGE as LARGE, __webpack_exports__MEDIUM as MEDIUM, __webpack_exports__NONE as NONE, __webpack_exports__SMALL as SMALL, __webpack_exports__TYPE_PANEL as TYPE_PANEL };
362
+ var components_ANIMATION_FADE = (/* inlined export .ANIMATION_FADE */"fade");
363
+ var components_ANIMATION_SLIDE = (/* inlined export .ANIMATION_SLIDE */"slide");
364
+ var components_LARGE = (/* inlined export .LARGE */"large");
365
+ var components_MEDIUM = (/* inlined export .MEDIUM */"medium");
366
+ var components_NONE = (/* inlined export .NONE */"none");
367
+ var components_SMALL = (/* inlined export .SMALL */"small");
368
+ var components_TYPE_PANEL = (/* inlined export .TYPE_PANEL */"panel");
369
+ export { DIALOG_CLASSNAME, Dialog, MESSAGEBOX_CLASSNAME, TYPE_MESSAGEBOX, components_ANIMATION_FADE as ANIMATION_FADE, components_ANIMATION_SLIDE as ANIMATION_SLIDE, components_LARGE as LARGE, components_MEDIUM as MEDIUM, components_NONE as NONE, components_SMALL as SMALL, components_TYPE_PANEL as TYPE_PANEL };