@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.
@@ -29,7 +29,7 @@ const props = withDefaults(defineProps<DatePickerProps>(), {
29
29
  })
30
30
 
31
31
  const emit = defineEmits<{
32
- 'update:modelValue': [value: Date | string | null]
32
+ 'update:modelValue': [value: Date | string | number | null]
33
33
  }>()
34
34
 
35
35
  const model = computed({
@@ -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
- /** v-model output format (e.g. 'yyyy-MM-dd', 'timestamp', 'iso') */
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 (!value) return undefined
57
- return value instanceof Date ? value : new Date(value)
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
- start?: Date | string | null
5
- end?: Date | string | null
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
- /** v-model output format (e.g. 'yyyy-MM-dd', 'timestamp', 'iso') */
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 && props.loading) return
65
+ if (!value && isLoading.value) return
63
66
  if (value) sheetOpen.value = true
64
- else onCancel()
67
+ else handleClose('cancel')
65
68
  }
66
69
 
67
70
  function onConfirm () {
68
71
  emit('confirm')
69
- sheetOpen.value = false
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="loading || disabled || undefined"
140
- :class="[ loading || disabled ? 'opacity-50' : undefined ]"
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="loading"
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="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="loading"
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 && props.loading) return
66
+ if (!value && isLoading.value) return
64
67
  if (value) dialogOpen.value = true
65
- else onCancel()
68
+ else handleClose('cancel')
66
69
  }
67
70
 
68
71
  function onConfirm () {
69
72
  emit('confirm')
70
- dialogOpen.value = false
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="loading || disabled || undefined"
147
- :class="[ loading || disabled ? 'opacity-50' : undefined ]"
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="loading"
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="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="loading"
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.6",
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": "c2aa82c36486efc10b9d5723fcd49bb764f473d9"
45
+ "gitHead": "802c526041ac9f00b5c54357d6a36fe4e549929c"
46
46
  }