@polymarbot/nuxt-layer-shadcn-ui 0.8.7 → 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.
@@ -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.7",
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": "1f8a4521c7e52de4063c33f47888be48cc99329b"
45
+ "gitHead": "802c526041ac9f00b5c54357d6a36fe4e549929c"
46
46
  }