@versini/ui-panel 8.0.4 → 8.0.6

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 (2) hide show
  1. package/dist/index.js +221 -494
  2. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /*!
2
- @versini/ui-panel v8.0.4
2
+ @versini/ui-panel v8.0.6
3
3
  © 2025 gizmette.com
4
4
  */
5
5
  try {
6
6
  if (!window.__VERSINI_UI_PANEL__) {
7
7
  window.__VERSINI_UI_PANEL__ = {
8
- version: "8.0.4",
9
- buildTime: "12/18/2025 02:05 PM EST",
8
+ version: "8.0.6",
9
+ buildTime: "12/18/2025 07:21 PM EST",
10
10
  homepage: "https://www.npmjs.com/package/@versini/ui-panel",
11
11
  license: "MIT",
12
12
  };
@@ -15,9 +15,10 @@ try {
15
15
  // nothing to declare officer
16
16
  }
17
17
 
18
- import { jsx, jsxs } from "react/jsx-runtime";
19
- import { useCallback, useEffect, useId, useRef, useState } from "react";
18
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
+ import { FloatingFocusManager, FloatingOverlay, FloatingPortal, useClick, useDismiss, useFloating, useInteractions, useMergeRefs, useRole } from "@floating-ui/react";
20
20
  import clsx from "clsx";
21
+ import { cloneElement, createContext, forwardRef, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
21
22
 
22
23
  ;// CONCATENATED MODULE: ./src/common/constants.ts
23
24
  const MESSAGEBOX_CLASSNAME = "av-messagebox";
@@ -33,447 +34,177 @@ const NONE = "none";
33
34
 
34
35
  ;// CONCATENATED MODULE: external "react/jsx-runtime"
35
36
 
36
- ;// CONCATENATED MODULE: external "react"
37
+ ;// CONCATENATED MODULE: external "@floating-ui/react"
37
38
 
38
39
  ;// CONCATENATED MODULE: external "clsx"
39
40
 
40
- ;// CONCATENATED MODULE: ./src/components/Panel/dialogStackManager.ts
41
- /**
42
- * Dialog Stack Manager
43
- *
44
- * Implements the W3C WAI-ARIA APG pattern for managing nested modal dialogs.
45
- * This module maintains a stack of open dialogs and coordinates focus event
46
- * listeners between them.
47
- *
48
- * Key features:
49
- * - Only the topmost dialog has active focus listeners
50
- * - When a nested dialog opens, parent listeners are suspended
51
- * - When a dialog closes, parent listeners are restored
52
- * - Programmatic focus changes are flagged to prevent event interference
53
- *
54
- * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/
55
- */ /**
56
- * Stack of currently open dialogs. The last item is the topmost dialog.
57
- */ const openDialogStack = [];
58
- /**
59
- * Flag to ignore focus changes during programmatic focus operations.
60
- * When true, focus event handlers should return early to prevent interference.
61
- *
62
- * This is critical for nested dialogs: when a child dialog closes and
63
- * programmatically returns focus to its trigger element inside the parent,
64
- * we don't want the parent's focusin handler to interfere.
65
- */ let ignoreFocusChanges = false;
66
- /**
67
- * Get whether focus changes should be ignored.
68
- * Focus event handlers should check this and return early if true.
69
- */ function shouldIgnoreFocusChanges() {
70
- return ignoreFocusChanges;
71
- }
72
- /**
73
- * Execute a function while ignoring focus change events.
74
- * Use this when programmatically moving focus to prevent event handlers
75
- * from interfering.
76
- *
77
- * @param fn - Function to execute (typically contains focus() calls)
78
- */ function withIgnoredFocusChanges(fn) {
79
- ignoreFocusChanges = true;
80
- try {
81
- fn();
82
- } finally{
83
- // Use setTimeout to ensure the flag stays true through any
84
- // microtasks that might fire focus events
85
- setTimeout(()=>{
86
- ignoreFocusChanges = false;
87
- }, 0);
88
- }
89
- }
90
- /**
91
- * Register a dialog in the stack when it opens.
92
- * If there's a parent dialog, its listeners are suspended.
93
- *
94
- * @param entry - The dialog entry to register
95
- */ function registerDialog(entry) {
96
- // If there's already a dialog open, suspend its listeners
97
- if (openDialogStack.length > 0) {
98
- const currentTop = openDialogStack[openDialogStack.length - 1];
99
- currentTop.removeListeners();
100
- }
101
- // Add the new dialog to the stack and activate its listeners
102
- openDialogStack.push(entry);
103
- entry.addListeners();
104
- }
105
- /**
106
- * Unregister a dialog from the stack when it closes.
107
- * If there's a parent dialog, its listeners are restored.
108
- *
109
- * @param dialogRef - Reference to the dialog element being closed
110
- */ function unregisterDialog(dialogRef) {
111
- const index = openDialogStack.findIndex((entry)=>entry.dialogRef === dialogRef);
112
- /* c8 ignore next 3 - defensive check */ if (index === -1) {
113
- return;
114
- }
115
- // Remove listeners from the dialog being closed
116
- const [removedEntry] = openDialogStack.splice(index, 1);
117
- removedEntry.removeListeners();
118
- // If there's a parent dialog, restore its listeners
119
- if (openDialogStack.length > 0) {
120
- const newTop = openDialogStack[openDialogStack.length - 1];
121
- newTop.addListeners();
122
- }
123
- }
124
- /**
125
- * Get the current number of open dialogs.
126
- * Useful for debugging and testing.
127
- */ function getOpenDialogCount() {
128
- return openDialogStack.length;
129
- }
130
- /**
131
- * Check if a given dialog is the topmost (current) dialog.
132
- *
133
- * @param dialogRef - Reference to the dialog element to check
134
- */ function isTopmostDialog(dialogRef) {
135
- if (openDialogStack.length === 0) {
136
- return false;
137
- }
138
- return openDialogStack[openDialogStack.length - 1].dialogRef === dialogRef;
139
- }
140
- /**
141
- * Reset the stack (for testing purposes only).
142
- * @internal
143
- */ function _resetStackForTesting() {
144
- while(openDialogStack.length > 0){
145
- openDialogStack.pop();
41
+ ;// CONCATENATED MODULE: external "react"
42
+
43
+ ;// CONCATENATED MODULE: ../ui-modal/dist/index.js
44
+ /*!
45
+ @versini/ui-modal v3.2.0
46
+ © 2025 gizmette.com
47
+ */ try {
48
+ if (!window.__VERSINI_UI_MODAL__) {
49
+ window.__VERSINI_UI_MODAL__ = {
50
+ version: "3.2.0",
51
+ buildTime: "12/18/2025 07:21 PM EST",
52
+ homepage: "https://github.com/aversini/ui-components",
53
+ license: "MIT"
54
+ };
146
55
  }
147
- ignoreFocusChanges = false;
56
+ } catch (error) {
57
+ // nothing to declare officer
148
58
  }
149
59
 
150
- ;// CONCATENATED MODULE: ./src/components/Panel/PanelPortal.tsx
151
-
152
-
153
60
 
154
61
 
155
62
 
156
- /**
157
- * Selector for all focusable elements within a container. Based on W3C WAI-ARIA
158
- * practices for dialog focus management.
159
- */ const FOCUSABLE_SELECTOR = [
160
- 'a[href]:not([disabled]):not([tabindex="-1"])',
161
- 'button:not([disabled]):not([tabindex="-1"])',
162
- 'textarea:not([disabled]):not([tabindex="-1"])',
163
- 'input:not([disabled]):not([tabindex="-1"])',
164
- 'select:not([disabled]):not([tabindex="-1"])',
165
- '[tabindex]:not([tabindex="-1"]):not([disabled])',
166
- 'audio[controls]:not([tabindex="-1"])',
167
- 'video[controls]:not([tabindex="-1"])',
168
- 'details:not([tabindex="-1"])'
169
- ].join(", ");
170
- /**
171
- * Portal component for rendering the Panel as a modal dialog using the native
172
- * HTML <dialog> element with showModal(). This provides:
173
- * - Native focus trapping (works correctly on iPad with physical keyboard)
174
- * - Native ESC key handling via cancel event
175
- * - Native backdrop via ::backdrop pseudo-element
176
- * - Native inert background (no need for manual scroll lock)
177
- * - Top layer rendering (no need for createPortal)
178
- *
179
- * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
180
- * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
181
- *
182
- */ function PanelPortal({ open, onClose, children, className, style, title, initialFocus = 0, kind = /* inlined export .TYPE_PANEL */ ("panel") }) {
183
- const labelId = useId();
184
- const descriptionId = useId();
185
- const dialogRef = useRef(null);
186
- const previouslyFocusedRef = useRef(null);
187
- /**
188
- * Get all focusable elements within the dialog. Excludes focus sentinel
189
- * elements used for circular focus trapping.
190
- */ const getFocusableElements = useCallback(()=>{
191
- /* c8 ignore next 3 - defensive check, dialogRef is always set when open */ if (!dialogRef.current) {
192
- return [];
193
- }
194
- const elements = dialogRef.current.querySelectorAll(FOCUSABLE_SELECTOR);
195
- return Array.from(elements).filter((el)=>el.offsetParent !== null && // Filter out hidden elements
196
- !el.hasAttribute("data-focus-sentinel"));
197
- }, []);
198
- /**
199
- * Focus a specific element by index, or the element referenced by a ref.
200
- */ const focusElement = useCallback((target)=>{
201
- if (typeof target === "number") {
202
- if (target === -1) {
203
- // -1 means don't focus anything.
204
- return;
205
- }
206
- const focusableElements = getFocusableElements();
207
- if (focusableElements.length > 0) {
208
- const index = Math.min(target, focusableElements.length - 1);
209
- focusableElements[index]?.focus();
210
- }
211
- } else if (target?.current) {
212
- target.current.focus();
213
- }
214
- }, [
215
- getFocusableElements
63
+ const ModalContext = /*#__PURE__*/ createContext(null);
64
+ function useModal({ initialOpen = false, open: controlledOpen, onOpenChange: setControlledOpen, initialFocus } = {}) {
65
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
66
+ const [labelId, setLabelId] = useState();
67
+ const [descriptionId, setDescriptionId] = useState();
68
+ /* v8 ignore next 2 */ const open = controlledOpen ?? uncontrolledOpen;
69
+ const setOpen = setControlledOpen ?? setUncontrolledOpen;
70
+ const data = useFloating({
71
+ open,
72
+ onOpenChange: setOpen
73
+ });
74
+ const context = data.context;
75
+ const click = useClick(context, {
76
+ enabled: controlledOpen == null
77
+ });
78
+ const dismiss = useDismiss(context, {
79
+ outsidePress: false,
80
+ outsidePressEvent: "mousedown"
81
+ });
82
+ const role = useRole(context);
83
+ const interactions = useInteractions([
84
+ click,
85
+ dismiss,
86
+ role
216
87
  ]);
217
- /**
218
- * Handle the native cancel event (fired when ESC is pressed). This replaces
219
- * the custom keydown handler since the native dialog handles ESC
220
- * automatically.
221
- */ const handleCancel = useCallback((event)=>{
222
- // Prevent the default close behavior so we can control it via onClose.
223
- event.preventDefault();
224
- onClose();
225
- }, [
226
- onClose
88
+ return useMemo(()=>({
89
+ open,
90
+ setOpen,
91
+ ...interactions,
92
+ ...data,
93
+ labelId,
94
+ descriptionId,
95
+ setLabelId,
96
+ setDescriptionId,
97
+ initialFocus
98
+ }), [
99
+ open,
100
+ setOpen,
101
+ interactions,
102
+ data,
103
+ labelId,
104
+ descriptionId,
105
+ initialFocus
227
106
  ]);
228
- /**
229
- * Handle Tab key to implement circular focus trapping. Native dialog focus
230
- * trapping may not be circular in all browsers, so we manually wrap focus from
231
- * last to first element (and vice versa). Uses document-level event listener
232
- * for better iPad physical keyboard support.
233
- *
234
- * IMPORTANT: On iPad Safari with a physical keyboard, the Tab key does not
235
- * automatically navigate between focusable elements. We must manually handle
236
- * ALL Tab key presses, not just wrapping cases.
237
- *
238
- */ const handleKeyDown = useCallback((event)=>{
239
- if (event.key !== "Tab" || !dialogRef.current) {
240
- return;
241
- }
242
- const focusableElements = getFocusableElements();
243
- /* c8 ignore next 4 - edge case: dialog with no focusable elements */ if (focusableElements.length === 0) {
244
- event.preventDefault();
245
- return;
246
- }
247
- const firstElement = focusableElements[0];
248
- const lastElement = focusableElements[focusableElements.length - 1];
249
- const activeElement = document.activeElement;
250
- // Find the current index of the focused element.
251
- const currentIndex = focusableElements.indexOf(activeElement);
252
- /**
253
- * Always prevent default and manually handle focus navigation. This is
254
- * required for iPad Safari with physical keyboard, where Tab key doesn't
255
- * automatically navigate between focusable elements.
256
- */ event.preventDefault();
257
- if (event.shiftKey) {
258
- // Shift+Tab: move to previous element, wrap to last if on first.
259
- if (activeElement === firstElement || currentIndex <= 0) {
260
- lastElement?.focus();
261
- } else {
262
- focusableElements[currentIndex - 1]?.focus();
263
- }
264
- } else {
265
- // Tab: move to next element, wrap to first if on last.
266
- if (activeElement === lastElement || currentIndex >= focusableElements.length - 1) {
267
- firstElement?.focus();
268
- } else {
269
- focusableElements[currentIndex + 1]?.focus();
270
- }
271
- }
272
- }, [
273
- getFocusableElements
107
+ }
108
+ const useModalContext = ()=>{
109
+ const context = useContext(ModalContext);
110
+ /* v8 ignore next 3 */ if (context == null) {
111
+ throw new Error("Modal components must be wrapped in <Modal />");
112
+ }
113
+ return context;
114
+ };
115
+ function Modal({ children, ...options }) {
116
+ const dialog = useModal(options);
117
+ return /*#__PURE__*/ jsx(ModalContext.Provider, {
118
+ value: dialog,
119
+ children: children
120
+ });
121
+ }
122
+ const Modal_ModalContent = /*#__PURE__*/ forwardRef(function ModalContent(props, propRef) {
123
+ const { context: floatingContext, ...context } = useModalContext();
124
+ const ref = useMergeRefs([
125
+ context.refs.setFloating,
126
+ propRef
274
127
  ]);
275
- /**
276
- * Handle focus events to ensure focus stays within the dialog. This catches
277
- * focus that escapes via Tab key on iPad Safari or other means.
278
- *
279
- * Uses the dialog stack manager's ignore flag to prevent interference during
280
- * programmatic focus operations (e.g., when a nested dialog closes and returns
281
- * focus to its trigger element).
282
- *
283
- */ /* v8 ignore next 20 - focus escape handling for iPad Safari, hard to test in jsdom */ const handleFocusIn = useCallback((event)=>{
284
- // Ignore focus changes triggered by programmatic focus operations.
285
- if (shouldIgnoreFocusChanges()) {
286
- return;
287
- }
288
- if (!dialogRef.current || dialogRef.current.contains(event.target)) {
289
- return;
290
- }
291
- // Focus escaped the dialog, bring it back.
292
- const focusableElements = getFocusableElements();
293
- if (focusableElements.length > 0) {
294
- focusableElements[0]?.focus();
295
- }
128
+ /* v8 ignore next 3 */ if (!floatingContext.open) {
129
+ return null;
130
+ }
131
+ const { overlayBackground, ...rest } = props;
132
+ const overlayClass = clsx("grid place-items-center", {
133
+ [`${overlayBackground}`]: overlayBackground,
134
+ "bg-black sm:bg-black/[.8]": !overlayBackground
135
+ });
136
+ return /*#__PURE__*/ jsx(FloatingPortal, {
137
+ children: /*#__PURE__*/ jsx(FloatingOverlay, {
138
+ className: overlayClass,
139
+ lockScroll: true,
140
+ children: /*#__PURE__*/ jsx(FloatingFocusManager, {
141
+ context: floatingContext,
142
+ initialFocus: context.initialFocus,
143
+ children: /*#__PURE__*/ jsx("div", {
144
+ ref: ref,
145
+ "aria-labelledby": context.labelId,
146
+ "aria-describedby": context.descriptionId,
147
+ ...context.getFloatingProps(rest),
148
+ children: rest.children
149
+ })
150
+ })
151
+ })
152
+ });
153
+ });
154
+ const Modal_ModalHeading = /*#__PURE__*/ forwardRef(function ModalHeading({ children, ...props }, ref) {
155
+ const { setLabelId } = useModalContext();
156
+ const id = useId();
157
+ // Only sets `aria-labelledby` on the Modal root element
158
+ // if this component is mounted inside it.
159
+ useLayoutEffect(()=>{
160
+ setLabelId(id);
161
+ return ()=>setLabelId(undefined);
296
162
  }, [
297
- getFocusableElements
163
+ id,
164
+ setLabelId
298
165
  ]);
299
- /**
300
- * Handle clicks on the backdrop (the area outside the dialog content). Native
301
- * dialog doesn't provide backdrop click handling, so we use the dialog
302
- * element's click event and check if the click target is the dialog itself
303
- * (not a child element).
304
- */ /* v8 ignore next 9 - backdrop clicks are disabled by design in current implementation */ const handleDialogClick = useCallback((_event)=>{
305
- /**
306
- * If the click is directly on the dialog element (the backdrop area), not on
307
- * any child element, then close the dialog. Currently disabled -
308
- * outsidePress is false by design. if (_event.target === dialogRef.current)
309
- * { onClose(); }
310
- */ }, []);
311
- /**
312
- * Focus sentinel handler - when a sentinel element receives focus, redirect
313
- * focus to the appropriate element inside the dialog. This handles iPad
314
- * Safari's Tab key behavior which can bypass event listeners.
315
- */ const handleSentinelFocus = useCallback((position)=>{
316
- const focusableElements = getFocusableElements();
317
- /* c8 ignore next 3 - edge case: dialog with no focusable elements */ if (focusableElements.length === 0) {
318
- return;
319
- }
320
- if (position === "start") {
321
- // Focus came from the end, wrap to last element.
322
- focusableElements[focusableElements.length - 1]?.focus();
323
- } else {
324
- // Focus came from the start, wrap to first element.
325
- focusableElements[0]?.focus();
326
- }
166
+ return /*#__PURE__*/ jsx("h1", {
167
+ ...props,
168
+ ref: ref,
169
+ id: id,
170
+ children: children
171
+ });
172
+ });
173
+ const Modal_ModalDescription = /*#__PURE__*/ forwardRef(function ModalDescription({ children, ...props }, ref) {
174
+ const { setDescriptionId } = useModalContext();
175
+ const id = useId();
176
+ // Only sets `aria-describedby` on the Modal root element
177
+ // if this component is mounted inside it.
178
+ useLayoutEffect(()=>{
179
+ setDescriptionId(id);
180
+ return ()=>setDescriptionId(undefined);
327
181
  }, [
328
- getFocusableElements
182
+ id,
183
+ setDescriptionId
329
184
  ]);
330
- /**
331
- * Effect to show/hide the dialog and manage focus. Uses the dialog stack
332
- * manager to coordinate listeners between nested dialogs.
333
- */ useEffect(()=>{
334
- const dialog = dialogRef.current;
335
- /* c8 ignore next 3 - defensive check */ if (!dialog) {
336
- return;
337
- }
338
- // Store the currently focused element to restore later.
339
- previouslyFocusedRef.current = document.activeElement;
340
- // Show the dialog as a modal.
341
- if (!dialog.open) {
342
- dialog.showModal();
343
- }
344
- /**
345
- * Add cancel event listener for ESC key (always needed, not managed by
346
- * stack).
347
- */ dialog.addEventListener("cancel", handleCancel);
348
- /**
349
- * Define listener management functions for the stack manager. These will be
350
- * called when this dialog becomes/stops being the topmost dialog.
351
- */ const addListeners = ()=>{
352
- document.addEventListener("keydown", handleKeyDown);
353
- document.addEventListener("focusin", handleFocusIn);
354
- };
355
- const removeListeners = ()=>{
356
- document.removeEventListener("keydown", handleKeyDown);
357
- document.removeEventListener("focusin", handleFocusIn);
358
- };
359
- /**
360
- * Register this dialog with the stack manager. This will suspend parent
361
- * dialog listeners if any exist.
362
- */ registerDialog({
363
- dialogRef: dialog,
364
- addListeners,
365
- removeListeners
366
- });
367
- /**
368
- * Set initial focus after a small delay to ensure the DOM is ready. This
369
- * works around React's autoFocus prop not working with native dialog.
370
- */ const focusTimer = setTimeout(()=>{
371
- focusElement(initialFocus);
372
- }, 0);
373
- // Capture the previously focused element for restoration in cleanup.
374
- const previouslyFocused = previouslyFocusedRef.current;
375
- return ()=>{
376
- clearTimeout(focusTimer);
377
- dialog.removeEventListener("cancel", handleCancel);
378
- /**
379
- * Unregister from the stack manager. This will restore parent dialog
380
- * listeners if any exist.
381
- */ unregisterDialog(dialog);
382
- // Close the dialog if it's still open.
383
- if (dialog.open) {
384
- dialog.close();
385
- }
386
- /**
387
- * Restore focus to the previously focused element if it's still in the DOM.
388
- * Use withIgnoredFocusChanges to prevent parent dialog's handleFocusIn from
389
- * interfering with focus restoration.
390
- */ if (previouslyFocused?.isConnected) {
391
- withIgnoredFocusChanges(()=>{
392
- if (previouslyFocused.isConnected) {
393
- previouslyFocused.focus();
394
- }
395
- });
396
- }
397
- };
398
- }, [
399
- handleCancel,
400
- handleKeyDown,
401
- handleFocusIn,
402
- initialFocus,
403
- focusElement
185
+ return /*#__PURE__*/ jsx("div", {
186
+ ...props,
187
+ ref: ref,
188
+ id: id,
189
+ children: children
190
+ });
191
+ });
192
+ const Modal_ModalClose = /*#__PURE__*/ forwardRef(function ModalClose(props, ref) {
193
+ const { setOpen } = useModalContext();
194
+ const { trigger, className, ...rest } = props;
195
+ const handleClose = useCallback(()=>setOpen(false), [
196
+ setOpen
404
197
  ]);
405
- /* c8 ignore next 3 - early return when panel is closed */ if (!open) {
406
- return null;
407
- }
408
- const isMessageBox = kind === TYPE_MESSAGEBOX;
409
- const dialogClass = clsx(/**
410
- * Center the dialog on screen with fixed positioning. Native dialog uses
411
- * position: fixed by default.
412
- * - Panel on mobile: inset-0 + no margin for full screen.
413
- * - Panel on desktop: inset-x-0 + top/bottom auto for vertical centering.
414
- * - MessageBox: Always centered (both mobile and desktop) since it doesn't
415
- * take full screen.
416
- */ "fixed max-h-none max-w-none p-0", {
417
- // Panel: full screen on mobile, centered on desktop
418
- "inset-0 m-0 sm:inset-auto sm:inset-x-0 sm:top-1/2 sm:-translate-y-1/2 sm:mx-auto": !isMessageBox,
419
- // MessageBox: always centered at all breakpoints
420
- "inset-auto inset-x-0 top-1/2 -translate-y-1/2 mx-auto z-100": isMessageBox
421
- }, /**
422
- * Backdrop styling via Tailwind's backdrop: variant for ::backdrop
423
- * pseudo-element. Full black on mobile, 80% opacity on desktop (matches
424
- * original overlay).
425
- */ "backdrop:bg-black sm:backdrop:bg-black/80", className);
426
- return /*#__PURE__*/ jsxs("dialog", {
427
- ref: dialogRef,
428
- "aria-labelledby": labelId,
429
- "aria-describedby": descriptionId,
430
- className: dialogClass,
431
- style: style,
432
- onClick: handleDialogClick,
433
- children: [
434
- /*#__PURE__*/ jsx("span", {
435
- tabIndex: 0,
436
- onFocus: ()=>handleSentinelFocus("start"),
437
- "data-focus-sentinel": "start",
438
- style: {
439
- position: "absolute",
440
- width: 1,
441
- height: 1,
442
- padding: 0,
443
- margin: -1,
444
- overflow: "hidden",
445
- clip: "rect(0, 0, 0, 0)",
446
- whiteSpace: "nowrap",
447
- border: 0
448
- },
449
- "aria-hidden": "true"
450
- }),
451
- /*#__PURE__*/ jsx("span", {
452
- id: labelId,
453
- className: "sr-only",
454
- children: title
455
- }),
456
- children,
457
- /*#__PURE__*/ jsx("span", {
458
- tabIndex: 0,
459
- onFocus: ()=>handleSentinelFocus("end"),
460
- "data-focus-sentinel": "end",
461
- style: {
462
- position: "absolute",
463
- width: 1,
464
- height: 1,
465
- padding: 0,
466
- margin: -1,
467
- overflow: "hidden",
468
- clip: "rect(0, 0, 0, 0)",
469
- whiteSpace: "nowrap",
470
- border: 0
471
- },
472
- "aria-hidden": "true"
473
- })
474
- ]
198
+ return /*#__PURE__*/ jsx("div", {
199
+ className: className,
200
+ children: /*#__PURE__*/ cloneElement(trigger, {
201
+ ref,
202
+ onClick: handleClose,
203
+ ...rest
204
+ })
475
205
  });
476
- }
206
+ });
207
+ /* v8 ignore next 1 */
477
208
 
478
209
  ;// CONCATENATED MODULE: ./src/components/Panel/utilities.ts
479
210
 
@@ -501,10 +232,10 @@ const getPanelClassName = ({ className, kind, borderMode, animation, maxWidth =
501
232
  ["w-full sm:w-[95%] md:max-w-4xl"]: kind === /* inlined export .TYPE_PANEL */ ("panel") && !className && maxWidth === /* inlined export .LARGE */ ("large"),
502
233
  /**
503
234
  * Heights and max heights for Panel
504
- * Mobile: full height (h-full works with inset-0), Desktop: shrink-to-fit with max-height constraint
505
- */ "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"),
506
- "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"),
507
- "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"),
235
+ * Mobile: full height, Desktop: clamp between min and max to allow flexible sizing
236
+ */ "min-h-40 max-h-full sm:max-h-[40vh]": kind === /* inlined export .TYPE_PANEL */ ("panel") && effectiveMaxHeight === /* inlined export .SMALL */ ("small"),
237
+ "min-h-40 max-h-full sm:max-h-[60vh]": kind === /* inlined export .TYPE_PANEL */ ("panel") && effectiveMaxHeight === /* inlined export .MEDIUM */ ("medium"),
238
+ "min-h-40 max-h-full sm:max-h-[95vh]": kind === /* inlined export .TYPE_PANEL */ ("panel") && effectiveMaxHeight === /* inlined export .LARGE */ ("large"),
508
239
  /**
509
240
  * Panel border colors
510
241
  */ "sm:border-border-dark": borderMode === "dark" && kind === /* inlined export .TYPE_PANEL */ ("panel"),
@@ -526,7 +257,7 @@ const getPanelClassName = ({ className, kind, borderMode, animation, maxWidth =
526
257
  "border-border-accent": borderMode === "light" && kind === TYPE_MESSAGEBOX,
527
258
  [`${className}`]: !!className
528
259
  }),
529
- innerWrapper: "content flex flex-col rounded-[inherit] relative min-h-full isolate",
260
+ innerWrapper: "content flex flex-col rounded-[inherit] relative min-h-full",
530
261
  scrollableContent: clsx("flex-1 overflow-y-auto overflow-x-hidden", "pt-12", {
531
262
  "pb-12": hasFooter
532
263
  }),
@@ -575,15 +306,9 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
575
306
  hasFooter: Boolean(footer)
576
307
  });
577
308
  /**
578
- * Handle close button click.
579
- */ const handleClose = useCallback(()=>{
580
- onOpenChange(false);
581
- }, [
582
- onOpenChange
583
- ]);
584
- /**
585
- * If the panel is opened, set the document title to the panel's title. If it's
586
- * closed, restore the original document.title.
309
+ * If the panel is opened, set the document
310
+ * title to the panel's title. If it's closed,
311
+ * restore the original document.title.
587
312
  */ useEffect(()=>{
588
313
  if (open) {
589
314
  originalTitleRef.current = document.title;
@@ -600,7 +325,7 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
600
325
  ]);
601
326
  /**
602
327
  * Effect to handle the opening and closing animations.
603
- */ /* v8 ignore next 31 */ useEffect(()=>{
328
+ */ /* v8 ignore next 30 */ useEffect(()=>{
604
329
  if (!animation) {
605
330
  return;
606
331
  }
@@ -610,10 +335,9 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
610
335
  } : {
611
336
  transform: "translateY(-666vh)"
612
337
  });
613
- /**
614
- * Small delay to ensure the opening state is applied after the component is
615
- * rendered.
616
- */ const timer = setTimeout(()=>{
338
+ // Small delay to ensure the opening state is applied after
339
+ // the component is rendered.
340
+ const timer = setTimeout(()=>{
617
341
  setAnimationStyles(!animation ? {} : animationType === /* inlined export .ANIMATION_FADE */ ("fade") ? {
618
342
  opacity: 1
619
343
  } : {
@@ -627,62 +351,65 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
627
351
  animation,
628
352
  animationType
629
353
  ]);
630
- return /*#__PURE__*/ jsx(PanelPortal, {
631
- open: open,
632
- onClose: handleClose,
633
- className: panelClassName.outerWrapper,
634
- style: animationStyles,
635
- title: title,
636
- initialFocus: initialFocus,
637
- kind: kind,
638
- children: /*#__PURE__*/ jsxs("div", {
639
- className: panelClassName.innerWrapper,
640
- children: [
641
- /*#__PURE__*/ jsxs("div", {
642
- className: panelClassName.header,
354
+ return /*#__PURE__*/ jsx(Fragment, {
355
+ children: open && /*#__PURE__*/ jsx(Modal, {
356
+ open: open,
357
+ onOpenChange: onOpenChange,
358
+ initialFocus: initialFocus,
359
+ children: /*#__PURE__*/ jsx(Modal_ModalContent, {
360
+ className: panelClassName.outerWrapper,
361
+ style: {
362
+ ...animationStyles
363
+ },
364
+ children: /*#__PURE__*/ jsxs(Modal_ModalDescription, {
365
+ className: panelClassName.innerWrapper,
643
366
  children: [
644
- /*#__PURE__*/ jsx("div", {
645
- className: panelClassName.closeWrapper,
646
- children: /*#__PURE__*/ jsx("button", {
647
- className: panelClassName.closeButton,
648
- type: "button",
649
- "aria-label": "Close",
650
- onClick: handleClose,
651
- children: /*#__PURE__*/ jsx("span", {
652
- children: /*#__PURE__*/ jsx("svg", {
653
- xmlns: "http://www.w3.org/2000/svg",
654
- className: "size-3",
655
- viewBox: "0 0 384 512",
656
- fill: "currentColor",
657
- role: "img",
658
- "aria-hidden": "true",
659
- focusable: "false",
660
- children: /*#__PURE__*/ jsx("path", {
661
- 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",
662
- opacity: "1"
367
+ /*#__PURE__*/ jsxs("div", {
368
+ className: panelClassName.header,
369
+ children: [
370
+ /*#__PURE__*/ jsx(Modal_ModalClose, {
371
+ className: panelClassName.closeWrapper,
372
+ trigger: /*#__PURE__*/ jsx("button", {
373
+ className: panelClassName.closeButton,
374
+ type: "button",
375
+ "aria-label": "Close",
376
+ children: /*#__PURE__*/ jsx("span", {
377
+ children: /*#__PURE__*/ jsx("svg", {
378
+ xmlns: "http://www.w3.org/2000/svg",
379
+ className: "size-3",
380
+ viewBox: "0 0 384 512",
381
+ fill: "currentColor",
382
+ role: "img",
383
+ "aria-hidden": "true",
384
+ focusable: "false",
385
+ children: /*#__PURE__*/ jsx("path", {
386
+ 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",
387
+ opacity: "1"
388
+ })
389
+ })
663
390
  })
664
391
  })
392
+ }),
393
+ /*#__PURE__*/ jsx(Modal_ModalHeading, {
394
+ className: panelClassName.title,
395
+ children: title
665
396
  })
397
+ ]
398
+ }),
399
+ /*#__PURE__*/ jsx("div", {
400
+ className: panelClassName.scrollableContent,
401
+ children: /*#__PURE__*/ jsx("div", {
402
+ className: panelClassName.content,
403
+ children: children
666
404
  })
667
405
  }),
668
- /*#__PURE__*/ jsx("h1", {
669
- className: panelClassName.title,
670
- children: title
406
+ footer && /*#__PURE__*/ jsx("div", {
407
+ className: panelClassName.footer,
408
+ children: footer
671
409
  })
672
410
  ]
673
- }),
674
- /*#__PURE__*/ jsx("div", {
675
- className: panelClassName.scrollableContent,
676
- children: /*#__PURE__*/ jsx("div", {
677
- className: panelClassName.content,
678
- children: children
679
- })
680
- }),
681
- footer && /*#__PURE__*/ jsx("div", {
682
- className: panelClassName.footer,
683
- children: footer
684
411
  })
685
- ]
412
+ })
686
413
  })
687
414
  });
688
415
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-panel",
3
- "version": "8.0.4",
3
+ "version": "8.0.6",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -42,7 +42,8 @@
42
42
  "test": "vitest run"
43
43
  },
44
44
  "devDependencies": {
45
- "@testing-library/jest-dom": "6.9.1"
45
+ "@testing-library/jest-dom": "6.9.1",
46
+ "@versini/ui-modal": "3.2.0"
46
47
  },
47
48
  "dependencies": {
48
49
  "@tailwindcss/typography": "0.5.19",
@@ -52,5 +53,5 @@
52
53
  "sideEffects": [
53
54
  "**/*.css"
54
55
  ],
55
- "gitHead": "1b9898aff5cad941492735a520a6940ab89712b3"
56
+ "gitHead": "3582aaec11fa1d50f3d6ef280d4f06adaf4746e4"
56
57
  }