@inertiaui/modal-react 1.0.0-beta-5 → 2.0.2

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 (49) hide show
  1. package/dist/CloseButton.d.ts +5 -0
  2. package/dist/Deferred.d.ts +11 -0
  3. package/dist/HeadlessModal.d.ts +64 -0
  4. package/dist/Modal.d.ts +48 -0
  5. package/dist/ModalContent.d.ts +24 -0
  6. package/dist/ModalLink.d.ts +29 -0
  7. package/dist/ModalRenderer.d.ts +4 -0
  8. package/dist/ModalRoot.d.ts +22 -0
  9. package/dist/SlideoverContent.d.ts +24 -0
  10. package/dist/WhenVisible.d.ts +16 -0
  11. package/dist/config.d.ts +21 -0
  12. package/dist/constants.d.ts +7 -0
  13. package/dist/helpers.d.ts +3 -0
  14. package/dist/inertiaui-modal.d.ts +2 -0
  15. package/dist/inertiaui-modal.js +1680 -2167
  16. package/dist/inertiaui-modal.js.map +1 -0
  17. package/dist/inertiaui-modal.umd.cjs +1854 -21
  18. package/dist/inertiaui-modal.umd.cjs.map +1 -0
  19. package/dist/inertiauiModal.d.ts +21 -0
  20. package/dist/types.d.ts +111 -0
  21. package/dist/useModal.d.ts +2 -0
  22. package/package.json +37 -22
  23. package/src/{CloseButton.jsx → CloseButton.tsx} +5 -1
  24. package/src/{Deferred.jsx → Deferred.tsx} +10 -3
  25. package/src/HeadlessModal.tsx +238 -0
  26. package/src/Modal.tsx +292 -0
  27. package/src/ModalContent.tsx +311 -0
  28. package/src/ModalLink.tsx +224 -0
  29. package/src/ModalRenderer.tsx +33 -0
  30. package/src/ModalRoot.tsx +880 -0
  31. package/src/SlideoverContent.tsx +319 -0
  32. package/src/{WhenVisible.jsx → WhenVisible.tsx} +20 -9
  33. package/src/config.ts +99 -0
  34. package/src/constants.ts +22 -0
  35. package/src/helpers.ts +17 -0
  36. package/src/inertiauiModal.ts +65 -0
  37. package/src/types.ts +150 -0
  38. package/src/useModal.ts +7 -0
  39. package/src/HeadlessModal.jsx +0 -143
  40. package/src/Modal.jsx +0 -136
  41. package/src/ModalContent.jsx +0 -56
  42. package/src/ModalLink.jsx +0 -123
  43. package/src/ModalRenderer.jsx +0 -34
  44. package/src/ModalRoot.jsx +0 -623
  45. package/src/SlideoverContent.jsx +0 -55
  46. package/src/config.js +0 -3
  47. package/src/helpers.js +0 -2
  48. package/src/inertiauiModal.js +0 -34
  49. package/src/useModal.js +0 -6
@@ -0,0 +1,319 @@
1
+ import { useState, useEffect, useRef, useCallback, useMemo, ReactNode, SyntheticEvent, MouseEvent } from 'react'
2
+ import CloseButton from './CloseButton'
3
+ import clsx from 'clsx'
4
+ import { createFocusTrap, onEscapeKey, animate, cancelAnimations } from '@inertiaui/vanilla'
5
+ import { getMaxWidthClass } from './constants'
6
+ import type { Modal } from './types'
7
+
8
+ interface SlideoverContentConfig {
9
+ maxWidth: string
10
+ paddingClasses: string
11
+ panelClasses: string
12
+ position: string
13
+ closeButton: boolean
14
+ closeExplicitly?: boolean
15
+ closeOnClickOutside?: boolean
16
+ }
17
+
18
+ interface SlideoverContentProps {
19
+ modalContext: Modal
20
+ config: SlideoverContentConfig
21
+ useNativeDialog: boolean
22
+ isFirstModal: boolean
23
+ onAfterLeave?: () => void
24
+ children: ReactNode | ((props: { modalContext: Modal; config: SlideoverContentConfig }) => ReactNode)
25
+ }
26
+
27
+ const SlideoverContent = ({ modalContext, config, useNativeDialog, isFirstModal, onAfterLeave, children }: SlideoverContentProps) => {
28
+ const [isRendered, setIsRendered] = useState(false)
29
+ const [isVisible, setIsVisible] = useState(false) // For backdrop sync
30
+ const [entered, setEntered] = useState(false) // After animation completes
31
+ const wrapperRef = useRef<HTMLDivElement>(null)
32
+ const dialogRef = useRef<HTMLDialogElement>(null)
33
+ const nativeWrapperRef = useRef<HTMLDivElement>(null)
34
+ const cleanupFocusTrapRef = useRef<(() => void) | null>(null)
35
+ const cleanupEscapeKeyRef = useRef<(() => void) | null>(null)
36
+
37
+ const isLeft = config.position === 'left'
38
+
39
+ const maxWidthClass = useMemo(() => getMaxWidthClass(config.maxWidth), [config.maxWidth])
40
+
41
+ // Get translate value based on position
42
+ const getTranslateX = useCallback(() => (isLeft ? '-100%' : '100%'), [isLeft])
43
+
44
+ // ============ Animation handlers using Web Animations API ============
45
+
46
+ const animateIn = useCallback(
47
+ async (element: HTMLElement | null) => {
48
+ if (!element) return
49
+
50
+ setIsVisible(true) // Trigger backdrop immediately
51
+ const translateX = getTranslateX()
52
+
53
+ await animate(element, [
54
+ { transform: `translate3d(${translateX}, 0, 0)`, opacity: 0 },
55
+ { transform: 'translate3d(0, 0, 0)', opacity: 1 },
56
+ ])
57
+
58
+ setEntered(true)
59
+ },
60
+ [getTranslateX],
61
+ )
62
+
63
+ const animateOut = useCallback(
64
+ async (element: HTMLElement | null) => {
65
+ if (!element) return
66
+
67
+ setIsVisible(false) // Trigger backdrop fade out immediately
68
+ const translateX = getTranslateX()
69
+
70
+ await animate(element, [
71
+ { transform: 'translate3d(0, 0, 0)', opacity: 1 },
72
+ { transform: `translate3d(${translateX}, 0, 0)`, opacity: 0 },
73
+ ])
74
+
75
+ setIsRendered(false)
76
+ if (useNativeDialog && dialogRef.current) {
77
+ dialogRef.current.close()
78
+ }
79
+ onAfterLeave?.()
80
+ modalContext.afterLeave()
81
+ },
82
+ [getTranslateX, useNativeDialog, onAfterLeave, modalContext],
83
+ )
84
+
85
+ // ============ Non-native dialog handlers ============
86
+
87
+ const setupFocusTrap = useCallback(() => {
88
+ if (useNativeDialog) return
89
+ if (!wrapperRef.current || !modalContext.onTopOfStack) return
90
+ if (cleanupFocusTrapRef.current) return
91
+
92
+ cleanupFocusTrapRef.current = createFocusTrap(wrapperRef.current, {
93
+ initialFocus: true,
94
+ returnFocus: false,
95
+ })
96
+ }, [modalContext.onTopOfStack, useNativeDialog])
97
+
98
+ const cleanupFocusTrap = useCallback(() => {
99
+ if (cleanupFocusTrapRef.current) {
100
+ cleanupFocusTrapRef.current()
101
+ cleanupFocusTrapRef.current = null
102
+ }
103
+ }, [])
104
+
105
+ const setupEscapeKey = useCallback(() => {
106
+ if (useNativeDialog) return
107
+ if (cleanupEscapeKeyRef.current) return
108
+ if (config?.closeExplicitly) return
109
+
110
+ cleanupEscapeKeyRef.current = onEscapeKey(() => {
111
+ if (modalContext.onTopOfStack) {
112
+ modalContext.close()
113
+ }
114
+ })
115
+ }, [config?.closeExplicitly, modalContext, useNativeDialog])
116
+
117
+ const cleanupEscapeKey = useCallback(() => {
118
+ if (cleanupEscapeKeyRef.current) {
119
+ cleanupEscapeKeyRef.current()
120
+ cleanupEscapeKeyRef.current = null
121
+ }
122
+ }, [])
123
+
124
+ const handleClickOutside = useCallback(
125
+ (event: MouseEvent) => {
126
+ if (useNativeDialog) return
127
+ if (!modalContext.onTopOfStack) return
128
+ if (config?.closeExplicitly) return
129
+ if (config?.closeOnClickOutside === false) return
130
+ if (!wrapperRef.current) return
131
+
132
+ if (!wrapperRef.current.contains(event.target as Node)) {
133
+ modalContext.close()
134
+ }
135
+ },
136
+ [modalContext, config?.closeExplicitly, config?.closeOnClickOutside, useNativeDialog],
137
+ )
138
+
139
+ // ============ Native dialog handlers ============
140
+
141
+ const handleCancel = useCallback(
142
+ (event: SyntheticEvent) => {
143
+ event.preventDefault()
144
+ if (modalContext.onTopOfStack && !config?.closeExplicitly) {
145
+ modalContext.close()
146
+ }
147
+ },
148
+ [modalContext, config?.closeExplicitly],
149
+ )
150
+
151
+ const handleDialogClick = useCallback(
152
+ (event: MouseEvent) => {
153
+ if (event.target === dialogRef.current) {
154
+ if (modalContext.onTopOfStack && !config?.closeExplicitly && config?.closeOnClickOutside !== false) {
155
+ modalContext.close()
156
+ }
157
+ }
158
+ },
159
+ [modalContext, config?.closeExplicitly, config?.closeOnClickOutside],
160
+ )
161
+
162
+ // ============ Lifecycle ============
163
+
164
+ // Track previous isOpen state for detecting close
165
+ const prevIsOpenRef = useRef(modalContext.isOpen)
166
+
167
+ // Initial mount and open state changes
168
+ useEffect(() => {
169
+ if (useNativeDialog) {
170
+ if (modalContext.isOpen && !dialogRef.current?.open) {
171
+ dialogRef.current?.showModal()
172
+ animateIn(nativeWrapperRef.current)
173
+ } else if (!modalContext.isOpen && prevIsOpenRef.current) {
174
+ setEntered(false)
175
+ animateOut(nativeWrapperRef.current)
176
+ }
177
+ } else {
178
+ if (modalContext.isOpen && !isRendered) {
179
+ setIsRendered(true)
180
+ } else if (!modalContext.isOpen && prevIsOpenRef.current) {
181
+ setEntered(false)
182
+ animateOut(wrapperRef.current)
183
+ }
184
+ }
185
+ prevIsOpenRef.current = modalContext.isOpen
186
+ }, [modalContext.isOpen, useNativeDialog, animateIn, animateOut, isRendered])
187
+
188
+ // Trigger animation after render (non-native)
189
+ useEffect(() => {
190
+ if (!useNativeDialog && isRendered && !entered && modalContext.isOpen) {
191
+ animateIn(wrapperRef.current).then(() => {
192
+ setupFocusTrap()
193
+ })
194
+ }
195
+ }, [isRendered, useNativeDialog, entered, modalContext.isOpen, animateIn, setupFocusTrap])
196
+
197
+ // Setup escape key (non-native)
198
+ useEffect(() => {
199
+ if (!useNativeDialog) {
200
+ setupEscapeKey()
201
+ }
202
+ return () => {
203
+ cleanupEscapeKey()
204
+ }
205
+ }, [useNativeDialog, setupEscapeKey, cleanupEscapeKey])
206
+
207
+ // Handle becoming top of stack / losing top of stack (non-native only)
208
+ useEffect(() => {
209
+ if (useNativeDialog) return
210
+
211
+ if (modalContext.onTopOfStack) {
212
+ setupEscapeKey()
213
+ if (entered) {
214
+ setupFocusTrap()
215
+ }
216
+ } else {
217
+ cleanupFocusTrap()
218
+ cleanupEscapeKey()
219
+ }
220
+ }, [modalContext.onTopOfStack, entered, setupEscapeKey, setupFocusTrap, cleanupFocusTrap, cleanupEscapeKey, useNativeDialog])
221
+
222
+ // Cleanup on unmount
223
+ useEffect(() => {
224
+ return () => {
225
+ const wrapper = useNativeDialog ? nativeWrapperRef.current : wrapperRef.current
226
+ if (wrapper) {
227
+ cancelAnimations(wrapper)
228
+ }
229
+ if (useNativeDialog) {
230
+ if (dialogRef.current?.open) {
231
+ dialogRef.current.close()
232
+ }
233
+ } else {
234
+ cleanupFocusTrap()
235
+ cleanupEscapeKey()
236
+ }
237
+ }
238
+ }, [useNativeDialog, cleanupFocusTrap, cleanupEscapeKey])
239
+
240
+ // ============ Render ============
241
+
242
+ const renderContent = () => (
243
+ <div
244
+ className={`im-slideover-content relative ${config.paddingClasses} ${config.panelClasses}`}
245
+ data-inertiaui-modal-entered={entered}
246
+ >
247
+ {config.closeButton && (
248
+ <div className="absolute right-0 top-0 pr-3 pt-3">
249
+ <CloseButton onClick={modalContext.close} />
250
+ </div>
251
+ )}
252
+ {typeof children === 'function' ? children({ modalContext, config }) : children}
253
+ </div>
254
+ )
255
+
256
+ // Native dialog mode
257
+ if (useNativeDialog) {
258
+ return (
259
+ <dialog
260
+ ref={dialogRef}
261
+ className={clsx(
262
+ 'im-slideover-dialog m-0 overflow-visible bg-transparent p-0',
263
+ 'size-full max-h-none max-w-none',
264
+ 'backdrop:bg-black/75 backdrop:transition-opacity backdrop:duration-300',
265
+ isVisible ? 'backdrop:opacity-100' : 'backdrop:opacity-0',
266
+ !isFirstModal && 'backdrop:bg-transparent',
267
+ )}
268
+ onCancel={handleCancel}
269
+ onClick={handleDialogClick}
270
+ >
271
+ <div className="im-slideover-container fixed inset-0 overflow-y-auto overflow-x-hidden">
272
+ <div
273
+ className={clsx('im-slideover-positioner flex min-h-full items-center', {
274
+ 'justify-start rtl:justify-end': config?.position === 'left',
275
+ 'justify-end rtl:justify-start': config?.position === 'right',
276
+ })}
277
+ >
278
+ <div
279
+ ref={nativeWrapperRef}
280
+ className={clsx('im-slideover-wrapper w-full transition-[filter] duration-300', modalContext.onTopOfStack ? '' : 'blur-xs', maxWidthClass)}
281
+ >
282
+ {renderContent()}
283
+ </div>
284
+ </div>
285
+ </div>
286
+ </dialog>
287
+ )
288
+ }
289
+
290
+ // Non-native dialog mode
291
+ if (!isRendered) return null
292
+
293
+ return (
294
+ <div
295
+ className="im-slideover-container fixed inset-0 z-40 overflow-y-auto overflow-x-hidden"
296
+ onMouseDown={handleClickOutside}
297
+ >
298
+ <div
299
+ className={clsx('im-slideover-positioner flex min-h-full items-center', {
300
+ 'justify-start rtl:justify-end': config?.position === 'left',
301
+ 'justify-end rtl:justify-start': config?.position === 'right',
302
+ })}
303
+ onMouseDown={handleClickOutside}
304
+ >
305
+ <div
306
+ ref={wrapperRef}
307
+ role="dialog"
308
+ aria-modal="true"
309
+ className={clsx('im-slideover-wrapper w-full transition-[filter] duration-300', modalContext.onTopOfStack ? '' : 'blur-xs', maxWidthClass)}
310
+ >
311
+ <span className="sr-only">Dialog</span>
312
+ {renderContent()}
313
+ </div>
314
+ </div>
315
+ </div>
316
+ )
317
+ }
318
+
319
+ export default SlideoverContent
@@ -1,8 +1,19 @@
1
1
  // See: https://github.com/inertiajs/inertia/blob/48bcd21fb7daf467d0df1bfde2408f161f94a579/packages/react/src/WhenVisible.ts
2
- import { createElement, useCallback, useEffect, useRef, useState } from 'react'
2
+ import { createElement, useCallback, useEffect, useRef, useState, ReactNode, ElementType } from 'react'
3
3
  import useModal from './useModal'
4
+ import type { ReloadOptions } from './types'
5
+
6
+ interface WhenVisibleProps {
7
+ children: ReactNode
8
+ data?: string | string[]
9
+ params?: ReloadOptions
10
+ buffer?: number
11
+ as?: ElementType
12
+ always?: boolean
13
+ fallback?: ReactNode
14
+ }
4
15
 
5
- const WhenVisible = ({ children, data, params, buffer, as, always, fallback }) => {
16
+ const WhenVisible = ({ children, data, params, buffer, as, always, fallback }: WhenVisibleProps) => {
6
17
  always = always ?? false
7
18
  as = as ?? 'div'
8
19
  fallback = fallback ?? null
@@ -10,11 +21,11 @@ const WhenVisible = ({ children, data, params, buffer, as, always, fallback }) =
10
21
  const [loaded, setLoaded] = useState(false)
11
22
  const hasFetched = useRef(false)
12
23
  const fetching = useRef(false)
13
- const ref = useRef(null)
24
+ const ref = useRef<HTMLElement>(null)
14
25
 
15
26
  const modal = useModal()
16
27
 
17
- const getReloadParams = useCallback(() => {
28
+ const getReloadParams = useCallback((): ReloadOptions => {
18
29
  if (data) {
19
30
  return {
20
31
  only: Array.isArray(data) ? data : [data],
@@ -52,16 +63,16 @@ const WhenVisible = ({ children, data, params, buffer, as, always, fallback }) =
52
63
 
53
64
  const reloadParams = getReloadParams()
54
65
 
55
- modal.reload({
66
+ modal?.reload({
56
67
  ...reloadParams,
57
- onStart: (e) => {
68
+ onStart: () => {
58
69
  fetching.current = true
59
- reloadParams.onStart?.(e)
70
+ reloadParams.onStart?.()
60
71
  },
61
- onFinish: (e) => {
72
+ onFinish: () => {
62
73
  setLoaded(true)
63
74
  fetching.current = false
64
- reloadParams.onFinish?.(e)
75
+ reloadParams.onFinish?.()
65
76
 
66
77
  if (!always) {
67
78
  observer.disconnect()
package/src/config.ts ADDED
@@ -0,0 +1,99 @@
1
+ export interface ModalTypeConfig {
2
+ closeButton: boolean
3
+ closeExplicitly: boolean
4
+ closeOnClickOutside: boolean
5
+ maxWidth: string
6
+ paddingClasses: string
7
+ panelClasses: string
8
+ position: string
9
+ }
10
+
11
+ export interface ModalConfig {
12
+ type: 'modal' | 'slideover'
13
+ navigate: boolean
14
+ useNativeDialog: boolean
15
+ appElement: string | null
16
+ modal: ModalTypeConfig
17
+ slideover: ModalTypeConfig
18
+ }
19
+
20
+ const defaultConfig: ModalConfig = {
21
+ type: 'modal',
22
+ navigate: false,
23
+ useNativeDialog: true,
24
+ appElement: '#app',
25
+ modal: {
26
+ closeButton: true,
27
+ closeExplicitly: false,
28
+ closeOnClickOutside: true,
29
+ maxWidth: '2xl',
30
+ paddingClasses: 'p-4 sm:p-6',
31
+ panelClasses: 'bg-white rounded',
32
+ position: 'center',
33
+ },
34
+ slideover: {
35
+ closeButton: true,
36
+ closeExplicitly: false,
37
+ closeOnClickOutside: true,
38
+ maxWidth: 'md',
39
+ paddingClasses: 'p-4 sm:p-6',
40
+ panelClasses: 'bg-white min-h-screen',
41
+ position: 'right',
42
+ },
43
+ }
44
+
45
+ class Config {
46
+ private config: ModalConfig
47
+
48
+ constructor() {
49
+ this.config = {} as ModalConfig
50
+ this.reset()
51
+ }
52
+
53
+ reset(): void {
54
+ this.config = JSON.parse(JSON.stringify(defaultConfig))
55
+ }
56
+
57
+ put(key: string | Partial<ModalConfig>, value?: unknown): void {
58
+ if (typeof key === 'object') {
59
+ this.config = {
60
+ type: key.type ?? defaultConfig.type,
61
+ navigate: key.navigate ?? defaultConfig.navigate,
62
+ useNativeDialog: key.useNativeDialog ?? defaultConfig.useNativeDialog,
63
+ appElement: key.appElement !== undefined ? key.appElement : defaultConfig.appElement,
64
+ modal: { ...defaultConfig.modal, ...(key.modal ?? {}) },
65
+ slideover: { ...defaultConfig.slideover, ...(key.slideover ?? {}) },
66
+ }
67
+ return
68
+ }
69
+ const keys = key.split('.')
70
+ let current: Record<string, unknown> = this.config as unknown as Record<string, unknown>
71
+ for (let i = 0; i < keys.length - 1; i++) {
72
+ current = (current[keys[i]] = current[keys[i]] || {}) as Record<string, unknown>
73
+ }
74
+ current[keys[keys.length - 1]] = value
75
+ }
76
+
77
+ get(key?: string): unknown {
78
+ if (typeof key === 'undefined') {
79
+ return this.config
80
+ }
81
+ const keys = key.split('.')
82
+ let current: unknown = this.config
83
+ for (const k of keys) {
84
+ if (current === null || current === undefined || typeof current !== 'object') {
85
+ return null
86
+ }
87
+ current = (current as Record<string, unknown>)[k]
88
+ }
89
+ return current === undefined ? null : current
90
+ }
91
+ }
92
+
93
+ const configInstance = new Config()
94
+
95
+ export const resetConfig = (): void => configInstance.reset()
96
+ export const putConfig = (key: string | Partial<ModalConfig>, value?: unknown): void => configInstance.put(key, value)
97
+ export const getConfig = (key?: string): unknown => configInstance.get(key)
98
+ export const getConfigByType = (isSlideover: boolean, key: string): unknown =>
99
+ configInstance.get(isSlideover ? `slideover.${key}` : `modal.${key}`)
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Max width classes for modals and slideovers.
3
+ * Uses a map lookup for Tailwind 4 compatibility (scanner picks up full class strings).
4
+ */
5
+ export const maxWidthClasses: Record<string, string> = {
6
+ sm: 'sm:max-w-sm',
7
+ md: 'sm:max-w-md',
8
+ lg: 'sm:max-w-md md:max-w-lg',
9
+ xl: 'sm:max-w-md md:max-w-xl',
10
+ '2xl': 'sm:max-w-md md:max-w-xl lg:max-w-2xl',
11
+ '3xl': 'sm:max-w-md md:max-w-xl lg:max-w-3xl',
12
+ '4xl': 'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-4xl',
13
+ '5xl': 'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl',
14
+ '6xl': 'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl 2xl:max-w-6xl',
15
+ '7xl': 'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl 2xl:max-w-7xl',
16
+ }
17
+
18
+ export const defaultMaxWidth = '2xl'
19
+
20
+ export function getMaxWidthClass(maxWidth: string): string {
21
+ return maxWidthClasses[maxWidth] || maxWidthClasses[defaultMaxWidth]
22
+ }
package/src/helpers.ts ADDED
@@ -0,0 +1,17 @@
1
+ // Re-export helper utilities from vanilla
2
+ export { sameUrlPath, except, only, rejectNullValues, kebabCase, isStandardDomEvent } from '@inertiaui/vanilla'
3
+ import { generateId as vanillaGenerateId } from '@inertiaui/vanilla'
4
+
5
+ // Wrap generateId with custom callback support for testing
6
+ let generateIdUsingCallback: (() => string) | null = null
7
+
8
+ export function generateIdUsing(callback: (() => string) | null): void {
9
+ generateIdUsingCallback = callback
10
+ }
11
+
12
+ export function generateId(prefix = 'inertiaui_'): string {
13
+ if (generateIdUsingCallback) {
14
+ return generateIdUsingCallback()
15
+ }
16
+ return vanillaGenerateId(prefix)
17
+ }
@@ -0,0 +1,65 @@
1
+ import { createElement } from 'react'
2
+ import { getConfig, putConfig, resetConfig } from './config'
3
+ import { useModalIndex } from './ModalRenderer'
4
+ import { useModalStack, ModalRoot, ModalStackProvider, renderApp, initFromPageProps, modalPropNames, prefetch } from './ModalRoot'
5
+ import useModal from './useModal'
6
+ import Deferred from './Deferred'
7
+ import HeadlessModal from './HeadlessModal'
8
+ import Modal from './Modal'
9
+ import ModalLink from './ModalLink'
10
+ import WhenVisible from './WhenVisible'
11
+ import * as dialogUtils from '@inertiaui/vanilla'
12
+
13
+ // Types
14
+ export type {
15
+ Modal as ModalInstance,
16
+ ModalConfig,
17
+ ModalResponseData,
18
+ ModalStackContextValue,
19
+ VisitOptions,
20
+ ReloadOptions,
21
+ EventCallback,
22
+ ComponentResolver,
23
+ PageProps,
24
+ ModalRootProps,
25
+ ModalRendererProps,
26
+ LocalModal,
27
+ // Prefetch types (#146)
28
+ PrefetchOption,
29
+ PrefetchOptions,
30
+ } from './types'
31
+
32
+ export type { ModalTypeConfig } from './config'
33
+
34
+ export type { CleanupFunction, FocusTrapOptions, EscapeKeyOptions } from '@inertiaui/vanilla'
35
+
36
+ const setPageLayout = <T extends { default: { layout?: (page: React.ReactNode) => React.ReactNode } }>(
37
+ layout: React.ComponentType<{ children?: React.ReactNode }>,
38
+ ) => (module: T): T => {
39
+ module.default.layout = (page) => createElement(layout, { children: page })
40
+ return module
41
+ }
42
+
43
+ export {
44
+ Deferred,
45
+ HeadlessModal,
46
+ Modal,
47
+ ModalLink,
48
+ ModalRoot,
49
+ ModalStackProvider,
50
+ WhenVisible,
51
+ getConfig,
52
+ initFromPageProps,
53
+ modalPropNames,
54
+ putConfig,
55
+ renderApp,
56
+ resetConfig,
57
+ setPageLayout,
58
+ useModal,
59
+ useModalIndex,
60
+ useModalStack,
61
+ // Prefetch function (#146)
62
+ prefetch,
63
+ // Dialog utilities (framework-agnostic)
64
+ dialogUtils,
65
+ }