@polymarbot/nuxt-layer-shadcn-ui 0.8.6 → 0.8.8
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/app/components/ui/DatePicker/index.vue +1 -1
- package/app/components/ui/DatePicker/types.ts +6 -2
- package/app/components/ui/DateRangePicker/index.vue +53 -16
- package/app/components/ui/DateRangePicker/types.ts +15 -3
- package/app/components/ui/Drawer/index.stories.ts +78 -1
- package/app/components/ui/Drawer/index.vue +31 -9
- package/app/components/ui/Drawer/types.ts +5 -0
- package/app/components/ui/Modal/index.stories.ts +78 -0
- package/app/components/ui/Modal/index.vue +31 -9
- package/app/components/ui/Modal/types.ts +5 -0
- package/package.json +2 -2
|
@@ -18,7 +18,7 @@ export interface DatePickerTimeConfig {
|
|
|
18
18
|
export type DatePickerType = 'date' | 'month' | 'year'
|
|
19
19
|
|
|
20
20
|
export interface DatePickerProps {
|
|
21
|
-
modelValue?: Date | string | null
|
|
21
|
+
modelValue?: Date | string | number | null
|
|
22
22
|
/** Picker type: date (default), month, or year */
|
|
23
23
|
type?: DatePickerType
|
|
24
24
|
/** Enable time selection, or pass DatePickerTimeConfig for fine-grained control */
|
|
@@ -33,7 +33,11 @@ export interface DatePickerProps {
|
|
|
33
33
|
minDate?: Date | string
|
|
34
34
|
/** Maximum selectable date */
|
|
35
35
|
maxDate?: Date | string
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* v-model output format. Accepts any VueDatePicker `model-type` value:
|
|
38
|
+
* `'iso'`, `'timestamp'`, or a date-fns pattern (e.g. `'yyyy-MM-dd'`).
|
|
39
|
+
* Omit to bind a `Date` object.
|
|
40
|
+
*/
|
|
37
41
|
valueFormat?: string
|
|
38
42
|
/** Auto apply selection without confirm button */
|
|
39
43
|
autoApply?: boolean
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { DateRangePickerProps } from './types'
|
|
3
|
+
import { format as formatDate, parse as parseDate } from 'date-fns'
|
|
3
4
|
|
|
4
5
|
defineOptions({ inheritAttrs: false })
|
|
5
6
|
|
|
@@ -20,30 +21,65 @@ const props = withDefaults(defineProps<DateRangePickerProps>(), {
|
|
|
20
21
|
})
|
|
21
22
|
|
|
22
23
|
const emit = defineEmits<{
|
|
23
|
-
'update:start': [value: Date | string | null]
|
|
24
|
-
'update:end': [value: Date | string | null]
|
|
24
|
+
'update:start': [value: Date | string | number | null]
|
|
25
|
+
'update:end': [value: Date | string | number | null]
|
|
25
26
|
}>()
|
|
26
27
|
|
|
27
28
|
const { t } = useI18n()
|
|
28
29
|
const T = useTranslations('components.ui.DateRangePicker')
|
|
29
30
|
|
|
31
|
+
// Convert a v-model value (whose shape depends on valueFormat) into a Date for manipulation.
|
|
32
|
+
function parseValue (value: Date | string | number): Date | null {
|
|
33
|
+
if (value instanceof Date) return new Date(value)
|
|
34
|
+
if (typeof value === 'number') return new Date(value)
|
|
35
|
+
const fmt = props.valueFormat
|
|
36
|
+
if (!fmt || fmt === 'iso') return new Date(value)
|
|
37
|
+
if (fmt === 'timestamp') return new Date(Number(value))
|
|
38
|
+
if (fmt === 'format') return null // uses VueDatePicker `format` prop, not exposed here
|
|
39
|
+
try {
|
|
40
|
+
return parseDate(value, fmt, new Date())
|
|
41
|
+
} catch {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Convert a Date back to the same shape as the original value.
|
|
47
|
+
function formatValue (date: Date, original: Date | string | number): Date | string | number {
|
|
48
|
+
if (original instanceof Date) return date
|
|
49
|
+
if (typeof original === 'number') return date.getTime()
|
|
50
|
+
const fmt = props.valueFormat
|
|
51
|
+
if (!fmt || fmt === 'iso') return date.toISOString()
|
|
52
|
+
if (fmt === 'timestamp') return date.getTime()
|
|
53
|
+
if (fmt === 'format') return original
|
|
54
|
+
try {
|
|
55
|
+
return formatDate(date, fmt)
|
|
56
|
+
} catch {
|
|
57
|
+
return original
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Normalize the time portion to start/end of day so the range is inclusive.
|
|
62
|
+
// For date-only patterns (e.g. 'yyyy-MM-dd') the output is unchanged since the
|
|
63
|
+
// formatted string carries no time component — the round-trip just preserves it.
|
|
64
|
+
function normalizeTimeOfDay (
|
|
65
|
+
value: Date | string | number | null,
|
|
66
|
+
end: boolean,
|
|
67
|
+
): Date | string | number | null {
|
|
68
|
+
if (value == null) return value
|
|
69
|
+
const date = parseValue(value)
|
|
70
|
+
if (!date || Number.isNaN(date.getTime())) return value
|
|
71
|
+
date.setHours(end ? 23 : 0, end ? 59 : 0, end ? 59 : 0, end ? 999 : 0)
|
|
72
|
+
return formatValue(date, value)
|
|
73
|
+
}
|
|
74
|
+
|
|
30
75
|
const start = computed({
|
|
31
76
|
get: () => props.start,
|
|
32
|
-
set: value => emit('update:start', value),
|
|
77
|
+
set: value => emit('update:start', props.showTime ? value : normalizeTimeOfDay(value, false)),
|
|
33
78
|
})
|
|
34
79
|
|
|
35
80
|
const end = computed({
|
|
36
81
|
get: () => props.end,
|
|
37
|
-
set: value =>
|
|
38
|
-
// When time is disabled, normalize end to end of day so range is inclusive
|
|
39
|
-
if (value instanceof Date && !props.showTime) {
|
|
40
|
-
const adjusted = new Date(value)
|
|
41
|
-
adjusted.setHours(23, 59, 59, 999)
|
|
42
|
-
emit('update:end', adjusted)
|
|
43
|
-
} else {
|
|
44
|
-
emit('update:end', value)
|
|
45
|
-
}
|
|
46
|
-
},
|
|
82
|
+
set: value => emit('update:end', props.showTime ? value : normalizeTimeOfDay(value, true)),
|
|
47
83
|
})
|
|
48
84
|
|
|
49
85
|
function addDays (date: Date, days: number): Date {
|
|
@@ -52,9 +88,10 @@ function addDays (date: Date, days: number): Date {
|
|
|
52
88
|
return result
|
|
53
89
|
}
|
|
54
90
|
|
|
55
|
-
function toDate (value: Date | string | null | undefined): Date | undefined {
|
|
56
|
-
if (
|
|
57
|
-
|
|
91
|
+
function toDate (value: Date | string | number | null | undefined): Date | undefined {
|
|
92
|
+
if (value == null) return undefined
|
|
93
|
+
if (value instanceof Date) return value
|
|
94
|
+
return parseValue(value) ?? undefined
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
const startMinDate = computed(() => {
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { DatePickerTimeConfig } from '../DatePicker/types'
|
|
2
2
|
|
|
3
3
|
export interface DateRangePickerProps {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Range start. When `showTime` is false, the emitted value is normalized to
|
|
6
|
+
* the start of the day (00:00:00.000) so the range is inclusive.
|
|
7
|
+
*/
|
|
8
|
+
start?: Date | string | number | null
|
|
9
|
+
/**
|
|
10
|
+
* Range end. When `showTime` is false, the emitted value is normalized to
|
|
11
|
+
* the end of the day (23:59:59.999) so the range is inclusive.
|
|
12
|
+
*/
|
|
13
|
+
end?: Date | string | number | null
|
|
6
14
|
/** Minimum selectable date */
|
|
7
15
|
minDate?: Date | string
|
|
8
16
|
/** Maximum selectable date */
|
|
@@ -19,7 +27,11 @@ export interface DateRangePickerProps {
|
|
|
19
27
|
endPlaceholder?: string
|
|
20
28
|
/** Maximum span in days between start and end date */
|
|
21
29
|
maxSpanDays?: number
|
|
22
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* v-model output format. Accepts any VueDatePicker `model-type` value:
|
|
32
|
+
* `'iso'`, `'timestamp'`, or a date-fns pattern (e.g. `'yyyy-MM-dd'`).
|
|
33
|
+
* Omit to bind a `Date` object.
|
|
34
|
+
*/
|
|
23
35
|
valueFormat?: string
|
|
24
36
|
/** Auto apply selection without confirm button */
|
|
25
37
|
autoApply?: boolean
|
|
@@ -4,7 +4,7 @@ import { useArgsModel } from '#storybook/argsModel'
|
|
|
4
4
|
import Button from '../Button/index.vue'
|
|
5
5
|
import Input from '../Input/index.vue'
|
|
6
6
|
import type { ButtonVariant } from '../Button/types'
|
|
7
|
-
import type { DrawerSide } from './types'
|
|
7
|
+
import type { DrawerAction, DrawerSide } from './types'
|
|
8
8
|
import Drawer from './index.vue'
|
|
9
9
|
|
|
10
10
|
const sides: DrawerSide[] = [ 'top', 'right', 'bottom', 'left' ]
|
|
@@ -257,6 +257,83 @@ export const WithTrigger: Story = {
|
|
|
257
257
|
}),
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
export const PreventClose: Story = {
|
|
261
|
+
parameters: {
|
|
262
|
+
...noControls,
|
|
263
|
+
docs: {
|
|
264
|
+
source: {
|
|
265
|
+
code: `
|
|
266
|
+
<template>
|
|
267
|
+
<Drawer
|
|
268
|
+
v-model:visible="visible"
|
|
269
|
+
title="Type to Continue"
|
|
270
|
+
description="beforeClose intercepts the confirm action; cancel/X/ESC close normally."
|
|
271
|
+
showCancel
|
|
272
|
+
confirmText="Submit"
|
|
273
|
+
:beforeClose="onBeforeClose"
|
|
274
|
+
>
|
|
275
|
+
<Input v-model="value" placeholder="Type 'confirm' to close" />
|
|
276
|
+
<p v-if="error" class="mt-2 text-sm text-destructive">{{ error }}</p>
|
|
277
|
+
</Drawer>
|
|
278
|
+
</template>
|
|
279
|
+
|
|
280
|
+
<script setup lang="ts">
|
|
281
|
+
import type { DrawerAction } from '#components'
|
|
282
|
+
|
|
283
|
+
const visible = ref(false)
|
|
284
|
+
const value = ref('')
|
|
285
|
+
const error = ref('')
|
|
286
|
+
|
|
287
|
+
function onBeforeClose (action: DrawerAction) {
|
|
288
|
+
if (action === 'cancel') return
|
|
289
|
+
if (value.value !== 'confirm') {
|
|
290
|
+
error.value = "Value must be 'confirm' to close."
|
|
291
|
+
return false
|
|
292
|
+
}
|
|
293
|
+
error.value = ''
|
|
294
|
+
return new Promise(resolve => setTimeout(resolve, 1000))
|
|
295
|
+
}
|
|
296
|
+
</script>
|
|
297
|
+
`.trim(),
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
render: () => ({
|
|
302
|
+
components: { Drawer, Button, Input },
|
|
303
|
+
setup () {
|
|
304
|
+
const visible = ref(false)
|
|
305
|
+
const value = ref('')
|
|
306
|
+
const error = ref('')
|
|
307
|
+
function onBeforeClose (action: DrawerAction) {
|
|
308
|
+
if (action === 'cancel') return
|
|
309
|
+
if (value.value !== 'confirm') {
|
|
310
|
+
error.value = 'Value must be "confirm" to close.'
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
error.value = ''
|
|
314
|
+
return new Promise<void>(resolve => setTimeout(resolve, 1000))
|
|
315
|
+
}
|
|
316
|
+
return { visible, value, error, onBeforeClose }
|
|
317
|
+
},
|
|
318
|
+
template: `
|
|
319
|
+
<div>
|
|
320
|
+
<Button @click="visible = true">Open Drawer</Button>
|
|
321
|
+
<Drawer
|
|
322
|
+
v-model:visible="visible"
|
|
323
|
+
title="Type to Continue"
|
|
324
|
+
description="beforeClose intercepts the confirm action; cancel/X/ESC close normally."
|
|
325
|
+
showCancel
|
|
326
|
+
confirmText="Submit"
|
|
327
|
+
:beforeClose="onBeforeClose"
|
|
328
|
+
>
|
|
329
|
+
<Input v-model="value" placeholder="Type 'confirm' to close" />
|
|
330
|
+
<p v-if="error" class="mt-2 text-sm text-destructive">{{ error }}</p>
|
|
331
|
+
</Drawer>
|
|
332
|
+
</div>
|
|
333
|
+
`,
|
|
334
|
+
}),
|
|
335
|
+
}
|
|
336
|
+
|
|
260
337
|
export const EventHandling: Story = {
|
|
261
338
|
parameters: noControls,
|
|
262
339
|
render: () => ({
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
SheetTitle,
|
|
10
10
|
SheetTrigger,
|
|
11
11
|
} from '../../shadcn/sheet'
|
|
12
|
-
import type { DrawerProps } from './types'
|
|
12
|
+
import type { DrawerAction, DrawerProps } from './types'
|
|
13
13
|
|
|
14
14
|
defineOptions({ inheritAttrs: false })
|
|
15
15
|
|
|
@@ -25,6 +25,7 @@ const props = withDefaults(defineProps<DrawerProps>(), {
|
|
|
25
25
|
cancelText: undefined,
|
|
26
26
|
confirmVariant: 'default',
|
|
27
27
|
cancelVariant: 'outline',
|
|
28
|
+
beforeClose: undefined,
|
|
28
29
|
class: undefined,
|
|
29
30
|
})
|
|
30
31
|
|
|
@@ -47,6 +48,8 @@ const resolvedCancelText = computed(
|
|
|
47
48
|
)
|
|
48
49
|
|
|
49
50
|
const sheetOpen = ref(props.visible ?? false)
|
|
51
|
+
const internalLoading = ref(false)
|
|
52
|
+
const isLoading = computed(() => internalLoading.value || props.loading)
|
|
50
53
|
|
|
51
54
|
watch(() => props.visible, value => {
|
|
52
55
|
if (value !== undefined) sheetOpen.value = value
|
|
@@ -59,18 +62,37 @@ watch(sheetOpen, value => {
|
|
|
59
62
|
})
|
|
60
63
|
|
|
61
64
|
function onOpenUpdate (value: boolean) {
|
|
62
|
-
if (!value &&
|
|
65
|
+
if (!value && isLoading.value) return
|
|
63
66
|
if (value) sheetOpen.value = true
|
|
64
|
-
else
|
|
67
|
+
else handleClose('cancel')
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
function onConfirm () {
|
|
68
71
|
emit('confirm')
|
|
69
|
-
|
|
72
|
+
handleClose('confirm')
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
function onCancel () {
|
|
73
76
|
emit('cancel')
|
|
77
|
+
handleClose('cancel')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handleClose (action: DrawerAction) {
|
|
81
|
+
if (!props.beforeClose) {
|
|
82
|
+
sheetOpen.value = false
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
const result = props.beforeClose(action)
|
|
86
|
+
if (result === false) return
|
|
87
|
+
if (result instanceof Promise) {
|
|
88
|
+
internalLoading.value = true
|
|
89
|
+
result.then(() => {
|
|
90
|
+
sheetOpen.value = false
|
|
91
|
+
}).finally(() => {
|
|
92
|
+
internalLoading.value = false
|
|
93
|
+
})
|
|
94
|
+
return
|
|
95
|
+
}
|
|
74
96
|
sheetOpen.value = false
|
|
75
97
|
}
|
|
76
98
|
|
|
@@ -136,8 +158,8 @@ const contentClass = computed(() =>
|
|
|
136
158
|
class="min-h-0 flex-1"
|
|
137
159
|
>
|
|
138
160
|
<div
|
|
139
|
-
:inert="
|
|
140
|
-
:class="[
|
|
161
|
+
:inert="isLoading || disabled || undefined"
|
|
162
|
+
:class="[ isLoading || disabled ? 'opacity-50' : undefined ]"
|
|
141
163
|
class="p-4"
|
|
142
164
|
>
|
|
143
165
|
<slot />
|
|
@@ -159,7 +181,7 @@ const contentClass = computed(() =>
|
|
|
159
181
|
v-if="showCancel"
|
|
160
182
|
class="min-w-24"
|
|
161
183
|
:variant="cancelVariant"
|
|
162
|
-
:disabled="
|
|
184
|
+
:disabled="isLoading"
|
|
163
185
|
@click="onCancel"
|
|
164
186
|
>
|
|
165
187
|
{{ resolvedCancelText }}
|
|
@@ -167,7 +189,7 @@ const contentClass = computed(() =>
|
|
|
167
189
|
<Button
|
|
168
190
|
:class="showCancel ? 'min-w-24' : 'min-w-32'"
|
|
169
191
|
:variant="confirmVariant"
|
|
170
|
-
:loading="
|
|
192
|
+
:loading="isLoading"
|
|
171
193
|
:disabled="disabled || confirmDisabled"
|
|
172
194
|
@click="onConfirm"
|
|
173
195
|
>
|
|
@@ -179,7 +201,7 @@ const contentClass = computed(() =>
|
|
|
179
201
|
|
|
180
202
|
<SheetClose
|
|
181
203
|
v-if="showClose"
|
|
182
|
-
:disabled="
|
|
204
|
+
:disabled="isLoading"
|
|
183
205
|
class="
|
|
184
206
|
top-3 right-3 size-8 text-muted-foreground ring-offset-background
|
|
185
207
|
hover:bg-accent/50 hover:text-foreground
|
|
@@ -2,6 +2,10 @@ import type { ButtonVariants } from '../../shadcn/button'
|
|
|
2
2
|
|
|
3
3
|
export type DrawerSide = 'top' | 'right' | 'bottom' | 'left'
|
|
4
4
|
|
|
5
|
+
export type DrawerAction = 'confirm' | 'cancel'
|
|
6
|
+
|
|
7
|
+
export type DrawerBeforeClose = (action: DrawerAction) => boolean | undefined | Promise<unknown>
|
|
8
|
+
|
|
5
9
|
export interface DrawerProps {
|
|
6
10
|
visible?: boolean
|
|
7
11
|
loading?: boolean
|
|
@@ -20,5 +24,6 @@ export interface DrawerProps {
|
|
|
20
24
|
cancelText?: string
|
|
21
25
|
confirmVariant?: ButtonVariants['variant']
|
|
22
26
|
cancelVariant?: ButtonVariants['variant']
|
|
27
|
+
beforeClose?: DrawerBeforeClose
|
|
23
28
|
class?: ClassValue
|
|
24
29
|
}
|
|
@@ -4,6 +4,7 @@ import EventLog from '#storybook/EventLog.vue'
|
|
|
4
4
|
import { useArgsModel } from '#storybook/argsModel'
|
|
5
5
|
import Button from '../Button/index.vue'
|
|
6
6
|
import Input from '../Input/index.vue'
|
|
7
|
+
import type { ModalAction } from './types'
|
|
7
8
|
import Modal from './index.vue'
|
|
8
9
|
|
|
9
10
|
const types: ModalContentType[] = [ 'default', 'success', 'info', 'help', 'warn', 'danger', 'error' ]
|
|
@@ -255,6 +256,83 @@ export const WithTrigger: Story = {
|
|
|
255
256
|
}),
|
|
256
257
|
}
|
|
257
258
|
|
|
259
|
+
export const PreventClose: Story = {
|
|
260
|
+
parameters: {
|
|
261
|
+
...noControls,
|
|
262
|
+
docs: {
|
|
263
|
+
source: {
|
|
264
|
+
code: `
|
|
265
|
+
<template>
|
|
266
|
+
<Modal
|
|
267
|
+
v-model:visible="visible"
|
|
268
|
+
title="Type to Continue"
|
|
269
|
+
description="beforeClose intercepts the confirm action; cancel/X/ESC close normally."
|
|
270
|
+
showCancel
|
|
271
|
+
confirmText="Submit"
|
|
272
|
+
:beforeClose="onBeforeClose"
|
|
273
|
+
>
|
|
274
|
+
<Input v-model="value" placeholder="Type 'confirm' to close" />
|
|
275
|
+
<p v-if="error" class="mt-2 text-sm text-destructive">{{ error }}</p>
|
|
276
|
+
</Modal>
|
|
277
|
+
</template>
|
|
278
|
+
|
|
279
|
+
<script setup lang="ts">
|
|
280
|
+
import type { ModalAction } from '#components'
|
|
281
|
+
|
|
282
|
+
const visible = ref(false)
|
|
283
|
+
const value = ref('')
|
|
284
|
+
const error = ref('')
|
|
285
|
+
|
|
286
|
+
function onBeforeClose (action: ModalAction) {
|
|
287
|
+
if (action === 'cancel') return
|
|
288
|
+
if (value.value !== 'confirm') {
|
|
289
|
+
error.value = "Value must be 'confirm' to close."
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
error.value = ''
|
|
293
|
+
return new Promise(resolve => setTimeout(resolve, 1000))
|
|
294
|
+
}
|
|
295
|
+
</script>
|
|
296
|
+
`.trim(),
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
render: () => ({
|
|
301
|
+
components: { Modal, Button, Input },
|
|
302
|
+
setup () {
|
|
303
|
+
const visible = ref(false)
|
|
304
|
+
const value = ref('')
|
|
305
|
+
const error = ref('')
|
|
306
|
+
function onBeforeClose (action: ModalAction) {
|
|
307
|
+
if (action === 'cancel') return
|
|
308
|
+
if (value.value !== 'confirm') {
|
|
309
|
+
error.value = 'Value must be "confirm" to close.'
|
|
310
|
+
return false
|
|
311
|
+
}
|
|
312
|
+
error.value = ''
|
|
313
|
+
return new Promise<void>(resolve => setTimeout(resolve, 1000))
|
|
314
|
+
}
|
|
315
|
+
return { visible, value, error, onBeforeClose }
|
|
316
|
+
},
|
|
317
|
+
template: `
|
|
318
|
+
<div>
|
|
319
|
+
<Button @click="visible = true">Open Modal</Button>
|
|
320
|
+
<Modal
|
|
321
|
+
v-model:visible="visible"
|
|
322
|
+
title="Type to Continue"
|
|
323
|
+
description="beforeClose intercepts the confirm action; cancel/X/ESC close normally."
|
|
324
|
+
showCancel
|
|
325
|
+
confirmText="Submit"
|
|
326
|
+
:beforeClose="onBeforeClose"
|
|
327
|
+
>
|
|
328
|
+
<Input v-model="value" placeholder="Type 'confirm' to close" />
|
|
329
|
+
<p v-if="error" class="mt-2 text-sm text-destructive">{{ error }}</p>
|
|
330
|
+
</Modal>
|
|
331
|
+
</div>
|
|
332
|
+
`,
|
|
333
|
+
}),
|
|
334
|
+
}
|
|
335
|
+
|
|
258
336
|
export const EventHandling: Story = {
|
|
259
337
|
parameters: {
|
|
260
338
|
...noControls,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
DialogTitle,
|
|
10
10
|
DialogTrigger,
|
|
11
11
|
} from '../../shadcn/dialog'
|
|
12
|
-
import type { ModalProps } from './types'
|
|
12
|
+
import type { ModalAction, ModalProps } from './types'
|
|
13
13
|
|
|
14
14
|
defineOptions({ inheritAttrs: false })
|
|
15
15
|
|
|
@@ -25,6 +25,7 @@ const props = withDefaults(defineProps<ModalProps>(), {
|
|
|
25
25
|
content: undefined,
|
|
26
26
|
confirmVariant: 'default',
|
|
27
27
|
cancelVariant: 'outline',
|
|
28
|
+
beforeClose: undefined,
|
|
28
29
|
type: undefined,
|
|
29
30
|
class: undefined,
|
|
30
31
|
})
|
|
@@ -48,6 +49,8 @@ const resolvedCancelText = computed(
|
|
|
48
49
|
)
|
|
49
50
|
|
|
50
51
|
const dialogOpen = ref(props.visible ?? false)
|
|
52
|
+
const internalLoading = ref(false)
|
|
53
|
+
const isLoading = computed(() => internalLoading.value || props.loading)
|
|
51
54
|
|
|
52
55
|
watch(() => props.visible, value => {
|
|
53
56
|
if (value !== undefined) dialogOpen.value = value
|
|
@@ -60,18 +63,37 @@ watch(dialogOpen, value => {
|
|
|
60
63
|
})
|
|
61
64
|
|
|
62
65
|
function onOpenUpdate (value: boolean) {
|
|
63
|
-
if (!value &&
|
|
66
|
+
if (!value && isLoading.value) return
|
|
64
67
|
if (value) dialogOpen.value = true
|
|
65
|
-
else
|
|
68
|
+
else handleClose('cancel')
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
function onConfirm () {
|
|
69
72
|
emit('confirm')
|
|
70
|
-
|
|
73
|
+
handleClose('confirm')
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
function onCancel () {
|
|
74
77
|
emit('cancel')
|
|
78
|
+
handleClose('cancel')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleClose (action: ModalAction) {
|
|
82
|
+
if (!props.beforeClose) {
|
|
83
|
+
dialogOpen.value = false
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
const result = props.beforeClose(action)
|
|
87
|
+
if (result === false) return
|
|
88
|
+
if (result instanceof Promise) {
|
|
89
|
+
internalLoading.value = true
|
|
90
|
+
result.then(() => {
|
|
91
|
+
dialogOpen.value = false
|
|
92
|
+
}).finally(() => {
|
|
93
|
+
internalLoading.value = false
|
|
94
|
+
})
|
|
95
|
+
return
|
|
96
|
+
}
|
|
75
97
|
dialogOpen.value = false
|
|
76
98
|
}
|
|
77
99
|
|
|
@@ -143,8 +165,8 @@ const contentClass = computed(() =>
|
|
|
143
165
|
<ModalContent
|
|
144
166
|
:type="type"
|
|
145
167
|
:content="content"
|
|
146
|
-
:inert="
|
|
147
|
-
:class="[
|
|
168
|
+
:inert="isLoading || disabled || undefined"
|
|
169
|
+
:class="[ isLoading || disabled ? 'opacity-50' : undefined ]"
|
|
148
170
|
class="p-1"
|
|
149
171
|
>
|
|
150
172
|
<slot />
|
|
@@ -171,7 +193,7 @@ const contentClass = computed(() =>
|
|
|
171
193
|
v-if="showCancel"
|
|
172
194
|
class="min-w-32"
|
|
173
195
|
:variant="cancelVariant"
|
|
174
|
-
:disabled="
|
|
196
|
+
:disabled="isLoading"
|
|
175
197
|
@click="onCancel"
|
|
176
198
|
>
|
|
177
199
|
{{ resolvedCancelText }}
|
|
@@ -179,7 +201,7 @@ const contentClass = computed(() =>
|
|
|
179
201
|
<Button
|
|
180
202
|
:class="showCancel ? 'min-w-32' : 'min-w-48'"
|
|
181
203
|
:variant="confirmVariant"
|
|
182
|
-
:loading="
|
|
204
|
+
:loading="isLoading"
|
|
183
205
|
:disabled="disabled || confirmDisabled"
|
|
184
206
|
@click="onConfirm"
|
|
185
207
|
>
|
|
@@ -191,7 +213,7 @@ const contentClass = computed(() =>
|
|
|
191
213
|
|
|
192
214
|
<DialogClose
|
|
193
215
|
v-if="showClose"
|
|
194
|
-
:disabled="
|
|
216
|
+
:disabled="isLoading"
|
|
195
217
|
class="
|
|
196
218
|
top-3 right-3 size-8 text-muted-foreground ring-offset-background
|
|
197
219
|
hover:bg-accent/50 hover:text-foreground
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ButtonVariants } from '../../shadcn/button'
|
|
2
2
|
import type { ModalContentProps } from '../ModalContent/types'
|
|
3
3
|
|
|
4
|
+
export type ModalAction = 'confirm' | 'cancel'
|
|
5
|
+
|
|
6
|
+
export type ModalBeforeClose = (action: ModalAction) => boolean | undefined | Promise<unknown>
|
|
7
|
+
|
|
4
8
|
export interface ModalProps {
|
|
5
9
|
visible?: boolean
|
|
6
10
|
loading?: boolean
|
|
@@ -20,6 +24,7 @@ export interface ModalProps {
|
|
|
20
24
|
cancelText?: string
|
|
21
25
|
confirmVariant?: ButtonVariants['variant']
|
|
22
26
|
cancelVariant?: ButtonVariants['variant']
|
|
27
|
+
beforeClose?: ModalBeforeClose
|
|
23
28
|
type?: ModalContentProps['type']
|
|
24
29
|
class?: ClassValue
|
|
25
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polymarbot/nuxt-layer-shadcn-ui",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
4
4
|
"description": "Nuxt layer providing shadcn-vue based UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"vue-i18n": "^11",
|
|
43
43
|
"vue-router": "^4 || ^5"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "802c526041ac9f00b5c54357d6a36fe4e549929c"
|
|
46
46
|
}
|