@veristone/nuxt-v-app 0.2.0 → 0.2.1

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.
@@ -111,7 +111,6 @@ const handleDelete = async () => {
111
111
  </div>
112
112
  </template>
113
113
 
114
- <!-- Modal Content -->
115
114
  <template #body>
116
115
  <div class="p-4 space-y-4">
117
116
  <div class="flex items-start gap-4">
@@ -1,195 +1,92 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * VAModalBase - Custom Modal Component
4
- * Pure Vue/Tailwind implementation without UModal
5
- *
6
- * Features:
7
- * - Size variants: sm, md, lg, xl, full
8
- * - Backdrop with optional blur
9
- * - Accessible: focus trap, escape key, click outside to close
10
- * - Header/body/footer slots
11
- * - Smooth enter/leave transitions
3
+ * VAModalBase - Base Modal Component using Nuxt UI v4
4
+ * Simple wrapper around UModal with consistent V-App styling
12
5
  */
13
- import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
14
-
15
6
  const props = withDefaults(defineProps<{
16
7
  modelValue?: boolean
17
8
  title?: string
18
9
  size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
19
- closable?: boolean
20
- closeOnEscape?: boolean
21
- closeOnClickOutside?: boolean
22
- showBackdrop?: boolean
23
- backdropBlur?: boolean
10
+ triggerLabel?: string
11
+ triggerIcon?: string
12
+ triggerColor?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'
13
+ triggerVariant?: 'solid' | 'outline' | 'soft' | 'ghost' | 'link'
14
+ triggerSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
24
15
  }>(), {
25
- modelValue: false,
16
+ modelValue: undefined,
26
17
  title: '',
27
18
  size: 'md',
28
- closable: true,
29
- closeOnEscape: true,
30
- closeOnClickOutside: true,
31
- showBackdrop: true,
32
- backdropBlur: true
19
+ triggerColor: 'primary',
20
+ triggerVariant: 'solid',
21
+ triggerSize: 'sm'
33
22
  })
34
23
 
35
- const emit = defineEmits<{
36
- 'update:modelValue': [value: boolean]
37
- 'open': []
38
- 'close': []
39
- }>()
24
+ const emit = defineEmits(['update:modelValue', 'open', 'close'])
40
25
 
41
- // Modal ref for focus trap
42
- const modalRef = ref<HTMLElement | null>(null)
43
- const { activate, deactivate } = useFocusTrap(modalRef, {
44
- immediate: false,
45
- allowOutsideClick: true,
46
- escapeDeactivates: false // We handle escape ourselves
47
- })
26
+ const internalOpen = ref(false)
48
27
 
49
- // Computed open state
50
28
  const isOpen = computed({
51
- get: () => props.modelValue,
52
- set: (val) => emit('update:modelValue', val)
53
- })
54
-
55
- // Close modal
56
- const close = () => {
57
- if (!props.closable) return
58
- isOpen.value = false
59
- emit('close')
60
- }
61
-
62
- // Open modal
63
- const open = () => {
64
- isOpen.value = true
65
- emit('open')
66
- }
67
-
68
- // Handle escape key
69
- const handleKeydown = (e: KeyboardEvent) => {
70
- if (e.key === 'Escape' && props.closeOnEscape && props.closable) {
71
- close()
72
- }
73
- }
74
-
75
- // Handle click outside
76
- const handleBackdropClick = (e: MouseEvent) => {
77
- if (e.target === e.currentTarget && props.closeOnClickOutside && props.closable) {
78
- close()
29
+ get: () => props.modelValue ?? internalOpen.value,
30
+ set: (val) => {
31
+ emit('update:modelValue', val)
32
+ internalOpen.value = val
33
+ if (val) emit('open')
34
+ else emit('close')
79
35
  }
80
- }
81
-
82
- // Watch for open state changes
83
- watch(isOpen, (val) => {
84
- if (val) {
85
- document.body.style.overflow = 'hidden'
86
- nextTick(() => {
87
- activate()
88
- })
89
- } else {
90
- document.body.style.overflow = ''
91
- deactivate()
92
- }
93
- })
94
-
95
- // Cleanup on unmount
96
- onUnmounted(() => {
97
- document.body.style.overflow = ''
98
- deactivate()
99
36
  })
100
37
 
101
- // Size classes
102
- const sizeClasses = computed(() => {
38
+ // Size to width mapping
39
+ const widthClass = computed(() => {
103
40
  switch (props.size) {
104
- case 'sm': return 'max-w-sm'
105
- case 'md': return 'max-w-md'
106
- case 'lg': return 'max-w-lg'
107
- case 'xl': return 'max-w-2xl'
108
- case 'full': return 'max-w-[calc(100vw-2rem)] max-h-[calc(100vh-2rem)] h-full'
109
- default: return 'max-w-md'
41
+ case 'sm': return 'sm:max-w-sm'
42
+ case 'md': return 'sm:max-w-lg'
43
+ case 'lg': return 'sm:max-w-2xl'
44
+ case 'xl': return 'sm:max-w-4xl'
45
+ case 'full': return 'sm:max-w-full'
46
+ default: return 'sm:max-w-lg'
110
47
  }
111
48
  })
112
-
113
- // Expose methods
114
- defineExpose({ open, close })
115
49
  </script>
116
50
 
117
51
  <template>
118
- <Teleport to="body">
119
- <Transition
120
- enter-active-class="duration-200 ease-out"
121
- enter-from-class="opacity-0"
122
- enter-to-class="opacity-100"
123
- leave-active-class="duration-150 ease-in"
124
- leave-from-class="opacity-100"
125
- leave-to-class="opacity-0"
126
- >
127
- <div
128
- v-if="isOpen"
129
- class="fixed inset-0 z-50 flex items-center justify-center p-4"
130
- :class="[
131
- showBackdrop ? (backdropBlur ? 'bg-gray-900/60 backdrop-blur-sm' : 'bg-gray-900/60') : ''
132
- ]"
133
- @click="handleBackdropClick"
134
- @keydown="handleKeydown"
135
- >
136
- <Transition
137
- enter-active-class="duration-200 ease-out"
138
- enter-from-class="opacity-0 scale-95 translate-y-4"
139
- enter-to-class="opacity-100 scale-100 translate-y-0"
140
- leave-active-class="duration-150 ease-in"
141
- leave-from-class="opacity-100 scale-100 translate-y-0"
142
- leave-to-class="opacity-0 scale-95 translate-y-4"
143
- >
144
- <div
145
- v-if="isOpen"
146
- ref="modalRef"
147
- class="relative w-full bg-white dark:bg-gray-900 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-800 flex flex-col overflow-hidden"
148
- :class="[sizeClasses, size === 'full' ? '' : 'max-h-[calc(100vh-2rem)]']"
149
- role="dialog"
150
- aria-modal="true"
151
- :aria-labelledby="title ? 'va-modal-title' : undefined"
152
- >
153
- <!-- Header -->
154
- <div
155
- v-if="title || $slots.header || closable"
156
- class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800 shrink-0"
157
- >
158
- <slot name="header">
159
- <h2
160
- v-if="title"
161
- id="va-modal-title"
162
- class="text-lg font-semibold text-gray-900 dark:text-white"
163
- >
164
- {{ title }}
165
- </h2>
166
- </slot>
167
- <button
168
- v-if="closable"
169
- type="button"
170
- class="p-1.5 rounded-lg text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500"
171
- aria-label="Close modal"
172
- @click="close"
173
- >
174
- <UIcon name="i-lucide-x" class="w-5 h-5" />
175
- </button>
176
- </div>
52
+ <UModal v-model:open="isOpen" :ui="{ width: widthClass }">
53
+ <!-- Trigger Button (required for Nuxt UI v4) -->
54
+ <slot name="trigger">
55
+ <UButton
56
+ v-if="triggerLabel || triggerIcon"
57
+ :label="triggerLabel"
58
+ :icon="triggerIcon"
59
+ :color="triggerColor"
60
+ :variant="triggerVariant"
61
+ :size="triggerSize"
62
+ />
63
+ </slot>
177
64
 
178
- <!-- Body -->
179
- <div class="flex-1 overflow-y-auto px-6 py-4">
180
- <slot />
181
- </div>
65
+ <template #header>
66
+ <div class="flex items-center justify-between w-full">
67
+ <slot name="header">
68
+ <h3 v-if="title" class="text-base font-semibold text-gray-900 dark:text-white">
69
+ {{ title }}
70
+ </h3>
71
+ </slot>
72
+ <UButton
73
+ icon="i-lucide-x"
74
+ color="neutral"
75
+ variant="ghost"
76
+ size="sm"
77
+ @click="isOpen = false"
78
+ />
79
+ </div>
80
+ </template>
182
81
 
183
- <!-- Footer -->
184
- <div
185
- v-if="$slots.footer"
186
- class="px-6 py-4 border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800/50 shrink-0"
187
- >
188
- <slot name="footer" />
189
- </div>
190
- </div>
191
- </Transition>
82
+ <template #body>
83
+ <div class="p-4">
84
+ <slot />
192
85
  </div>
193
- </Transition>
194
- </Teleport>
86
+ </template>
87
+
88
+ <template v-if="$slots.footer" #footer>
89
+ <slot name="footer" />
90
+ </template>
91
+ </UModal>
195
92
  </template>
@@ -8,13 +8,22 @@ const props = withDefaults(defineProps<{
8
8
  endpoint?: string
9
9
  recordId?: string | number
10
10
  initialData?: any
11
- size?: string
11
+ size?: 'sm' | 'md' | 'lg' | 'xl'
12
12
  loading?: boolean
13
13
  error?: string
14
+ triggerLabel?: string
15
+ triggerIcon?: string
16
+ triggerColor?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'
17
+ triggerVariant?: 'solid' | 'outline' | 'soft' | 'ghost' | 'link'
18
+ triggerSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
14
19
  }>(), {
15
- modelValue: false,
20
+ modelValue: undefined,
16
21
  title: 'Edit Record',
17
- size: 'md'
22
+ size: 'md',
23
+ triggerIcon: 'i-lucide-edit',
24
+ triggerColor: 'primary',
25
+ triggerVariant: 'solid',
26
+ triggerSize: 'sm'
18
27
  })
19
28
 
20
29
  const emit = defineEmits(['update:modelValue', 'submit', 'success', 'error'])
@@ -25,7 +34,7 @@ const formData = ref({ ...(props.initialData || {}) })
25
34
  const isEdit = computed(() => !!props.recordId)
26
35
 
27
36
  const isOpen = computed({
28
- get: () => props.modelValue !== undefined ? props.modelValue : internalOpen.value,
37
+ get: () => props.modelValue ?? internalOpen.value,
29
38
  set: (val) => {
30
39
  emit('update:modelValue', val)
31
40
  internalOpen.value = val
@@ -65,18 +74,36 @@ const handleSubmit = async (data: any = formData.value) => {
65
74
  </script>
66
75
 
67
76
  <template>
68
- <UModal v-model="isOpen" :ui="{ width: size === 'lg' ? 'sm:max-w-4xl' : 'sm:max-w-xl' }">
69
- <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
70
- <template #header>
71
- <div class="flex items-center justify-between">
72
- <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
73
- {{ title }}
74
- </h3>
75
- <UButton color="gray" variant="ghost" icon="i-lucide-x" class="-my-1" @click="isOpen = false" />
76
- </div>
77
- </template>
77
+ <UModal v-model:open="isOpen" :ui="{ width: size === 'lg' ? 'sm:max-w-4xl' : 'sm:max-w-xl' }">
78
+ <!-- Trigger Button (required for Nuxt UI v4) -->
79
+ <slot name="trigger">
80
+ <UButton
81
+ v-if="triggerLabel || triggerIcon"
82
+ :label="triggerLabel"
83
+ :icon="triggerIcon"
84
+ :color="triggerColor"
85
+ :variant="triggerVariant"
86
+ :size="triggerSize"
87
+ />
88
+ </slot>
89
+
90
+ <template #header>
91
+ <div class="flex items-center justify-between w-full">
92
+ <h3 class="text-base font-semibold text-gray-900 dark:text-white">
93
+ {{ title }}
94
+ </h3>
95
+ <UButton
96
+ icon="i-lucide-x"
97
+ color="neutral"
98
+ variant="ghost"
99
+ size="sm"
100
+ @click="isOpen = false"
101
+ />
102
+ </div>
103
+ </template>
78
104
 
79
- <div class="p-6">
105
+ <template #body>
106
+ <div class="p-6 space-y-4">
80
107
  <slot
81
108
  :state="formData"
82
109
  :loading="loading || crudLoading"
@@ -86,20 +113,19 @@ const handleSubmit = async (data: any = formData.value) => {
86
113
  <!-- Default content if no slot -->
87
114
  <form @submit.prevent="handleSubmit()">
88
115
  <div class="space-y-4">
89
- <!-- If we knew fields we'd render them, but this is generic -->
90
116
  <p class="text-gray-500 italic">Form content goes here...</p>
91
117
  </div>
92
118
  </form>
93
119
  </slot>
94
120
 
95
- <div v-if="error || errorState" class="mt-4 p-3 rounded bg-red-50 text-red-600 text-sm">
121
+ <div v-if="error || errorState" class="p-3 rounded bg-red-50 text-red-600 text-sm">
96
122
  {{ error || errorState }}
97
123
  </div>
98
124
  </div>
125
+ </template>
99
126
 
100
- <template v-if="$slots.footer" #footer>
101
- <slot name="footer" :submit="handleSubmit" :loading="loading || crudLoading" />
102
- </template>
103
- </UCard>
127
+ <template v-if="$slots.footer" #footer>
128
+ <slot name="footer" :submit="handleSubmit" :loading="loading || crudLoading" />
129
+ </template>
104
130
  </UModal>
105
131
  </template>
@@ -50,7 +50,7 @@ const widthClass = computed(() => {
50
50
  case 'sm': return 'max-w-xs'
51
51
  case 'md': return 'max-w-md'
52
52
  case 'lg': return 'max-w-lg'
53
- case 'xl': return 'max-w-2xl' // Variation: XL is wider here
53
+ case 'xl': return 'max-w-2xl'
54
54
  case 'full': return 'max-w-full'
55
55
  default: return 'max-w-md'
56
56
  }
@@ -74,36 +74,34 @@ const widthClass = computed(() => {
74
74
 
75
75
  <!-- Slideover -->
76
76
  <USlideover
77
- v-model="isOpen"
77
+ v-model:open="isOpen"
78
78
  :side="side"
79
- :ui="{ width: widthClass, overlay: { background: 'bg-gray-900/50 backdrop-blur-sm' } }"
79
+ :ui="{ width: widthClass }"
80
80
  >
81
- <div class="flex flex-col h-full bg-white dark:bg-gray-900 shadow-xl">
82
- <!-- Header -->
83
- <div class="px-6 py-4 flex items-center justify-between border-b border-gray-100 dark:border-gray-800">
81
+ <template #header>
82
+ <div class="flex items-center justify-between w-full">
84
83
  <slot name="header">
85
84
  <h2 class="text-lg font-bold text-gray-900 dark:text-white">{{ title }}</h2>
86
85
  </slot>
87
- <UButton
88
- color="gray"
89
- variant="ghost"
90
- icon="i-lucide-x"
86
+ <UButton
87
+ icon="i-lucide-x"
88
+ color="neutral"
89
+ variant="ghost"
91
90
  size="sm"
92
- class="-mr-2"
93
- @click="close"
91
+ @click="close"
94
92
  />
95
93
  </div>
94
+ </template>
96
95
 
97
- <!-- Body -->
98
- <div class="flex-1 overflow-y-auto px-6 py-4">
96
+ <template #body>
97
+ <div class="flex-1 overflow-y-auto">
99
98
  <slot />
100
99
  </div>
100
+ </template>
101
101
 
102
- <!-- Footer -->
103
- <div v-if="$slots.footer" class="px-6 py-4 border-t border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800/50">
104
- <slot name="footer" />
105
- </div>
106
- </div>
102
+ <template v-if="$slots.footer" #footer>
103
+ <slot name="footer" />
104
+ </template>
107
105
  </USlideover>
108
106
  </div>
109
107
  </template>
@@ -1,36 +1,69 @@
1
1
  <script setup lang="ts">
2
- const props = defineProps<{
2
+ const props = withDefaults(defineProps<{
3
3
  title?: string
4
4
  modelValue?: boolean
5
- }>()
5
+ triggerLabel?: string
6
+ triggerIcon?: string
7
+ triggerColor?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'
8
+ triggerVariant?: 'solid' | 'outline' | 'soft' | 'ghost' | 'link'
9
+ triggerSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
10
+ }>(), {
11
+ modelValue: undefined,
12
+ triggerColor: 'primary',
13
+ triggerVariant: 'solid',
14
+ triggerSize: 'sm'
15
+ })
6
16
 
7
17
  const emit = defineEmits(['update:modelValue'])
8
18
 
19
+ const internalOpen = ref(false)
20
+
9
21
  const isOpen = computed({
10
- get: () => props.modelValue,
11
- set: (val) => emit('update:modelValue', val)
22
+ get: () => props.modelValue ?? internalOpen.value,
23
+ set: (val) => {
24
+ emit('update:modelValue', val)
25
+ internalOpen.value = val
26
+ }
12
27
  })
13
28
  </script>
14
29
 
15
30
  <template>
16
- <UModal v-model="isOpen">
17
- <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
18
- <template #header>
19
- <div class="flex items-center justify-between">
20
- <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
21
- {{ title }}
22
- </h3>
23
- <UButton color="gray" variant="ghost" icon="i-lucide-x" class="-my-1" @click="isOpen = false" />
24
- </div>
25
- </template>
31
+ <UModal v-model:open="isOpen">
32
+ <!-- Trigger Button (required for Nuxt UI v4) -->
33
+ <slot name="trigger">
34
+ <UButton
35
+ v-if="triggerLabel || triggerIcon"
36
+ :label="triggerLabel"
37
+ :icon="triggerIcon"
38
+ :color="triggerColor"
39
+ :variant="triggerVariant"
40
+ :size="triggerSize"
41
+ />
42
+ </slot>
43
+
44
+ <template #header>
45
+ <div class="flex items-center justify-between w-full">
46
+ <h3 class="text-base font-semibold text-gray-900 dark:text-white">
47
+ {{ title }}
48
+ </h3>
49
+ <UButton
50
+ icon="i-lucide-x"
51
+ color="neutral"
52
+ variant="ghost"
53
+ size="sm"
54
+ @click="isOpen = false"
55
+ />
56
+ </div>
57
+ </template>
26
58
 
59
+ <template #body>
27
60
  <div class="p-4">
28
61
  <slot />
29
62
  </div>
63
+ </template>
30
64
 
31
- <template v-if="$slots.footer" #footer>
32
- <slot name="footer" />
33
- </template>
34
- </UCard>
65
+ <template v-if="$slots.footer" #footer>
66
+ <slot name="footer" />
67
+ </template>
35
68
  </UModal>
36
69
  </template>
@@ -11,8 +11,8 @@ export function useVToast() {
11
11
  title,
12
12
  description,
13
13
  icon: 'i-lucide-check-circle',
14
- color: 'green',
15
- timeout
14
+ color: 'success',
15
+ duration: timeout
16
16
  })
17
17
  }
18
18
 
@@ -21,8 +21,8 @@ export function useVToast() {
21
21
  title,
22
22
  description,
23
23
  icon: 'i-lucide-info',
24
- color: 'blue',
25
- timeout
24
+ color: 'info',
25
+ duration: timeout
26
26
  })
27
27
  }
28
28
 
@@ -31,8 +31,8 @@ export function useVToast() {
31
31
  title,
32
32
  description,
33
33
  icon: 'i-lucide-alert-circle',
34
- color: 'red',
35
- timeout
34
+ color: 'error',
35
+ duration: timeout
36
36
  })
37
37
  }
38
38
 
@@ -41,8 +41,8 @@ export function useVToast() {
41
41
  title,
42
42
  description,
43
43
  icon: 'i-lucide-alert-triangle',
44
- color: 'amber',
45
- timeout
44
+ color: 'warning',
45
+ duration: timeout
46
46
  })
47
47
  }
48
48
 
@@ -50,7 +50,7 @@ export function useVToast() {
50
50
  err: any,
51
51
  friendlyName: string,
52
52
  action: string,
53
- successColor: string = 'green'
53
+ successColor: string = 'success'
54
54
  ) => {
55
55
  if (err.value) {
56
56
  error(
@@ -63,7 +63,7 @@ export function useVToast() {
63
63
  title: `${friendlyName} ${action} successful`,
64
64
  icon: 'i-lucide-check-circle',
65
65
  color: successColor as any,
66
- timeout
66
+ duration: timeout
67
67
  })
68
68
  return true
69
69
  }
@@ -106,7 +106,7 @@ const groups = computed(() => [
106
106
  </script>
107
107
 
108
108
  <template>
109
- <UDashboardGroup unit="rem">
109
+ <UDashboardGroup unit="rem" class="h-full overflow-hidden">
110
110
  <UDashboardSidebar
111
111
  resizable
112
112
  collapsible
@@ -173,7 +173,9 @@ const groups = computed(() => [
173
173
 
174
174
  <UDashboardSearch :groups="groups" />
175
175
 
176
- <slot />
176
+ <div class="flex-1 overflow-y-auto">
177
+ <slot />
178
+ </div>
177
179
  <VaLayoutNotificationsSlideover />
178
180
  </UDashboardGroup>
179
181
  </template>