@innertia-solutions/nuxt-theme-spark 0.1.11

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.
Files changed (34) hide show
  1. package/components/Admin/Base.vue +73 -0
  2. package/components/Admin/Header.vue +32 -0
  3. package/components/Admin/Page.vue +5 -0
  4. package/components/Admin/PageHeader.vue +18 -0
  5. package/components/App/Button.vue +59 -0
  6. package/components/App/Dropdown.vue +286 -0
  7. package/components/App/EmptyState.vue +433 -0
  8. package/components/App/LoadingState.vue +40 -0
  9. package/components/App/PageLoadingSpinner.vue +118 -0
  10. package/components/App/SwitchColorTheme.vue +55 -0
  11. package/components/App/Tag.vue +193 -0
  12. package/components/Forms/DatePicker.vue +255 -0
  13. package/components/Forms/Input.vue +72 -0
  14. package/components/Forms/Select.vue +100 -0
  15. package/components/Forms/SelectServer.vue +726 -0
  16. package/components/Layout/Admin.vue +32 -0
  17. package/components/Layout/Auth.vue +29 -0
  18. package/components/Modal/DeleteConfirm.vue +48 -0
  19. package/components/Modal.vue +103 -0
  20. package/components/Nav/Tabs.vue +39 -0
  21. package/components/Table/DownloadDropdown.vue +111 -0
  22. package/components/Table/FilterDropdown.vue +226 -0
  23. package/components/Toast/Alert.vue +113 -0
  24. package/components/Toast/Container.vue +34 -0
  25. package/components/Toast/Notification.vue +45 -0
  26. package/components/Toast/Process.vue +88 -0
  27. package/nuxt.config.ts +15 -0
  28. package/package.json +45 -0
  29. package/plugins/preline.client.ts +68 -0
  30. package/shared/composables/useForm.js +119 -0
  31. package/shared/composables/useTable.ts +84 -0
  32. package/shared/composables/useToast.js +69 -0
  33. package/shared/stores/toast.js +129 -0
  34. package/spark.css +207 -0
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ const isOpen = ref(false)
3
+ const open = () => { isOpen.value = true }
4
+ const close = () => { isOpen.value = false }
5
+
6
+ provide('vantage:sidebar', { isOpen, open, close })
7
+ </script>
8
+
9
+ <template>
10
+ <div class="bg-slate-50 dark:bg-slate-900 min-h-screen">
11
+
12
+ <!-- Backdrop mobile -->
13
+ <Transition enter-from-class="opacity-0" enter-active-class="transition-opacity duration-300"
14
+ leave-to-class="opacity-0" leave-active-class="transition-opacity duration-300">
15
+ <div v-if="isOpen" class="lg:hidden fixed inset-0 z-50 bg-black/40 backdrop-blur-sm" @click="close" />
16
+ </Transition>
17
+
18
+ <!-- Sidebar -->
19
+ <aside
20
+ tabindex="-1"
21
+ aria-label="Sidebar"
22
+ :class="[
23
+ 'fixed inset-y-0 start-0 z-60 w-65 h-full',
24
+ 'bg-white dark:bg-slate-800 border-e border-slate-200 dark:border-slate-700',
25
+ 'transition-transform duration-300',
26
+ 'lg:translate-x-0',
27
+ isOpen ? 'translate-x-0' : 'max-lg:-translate-x-full',
28
+ ]"
29
+ >
30
+ <div class="flex flex-col h-full pt-3 lg:pt-6">
31
+
32
+ <!-- Logo + close mobile -->
33
+ <header class="h-11.5 ps-2 pe-2 lg:ps-5 flex items-center gap-x-1 shrink-0">
34
+ <slot name="logo" />
35
+ <div class="lg:hidden ms-auto">
36
+ <button type="button"
37
+ class="w-6 h-7 inline-flex justify-center items-center rounded-md border border-slate-200 bg-white text-slate-500 hover:bg-slate-50 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-700"
38
+ @click="close">
39
+ <svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
40
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
41
+ <polyline points="7 8 3 12 7 16" />
42
+ <line x1="21" x2="11" y1="12" y2="12" />
43
+ <line x1="21" x2="11" y1="6" y2="6" />
44
+ <line x1="21" x2="11" y1="18" y2="18" />
45
+ </svg>
46
+ </button>
47
+ </div>
48
+ </header>
49
+
50
+ <!-- Menu scrollable -->
51
+ <div class="flex-1 min-h-0 mt-1.5 overflow-y-auto
52
+ [&::-webkit-scrollbar]:w-2
53
+ [&::-webkit-scrollbar-thumb]:rounded-full
54
+ [&::-webkit-scrollbar-track]:bg-slate-100
55
+ [&::-webkit-scrollbar-thumb]:bg-slate-300
56
+ dark:[&::-webkit-scrollbar-track]:bg-slate-700
57
+ dark:[&::-webkit-scrollbar-thumb]:bg-slate-500">
58
+ <slot name="menu" />
59
+ </div>
60
+
61
+ <!-- User footer -->
62
+ <div class="shrink-0 border-t border-slate-200 dark:border-slate-700 p-5">
63
+ <slot name="user-footer" />
64
+ </div>
65
+
66
+ </div>
67
+ </aside>
68
+
69
+ <!-- Main content -->
70
+ <slot />
71
+
72
+ </div>
73
+ </template>
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ title?: string
4
+ }>()
5
+
6
+ const sidebar = inject('vantage:sidebar', null) as any
7
+ </script>
8
+
9
+ <template>
10
+ <header class="sticky top-0 z-40 bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-4 lg:px-6 h-14 flex items-center gap-x-3">
11
+ <!-- Hamburger mobile -->
12
+ <button type="button"
13
+ class="lg:hidden size-8 flex items-center justify-center text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
14
+ @click="sidebar?.open()">
15
+ <svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
16
+ <line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" />
17
+ </svg>
18
+ </button>
19
+
20
+ <!-- Left slot -->
21
+ <div class="flex-1 flex items-center gap-x-3">
22
+ <slot name="left">
23
+ <span v-if="title" class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ title }}</span>
24
+ </slot>
25
+ </div>
26
+
27
+ <!-- Right slot -->
28
+ <div class="flex items-center gap-x-2">
29
+ <slot name="right" />
30
+ </div>
31
+ </header>
32
+ </template>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <div class="lg:ps-65">
3
+ <slot />
4
+ </div>
5
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ title: string
4
+ description?: string
5
+ }>()
6
+ </script>
7
+
8
+ <template>
9
+ <div class="flex items-center justify-between mb-6">
10
+ <div>
11
+ <h1 class="text-xl font-semibold text-slate-800 dark:text-slate-100">{{ title }}</h1>
12
+ <p v-if="description" class="text-sm text-slate-500 dark:text-slate-400 mt-0.5">{{ description }}</p>
13
+ </div>
14
+ <div class="flex items-center gap-x-2">
15
+ <slot name="actions" />
16
+ </div>
17
+ </div>
18
+ </template>
@@ -0,0 +1,59 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ text: { type: String, required: true },
4
+ size: { type: String, default: "sm", validator: (v) => ["xs","sm","md","lg"].includes(v) },
5
+ class: { type: String, default: "" },
6
+ iconClass: { type: String, default: "" },
7
+ textClass: { type: String, default: "" },
8
+ outline: { type: Boolean, default: false },
9
+ severity: { type: String, default: "secondary", validator: (v) => ["primary","secondary","success","danger","warning","info"].includes(v) },
10
+ icon: { type: [Object, Function], default: null },
11
+ iconPosition: { type: String, default: "left", validator: (v) => ["left","right"].includes(v) },
12
+ loading: { type: Boolean, default: false },
13
+ loadingText: { type: String, default: "Cargando..." },
14
+ disabled: { type: Boolean, default: false },
15
+ type: { type: String, default: "button", validator: (v) => ["button","link"].includes(v) },
16
+ link: { type: String, default: "" },
17
+ variant: { type: String, default: "default", validator: (v) => ["default","dropdown"].includes(v) },
18
+ })
19
+
20
+ const sizeClasses = computed(() => ({ xs:"py-0.5 px-2 text-xs font-light", sm:"py-1 px-2.5 text-sm font-light", md:"py-2.5 px-3 text-sm font-light", lg:"py-3 px-4 text-base font-light" }[props.size] || "py-1 px-2.5 text-sm font-light"))
21
+ const iconSizeClasses = computed(() => ({ xs:"size-2", sm:"size-3", md:"size-4", lg:"size-5" }[props.size] || "size-3"))
22
+
23
+ const severityClasses = computed(() => {
24
+ if (props.variant === "dropdown") {
25
+ const d = { primary:"text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20", secondary:"text-gray-800 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-gray-300", success:"text-emerald-600 hover:bg-emerald-50 dark:text-emerald-400 dark:hover:bg-emerald-900/20", danger:"text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20", warning:"text-yellow-600 hover:bg-yellow-50 dark:text-yellow-400 dark:hover:bg-yellow-900/20", info:"text-cyan-600 hover:bg-cyan-50 dark:text-cyan-400 dark:hover:bg-cyan-900/20" }
26
+ return d[props.severity] || d.secondary
27
+ }
28
+ const base = "rounded-lg border transition-colors"
29
+ const v = { primary:"border-blue-600 bg-blue-50 text-blue-700 hover:bg-blue-100 dark:border-blue-500 dark:bg-blue-900/20 dark:text-blue-300 dark:hover:bg-blue-900/35", secondary:"border-slate-300 bg-slate-50 text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700", success:"border-emerald-600 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-500 dark:bg-emerald-900/20 dark:text-emerald-300 dark:hover:bg-emerald-900/35", danger:"border-red-600 bg-red-50 text-red-700 hover:bg-red-100 dark:border-red-500 dark:bg-red-900/20 dark:text-red-300 dark:hover:bg-red-900/35", warning:"border-yellow-600 bg-yellow-50 text-yellow-700 hover:bg-yellow-100 dark:border-yellow-500 dark:bg-yellow-900/20 dark:text-yellow-300 dark:hover:bg-yellow-900/35", info:"border-cyan-600 bg-cyan-50 text-cyan-700 hover:bg-cyan-100 dark:border-cyan-500 dark:bg-cyan-900/20 dark:text-cyan-300 dark:hover:bg-cyan-900/35" }
30
+ return `${base} ${v[props.severity] || v.primary}`
31
+ })
32
+
33
+ const buttonClasses = computed(() => {
34
+ if (props.variant === "dropdown") {
35
+ const dis = props.type === "button" ? "disabled:opacity-50 disabled:pointer-events-none" : isDisabled.value ? "opacity-50 pointer-events-none" : ""
36
+ return `w-full flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm transition-colors text-left ${severityClasses.value} ${dis} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800 ${props.class}`
37
+ }
38
+ const dis = props.type === "button" ? "disabled:opacity-50 disabled:pointer-events-none" : isDisabled.value ? "opacity-50 pointer-events-none" : ""
39
+ const cursor = props.type === "link" ? "cursor-pointer" : ""
40
+ return `${sizeClasses.value} ${severityClasses.value} ${dis} focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800 ${cursor} inline-flex justify-center items-center gap-x-2 whitespace-nowrap ${props.class}`
41
+ })
42
+
43
+ const displayText = computed(() => props.loading ? props.loadingText : props.text)
44
+ const isDisabled = computed(() => props.disabled || props.loading)
45
+ </script>
46
+ <template>
47
+ <NuxtLink v-if="type === 'link'" :to="link" :class="buttonClasses">
48
+ <div v-if="loading" :class="`animate-spin border-[2.5px] border-t-transparent rounded-full ${iconSizeClasses}`" :style="`border-color: currentColor; border-top-color: transparent`" role="status" />
49
+ <component v-else-if="icon && iconPosition === 'left'" :is="icon" :class="variant === 'dropdown' ? 'size-4 shrink-0' : `${iconSizeClasses} ${iconClass}`" />
50
+ <slot><span :class="textClass">{{ displayText }}</span></slot>
51
+ <component v-if="!loading && icon && iconPosition === 'right'" :is="icon" :class="variant === 'dropdown' ? 'size-4 shrink-0' : `${iconSizeClasses} ${iconClass}`" />
52
+ </NuxtLink>
53
+ <button v-else :disabled="isDisabled" :class="buttonClasses" type="button">
54
+ <div v-if="loading" :class="`animate-spin border-[2.5px] border-t-transparent rounded-full ${iconSizeClasses}`" :style="`border-color: currentColor; border-top-color: transparent`" role="status" />
55
+ <component v-else-if="icon && iconPosition === 'left'" :is="icon" :class="variant === 'dropdown' ? 'size-4 shrink-0' : `${iconSizeClasses} ${iconClass}`" />
56
+ <slot><span :class="textClass">{{ displayText }}</span></slot>
57
+ <component v-if="!loading && icon && iconPosition === 'right'" :is="icon" :class="variant === 'dropdown' ? 'size-4 shrink-0' : `${iconSizeClasses} ${iconClass}`" />
58
+ </button>
59
+ </template>
@@ -0,0 +1,286 @@
1
+ <script setup>
2
+ // Props
3
+ const props = defineProps({
4
+ triggerText: {
5
+ type: String,
6
+ default: "Dropdown",
7
+ },
8
+ triggerLabel: {
9
+ type: String,
10
+ default: "Dropdown menu",
11
+ },
12
+ triggerClass: {
13
+ type: String,
14
+ default: "",
15
+ },
16
+ triggerSize: {
17
+ type: String,
18
+ default: "sm",
19
+ validator: (value) => ["xs", "sm", "md", "lg"].includes(value),
20
+ },
21
+ triggerSeverity: {
22
+ type: String,
23
+ default: "secondary",
24
+ validator: (value) =>
25
+ ["primary", "secondary", "success", "danger", "warning", "info"].includes(
26
+ value
27
+ ),
28
+ },
29
+ triggerOutline: {
30
+ type: Boolean,
31
+ default: true,
32
+ },
33
+ wrapperClass: {
34
+ type: String,
35
+ default: "",
36
+ },
37
+ menuClass: {
38
+ type: String,
39
+ default: "",
40
+ },
41
+ placement: {
42
+ type: String,
43
+ default: "bottom-right",
44
+ validator: (value) =>
45
+ [
46
+ "bottom",
47
+ "bottom-left",
48
+ "bottom-right",
49
+ "top",
50
+ "top-left",
51
+ "top-right",
52
+ "left",
53
+ "right",
54
+ ].includes(value),
55
+ },
56
+ items: {
57
+ type: Array,
58
+ default: () => [],
59
+ },
60
+ autoClose: {
61
+ type: String,
62
+ default: "true",
63
+ validator: (value) =>
64
+ ["true", "false", "inside", "outside"].includes(value),
65
+ },
66
+ });
67
+
68
+ // Emits
69
+ const emit = defineEmits(["item-click", "open", "close"]);
70
+
71
+ // Reactive data
72
+ const triggerId = ref(
73
+ `dropdown-trigger-${Math.random().toString(36).substr(2, 9)}`
74
+ );
75
+
76
+ // Computed
77
+ const triggerSizeClasses = computed(() => {
78
+ const sizes = {
79
+ xs: "py-0.5 px-2 text-xs font-light",
80
+ sm: "py-1 px-2.5 text-sm font-light",
81
+ md: "py-2.5 px-3 text-sm font-light",
82
+ lg: "py-3 px-4 text-base font-light",
83
+ };
84
+ return sizes[props.triggerSize] || sizes.sm;
85
+ });
86
+
87
+ const triggerSeverityClasses = computed(() => {
88
+ const base = "rounded-lg border transition-colors";
89
+ const variants = {
90
+ primary: props.triggerOutline
91
+ ? "border-blue-600 text-blue-600 hover:bg-blue-50 dark:border-blue-500 dark:text-blue-500 dark:hover:bg-blue-900/20"
92
+ : "border-transparent bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700",
93
+ secondary: props.triggerOutline
94
+ ? "border-gray-300 text-gray-700 hover:bg-gray-100 dark:border-gray-400 dark:text-gray-300 dark:hover:bg-gray-700/50"
95
+ : "border-transparent bg-gray-600 text-white hover:bg-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700",
96
+ success: props.triggerOutline
97
+ ? "border-green-600 text-green-600 hover:bg-green-50 dark:border-green-500 dark:text-green-500 dark:hover:bg-green-900/20"
98
+ : "border-transparent bg-green-600 text-white hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700",
99
+ danger: props.triggerOutline
100
+ ? "border-red-600 text-red-600 hover:bg-red-50 dark:border-red-500 dark:text-red-500 dark:hover:bg-red-900/20"
101
+ : "border-transparent bg-red-600 text-white hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700",
102
+ warning: props.triggerOutline
103
+ ? "border-yellow-600 text-yellow-600 hover:bg-yellow-50 dark:border-yellow-500 dark:text-yellow-500 dark:hover:bg-yellow-900/20"
104
+ : "border-transparent bg-yellow-600 text-white hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700",
105
+ info: props.triggerOutline
106
+ ? "border-cyan-600 text-cyan-600 hover:bg-cyan-50 dark:border-cyan-500 dark:text-cyan-500 dark:hover:bg-cyan-900/20"
107
+ : "border-transparent bg-cyan-600 text-white hover:bg-cyan-700 dark:bg-cyan-600 dark:hover:bg-cyan-700",
108
+ };
109
+
110
+ return `${base} ${variants[props.triggerSeverity] || variants.secondary}`;
111
+ });
112
+
113
+ const triggerButtonClasses = computed(() => {
114
+ const disabled = "disabled:opacity-50 disabled:pointer-events-none";
115
+ const focus =
116
+ "focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800";
117
+
118
+ return `hs-dropdown-toggle ${triggerSizeClasses.value} ${triggerSeverityClasses.value} ${disabled} ${focus} inline-flex justify-center items-center gap-x-2 ${props.triggerClass}`;
119
+ });
120
+
121
+ const placementClass = computed(() => {
122
+ const placements = {
123
+ bottom: "[--placement:bottom]",
124
+ "bottom-left": "[--placement:bottom-left]",
125
+ "bottom-right": "[--placement:bottom-right]",
126
+ top: "[--placement:top]",
127
+ "top-left": "[--placement:top-left]",
128
+ "top-right": "[--placement:top-right]",
129
+ left: "[--placement:left]",
130
+ right: "[--placement:right]",
131
+ };
132
+ return placements[props.placement] || "[--placement:bottom-left]";
133
+ });
134
+
135
+ const defaultItems = computed(() => {
136
+ return props.items.length
137
+ ? props.items
138
+ : [
139
+ {
140
+ label: "Editar",
141
+ type: "button",
142
+ severity: "primary",
143
+ action: () => {},
144
+ },
145
+ {
146
+ label: "Duplicar",
147
+ type: "button",
148
+ severity: "success",
149
+ action: () => {},
150
+ },
151
+ {
152
+ label: "Eliminar",
153
+ type: "button",
154
+ severity: "danger",
155
+ action: () => {},
156
+ },
157
+ { label: "Ver detalles", type: "link", href: "#" },
158
+ ];
159
+ });
160
+
161
+ // Methods
162
+ const getItemButtonClasses = (item) => {
163
+ const severity = item.severity || "default";
164
+
165
+ const severityClasses = {
166
+ default:
167
+ "text-gray-800 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300",
168
+ primary:
169
+ "text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20",
170
+ success:
171
+ "text-green-600 hover:bg-green-50 dark:text-green-400 dark:hover:bg-green-900/20",
172
+ danger:
173
+ "text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20",
174
+ warning:
175
+ "text-yellow-600 hover:bg-yellow-50 dark:text-yellow-400 dark:hover:bg-yellow-900/20",
176
+ info: "text-cyan-600 hover:bg-cyan-50 dark:text-cyan-400 dark:hover:bg-cyan-900/20",
177
+ };
178
+
179
+ return severityClasses[severity] || severityClasses.default;
180
+ };
181
+
182
+ const toggleDropdown = () => {
183
+ // Handled by Preline JS automatically
184
+ };
185
+
186
+ const closeDropdown = () => {
187
+ // Handled by Preline JS automatically
188
+ };
189
+
190
+ const handleItemClick = (item, event) => {
191
+ emit("item-click", { item, event });
192
+
193
+ if (item.action && typeof item.action === "function") {
194
+ item.action();
195
+ }
196
+
197
+ if (item.href === "#" || item.href === "") {
198
+ event.preventDefault();
199
+ }
200
+ };
201
+ </script>
202
+ <template>
203
+ <div class="hs-dropdown relative inline-flex" :class="wrapperClass">
204
+ <!-- Trigger button -->
205
+ <button
206
+ :id="triggerId"
207
+ type="button"
208
+ :class="triggerButtonClasses"
209
+ aria-haspopup="menu"
210
+ :aria-expanded="false"
211
+ :aria-label="triggerLabel"
212
+ >
213
+ <slot name="trigger" :toggle="toggleDropdown">
214
+ {{ triggerText }}
215
+ <svg
216
+ class="hs-dropdown-open:rotate-180 size-4 transition-transform duration-200"
217
+ xmlns="http://www.w3.org/2000/svg"
218
+ width="24"
219
+ height="24"
220
+ viewBox="0 0 24 24"
221
+ fill="none"
222
+ stroke="currentColor"
223
+ stroke-width="2"
224
+ stroke-linecap="round"
225
+ stroke-linejoin="round"
226
+ >
227
+ <path d="m6 9 6 6 6-6" />
228
+ </svg>
229
+ </slot>
230
+ </button>
231
+
232
+ <!-- Dropdown menu -->
233
+ <div
234
+ class="hs-dropdown-menu transition-[opacity,margin] duration hs-dropdown-open:opacity-100 opacity-0 hidden min-w-40 bg-white shadow-md rounded-lg p-1 space-y-0.5 mt-2 dark:bg-gray-800 dark:border dark:border-gray-700 dark:divide-gray-700 z-50 border border-gray-200"
235
+ :class="[menuClass, placementClass]"
236
+ role="menu"
237
+ :aria-orientation="'vertical'"
238
+ :aria-labelledby="triggerId"
239
+ >
240
+ <slot name="header" v-if="$slots.header"></slot>
241
+
242
+ <slot name="items" :close="closeDropdown">
243
+ <!-- Default items if no slot content provided -->
244
+ <template v-for="(item, index) in defaultItems" :key="index">
245
+ <!-- Button if type is button or has action -->
246
+ <button
247
+ v-if="item.type === 'button' || item.action"
248
+ type="button"
249
+ class="w-full flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm transition-colors text-left focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800"
250
+ :class="[
251
+ item.class || getItemButtonClasses(item),
252
+ { 'opacity-50 pointer-events-none': item.disabled },
253
+ ]"
254
+ :disabled="item.disabled"
255
+ @click="handleItemClick(item, $event)"
256
+ >
257
+ <component
258
+ :is="item.icon"
259
+ v-if="item.icon"
260
+ class="size-4 shrink-0"
261
+ />
262
+ {{ item.label }}
263
+ </button>
264
+
265
+ <!-- Link if type is link or has href -->
266
+ <a
267
+ v-else
268
+ class="flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800 text-gray-800 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300"
269
+ :class="item.class"
270
+ :href="item.href || '#'"
271
+ @click="handleItemClick(item, $event)"
272
+ >
273
+ <component
274
+ :is="item.icon"
275
+ v-if="item.icon"
276
+ class="size-4 shrink-0"
277
+ />
278
+ {{ item.label }}
279
+ </a>
280
+ </template>
281
+ </slot>
282
+
283
+ <slot name="footer" v-if="$slots.footer"></slot>
284
+ </div>
285
+ </div>
286
+ </template>