@inertia-vuetify/modal 0.0.0-stage → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/keys.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { InjectionKey } from 'vue';
2
+ export declare const requestCloseKey: InjectionKey<() => Promise<void>>;
@@ -0,0 +1,64 @@
1
+ import { type HttpResponse, type RequestPayload } from '@inertiajs/core';
2
+ import { type Component, type ComputedRef, type Ref } from 'vue';
3
+ import type { HttpMethod, ModalConfig, ModalContext, ModalResponseData, PrefetchOptions, ReloadOptions, VisitOptions } from './types';
4
+ type EventCallback = (...args: unknown[]) => void;
5
+ type ComponentResolver = (name: string) => Promise<Component>;
6
+ export declare function prefetch(href: string, options?: PrefetchOptions): Promise<void>;
7
+ /** @deprecated A no-op kept for setup code written for Inertia 2. */
8
+ export declare const initFromPageProps: (_pageProps: Record<string, unknown>) => void;
9
+ export declare class ModalInstance {
10
+ id: string;
11
+ isOpen: boolean;
12
+ shouldRender: boolean;
13
+ listeners: Record<string, EventCallback[]>;
14
+ component: Component | null;
15
+ props: Ref<Record<string, unknown>>;
16
+ response: ModalResponseData;
17
+ config: ModalConfig;
18
+ onCloseCallback: (() => void) | null;
19
+ afterLeaveCallback: (() => void) | null;
20
+ index: ComputedRef<number>;
21
+ onTopOfStack: ComputedRef<boolean>;
22
+ name?: string;
23
+ constructor(component: Component | null, response: ModalResponseData, config?: ModalConfig | null, onClose?: (() => void) | null, afterLeave?: (() => void) | null);
24
+ getComponentPropKeys: () => string[];
25
+ getParentModal: () => ModalContext | null;
26
+ getChildModal: () => ModalContext | null;
27
+ show: () => void;
28
+ close: () => void;
29
+ setOpen: (open: boolean) => void;
30
+ afterLeave: () => void;
31
+ on: (event: string, callback: EventCallback) => void;
32
+ off: (event: string, callback?: EventCallback) => void;
33
+ emit: (event: string, ...args: unknown[]) => void;
34
+ registerEventListenersFromAttrs: (attrs: Record<string, unknown>) => (() => void);
35
+ reload: (options?: ReloadOptions) => Promise<void>;
36
+ updateProps: (props: Record<string, unknown>) => void;
37
+ }
38
+ declare function registerLocalModal(name: string, callback: (modal: ModalInstance) => void): void;
39
+ /** Removes a modal without waiting for its leave transition. */
40
+ export declare function discardModal(modal: ModalContext): void;
41
+ declare function pushFromResponseData(responseData: ModalResponseData, config?: ModalConfig, onClose?: (() => void) | null, onAfterLeave?: (() => void) | null): Promise<ModalInstance>;
42
+ declare function visit(href: string, method: HttpMethod, payload?: RequestPayload, headers?: Record<string, string>, config?: ModalConfig, onClose?: (() => void) | null, onAfterLeave?: (() => void) | null, queryStringArrayFormat?: 'brackets' | 'indices', useBrowserHistory?: boolean, onStart?: (() => void) | null, onSuccess?: ((response?: HttpResponse) => void) | null, onError?: ((...args: unknown[]) => void) | null, props?: Record<string, unknown> | null): Promise<ModalInstance>;
43
+ /** Opens a modal for a URL, or a local modal for `#name`. */
44
+ export declare function visitModal(url: string, options?: VisitOptions): Promise<ModalInstance>;
45
+ declare function push(component: Component | null, response: ModalResponseData, config?: ModalConfig | null, onClose?: (() => void) | null, afterLeave?: (() => void) | null): ModalInstance;
46
+ export declare const modalPropNames: string[];
47
+ export interface ModalStack {
48
+ /** @deprecated A no-op kept for setup code written for Inertia 2. */
49
+ setComponentResolver: (resolver: ComponentResolver) => void;
50
+ getBaseUrl: () => string | null;
51
+ setBaseUrl: (url: string | null) => void;
52
+ isClosingToBaseUrl: (pageUrl: string) => boolean;
53
+ clearClosingToBaseUrl: () => void;
54
+ stack: Readonly<Ref<readonly ModalContext[]>>;
55
+ push: typeof push;
56
+ pushFromResponseData: typeof pushFromResponseData;
57
+ closeAll: (force?: boolean) => void;
58
+ reset: () => void;
59
+ visit: typeof visit;
60
+ registerLocalModal: typeof registerLocalModal;
61
+ removeLocalModal: (name: string) => boolean;
62
+ }
63
+ export declare function useModalStack(): ModalStack;
64
+ export {};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A `v-dialog` transition that moves the panel in from off-screen, e.g. `translateX(100%)`.
3
+ * Vuetify only ships short slide transitions, so this uses the Web Animations API.
4
+ */
5
+ export declare function slideTransition(hiddenTransform: string): {
6
+ component: import("vue").FunctionalComponent<import("vue").TransitionProps, {}, any, {}>;
7
+ css: boolean;
8
+ onEnter: (el: Element, done: () => void) => void;
9
+ onLeave: (el: Element, done: () => void) => void;
10
+ };
@@ -0,0 +1,109 @@
1
+ import type { HttpResponse, Method, RequestPayload } from '@inertiajs/core';
2
+ import type { UnwrapNestedRefs } from 'vue';
3
+ import type { ModalInstance } from './modalStack';
4
+ export type MaxWidth = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl';
5
+ export type ModalPosition = 'top' | 'center' | 'bottom';
6
+ export type SlideoverPosition = 'left' | 'right' | 'bottom';
7
+ export type Breakpoint = 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
8
+ export type HttpMethod = Method;
9
+ /**
10
+ * `true`/`false`, `'mobile'` for Vuetify's `display.mobile`, or fullscreen
11
+ * below a Vuetify breakpoint or pixel width.
12
+ */
13
+ export type Fullscreen = boolean | 'mobile' | Breakpoint | number;
14
+ /** Runs before a user-initiated close; return `false` to keep the modal open. */
15
+ export type BeforeClose = () => boolean | void | Promise<boolean | void>;
16
+ export interface ModalTypeConfig {
17
+ closeButton: boolean;
18
+ closeExplicitly: boolean;
19
+ closeOnClickOutside: boolean;
20
+ maxWidth: MaxWidth | (string & {});
21
+ paddingClasses: string | false;
22
+ panelClasses: string | false;
23
+ position: ModalPosition | SlideoverPosition;
24
+ fullscreen: Fullscreen;
25
+ }
26
+ /** The configuration of a single modal, as resolved from its props, the visit, and the defaults. */
27
+ export type ModalConfig = Partial<ModalTypeConfig & {
28
+ slideover: boolean;
29
+ }>;
30
+ /** What `putConfig()` accepts; omitted keys get their defaults. */
31
+ export interface GlobalModalConfig {
32
+ type?: 'modal' | 'slideover';
33
+ navigate?: boolean;
34
+ appElement?: string | null;
35
+ modal?: Partial<ModalTypeConfig>;
36
+ slideover?: Partial<ModalTypeConfig>;
37
+ }
38
+ /** The `_inertiaui_modal` payload the `inertiaui/modal` Laravel package sends. */
39
+ export interface ModalResponseData {
40
+ id?: string;
41
+ component: string;
42
+ props: Record<string, unknown>;
43
+ url?: string;
44
+ version?: string;
45
+ meta?: {
46
+ deferredProps?: Record<string, string[]>;
47
+ };
48
+ baseUrl?: string;
49
+ }
50
+ export interface ReloadOptions {
51
+ only?: string[];
52
+ except?: string[];
53
+ method?: HttpMethod;
54
+ data?: Record<string, unknown>;
55
+ headers?: Record<string, string>;
56
+ onStart?: () => void;
57
+ onSuccess?: (response: HttpResponse) => void;
58
+ onError?: (error: unknown) => void;
59
+ onFinish?: () => void;
60
+ }
61
+ export interface VisitOptions {
62
+ method?: HttpMethod;
63
+ data?: RequestPayload;
64
+ headers?: Record<string, string>;
65
+ config?: ModalConfig;
66
+ onClose?: () => void;
67
+ onAfterLeave?: () => void;
68
+ queryStringArrayFormat?: 'brackets' | 'indices';
69
+ navigate?: boolean;
70
+ onStart?: () => void;
71
+ onSuccess?: (response?: HttpResponse) => void;
72
+ onError?: (...args: unknown[]) => void;
73
+ listeners?: Record<string, (...args: unknown[]) => void>;
74
+ /** Props for local modals. */
75
+ props?: Record<string, unknown>;
76
+ }
77
+ export type PrefetchOption = boolean | 'hover' | 'click' | 'mount' | Array<'hover' | 'click' | 'mount'>;
78
+ export interface PrefetchOptions {
79
+ method?: HttpMethod;
80
+ data?: RequestPayload;
81
+ headers?: Record<string, string>;
82
+ queryStringArrayFormat?: 'brackets' | 'indices';
83
+ cacheFor?: number;
84
+ onPrefetching?: () => void;
85
+ onPrefetched?: () => void;
86
+ }
87
+ /** A modal instance as components see it, with its computed refs unwrapped. */
88
+ export type ModalContext = UnwrapNestedRefs<ModalInstance>;
89
+ export interface HeadlessModalSlotProps extends Record<string, unknown> {
90
+ id: string;
91
+ index: number;
92
+ isOpen: boolean;
93
+ onTopOfStack: boolean;
94
+ shouldRender: boolean;
95
+ config: ModalConfig;
96
+ modalContext: ModalContext;
97
+ afterLeave: () => void;
98
+ /** Closes immediately, skipping `before-close`. */
99
+ close: () => void;
100
+ emit: (event: string, ...args: unknown[]) => void;
101
+ getChildModal: () => ModalContext | null;
102
+ getParentModal: () => ModalContext | null;
103
+ reload: (options?: ReloadOptions) => Promise<void>;
104
+ setOpen: (open: boolean) => void;
105
+ }
106
+ export interface ModalSlotProps extends HeadlessModalSlotProps {
107
+ /** Closes like the close button does, so `before-close` can veto it. */
108
+ requestClose: () => Promise<void>;
109
+ }
@@ -0,0 +1,3 @@
1
+ import type { ModalContext } from './types';
2
+ /** The modal the calling component is rendered in, or `null` outside a modal. */
3
+ export default function useModal(): ModalContext | null;
package/package.json CHANGED
@@ -1,6 +1,73 @@
1
1
  {
2
2
  "name": "@inertia-vuetify/modal",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "3.0.0",
4
+ "description": "Vuetify modals and slideovers for Inertia.js, compatible with the inertiaui/modal Laravel package",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/inertia-vuetify/modal.git"
9
+ },
10
+ "main": "./dist/inertia-modal-vuetify.js",
11
+ "types": "./dist/index.d.ts",
12
+ "files": [
13
+ "dist",
14
+ "src"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/inertia-modal-vuetify.js"
20
+ },
21
+ "./src": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./src/index.ts"
24
+ }
25
+ },
26
+ "scripts": {
27
+ "dev": "vite serve example",
28
+ "build": "vite build && vue-tsc -p tsconfig.build.json",
29
+ "build:example": "vite build example",
30
+ "test": "vitest run",
31
+ "typecheck": "vue-tsc --noEmit",
32
+ "lint": "oxlint",
33
+ "format": "oxfmt",
34
+ "format:check": "oxfmt --check",
35
+ "prepublishOnly": "npm run build"
36
+ },
37
+ "peerDependencies": {
38
+ "vue": "^3.5.0",
39
+ "vuetify": "^4.2.0",
40
+ "@inertiajs/core": "^3.0.0",
41
+ "@inertiajs/vue3": "^3.0.0"
42
+ },
43
+ "dependencies": {
44
+ "@inertiaui/vanilla": "^1.5.0"
45
+ },
46
+ "devDependencies": {
47
+ "@inertiajs/core": "^3.9.1",
48
+ "@inertiajs/vue3": "^3.9.1",
49
+ "@mdi/font": "^7.4.47",
50
+ "@vitejs/plugin-vue": "^6.0.10",
51
+ "@vitest/browser-playwright": "^5.0.5",
52
+ "@vue/test-utils": "^2.5.1",
53
+ "oxfmt": "^0.74.0",
54
+ "oxlint": "^1.89.0",
55
+ "playwright": "^1.64.1",
56
+ "typescript": "^6.0.3",
57
+ "vite": "^8.3.5",
58
+ "vite-plugin-vuetify": "^2.1.3",
59
+ "vitest": "^5.0.5",
60
+ "vue": "^3.5.44",
61
+ "vue-tsc": "^3.3.13",
62
+ "vuetify": "^4.3.0"
63
+ },
64
+ "keywords": [
65
+ "inertia",
66
+ "inertiajs",
67
+ "modal",
68
+ "vuetify",
69
+ "vue"
70
+ ],
71
+ "author": "",
72
+ "license": "MIT"
73
+ }
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ import { inject } from 'vue'
3
+ import { useLocale } from 'vuetify'
4
+ import { VBtn } from 'vuetify/components/VBtn'
5
+ import { requestCloseKey } from './keys'
6
+
7
+ const requestClose = inject(requestCloseKey)
8
+ const { t } = useLocale()
9
+ </script>
10
+
11
+ <template>
12
+ <VBtn class="im-close-button" icon="$close" variant="text" size="small" :aria-label="t('$vuetify.close')" @click="requestClose?.()" />
13
+ </template>
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject, type Ref } from 'vue'
3
+ import type { ModalContext } from './types'
4
+
5
+ const props = defineProps<{ data: string | string[] }>()
6
+
7
+ defineSlots<{ default?(): unknown; fallback?(): unknown }>()
8
+
9
+ const modalContext = inject<Ref<ModalContext | null>>('modalContext')
10
+
11
+ if (!modalContext) {
12
+ throw new Error('Deferred component must be used inside a Modal component')
13
+ }
14
+
15
+ const allKeysAreAvailable = computed(() => [props.data].flat().every((key) => modalContext.value?.props[key] !== undefined))
16
+ </script>
17
+
18
+ <template>
19
+ <slot v-if="allKeysAreAvailable" />
20
+ <slot v-else name="fallback" />
21
+ </template>
@@ -0,0 +1,174 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject, onBeforeUnmount, onMounted, reactive, ref, useAttrs, watch, type PropType, type Ref } from 'vue'
3
+ import { getConfig, getConfigByType } from './config'
4
+ import ModalRenderer from './ModalRenderer.vue'
5
+ import { discardModal, useModalStack } from './modalStack'
6
+ import type { Fullscreen, HeadlessModalSlotProps, MaxWidth, ModalConfig, ModalContext, ModalPosition, SlideoverPosition } from './types'
7
+
8
+ defineOptions({ inheritAttrs: false })
9
+
10
+ // Props default to null so unset ones fall back to the global config.
11
+ const props = defineProps({
12
+ name: String,
13
+ slideover: { type: Boolean, default: null },
14
+ closeButton: { type: Boolean, default: null },
15
+ closeExplicitly: { type: Boolean, default: null },
16
+ closeOnClickOutside: { type: Boolean, default: null },
17
+ maxWidth: { type: String as PropType<MaxWidth>, default: null },
18
+ paddingClasses: { type: [Boolean, String] as PropType<string | false>, default: null },
19
+ panelClasses: { type: [Boolean, String] as PropType<string | false>, default: null },
20
+ position: { type: String as PropType<ModalPosition | SlideoverPosition>, default: null },
21
+ fullscreen: { type: [Boolean, String, Number] as PropType<Fullscreen>, default: null },
22
+ })
23
+
24
+ const emits = defineEmits<{
25
+ 'modal-event': [event: string, ...args: unknown[]]
26
+ focus: []
27
+ blur: []
28
+ close: []
29
+ success: []
30
+ }>()
31
+
32
+ defineSlots<{ default?(props: HeadlessModalSlotProps): unknown }>()
33
+
34
+ const modalStack = useModalStack()
35
+ const modalContext: Ref<ModalContext | null> = props.name ? ref(null) : inject('modalContext', ref(null))
36
+
37
+ const config = computed<ModalConfig>(() => {
38
+ const slideover = modalContext.value?.config?.slideover ?? props.slideover ?? getConfig('type') === 'slideover'
39
+
40
+ return {
41
+ slideover,
42
+ closeButton: props.closeButton ?? getConfigByType(slideover, 'closeButton'),
43
+ closeExplicitly: props.closeExplicitly ?? getConfigByType(slideover, 'closeExplicitly'),
44
+ closeOnClickOutside: props.closeOnClickOutside ?? getConfigByType(slideover, 'closeOnClickOutside'),
45
+ maxWidth: props.maxWidth ?? getConfigByType(slideover, 'maxWidth'),
46
+ paddingClasses: props.paddingClasses ?? getConfigByType(slideover, 'paddingClasses'),
47
+ panelClasses: props.panelClasses ?? getConfigByType(slideover, 'panelClasses'),
48
+ position: props.position ?? getConfigByType(slideover, 'position'),
49
+ fullscreen: props.fullscreen ?? getConfigByType(slideover, 'fullscreen'),
50
+ ...modalContext.value?.config,
51
+ }
52
+ })
53
+
54
+ const attrs = useAttrs()
55
+ let unsubscribeEventListeners: (() => void) | undefined
56
+
57
+ function registerEventListeners() {
58
+ unsubscribeEventListeners = modalContext.value?.registerEventListenersFromAttrs(attrs)
59
+ }
60
+
61
+ if (props.name) {
62
+ modalStack.registerLocalModal(props.name, (context) => {
63
+ const previous = modalContext.value
64
+
65
+ // Reopened before its leave transition finished: that instance would never get afterLeave().
66
+ if (previous?.shouldRender && previous.id !== context.id) {
67
+ discardModal(previous)
68
+ }
69
+
70
+ modalContext.value = reactive(context)
71
+ registerEventListeners()
72
+ })
73
+
74
+ onBeforeUnmount(() => modalStack.removeLocalModal(props.name!))
75
+ }
76
+
77
+ onMounted(() => {
78
+ if (!props.name) {
79
+ registerEventListeners()
80
+ }
81
+ })
82
+
83
+ onBeforeUnmount(() => unsubscribeEventListeners?.())
84
+
85
+ function emit(event: string, ...args: unknown[]) {
86
+ emits('modal-event', event, ...args)
87
+ }
88
+
89
+ defineExpose({
90
+ emit,
91
+ afterLeave: () => modalContext.value?.afterLeave(),
92
+ close: () => modalContext.value?.close(),
93
+ reload: (...args: Parameters<ModalContext['reload']>) => modalContext.value?.reload(...args),
94
+ setOpen: (open: boolean) => modalContext.value?.setOpen(open),
95
+ getChildModal: () => modalContext.value?.getChildModal(),
96
+ getParentModal: () => modalContext.value?.getParentModal(),
97
+
98
+ get config() {
99
+ return modalContext.value?.config
100
+ },
101
+ get id() {
102
+ return modalContext.value?.id
103
+ },
104
+ get index() {
105
+ return modalContext.value?.index
106
+ },
107
+ get isOpen() {
108
+ return modalContext.value?.isOpen
109
+ },
110
+ get modalContext() {
111
+ return modalContext.value
112
+ },
113
+ get onTopOfStack() {
114
+ return modalContext.value?.onTopOfStack
115
+ },
116
+ get shouldRender() {
117
+ return modalContext.value?.shouldRender
118
+ },
119
+ })
120
+
121
+ watch(
122
+ () => modalContext.value?.onTopOfStack,
123
+ (onTopOfStack, previousOnTopOfStack) => {
124
+ if (onTopOfStack && !previousOnTopOfStack) {
125
+ emits('focus')
126
+ } else if (!onTopOfStack && previousOnTopOfStack) {
127
+ emits('blur')
128
+ }
129
+ },
130
+ )
131
+
132
+ watch(
133
+ () => modalContext.value?.isOpen,
134
+ (isOpen, previousIsOpen) => {
135
+ if (isOpen) {
136
+ emits('success')
137
+ } else if (previousIsOpen === true) {
138
+ // Not when first mounting closed.
139
+ emits('close')
140
+ }
141
+ },
142
+ { immediate: true },
143
+ )
144
+
145
+ // The next modal in the stack renders inside this one.
146
+ const nextIndex = computed(() => {
147
+ const context = modalContext.value
148
+
149
+ return context ? modalStack.stack.value.find((modal) => modal.shouldRender && modal.index > context.index)?.index : undefined
150
+ })
151
+ </script>
152
+
153
+ <template>
154
+ <slot
155
+ v-if="modalContext?.shouldRender"
156
+ v-bind="modalContext.props"
157
+ :id="modalContext.id"
158
+ :after-leave="modalContext.afterLeave"
159
+ :close="modalContext.close"
160
+ :config
161
+ :emit
162
+ :get-child-modal="modalContext.getChildModal"
163
+ :get-parent-modal="modalContext.getParentModal"
164
+ :index="modalContext.index"
165
+ :is-open="modalContext.isOpen"
166
+ :modal-context
167
+ :on-top-of-stack="modalContext.onTopOfStack"
168
+ :reload="modalContext.reload"
169
+ :set-open="modalContext.setOpen"
170
+ :should-render="modalContext.shouldRender"
171
+ />
172
+
173
+ <ModalRenderer v-if="nextIndex" :index="nextIndex" />
174
+ </template>
package/src/Modal.vue ADDED
@@ -0,0 +1,120 @@
1
+ <script setup lang="ts">
2
+ import { computed, useTemplateRef, type PropType } from 'vue'
3
+ import HeadlessModal from './HeadlessModal.vue'
4
+ import ModalContent from './ModalContent.vue'
5
+ import type { BeforeClose, Fullscreen, MaxWidth, ModalPosition, ModalSlotProps, ReloadOptions, SlideoverPosition } from './types'
6
+
7
+ // Upstream props default to null so unset ones fall back to the global config.
8
+ const props = defineProps({
9
+ name: String,
10
+ slideover: { type: Boolean, default: null },
11
+ closeButton: { type: Boolean, default: null },
12
+ closeExplicitly: { type: Boolean, default: null },
13
+ closeOnClickOutside: { type: Boolean, default: null },
14
+ maxWidth: { type: String as PropType<MaxWidth>, default: null },
15
+ paddingClasses: { type: [Boolean, String] as PropType<string | false>, default: null },
16
+ panelClasses: { type: [Boolean, String] as PropType<string | false>, default: null },
17
+ position: { type: String as PropType<ModalPosition | SlideoverPosition>, default: null },
18
+ title: String,
19
+ fullscreen: { type: [Boolean, String, Number] as PropType<Fullscreen>, default: null },
20
+ beforeClose: Function as PropType<BeforeClose>,
21
+ dialogProps: Object as PropType<Record<string, unknown>>,
22
+ cardProps: Object as PropType<Record<string, unknown>>,
23
+ })
24
+
25
+ const emit = defineEmits<{
26
+ 'after-leave': []
27
+ blur: []
28
+ close: []
29
+ focus: []
30
+ success: []
31
+ }>()
32
+
33
+ defineSlots<{
34
+ default?(props: ModalSlotProps): unknown
35
+ title?(props: ModalSlotProps): unknown
36
+ actions?(props: ModalSlotProps): unknown
37
+ }>()
38
+
39
+ const headlessProps = computed(() => ({
40
+ name: props.name,
41
+ slideover: props.slideover,
42
+ closeButton: props.closeButton,
43
+ closeExplicitly: props.closeExplicitly,
44
+ closeOnClickOutside: props.closeOnClickOutside,
45
+ maxWidth: props.maxWidth,
46
+ paddingClasses: props.paddingClasses,
47
+ panelClasses: props.panelClasses,
48
+ position: props.position,
49
+ fullscreen: props.fullscreen,
50
+ }))
51
+
52
+ const modal = useTemplateRef<InstanceType<typeof HeadlessModal>>('modal')
53
+
54
+ defineExpose({
55
+ afterLeave: () => modal.value?.afterLeave(),
56
+ close: () => modal.value?.close(),
57
+ emit: (event: string, ...args: unknown[]) => modal.value?.emit(event, ...args),
58
+ getChildModal: () => modal.value?.getChildModal(),
59
+ getParentModal: () => modal.value?.getParentModal(),
60
+ reload: (options?: ReloadOptions) => modal.value?.reload(options),
61
+ setOpen: (open: boolean) => modal.value?.setOpen(open),
62
+
63
+ get config() {
64
+ return modal.value?.config
65
+ },
66
+ get id() {
67
+ return modal.value?.id
68
+ },
69
+ get index() {
70
+ return modal.value?.index
71
+ },
72
+ get isOpen() {
73
+ return modal.value?.isOpen
74
+ },
75
+ get modalContext() {
76
+ return modal.value?.modalContext
77
+ },
78
+ get onTopOfStack() {
79
+ return modal.value?.onTopOfStack
80
+ },
81
+ get shouldRender() {
82
+ return modal.value?.shouldRender
83
+ },
84
+ })
85
+ </script>
86
+
87
+ <template>
88
+ <HeadlessModal
89
+ ref="modal"
90
+ v-bind="headlessProps"
91
+ v-slot="slotProps"
92
+ @success="emit('success')"
93
+ @close="emit('close')"
94
+ @focus="emit('focus')"
95
+ @blur="emit('blur')"
96
+ >
97
+ <ModalContent
98
+ :key="slotProps.id"
99
+ :modal-context="slotProps.modalContext"
100
+ :config="slotProps.config"
101
+ :title
102
+ :before-close
103
+ :dialog-props
104
+ :card-props
105
+ @after-leave="emit('after-leave')"
106
+ >
107
+ <template #default="{ requestClose }">
108
+ <slot v-bind="{ ...slotProps, requestClose }" />
109
+ </template>
110
+
111
+ <template v-if="$slots.title" #title="{ requestClose }">
112
+ <slot name="title" v-bind="{ ...slotProps, requestClose }" />
113
+ </template>
114
+
115
+ <template v-if="$slots.actions" #actions="{ requestClose }">
116
+ <slot name="actions" v-bind="{ ...slotProps, requestClose }" />
117
+ </template>
118
+ </ModalContent>
119
+ </HeadlessModal>
120
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import { VBtn } from 'vuetify/components/VBtn'
4
+ import ModalLink from './ModalLink.vue'
5
+
6
+ defineOptions({ inheritAttrs: false })
7
+
8
+ defineProps<{ href: string }>()
9
+
10
+ defineSlots<{ default?(props: { loading: boolean }): unknown }>()
11
+
12
+ const loading = ref(false)
13
+ </script>
14
+
15
+ <template>
16
+ <ModalLink
17
+ v-slot="slotProps"
18
+ v-bind="$attrs"
19
+ :as="VBtn"
20
+ :href
21
+ :loading
22
+ @start="loading = true"
23
+ @success="loading = false"
24
+ @error="loading = false"
25
+ >
26
+ <slot v-bind="slotProps" />
27
+ </ModalLink>
28
+ </template>