@inertia-vuetify/modal 0.0.0-stage → 3.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/app.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { h, type App, type Component, type VNode } from 'vue'
2
+ import ModalRoot from './ModalRoot.vue'
3
+
4
+ /** Renders the Inertia app inside `ModalRoot`, which renders the modal stack. */
5
+ export function renderApp(App: Component, props: Record<string, any>): () => VNode {
6
+ return () => h(ModalRoot, () => h(App, props))
7
+ }
8
+
9
+ /** Wraps an app's existing render function in `ModalRoot`. */
10
+ export function withInertiaModal(app: App): void {
11
+ const component = app._component as { render?: () => VNode }
12
+ const render = component.render
13
+
14
+ if (render) {
15
+ component.render = () => h(ModalRoot, () => render())
16
+ }
17
+ }
package/src/cache.ts ADDED
@@ -0,0 +1,67 @@
1
+ interface CacheEntry<T> {
2
+ response: T
3
+ expiresAt: number
4
+ }
5
+
6
+ export class ResponseCache<T> {
7
+ private cache = new Map<string, CacheEntry<T>>()
8
+ private timers = new Map<string, ReturnType<typeof setTimeout>>()
9
+ private inFlight = new Map<string, Promise<T>>()
10
+
11
+ static key(method: string, url: string, data: unknown): string {
12
+ return `${method}:${url}:${JSON.stringify(data)}`
13
+ }
14
+
15
+ get(key: string): T | null {
16
+ const cached = this.cache.get(key)
17
+
18
+ if (!cached) {
19
+ return null
20
+ }
21
+
22
+ if (Date.now() > cached.expiresAt) {
23
+ this.delete(key)
24
+ return null
25
+ }
26
+
27
+ return cached.response
28
+ }
29
+
30
+ set(key: string, response: T, cacheFor: number): void {
31
+ this.delete(key)
32
+
33
+ this.cache.set(key, {
34
+ response,
35
+ expiresAt: Date.now() + cacheFor,
36
+ })
37
+
38
+ if (cacheFor > 0) {
39
+ this.timers.set(
40
+ key,
41
+ setTimeout(() => this.delete(key), cacheFor),
42
+ )
43
+ }
44
+ }
45
+
46
+ delete(key: string): void {
47
+ this.cache.delete(key)
48
+
49
+ const timer = this.timers.get(key)
50
+ if (timer) {
51
+ clearTimeout(timer)
52
+ this.timers.delete(key)
53
+ }
54
+ }
55
+
56
+ getInFlight(key: string): Promise<T> | undefined {
57
+ return this.inFlight.get(key)
58
+ }
59
+
60
+ setInFlight(key: string, promise: Promise<T>): void {
61
+ this.inFlight.set(key, promise)
62
+ }
63
+
64
+ deleteInFlight(key: string): void {
65
+ this.inFlight.delete(key)
66
+ }
67
+ }
package/src/config.ts ADDED
@@ -0,0 +1,116 @@
1
+ import type { GlobalModalConfig, ModalPosition, ModalTypeConfig } from './types'
2
+
3
+ type Config = Required<Omit<GlobalModalConfig, 'modal' | 'slideover'>> & {
4
+ modal: ModalTypeConfig
5
+ slideover: ModalTypeConfig
6
+ }
7
+
8
+ const defaultConfig: Config = {
9
+ type: 'modal',
10
+ navigate: false,
11
+ appElement: '#app',
12
+ modal: {
13
+ closeButton: true,
14
+ closeExplicitly: false,
15
+ closeOnClickOutside: true,
16
+ maxWidth: '2xl',
17
+ paddingClasses: 'pa-4 pa-sm-6',
18
+ panelClasses: '',
19
+ position: 'center',
20
+ fullscreen: 'sm',
21
+ },
22
+ slideover: {
23
+ closeButton: true,
24
+ closeExplicitly: false,
25
+ closeOnClickOutside: true,
26
+ maxWidth: 'md',
27
+ paddingClasses: 'pa-4 pa-sm-6',
28
+ panelClasses: '',
29
+ position: 'right',
30
+ fullscreen: false,
31
+ },
32
+ }
33
+
34
+ let config: Config
35
+
36
+ export function resetConfig(): void {
37
+ config = structuredClone(defaultConfig)
38
+ }
39
+
40
+ /** Replaces the configuration (omitted keys get their defaults) or sets one dot-notation key. */
41
+ export function putConfig(config: GlobalModalConfig): void
42
+ export function putConfig(key: string, value: unknown): void
43
+ export function putConfig(key: string | GlobalModalConfig, value?: unknown): void {
44
+ if (typeof key === 'object') {
45
+ config = {
46
+ type: key.type ?? defaultConfig.type,
47
+ navigate: key.navigate ?? defaultConfig.navigate,
48
+ appElement: key.appElement !== undefined ? key.appElement : defaultConfig.appElement,
49
+ modal: { ...defaultConfig.modal, ...key.modal },
50
+ slideover: { ...defaultConfig.slideover, ...key.slideover },
51
+ }
52
+ return
53
+ }
54
+
55
+ const keys = key.split('.')
56
+ let current = config as Record<string, unknown>
57
+
58
+ for (const segment of keys.slice(0, -1)) {
59
+ current = (current[segment] ??= {}) as Record<string, unknown>
60
+ }
61
+
62
+ current[keys[keys.length - 1]] = value
63
+ }
64
+
65
+ /** Reads the whole configuration or one dot-notation key; missing keys return `null`. */
66
+ export function getConfig(key?: string): unknown {
67
+ if (key === undefined) {
68
+ return config
69
+ }
70
+
71
+ let current: unknown = config
72
+
73
+ for (const segment of key.split('.')) {
74
+ if (current === null || typeof current !== 'object') {
75
+ return null
76
+ }
77
+
78
+ current = (current as Record<string, unknown>)[segment]
79
+ }
80
+
81
+ return current ?? null
82
+ }
83
+
84
+ export function getConfigByType<K extends keyof ModalTypeConfig>(slideover: boolean, key: K): ModalTypeConfig[K] {
85
+ return getConfig(`${slideover ? 'slideover' : 'modal'}.${key}`) as ModalTypeConfig[K]
86
+ }
87
+
88
+ // Max-width tokens name Tailwind's max-w-* scale, like the original package.
89
+ const maxWidths: Record<string, string> = {
90
+ sm: '384px',
91
+ md: '448px',
92
+ lg: '512px',
93
+ xl: '576px',
94
+ '2xl': '672px',
95
+ '3xl': '768px',
96
+ '4xl': '896px',
97
+ '5xl': '1024px',
98
+ '6xl': '1152px',
99
+ '7xl': '1280px',
100
+ }
101
+
102
+ /**
103
+ * Maps a max-width token to a CSS width; unknown tokens fall back to `2xl`.
104
+ */
105
+ export function getMaxWidth(maxWidth?: string | null): string {
106
+ return maxWidths[maxWidth ?? ''] ?? maxWidths['2xl']
107
+ }
108
+
109
+ /**
110
+ * Maps a modal position to a Vuetify dialog location.
111
+ */
112
+ export function getDialogLocation(position?: string | null): ModalPosition {
113
+ return position === 'top' || position === 'bottom' ? position : 'center'
114
+ }
115
+
116
+ resetConfig()
package/src/helpers.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { generateId as generateVanillaId } from '@inertiaui/vanilla'
2
+
3
+ export { except, kebabCase, only, rejectNullValues, sameUrlPath } from '@inertiaui/vanilla'
4
+
5
+ export function parseResponseData(data: unknown): unknown {
6
+ return typeof data === 'string' ? JSON.parse(data) : data
7
+ }
8
+
9
+ let generateIdUsingCallback: (() => string) | null = null
10
+
11
+ /** Overrides id generation, e.g. to make request headers predictable in tests. */
12
+ export function generateIdUsing(callback: (() => string) | null): void {
13
+ generateIdUsingCallback = callback
14
+ }
15
+
16
+ export function generateId(prefix = 'inertiaui_modal_'): string {
17
+ return generateIdUsingCallback?.() ?? generateVanillaId(prefix)
18
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export * as dialogUtils from '@inertiaui/vanilla'
2
+ export type { CleanupFunction, EscapeKeyOptions, FocusTrapOptions } from '@inertiaui/vanilla'
3
+
4
+ export { renderApp, withInertiaModal } from './app'
5
+ export { getConfig, getDialogLocation, getMaxWidth, putConfig, resetConfig } from './config'
6
+ export { default as Deferred } from './Deferred.vue'
7
+ export { default as HeadlessModal } from './HeadlessModal.vue'
8
+ export { default as Modal } from './Modal.vue'
9
+ export { default as ModalBtn } from './ModalBtn.vue'
10
+ export { default as ModalLink } from './ModalLink.vue'
11
+ export { default as ModalRoot } from './ModalRoot.vue'
12
+ export { initFromPageProps, ModalInstance, modalPropNames, prefetch, useModalStack, visitModal, type ModalStack } from './modalStack'
13
+ export type * from './types'
14
+ export { default as useModal } from './useModal'
15
+ export { default as WhenVisible } from './WhenVisible.vue'
package/src/keys.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { InjectionKey } from 'vue'
2
+
3
+ export const requestCloseKey: InjectionKey<() => Promise<void>> = Symbol('inertia-modal-request-close')