@jmruthers/pace-core 0.6.4 → 0.6.5
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/{DataTable-E7YQZD7D.js → DataTable-AOVNCPTX.js} +8 -8
- package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-QTFVrL-Z.d.ts} +65 -83
- package/dist/{UnifiedAuthProvider-QPXO24B4.js → UnifiedAuthProvider-4SBX4LU5.js} +4 -4
- package/dist/{api-6LVZTHDS.js → api-O6HTBX5Y.js} +3 -3
- package/dist/{chunk-I6DAQMWX.js → chunk-6COVEUS7.js} +130 -106
- package/dist/chunk-6COVEUS7.js.map +1 -0
- package/dist/{chunk-36LVWXB2.js → chunk-AFVQODI2.js} +37 -1
- package/dist/{chunk-36LVWXB2.js.map → chunk-AFVQODI2.js.map} +1 -1
- package/dist/{chunk-3LPHPB62.js → chunk-EFN2EIMK.js} +2 -2
- package/dist/{chunk-ATKZM7RX.js → chunk-G7QEZTYQ.js} +31 -31
- package/dist/{chunk-ATKZM7RX.js.map → chunk-G7QEZTYQ.js.map} +1 -1
- package/dist/{chunk-NN6WWZ5U.js → chunk-HU2C6SSC.js} +29 -18
- package/dist/chunk-HU2C6SSC.js.map +1 -0
- package/dist/{chunk-AVMLPIM7.js → chunk-IHB5DR3H.js} +102 -51
- package/dist/chunk-IHB5DR3H.js.map +1 -0
- package/dist/{chunk-7JPAB3T5.js → chunk-IVOFDYWT.js} +364 -208
- package/dist/chunk-IVOFDYWT.js.map +1 -0
- package/dist/{chunk-6SOIHG6Z.js → chunk-JGRYX5UX.js} +120 -20
- package/dist/chunk-JGRYX5UX.js.map +1 -0
- package/dist/{chunk-OEWDTMG7.js → chunk-NTM7ZSB6.js} +4 -4
- package/dist/chunk-NTM7ZSB6.js.map +1 -0
- package/dist/{chunk-5EC5MEWX.js → chunk-RGAWHO7N.js} +4 -4
- package/dist/chunk-RGAWHO7N.js.map +1 -0
- package/dist/{chunk-YKRAFF5K.js → chunk-UPPMRMYG.js} +3 -3
- package/dist/{chunk-YKRAFF5K.js.map → chunk-UPPMRMYG.js.map} +1 -1
- package/dist/components.d.ts +2 -3
- package/dist/components.js +24 -28
- package/dist/components.js.map +1 -1
- package/dist/{contextValidator-OOPCLPZW.js → contextValidator-5OGXSPKS.js} +2 -2
- package/dist/hooks.d.ts +3 -3
- package/dist/hooks.js +41 -139
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +27 -18
- package/dist/index.js +41 -50
- package/dist/index.js.map +1 -1
- package/dist/providers.js +3 -3
- package/dist/rbac/index.d.ts +16 -9
- package/dist/rbac/index.js +6 -6
- package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-ClnV4tnv.d.ts} +8 -8
- package/dist/utils.js +1 -1
- package/docs/api/modules.md +210 -100
- package/package.json +1 -2
- package/scripts/validate-master.js +1 -1
- package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
- package/src/components/DataTable/components/ImportModal.tsx +4 -6
- package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
- package/src/components/DataTable/core/DataTableContext.tsx +1 -1
- package/src/components/DateTimeField/DateTimeField.tsx +17 -19
- package/src/components/DateTimeField/README.md +5 -2
- package/src/components/Dialog/Dialog.test.tsx +248 -228
- package/src/components/Dialog/Dialog.tsx +455 -325
- package/src/components/Dialog/index.ts +3 -3
- package/src/components/FileDisplay/FileDisplay.test.tsx +41 -0
- package/src/components/FileDisplay/FileDisplay.tsx +5 -5
- package/src/components/Form/Form.test.tsx +3 -2
- package/src/components/Form/Form.tsx +4 -5
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
- package/src/components/LoginForm/LoginForm.tsx +2 -2
- package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +32 -39
- package/src/components/PaceAppLayout/README.md +10 -9
- package/src/components/PaceAppLayout/test-setup.tsx +40 -31
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
- package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
- package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
- package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
- package/src/components/UserMenu/UserMenu.test.tsx +38 -6
- package/src/components/UserMenu/UserMenu.tsx +36 -34
- package/src/components/index.ts +3 -4
- package/src/hooks/useEventTheme.ts +4 -4
- package/src/hooks/useEvents.ts +11 -7
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useOrganisationPermissions.ts +4 -4
- package/src/hooks/useOrganisations.ts +13 -7
- package/src/index.ts +11 -1
- package/src/rbac/README.md +20 -20
- package/src/rbac/hooks/useRBAC.test.ts +21 -3
- package/src/rbac/hooks/useRBAC.ts +4 -3
- package/src/rbac/hooks/useResourcePermissions.test.ts +125 -30
- package/src/rbac/hooks/useResourcePermissions.ts +57 -29
- package/src/rbac/permissions.ts +17 -17
- package/src/rbac/utils/contextValidator.ts +36 -0
- package/src/services/AuthService.ts +2 -5
- package/src/services/InactivityService.ts +139 -58
- package/src/styles/core.css +4 -0
- package/src/utils/formatting/formatTime.test.ts +3 -2
- package/dist/chunk-5EC5MEWX.js.map +0 -1
- package/dist/chunk-6SOIHG6Z.js.map +0 -1
- package/dist/chunk-7JPAB3T5.js.map +0 -1
- package/dist/chunk-AVMLPIM7.js.map +0 -1
- package/dist/chunk-I6DAQMWX.js.map +0 -1
- package/dist/chunk-NN6WWZ5U.js.map +0 -1
- package/dist/chunk-OEWDTMG7.js.map +0 -1
- /package/dist/{DataTable-E7YQZD7D.js.map → DataTable-AOVNCPTX.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-QPXO24B4.js.map → UnifiedAuthProvider-4SBX4LU5.js.map} +0 -0
- /package/dist/{api-6LVZTHDS.js.map → api-O6HTBX5Y.js.map} +0 -0
- /package/dist/{chunk-3LPHPB62.js.map → chunk-EFN2EIMK.js.map} +0 -0
- /package/dist/{contextValidator-OOPCLPZW.js.map → contextValidator-5OGXSPKS.js.map} +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module Components/Dialog
|
|
5
5
|
* @since 0.1.0
|
|
6
6
|
*
|
|
7
|
-
* A comprehensive dialog component system
|
|
7
|
+
* A comprehensive dialog component system using native HTML `<dialog>` element.
|
|
8
8
|
* Provides accessible modal dialogs with focus management and keyboard navigation.
|
|
9
9
|
* Uses semantic HTML elements including native <dialog> element for maximum accessibility.
|
|
10
10
|
*
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
* - Sticky headers/footers with scrollable body
|
|
25
25
|
* - Overlay backdrop with customization
|
|
26
26
|
* - Close button with accessibility (optional)
|
|
27
|
-
* - Header
|
|
27
|
+
* - Header and footer components
|
|
28
28
|
* - Configurable close behaviors
|
|
29
|
+
* - Native dialog title and aria-description attributes for accessibility
|
|
29
30
|
*
|
|
30
31
|
* @example
|
|
31
32
|
* ```tsx
|
|
@@ -34,12 +35,9 @@
|
|
|
34
35
|
* <DialogTrigger asChild>
|
|
35
36
|
* <Button>Open Dialog</Button>
|
|
36
37
|
* </DialogTrigger>
|
|
37
|
-
* <DialogContent size="lg">
|
|
38
|
+
* <DialogContent size="lg" title="Edit Profile" description="Make changes to your profile here. Click save when you're done.">
|
|
38
39
|
* <DialogHeader>
|
|
39
|
-
* <
|
|
40
|
-
* <DialogDescription>
|
|
41
|
-
* Make changes to your profile here. Click save when you're done.
|
|
42
|
-
* </DialogDescription>
|
|
40
|
+
* <h2>Edit Profile</h2>
|
|
43
41
|
* </DialogHeader>
|
|
44
42
|
* <DialogBody>
|
|
45
43
|
* <section className="space-y-4">
|
|
@@ -54,81 +52,12 @@
|
|
|
54
52
|
* </DialogFooter>
|
|
55
53
|
* </DialogContent>
|
|
56
54
|
* </Dialog>
|
|
57
|
-
*
|
|
58
|
-
* // Dialog with semantic scrolling content
|
|
59
|
-
* <Dialog>
|
|
60
|
-
* <DialogTrigger asChild>
|
|
61
|
-
* <Button>Scrollable Dialog</Button>
|
|
62
|
-
* </DialogTrigger>
|
|
63
|
-
* <DialogContent
|
|
64
|
-
* size="lg"
|
|
65
|
-
* enableScrolling={true}
|
|
66
|
-
* maxHeightPercent={80}
|
|
67
|
-
* >
|
|
68
|
-
* <DialogHeader>
|
|
69
|
-
* <DialogTitle>Large Content Dialog</DialogTitle>
|
|
70
|
-
* <DialogDescription>
|
|
71
|
-
* This dialog has lots of content and will scroll if needed.
|
|
72
|
-
* </DialogDescription>
|
|
73
|
-
* </DialogHeader>
|
|
74
|
-
* <DialogBody>
|
|
75
|
-
* <section className="space-y-4">
|
|
76
|
-
* {Array.from({ length: 50 }, (_, i) => (
|
|
77
|
-
* <article key={i}>
|
|
78
|
-
* <h4>Content Item {i + 1}</h4>
|
|
79
|
-
* <p>This is semantic content within the dialog body.</p>
|
|
80
|
-
* </article>
|
|
81
|
-
* ))}
|
|
82
|
-
* </section>
|
|
83
|
-
* </DialogBody>
|
|
84
|
-
* <DialogFooter>
|
|
85
|
-
* <Button>Save</Button>
|
|
86
|
-
* </DialogFooter>
|
|
87
|
-
* </DialogContent>
|
|
88
|
-
* </Dialog>
|
|
89
|
-
*
|
|
90
|
-
* // Auto-sizing dialog that fits content
|
|
91
|
-
* <Dialog>
|
|
92
|
-
* <DialogTrigger asChild>
|
|
93
|
-
* <Button>Auto Size Dialog</Button>
|
|
94
|
-
* </DialogTrigger>
|
|
95
|
-
* <DialogContent size="auto">
|
|
96
|
-
* <DialogHeader>
|
|
97
|
-
* <DialogTitle>Auto-Sized Dialog</DialogTitle>
|
|
98
|
-
* <DialogDescription>
|
|
99
|
-
* This dialog automatically adjusts its width to fit the content.
|
|
100
|
-
* </DialogDescription>
|
|
101
|
-
* </DialogHeader>
|
|
102
|
-
* <DialogBody>
|
|
103
|
-
* <section>
|
|
104
|
-
* <p>Content that determines the dialog width...</p>
|
|
105
|
-
* </section>
|
|
106
|
-
* </DialogBody>
|
|
107
|
-
* </DialogContent>
|
|
108
|
-
* </Dialog>
|
|
109
|
-
*
|
|
110
|
-
* // Full-screen dialog with semantic structure
|
|
111
|
-
* <Dialog>
|
|
112
|
-
* <DialogTrigger asChild>
|
|
113
|
-
* <Button>Full Screen</Button>
|
|
114
|
-
* </DialogTrigger>
|
|
115
|
-
* <DialogContent size="full">
|
|
116
|
-
* <DialogHeader>
|
|
117
|
-
* <DialogTitle>Full Screen Dialog</DialogTitle>
|
|
118
|
-
* </DialogHeader>
|
|
119
|
-
* <DialogBody>
|
|
120
|
-
* <section>
|
|
121
|
-
* <p>Full screen content with semantic structure...</p>
|
|
122
|
-
* </section>
|
|
123
|
-
* </DialogBody>
|
|
124
|
-
* </DialogContent>
|
|
125
|
-
* </Dialog>
|
|
126
55
|
* ```
|
|
127
56
|
*
|
|
128
57
|
* @accessibility
|
|
129
58
|
* - WCAG 2.1 AA compliant
|
|
130
59
|
* - Uses semantic HTML structure (dialog, header, main, footer)
|
|
131
|
-
* - Native dialog element with
|
|
60
|
+
* - Native dialog element with proper ARIA attributes
|
|
132
61
|
* - Focus trapping within dialog content
|
|
133
62
|
* - Keyboard navigation support
|
|
134
63
|
* - Screen reader announcements
|
|
@@ -147,18 +76,24 @@
|
|
|
147
76
|
* - Optimized scroll handling
|
|
148
77
|
*
|
|
149
78
|
* @dependencies
|
|
150
|
-
* - @radix-ui/react-dialog - Core dialog functionality
|
|
151
79
|
* - lucide-react - Icons
|
|
152
|
-
* - React 19+ - Hooks and
|
|
80
|
+
* - React 19+ - Hooks, refs, and createPortal
|
|
153
81
|
* - Tailwind CSS - Styling and animations
|
|
82
|
+
*
|
|
83
|
+
* @note
|
|
84
|
+
* This component uses native HTML dialog element with manual focus management.
|
|
85
|
+
* Title and description are provided via props on DialogContent, which set the native
|
|
86
|
+
* title and aria-description attributes on the dialog element for accessibility.
|
|
87
|
+
* See https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaDescription for details.
|
|
154
88
|
*/
|
|
155
89
|
|
|
156
90
|
import * as React from 'react';
|
|
157
|
-
import
|
|
91
|
+
import { createPortal } from 'react-dom';
|
|
158
92
|
import { X } from 'lucide-react';
|
|
159
93
|
import { cn } from '../../utils/core/cn';
|
|
160
94
|
import { renderSafeHtml } from '../../utils/validation/htmlSanitization';
|
|
161
|
-
import { useState, useEffect } from 'react';
|
|
95
|
+
import { useState, useEffect, useRef, useCallback, useId } from 'react';
|
|
96
|
+
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
|
162
97
|
|
|
163
98
|
/**
|
|
164
99
|
* Simple debounce function that matches lodash debounce API
|
|
@@ -195,30 +130,62 @@ function debounce<T extends (...args: any[]) => void>(
|
|
|
195
130
|
*/
|
|
196
131
|
export type DialogSize = 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'auto';
|
|
197
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Dialog context value
|
|
135
|
+
*/
|
|
136
|
+
interface DialogContextValue {
|
|
137
|
+
open: boolean;
|
|
138
|
+
onOpenChange: (open: boolean) => void;
|
|
139
|
+
dialogRef: React.RefObject<HTMLDialogElement | null>;
|
|
140
|
+
titleId: string;
|
|
141
|
+
descriptionId: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const DialogContext = React.createContext<DialogContextValue | null>(null);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Hook to access Dialog context
|
|
148
|
+
*/
|
|
149
|
+
function useDialogContext(): DialogContextValue {
|
|
150
|
+
const context = React.useContext(DialogContext);
|
|
151
|
+
if (!context) {
|
|
152
|
+
throw new Error('Dialog components must be used within a Dialog');
|
|
153
|
+
}
|
|
154
|
+
return context;
|
|
155
|
+
}
|
|
156
|
+
|
|
198
157
|
/**
|
|
199
158
|
* Props for the Dialog root component
|
|
200
159
|
* @public
|
|
201
160
|
*/
|
|
202
|
-
export interface DialogProps
|
|
161
|
+
export interface DialogProps {
|
|
162
|
+
children: React.ReactNode;
|
|
163
|
+
open?: boolean;
|
|
164
|
+
defaultOpen?: boolean;
|
|
165
|
+
onOpenChange?: (open: boolean) => void;
|
|
166
|
+
}
|
|
203
167
|
|
|
204
168
|
/**
|
|
205
169
|
* Props for the DialogTrigger component
|
|
206
170
|
* @public
|
|
207
171
|
*/
|
|
208
|
-
export interface DialogTriggerProps
|
|
172
|
+
export interface DialogTriggerProps {
|
|
173
|
+
children: React.ReactNode;
|
|
174
|
+
asChild?: boolean;
|
|
175
|
+
className?: string;
|
|
176
|
+
onClick?: (e: React.MouseEvent) => void;
|
|
177
|
+
}
|
|
209
178
|
|
|
210
179
|
/**
|
|
211
180
|
* Enhanced props for the DialogContent component with size variants and customization
|
|
212
|
-
* Uses semantic HTML dialog element
|
|
181
|
+
* Uses semantic HTML dialog element
|
|
213
182
|
* @public
|
|
214
183
|
*/
|
|
215
|
-
export interface DialogContentProps extends React.
|
|
184
|
+
export interface DialogContentProps extends React.HTMLAttributes<HTMLDialogElement> {
|
|
216
185
|
/** Dialog size variant */
|
|
217
186
|
size?: DialogSize;
|
|
218
187
|
/** Whether to show the close button */
|
|
219
188
|
showCloseButton?: boolean;
|
|
220
|
-
/** Custom className for the overlay */
|
|
221
|
-
overlayClassName?: string;
|
|
222
189
|
/** Whether to prevent closing on escape key */
|
|
223
190
|
preventCloseOnEscape?: boolean;
|
|
224
191
|
/** Whether to prevent closing on outside click */
|
|
@@ -237,13 +204,31 @@ export interface DialogContentProps extends React.ComponentPropsWithoutRef<typeo
|
|
|
237
204
|
minHeight?: string;
|
|
238
205
|
/** Minimum width in CSS units */
|
|
239
206
|
minWidth?: string;
|
|
207
|
+
/** Dialog title for accessibility (sets native title attribute) */
|
|
208
|
+
title?: string;
|
|
209
|
+
/** Dialog description for accessibility (sets aria-description attribute) */
|
|
210
|
+
description?: string;
|
|
240
211
|
}
|
|
241
212
|
|
|
242
213
|
/**
|
|
243
214
|
* Props for the DialogOverlay component
|
|
244
215
|
* @public
|
|
245
216
|
*/
|
|
246
|
-
export interface DialogOverlayProps extends React.
|
|
217
|
+
export interface DialogOverlayProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Props for the DialogPortal component
|
|
221
|
+
* @public
|
|
222
|
+
*/
|
|
223
|
+
export interface DialogPortalProps {
|
|
224
|
+
children: React.ReactNode;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Props for the DialogClose component
|
|
229
|
+
* @public
|
|
230
|
+
*/
|
|
231
|
+
export interface DialogCloseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
|
|
247
232
|
|
|
248
233
|
/**
|
|
249
234
|
* Props for the DialogHeader component (semantic header element)
|
|
@@ -284,7 +269,7 @@ export interface DialogBodyProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
284
269
|
* Props for the DialogTitle component
|
|
285
270
|
* @public
|
|
286
271
|
*/
|
|
287
|
-
export interface DialogTitleProps extends React.
|
|
272
|
+
export interface DialogTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
|
288
273
|
/** HTML content to render as title (will be sanitized for security) */
|
|
289
274
|
htmlContent?: string;
|
|
290
275
|
/** Whether to allow HTML content rendering (default: true) */
|
|
@@ -295,7 +280,7 @@ export interface DialogTitleProps extends React.ComponentPropsWithoutRef<typeof
|
|
|
295
280
|
* Props for the DialogDescription component
|
|
296
281
|
* @public
|
|
297
282
|
*/
|
|
298
|
-
export interface DialogDescriptionProps extends React.
|
|
283
|
+
export interface DialogDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
|
299
284
|
/** HTML content to render as description (will be sanitized for security) */
|
|
300
285
|
htmlContent?: string;
|
|
301
286
|
/** Whether to allow HTML content rendering (default: true) */
|
|
@@ -312,27 +297,117 @@ const sizeClasses = {
|
|
|
312
297
|
auto: 'max-w-none w-auto min-w-0'
|
|
313
298
|
};
|
|
314
299
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
<
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
)
|
|
335
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Dialog root component
|
|
302
|
+
* Provides context for dialog state management
|
|
303
|
+
*/
|
|
304
|
+
const Dialog = React.memo<DialogProps>(function Dialog({
|
|
305
|
+
children,
|
|
306
|
+
open: controlledOpen,
|
|
307
|
+
defaultOpen = false,
|
|
308
|
+
onOpenChange,
|
|
309
|
+
}) {
|
|
310
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
311
|
+
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
|
312
|
+
const titleId = useId();
|
|
313
|
+
const descriptionId = useId();
|
|
314
|
+
|
|
315
|
+
const isControlled = controlledOpen !== undefined;
|
|
316
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
317
|
+
|
|
318
|
+
const handleOpenChange = useCallback((newOpen: boolean) => {
|
|
319
|
+
if (!isControlled) {
|
|
320
|
+
setInternalOpen(newOpen);
|
|
321
|
+
}
|
|
322
|
+
onOpenChange?.(newOpen);
|
|
323
|
+
}, [isControlled, onOpenChange]);
|
|
324
|
+
|
|
325
|
+
const contextValue = React.useMemo<DialogContextValue>(() => ({
|
|
326
|
+
open,
|
|
327
|
+
onOpenChange: handleOpenChange,
|
|
328
|
+
dialogRef,
|
|
329
|
+
titleId,
|
|
330
|
+
descriptionId,
|
|
331
|
+
}), [open, handleOpenChange, titleId, descriptionId]);
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<DialogContext.Provider value={contextValue}>
|
|
335
|
+
{children}
|
|
336
|
+
</DialogContext.Provider>
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
Dialog.displayName = 'Dialog';
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* DialogTrigger component
|
|
343
|
+
* Opens the dialog when clicked
|
|
344
|
+
*/
|
|
345
|
+
const DialogTrigger = React.forwardRef<HTMLElement, DialogTriggerProps>(
|
|
346
|
+
({ children, asChild = false, className, onClick, ...props }, ref) => {
|
|
347
|
+
const { onOpenChange } = useDialogContext();
|
|
348
|
+
|
|
349
|
+
const handleClick = useCallback((e: React.MouseEvent) => {
|
|
350
|
+
onClick?.(e);
|
|
351
|
+
onOpenChange(true);
|
|
352
|
+
}, [onOpenChange, onClick]);
|
|
353
|
+
|
|
354
|
+
if (asChild && React.isValidElement(children)) {
|
|
355
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
356
|
+
ref,
|
|
357
|
+
onClick: handleClick,
|
|
358
|
+
className: cn(className, (children as any).props?.className),
|
|
359
|
+
...props,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<button
|
|
365
|
+
ref={ref as React.RefObject<HTMLButtonElement>}
|
|
366
|
+
type="button"
|
|
367
|
+
onClick={handleClick}
|
|
368
|
+
className={className}
|
|
369
|
+
{...props}
|
|
370
|
+
>
|
|
371
|
+
{children}
|
|
372
|
+
</button>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
);
|
|
376
|
+
DialogTrigger.displayName = 'DialogTrigger';
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* DialogPortal component
|
|
380
|
+
* Portals dialog content to document.body
|
|
381
|
+
*/
|
|
382
|
+
const DialogPortal: React.FC<DialogPortalProps> = ({ children }) => {
|
|
383
|
+
const [mounted, setMounted] = useState(false);
|
|
384
|
+
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
setMounted(true);
|
|
387
|
+
return () => setMounted(false);
|
|
388
|
+
}, []);
|
|
389
|
+
|
|
390
|
+
if (!mounted) return null;
|
|
391
|
+
|
|
392
|
+
return createPortal(children, document.body);
|
|
393
|
+
};
|
|
394
|
+
DialogPortal.displayName = 'DialogPortal';
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* DialogOverlay component
|
|
398
|
+
* Backdrop overlay for the dialog (optional, native dialog provides ::backdrop)
|
|
399
|
+
* This component is kept for backward compatibility but may not be needed
|
|
400
|
+
* when using native dialog element which provides ::backdrop automatically
|
|
401
|
+
*/
|
|
402
|
+
const DialogOverlay = React.forwardRef<HTMLDivElement, DialogOverlayProps>(
|
|
403
|
+
({ className, ...props }, ref) => {
|
|
404
|
+
// Note: Native dialog element provides ::backdrop automatically
|
|
405
|
+
// This component is kept for API compatibility but may not render
|
|
406
|
+
// The native dialog's ::backdrop is styled via CSS
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
DialogOverlay.displayName = 'DialogOverlay';
|
|
336
411
|
|
|
337
412
|
/**
|
|
338
413
|
* Custom hook for managing smart dialog dimensions
|
|
@@ -367,7 +442,7 @@ const useSmartDimensions = ({
|
|
|
367
442
|
|
|
368
443
|
// Handle height constraints
|
|
369
444
|
if (maxHeightPercent && typeof maxHeightPercent === 'number') {
|
|
370
|
-
const constrainedHeight = Math.min(maxHeightPercent, 95);
|
|
445
|
+
const constrainedHeight = Math.min(maxHeightPercent, 95);
|
|
371
446
|
result.maxHeight = `${constrainedHeight}vh`;
|
|
372
447
|
} else if (maxHeight) {
|
|
373
448
|
result.maxHeight = maxHeight;
|
|
@@ -375,7 +450,7 @@ const useSmartDimensions = ({
|
|
|
375
450
|
|
|
376
451
|
// Handle width constraints
|
|
377
452
|
if (maxWidthPercent && typeof maxWidthPercent === 'number') {
|
|
378
|
-
const constrainedWidth = Math.min(maxWidthPercent, 95);
|
|
453
|
+
const constrainedWidth = Math.min(maxWidthPercent, 95);
|
|
379
454
|
result.maxWidth = `${constrainedWidth}vw`;
|
|
380
455
|
} else if (maxWidth) {
|
|
381
456
|
result.maxWidth = maxWidth;
|
|
@@ -407,7 +482,7 @@ const useSmartDimensions = ({
|
|
|
407
482
|
};
|
|
408
483
|
}, [maxHeightPercent, maxWidthPercent, maxHeight, maxWidth, minHeight, minWidth, enableScrolling]);
|
|
409
484
|
|
|
410
|
-
//
|
|
485
|
+
// Return dimensions
|
|
411
486
|
const result: React.CSSProperties = {};
|
|
412
487
|
|
|
413
488
|
// Handle height constraints
|
|
@@ -440,146 +515,229 @@ const useSmartDimensions = ({
|
|
|
440
515
|
/**
|
|
441
516
|
* DialogContent component
|
|
442
517
|
* The main content container using semantic HTML <dialog> element with enhanced features
|
|
443
|
-
* Built on Radix UI primitives for accessibility while providing semantic structure
|
|
444
518
|
*
|
|
445
519
|
* @param props - Content configuration and styling
|
|
446
520
|
* @param ref - Forwarded ref to the dialog element
|
|
447
521
|
* @returns JSX.Element - The semantic dialog content with overlay and optional close button
|
|
448
|
-
*
|
|
449
|
-
* @example
|
|
450
|
-
* ```tsx
|
|
451
|
-
* <DialogContent size="lg" enableScrolling={true} maxHeightPercent={80}>
|
|
452
|
-
* <DialogHeader>
|
|
453
|
-
* <DialogTitle>Scrollable Dialog</DialogTitle>
|
|
454
|
-
* <DialogDescription>This dialog will scroll if content overflows.</DialogDescription>
|
|
455
|
-
* </DialogHeader>
|
|
456
|
-
* <DialogBody>
|
|
457
|
-
* <section>Large amount of semantic content here...</section>
|
|
458
|
-
* </DialogBody>
|
|
459
|
-
* <DialogFooter>
|
|
460
|
-
* <Button>Save</Button>
|
|
461
|
-
* </DialogFooter>
|
|
462
|
-
* </DialogContent>
|
|
463
|
-
* ```
|
|
464
522
|
*/
|
|
465
|
-
const DialogContent = React.forwardRef<
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
preventCloseOnEscape = false,
|
|
475
|
-
preventCloseOnOutsideClick = false,
|
|
476
|
-
maxHeightPercent,
|
|
477
|
-
maxWidthPercent,
|
|
478
|
-
enableScrolling = false,
|
|
479
|
-
maxHeight,
|
|
480
|
-
maxWidth,
|
|
481
|
-
minHeight,
|
|
482
|
-
minWidth,
|
|
483
|
-
style,
|
|
484
|
-
...props
|
|
485
|
-
}, ref) => {
|
|
486
|
-
const smartDimensions = useSmartDimensions({
|
|
487
|
-
maxHeightPercent,
|
|
523
|
+
const DialogContent = React.forwardRef<HTMLDialogElement, DialogContentProps>(
|
|
524
|
+
({
|
|
525
|
+
className,
|
|
526
|
+
children,
|
|
527
|
+
size = 'md',
|
|
528
|
+
showCloseButton = true,
|
|
529
|
+
preventCloseOnEscape = false,
|
|
530
|
+
preventCloseOnOutsideClick = false,
|
|
531
|
+
maxHeightPercent,
|
|
488
532
|
maxWidthPercent,
|
|
489
|
-
|
|
533
|
+
enableScrolling = false,
|
|
534
|
+
maxHeight,
|
|
490
535
|
maxWidth,
|
|
491
536
|
minHeight,
|
|
492
537
|
minWidth,
|
|
493
|
-
|
|
494
|
-
|
|
538
|
+
title,
|
|
539
|
+
description,
|
|
540
|
+
style,
|
|
541
|
+
...props
|
|
542
|
+
}, ref) => {
|
|
543
|
+
const { open, onOpenChange, dialogRef, titleId, descriptionId } = useDialogContext();
|
|
544
|
+
const internalRef = useRef<HTMLDialogElement>(null);
|
|
545
|
+
|
|
546
|
+
// Use the dialogRef from context, or fall back to internal ref or forwarded ref
|
|
547
|
+
const actualDialogRef = dialogRef.current ? dialogRef : (ref ? (ref as React.RefObject<HTMLDialogElement>) : internalRef);
|
|
548
|
+
|
|
549
|
+
const smartDimensions = useSmartDimensions({
|
|
550
|
+
maxHeightPercent,
|
|
551
|
+
maxWidthPercent,
|
|
552
|
+
maxHeight,
|
|
553
|
+
maxWidth,
|
|
554
|
+
minHeight,
|
|
555
|
+
minWidth,
|
|
556
|
+
enableScrolling
|
|
557
|
+
});
|
|
495
558
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
559
|
+
// Focus trap
|
|
560
|
+
const { containerRef } = useFocusTrap({
|
|
561
|
+
isActive: open,
|
|
562
|
+
autoFocus: true,
|
|
563
|
+
restoreFocus: true,
|
|
564
|
+
onEscape: preventCloseOnEscape ? undefined : () => onOpenChange(false),
|
|
565
|
+
});
|
|
502
566
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
567
|
+
// Merge refs
|
|
568
|
+
const mergedRef = useCallback((node: HTMLDialogElement | null) => {
|
|
569
|
+
// Set context dialog ref
|
|
570
|
+
if (dialogRef && 'current' in dialogRef) {
|
|
571
|
+
(dialogRef as React.MutableRefObject<HTMLDialogElement | null>).current = node;
|
|
572
|
+
}
|
|
573
|
+
// Set internal ref
|
|
574
|
+
if (internalRef && 'current' in internalRef) {
|
|
575
|
+
internalRef.current = node;
|
|
576
|
+
}
|
|
577
|
+
// Set focus trap container ref
|
|
578
|
+
if (containerRef && 'current' in containerRef) {
|
|
579
|
+
(containerRef as React.MutableRefObject<HTMLElement | null>).current = node;
|
|
580
|
+
}
|
|
581
|
+
// Handle forwarded ref
|
|
582
|
+
if (typeof ref === 'function') {
|
|
583
|
+
ref(node);
|
|
584
|
+
} else if (ref && 'current' in ref) {
|
|
585
|
+
(ref as React.MutableRefObject<HTMLDialogElement | null>).current = node;
|
|
586
|
+
}
|
|
587
|
+
}, [dialogRef, containerRef, ref]);
|
|
588
|
+
|
|
589
|
+
// Handle dialog open/close
|
|
590
|
+
useEffect(() => {
|
|
591
|
+
const dialog = dialogRef.current || internalRef.current;
|
|
592
|
+
if (!dialog) return;
|
|
593
|
+
|
|
594
|
+
if (open) {
|
|
595
|
+
// Use requestAnimationFrame to ensure DOM is ready
|
|
596
|
+
requestAnimationFrame(() => {
|
|
597
|
+
if (dialog && open) {
|
|
598
|
+
dialog.showModal();
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
} else {
|
|
602
|
+
// Close dialog before it's removed from DOM
|
|
603
|
+
if (dialog.open) {
|
|
604
|
+
dialog.close();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}, [open, dialogRef]);
|
|
608
|
+
|
|
609
|
+
// Handle close event - sync state when dialog is closed externally
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
const dialog = dialogRef.current || internalRef.current;
|
|
612
|
+
if (!dialog) return;
|
|
613
|
+
|
|
614
|
+
const handleClose = () => {
|
|
615
|
+
// Only update state if dialog was closed externally (not via our state change)
|
|
616
|
+
// Check if dialog is actually closed and our state says it should be open
|
|
617
|
+
if (!dialog.open && open) {
|
|
618
|
+
onOpenChange(false);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
dialog.addEventListener('close', handleClose);
|
|
623
|
+
return () => {
|
|
624
|
+
dialog.removeEventListener('close', handleClose);
|
|
625
|
+
};
|
|
626
|
+
}, [open, onOpenChange, dialogRef]);
|
|
627
|
+
|
|
628
|
+
// Handle cancel event (Escape or backdrop click)
|
|
629
|
+
useEffect(() => {
|
|
630
|
+
const dialog = dialogRef.current || internalRef.current;
|
|
631
|
+
if (!dialog) return;
|
|
632
|
+
|
|
633
|
+
const handleCancel = (e: Event) => {
|
|
634
|
+
if (preventCloseOnEscape || preventCloseOnOutsideClick) {
|
|
635
|
+
e.preventDefault();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
onOpenChange(false);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
dialog.addEventListener('cancel', handleCancel);
|
|
642
|
+
return () => {
|
|
643
|
+
dialog.removeEventListener('cancel', handleCancel);
|
|
644
|
+
};
|
|
645
|
+
}, [preventCloseOnEscape, preventCloseOnOutsideClick, onOpenChange, dialogRef]);
|
|
646
|
+
|
|
647
|
+
// Merge smart dimensions with provided style
|
|
648
|
+
const mergedStyle = React.useMemo(() => {
|
|
649
|
+
if (Object.keys(smartDimensions).length === 0) {
|
|
650
|
+
return style;
|
|
651
|
+
}
|
|
508
652
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
653
|
+
const finalStyle: React.CSSProperties = { ...smartDimensions, ...style };
|
|
654
|
+
|
|
655
|
+
if (!maxWidth && !maxWidthPercent) {
|
|
656
|
+
const { maxWidth: _, ...styleWithoutMaxWidth } = finalStyle;
|
|
657
|
+
return styleWithoutMaxWidth;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return finalStyle;
|
|
661
|
+
}, [smartDimensions, style, maxWidth, maxWidthPercent]);
|
|
662
|
+
|
|
663
|
+
return (
|
|
664
|
+
<DialogPortal>
|
|
665
|
+
{open && (
|
|
666
|
+
<dialog
|
|
667
|
+
ref={mergedRef}
|
|
668
|
+
className={cn(
|
|
669
|
+
'fixed left-[50%] top-[50%] z-[51] w-full translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg duration-200',
|
|
670
|
+
'animate-in fade-in-0 zoom-in-95 slide-in-from-left-1/2 slide-in-from-top-[48%]',
|
|
671
|
+
'sm:rounded-lg',
|
|
672
|
+
// Reset native dialog styles
|
|
673
|
+
'm-0 p-0 max-w-none max-h-none w-auto h-auto border-0 bg-transparent outline-none',
|
|
674
|
+
// Apply our custom styling
|
|
675
|
+
'border bg-background shadow-lg',
|
|
676
|
+
// Style native backdrop pseudo-element (Tailwind v4 supports arbitrary variants)
|
|
677
|
+
'[&::backdrop]:bg-black/50 [&::backdrop]:animate-in [&::backdrop]:fade-in-0',
|
|
678
|
+
// Only apply size classes if not using smart width
|
|
679
|
+
!maxWidth && !maxWidthPercent && sizeClasses[size],
|
|
680
|
+
// Auto size gets special handling
|
|
681
|
+
size === 'auto' && 'w-fit max-w-[90vw] sm:max-w-[80vw]',
|
|
682
|
+
// Layout classes based on scrolling mode
|
|
683
|
+
enableScrolling ? 'flex flex-col px-6' : 'grid gap-4 p-6',
|
|
684
|
+
// Full screen handling
|
|
685
|
+
size === 'full' && 'sm:left-[50%] sm:top-[50%] sm:translate-x-[-50%] sm:translate-y-[-50%] left-0 top-0 translate-x-0 translate-y-0 h-full rounded-none sm:h-auto sm:rounded-lg',
|
|
686
|
+
// Overflow handling for scrolling mode
|
|
687
|
+
enableScrolling && 'overflow-hidden',
|
|
688
|
+
className
|
|
689
|
+
)}
|
|
690
|
+
style={mergedStyle}
|
|
691
|
+
role="dialog"
|
|
692
|
+
aria-modal="true"
|
|
693
|
+
aria-labelledby={titleId}
|
|
694
|
+
aria-describedby={descriptionId}
|
|
695
|
+
title={title}
|
|
696
|
+
aria-description={description}
|
|
697
|
+
{...props}
|
|
698
|
+
>
|
|
699
|
+
{children}
|
|
700
|
+
{showCloseButton && (
|
|
701
|
+
<DialogClose />
|
|
702
|
+
)}
|
|
703
|
+
</dialog>
|
|
704
|
+
)}
|
|
705
|
+
</DialogPortal>
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
);
|
|
709
|
+
DialogContent.displayName = 'DialogContent';
|
|
515
710
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
return finalStyle;
|
|
526
|
-
}, [smartDimensions, style, maxWidth, maxWidthPercent]);
|
|
711
|
+
/**
|
|
712
|
+
* DialogClose component
|
|
713
|
+
* Button to close the dialog
|
|
714
|
+
*/
|
|
715
|
+
const DialogClose = React.forwardRef<HTMLButtonElement, DialogCloseProps>(
|
|
716
|
+
({ className, ...props }, ref) => {
|
|
717
|
+
const { onOpenChange } = useDialogContext();
|
|
527
718
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
<DialogOverlay className={overlayClassName} />
|
|
531
|
-
<DialogPrimitive.Content
|
|
719
|
+
return (
|
|
720
|
+
<button
|
|
532
721
|
ref={ref}
|
|
533
|
-
|
|
534
|
-
|
|
722
|
+
type="button"
|
|
723
|
+
onClick={() => onOpenChange(false)}
|
|
535
724
|
className={cn(
|
|
536
|
-
'
|
|
537
|
-
// Reset native dialog styles that interfere with our custom styling
|
|
538
|
-
'm-0 p-0 max-w-none max-h-none w-auto h-auto border-0 bg-transparent outline-none',
|
|
539
|
-
// Apply our custom styling
|
|
540
|
-
'border bg-background shadow-lg',
|
|
541
|
-
// Only apply size classes if not using smart width
|
|
542
|
-
!maxWidth && !maxWidthPercent && sizeClasses[size],
|
|
543
|
-
// Auto size gets special handling for content fitting
|
|
544
|
-
size === 'auto' && 'w-fit max-w-[90vw] sm:max-w-[80vw]',
|
|
545
|
-
// Layout classes based on scrolling mode
|
|
546
|
-
enableScrolling ? 'flex flex-col' : 'grid gap-4 p-6',
|
|
547
|
-
// Full screen handling
|
|
548
|
-
size === 'full' && 'sm:left-[50%] sm:top-[50%] sm:translate-x-[-50%] sm:translate-y-[-50%] left-0 top-0 translate-x-0 translate-y-0 h-full rounded-none sm:h-auto sm:rounded-lg',
|
|
549
|
-
// Overflow handling for scrolling mode
|
|
550
|
-
enableScrolling && 'overflow-hidden',
|
|
725
|
+
'absolute right-4 top-4 z-10 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none',
|
|
551
726
|
className
|
|
552
727
|
)}
|
|
553
|
-
style={mergedStyle}
|
|
554
728
|
{...props}
|
|
555
729
|
>
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
</DialogPrimitive.Content>
|
|
564
|
-
</DialogPortal>
|
|
565
|
-
);
|
|
566
|
-
});
|
|
567
|
-
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
|
730
|
+
<X className="size-4" />
|
|
731
|
+
<span className="sr-only">Close</span>
|
|
732
|
+
</button>
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
);
|
|
736
|
+
DialogClose.displayName = 'DialogClose';
|
|
568
737
|
|
|
569
738
|
/**
|
|
570
739
|
* DialogHeader component
|
|
571
740
|
* Semantic header container for dialog title and description with optional sticky behavior
|
|
572
|
-
*
|
|
573
|
-
* @param props - Header configuration and styling
|
|
574
|
-
* @returns JSX.Element - The dialog header container using semantic <header> element
|
|
575
|
-
*
|
|
576
|
-
* @example
|
|
577
|
-
* ```tsx
|
|
578
|
-
* <DialogHeader sticky={true}>
|
|
579
|
-
* <DialogTitle>Sticky Header</DialogTitle>
|
|
580
|
-
* <DialogDescription>This header stays visible while scrolling.</DialogDescription>
|
|
581
|
-
* </DialogHeader>
|
|
582
|
-
* ```
|
|
583
741
|
*/
|
|
584
742
|
const DialogHeader = ({
|
|
585
743
|
className,
|
|
@@ -589,7 +747,7 @@ const DialogHeader = ({
|
|
|
589
747
|
<header
|
|
590
748
|
className={cn(
|
|
591
749
|
'flex flex-col space-y-1.5 text-center sm:text-left',
|
|
592
|
-
sticky ? 'sticky top-0 z-10 bg-background
|
|
750
|
+
sticky ? 'sticky top-0 z-10 bg-background pt-6 pb-4 border-b' : 'py-2',
|
|
593
751
|
className
|
|
594
752
|
)}
|
|
595
753
|
{...props}
|
|
@@ -600,27 +758,6 @@ DialogHeader.displayName = 'DialogHeader';
|
|
|
600
758
|
/**
|
|
601
759
|
* DialogBody component
|
|
602
760
|
* Semantic main content area for dialog body content with scrollable functionality
|
|
603
|
-
* Supports both React children and safe HTML content rendering
|
|
604
|
-
*
|
|
605
|
-
* @param props - Body configuration and styling
|
|
606
|
-
* @returns JSX.Element - The scrollable dialog body container using semantic <main> element
|
|
607
|
-
*
|
|
608
|
-
* @example
|
|
609
|
-
* ```tsx
|
|
610
|
-
* // Using React children
|
|
611
|
-
* <DialogBody>
|
|
612
|
-
* <section className="space-y-4">
|
|
613
|
-
* <h4>Content Title</h4>
|
|
614
|
-
* <p>Long content that will scroll...</p>
|
|
615
|
-
* </section>
|
|
616
|
-
* </DialogBody>
|
|
617
|
-
*
|
|
618
|
-
* // Using HTML content
|
|
619
|
-
* <DialogBody
|
|
620
|
-
* htmlContent="<h2>Import Instructions</h2><p>Upload a CSV file with the following format:</p><ul><li>Required columns: name, email</li><li>Optional columns: phone, address</li></ul>"
|
|
621
|
-
* allowHtml={true}
|
|
622
|
-
* />
|
|
623
|
-
* ```
|
|
624
761
|
*/
|
|
625
762
|
const DialogBody = ({
|
|
626
763
|
className,
|
|
@@ -640,7 +777,6 @@ const DialogBody = ({
|
|
|
640
777
|
};
|
|
641
778
|
}, [maxHeight, style]);
|
|
642
779
|
|
|
643
|
-
// Process HTML content if provided
|
|
644
780
|
const processedHtmlContent = React.useMemo(() => {
|
|
645
781
|
if (!htmlContent || !allowHtml) {
|
|
646
782
|
return null;
|
|
@@ -654,13 +790,12 @@ const DialogBody = ({
|
|
|
654
790
|
return result.html;
|
|
655
791
|
}, [htmlContent, allowHtml, strictSanitization, logWarnings]);
|
|
656
792
|
|
|
657
|
-
// Determine if htmlContent was provided (even if processing failed)
|
|
658
793
|
const hasHtmlContent = Boolean(htmlContent && allowHtml);
|
|
659
794
|
|
|
660
795
|
return (
|
|
661
796
|
<main
|
|
662
797
|
className={cn(
|
|
663
|
-
'overflow-y-auto
|
|
798
|
+
'overflow-y-auto py-2',
|
|
664
799
|
className
|
|
665
800
|
)}
|
|
666
801
|
style={mergedStyle}
|
|
@@ -669,16 +804,16 @@ const DialogBody = ({
|
|
|
669
804
|
{...props}
|
|
670
805
|
>
|
|
671
806
|
{processedHtmlContent ? (
|
|
672
|
-
<
|
|
807
|
+
<p
|
|
673
808
|
dangerouslySetInnerHTML={{ __html: processedHtmlContent }}
|
|
674
809
|
className="prose prose-sm max-w-none"
|
|
675
810
|
/>
|
|
676
811
|
) : (
|
|
677
812
|
<>
|
|
678
813
|
{hasHtmlContent && !processedHtmlContent && (
|
|
679
|
-
<
|
|
814
|
+
<p className="text-acc-500 mb-2">
|
|
680
815
|
No HTML content processed. Showing children instead.
|
|
681
|
-
</
|
|
816
|
+
</p>
|
|
682
817
|
)}
|
|
683
818
|
{children}
|
|
684
819
|
</>
|
|
@@ -691,17 +826,6 @@ DialogBody.displayName = 'DialogBody';
|
|
|
691
826
|
/**
|
|
692
827
|
* DialogFooter component
|
|
693
828
|
* Semantic footer container for dialog action buttons with optional sticky behavior
|
|
694
|
-
*
|
|
695
|
-
* @param props - Footer configuration and styling
|
|
696
|
-
* @returns JSX.Element - The dialog footer container using semantic <footer> element
|
|
697
|
-
*
|
|
698
|
-
* @example
|
|
699
|
-
* ```tsx
|
|
700
|
-
* <DialogFooter sticky={true}>
|
|
701
|
-
* <Button variant="outline">Cancel</Button>
|
|
702
|
-
* <Button>Save changes</Button>
|
|
703
|
-
* </DialogFooter>
|
|
704
|
-
* ```
|
|
705
829
|
*/
|
|
706
830
|
const DialogFooter = ({
|
|
707
831
|
className,
|
|
@@ -710,9 +834,8 @@ const DialogFooter = ({
|
|
|
710
834
|
}: DialogFooterProps) => (
|
|
711
835
|
<footer
|
|
712
836
|
className={cn(
|
|
713
|
-
// Only apply default layout classes if no custom className is provided
|
|
714
837
|
!className && 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
|
715
|
-
!className && (sticky ? 'sticky bottom-0 z-10 bg-background
|
|
838
|
+
!className && (sticky ? 'sticky bottom-0 z-10 bg-background pt-4 pb-6 border-t' : 'py-2'),
|
|
716
839
|
className
|
|
717
840
|
)}
|
|
718
841
|
{...props}
|
|
@@ -720,62 +843,70 @@ const DialogFooter = ({
|
|
|
720
843
|
);
|
|
721
844
|
DialogFooter.displayName = 'DialogFooter';
|
|
722
845
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
846
|
+
/**
|
|
847
|
+
* DialogTitle component
|
|
848
|
+
* Title element with ARIA support
|
|
849
|
+
*/
|
|
850
|
+
const DialogTitle = React.forwardRef<HTMLHeadingElement, DialogTitleProps>(
|
|
851
|
+
({ className, htmlContent, allowHtml = true, children, ...props }, ref) => {
|
|
852
|
+
const { titleId } = useDialogContext();
|
|
731
853
|
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
854
|
+
const processedHtmlContent = React.useMemo(() => {
|
|
855
|
+
if (!htmlContent || !allowHtml) {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
736
858
|
|
|
737
|
-
|
|
738
|
-
|
|
859
|
+
const result = renderSafeHtml(htmlContent, {
|
|
860
|
+
strict: true,
|
|
861
|
+
logWarnings: false
|
|
862
|
+
});
|
|
739
863
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
ref={ref}
|
|
743
|
-
className={cn(
|
|
744
|
-
className
|
|
745
|
-
)}
|
|
746
|
-
{...props}
|
|
747
|
-
>
|
|
748
|
-
{processedHtmlContent ? (
|
|
749
|
-
<span dangerouslySetInnerHTML={{ __html: processedHtmlContent }} />
|
|
750
|
-
) : (
|
|
751
|
-
children
|
|
752
|
-
)}
|
|
753
|
-
</DialogPrimitive.Title>
|
|
754
|
-
);
|
|
755
|
-
});
|
|
756
|
-
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
|
864
|
+
return result.html;
|
|
865
|
+
}, [htmlContent, allowHtml]);
|
|
757
866
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
867
|
+
return (
|
|
868
|
+
<h2
|
|
869
|
+
ref={ref}
|
|
870
|
+
id={titleId}
|
|
871
|
+
className={cn(className)}
|
|
872
|
+
{...props}
|
|
873
|
+
>
|
|
874
|
+
{processedHtmlContent ? (
|
|
875
|
+
<span dangerouslySetInnerHTML={{ __html: processedHtmlContent }} />
|
|
876
|
+
) : (
|
|
877
|
+
children
|
|
878
|
+
)}
|
|
879
|
+
</h2>
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
);
|
|
883
|
+
DialogTitle.displayName = 'DialogTitle';
|
|
766
884
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
885
|
+
/**
|
|
886
|
+
* DialogDescription component
|
|
887
|
+
* Description element with ARIA support
|
|
888
|
+
*/
|
|
889
|
+
const DialogDescription = React.forwardRef<HTMLParagraphElement, DialogDescriptionProps>(
|
|
890
|
+
({ className, htmlContent, allowHtml = true, children, ...props }, ref) => {
|
|
891
|
+
const { descriptionId } = useDialogContext();
|
|
771
892
|
|
|
772
|
-
|
|
773
|
-
|
|
893
|
+
const processedHtmlContent = React.useMemo(() => {
|
|
894
|
+
if (!htmlContent || !allowHtml) {
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
774
897
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
898
|
+
const result = renderSafeHtml(htmlContent, {
|
|
899
|
+
strict: true,
|
|
900
|
+
logWarnings: false
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
return result.html;
|
|
904
|
+
}, [htmlContent, allowHtml]);
|
|
905
|
+
|
|
906
|
+
return (
|
|
907
|
+
<p
|
|
778
908
|
ref={ref}
|
|
909
|
+
id={descriptionId}
|
|
779
910
|
className={cn(className)}
|
|
780
911
|
{...props}
|
|
781
912
|
>
|
|
@@ -784,16 +915,15 @@ const DialogDescription = React.forwardRef<
|
|
|
784
915
|
) : (
|
|
785
916
|
children
|
|
786
917
|
)}
|
|
787
|
-
</
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
DialogDescription.displayName =
|
|
918
|
+
</p>
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
);
|
|
922
|
+
DialogDescription.displayName = 'DialogDescription';
|
|
792
923
|
|
|
793
924
|
export {
|
|
794
925
|
Dialog,
|
|
795
926
|
DialogPortal,
|
|
796
|
-
DialogOverlay,
|
|
797
927
|
DialogClose,
|
|
798
928
|
DialogTrigger,
|
|
799
929
|
DialogContent,
|