@innertia-solutions/ui 0.1.4 → 0.1.6
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/components/App/Button.vue +59 -0
- package/components/App/Dropdown.vue +286 -0
- package/components/App/LinkButton.vue +23 -0
- package/components/App/PageLoadingSpinner.vue +118 -0
- package/components/App/Tag.vue +193 -0
- package/components/Forms/DatePicker.vue +255 -0
- package/components/Forms/Select.vue +82 -71
- package/components/Forms/SelectServer.vue +726 -0
- package/package.json +3 -2
- package/plugins/preline.client.ts +77 -0
|
@@ -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>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
size: { type: String, default: "sm", validator: (v) => ["xs","sm","md","lg"].includes(v) },
|
|
4
|
+
class: { type: String, default: "" },
|
|
5
|
+
iconClass: { type: String, default: "" },
|
|
6
|
+
textClass: { type: String, default: "" },
|
|
7
|
+
outline: { type: Boolean, default: false },
|
|
8
|
+
severity: { type: String, default: "secondary", validator: (v) => ["primary","secondary","success","danger","warning","info"].includes(v) },
|
|
9
|
+
icon: { type: [Object, Function], default: null },
|
|
10
|
+
iconPosition: { type: String, default: "left", validator: (v) => ["left","right"].includes(v) },
|
|
11
|
+
loading: { type: Boolean, default: false },
|
|
12
|
+
loadingText: { type: String, default: "Cargando..." },
|
|
13
|
+
disabled: { type: Boolean, default: false },
|
|
14
|
+
to: { type: String, required: true },
|
|
15
|
+
variant: { type: String, default: "default", validator: (v) => ["default","dropdown"].includes(v) },
|
|
16
|
+
text: { type: String, default: "" },
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const buttonProps = computed(() => ({ ...props, link: props.to, type: "link" }))
|
|
20
|
+
</script>
|
|
21
|
+
<template>
|
|
22
|
+
<AppButton v-bind="buttonProps"><slot /></AppButton>
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const isLoading = ref(false);
|
|
3
|
+
let hideTimer;
|
|
4
|
+
let cleanup = [];
|
|
5
|
+
|
|
6
|
+
const showLoader = () => {
|
|
7
|
+
clearTimeout(hideTimer);
|
|
8
|
+
isLoading.value = true;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const hideLoader = () => {
|
|
12
|
+
clearTimeout(hideTimer);
|
|
13
|
+
hideTimer = setTimeout(() => {
|
|
14
|
+
isLoading.value = false;
|
|
15
|
+
}, 120);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
onMounted(() => {
|
|
19
|
+
const nuxtApp = useNuxtApp();
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
|
|
22
|
+
cleanup = [
|
|
23
|
+
nuxtApp.hooks.hook("page:start", showLoader),
|
|
24
|
+
nuxtApp.hooks.hook("page:finish", hideLoader),
|
|
25
|
+
nuxtApp.hooks.hook("app:error", hideLoader),
|
|
26
|
+
router.beforeEach((to, from) => {
|
|
27
|
+
if (to.fullPath !== from.fullPath) {
|
|
28
|
+
showLoader();
|
|
29
|
+
}
|
|
30
|
+
}),
|
|
31
|
+
router.afterEach(hideLoader),
|
|
32
|
+
router.onError(hideLoader),
|
|
33
|
+
].filter(Boolean);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
onBeforeUnmount(() => {
|
|
37
|
+
clearTimeout(hideTimer);
|
|
38
|
+
cleanup.forEach((unregister) => unregister());
|
|
39
|
+
});
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<ClientOnly>
|
|
44
|
+
<Transition name="page-loading-spinner">
|
|
45
|
+
<div
|
|
46
|
+
v-if="isLoading"
|
|
47
|
+
class="page-loading-spinner"
|
|
48
|
+
aria-label="Cargando pagina"
|
|
49
|
+
role="status"
|
|
50
|
+
>
|
|
51
|
+
<span class="page-loading-spinner__ring"></span>
|
|
52
|
+
</div>
|
|
53
|
+
</Transition>
|
|
54
|
+
</ClientOnly>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<style scoped>
|
|
58
|
+
.page-loading-spinner {
|
|
59
|
+
position: fixed;
|
|
60
|
+
top: 0.625rem;
|
|
61
|
+
right: 0.5rem;
|
|
62
|
+
z-index: 2147483647;
|
|
63
|
+
display: flex;
|
|
64
|
+
width: 2.5rem;
|
|
65
|
+
height: 2.5rem;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
border: 1px solid rgb(226 232 240 / 0.9);
|
|
69
|
+
border-radius: 9999px;
|
|
70
|
+
background: rgb(255 255 255 / 0.88);
|
|
71
|
+
box-shadow: 0 10px 30px rgb(15 23 42 / 0.16);
|
|
72
|
+
backdrop-filter: blur(10px);
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.page-loading-spinner__ring {
|
|
77
|
+
width: 1.25rem;
|
|
78
|
+
height: 1.25rem;
|
|
79
|
+
border: 2px solid rgb(148 163 184 / 0.38);
|
|
80
|
+
border-top-color: rgb(20 184 166);
|
|
81
|
+
border-radius: 9999px;
|
|
82
|
+
animation: page-loading-spinner-rotate 0.75s linear infinite;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.page-loading-spinner-enter-active,
|
|
86
|
+
.page-loading-spinner-leave-active {
|
|
87
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.page-loading-spinner-enter-from,
|
|
91
|
+
.page-loading-spinner-leave-to {
|
|
92
|
+
opacity: 0;
|
|
93
|
+
transform: scale(0.92);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@media (min-width: 640px) {
|
|
97
|
+
.page-loading-spinner {
|
|
98
|
+
right: 1.25rem;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
:global(.dark) .page-loading-spinner {
|
|
103
|
+
border-color: rgb(51 65 85 / 0.85);
|
|
104
|
+
background: rgb(15 23 42 / 0.78);
|
|
105
|
+
box-shadow: 0 10px 30px rgb(0 0 0 / 0.3);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
:global(.dark) .page-loading-spinner__ring {
|
|
109
|
+
border-color: rgb(100 116 139 / 0.42);
|
|
110
|
+
border-top-color: rgb(45 212 191);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@keyframes page-loading-spinner-rotate {
|
|
114
|
+
to {
|
|
115
|
+
transform: rotate(360deg);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
text: {
|
|
4
|
+
type: String,
|
|
5
|
+
required: true,
|
|
6
|
+
},
|
|
7
|
+
size: {
|
|
8
|
+
type: String,
|
|
9
|
+
required: false,
|
|
10
|
+
default: "sm",
|
|
11
|
+
validator: (value) => ["xs", "sm", "md", "lg"].includes(value),
|
|
12
|
+
},
|
|
13
|
+
severity: {
|
|
14
|
+
type: String,
|
|
15
|
+
required: false,
|
|
16
|
+
default: "secondary",
|
|
17
|
+
validator: (value) =>
|
|
18
|
+
["primary", "secondary", "success", "danger", "warning", "info"].includes(
|
|
19
|
+
value
|
|
20
|
+
),
|
|
21
|
+
},
|
|
22
|
+
outlined: {
|
|
23
|
+
type: Boolean,
|
|
24
|
+
required: false,
|
|
25
|
+
default: false,
|
|
26
|
+
},
|
|
27
|
+
icon: {
|
|
28
|
+
type: Object,
|
|
29
|
+
required: false,
|
|
30
|
+
default: null,
|
|
31
|
+
},
|
|
32
|
+
iconPosition: {
|
|
33
|
+
type: String,
|
|
34
|
+
required: false,
|
|
35
|
+
default: "left",
|
|
36
|
+
validator: (value) => ["left", "right"].includes(value),
|
|
37
|
+
},
|
|
38
|
+
class: {
|
|
39
|
+
type: String,
|
|
40
|
+
required: false,
|
|
41
|
+
default: "",
|
|
42
|
+
},
|
|
43
|
+
iconClass: {
|
|
44
|
+
type: String,
|
|
45
|
+
required: false,
|
|
46
|
+
default: "",
|
|
47
|
+
},
|
|
48
|
+
textClass: {
|
|
49
|
+
type: String,
|
|
50
|
+
required: false,
|
|
51
|
+
default: "",
|
|
52
|
+
},
|
|
53
|
+
tooltip: {
|
|
54
|
+
type: String,
|
|
55
|
+
required: false,
|
|
56
|
+
default: "",
|
|
57
|
+
},
|
|
58
|
+
tooltipPosition: {
|
|
59
|
+
type: String,
|
|
60
|
+
required: false,
|
|
61
|
+
default: "top",
|
|
62
|
+
validator: (value) =>
|
|
63
|
+
["top", "bottom", "left", "right", "auto"].includes(value),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Computed classes for sizes
|
|
68
|
+
const sizeClasses = computed(() => {
|
|
69
|
+
const sizes = {
|
|
70
|
+
xs: "px-1.5 py-0.5 text-xs",
|
|
71
|
+
sm: "px-2 py-1.5 text-xs",
|
|
72
|
+
md: "px-2.5 py-1.5 text-sm",
|
|
73
|
+
lg: "px-3 py-2 text-sm",
|
|
74
|
+
};
|
|
75
|
+
return sizes[props.size] || sizes.sm;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Computed classes for icon sizes
|
|
79
|
+
const iconSizeClasses = computed(() => {
|
|
80
|
+
const sizes = {
|
|
81
|
+
xs: "size-2.5",
|
|
82
|
+
sm: "size-3",
|
|
83
|
+
md: "size-3.5",
|
|
84
|
+
lg: "size-4",
|
|
85
|
+
};
|
|
86
|
+
return sizes[props.size] || sizes.sm;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Computed classes for severity colors
|
|
90
|
+
const severityClasses = computed(() => {
|
|
91
|
+
const base = "font-light rounded-md";
|
|
92
|
+
|
|
93
|
+
if (props.outlined) {
|
|
94
|
+
const variants = {
|
|
95
|
+
primary:
|
|
96
|
+
"border border-blue-600 text-blue-600 bg-blue-50 dark:border-blue-500 dark:text-blue-500 dark:bg-blue-900/20",
|
|
97
|
+
secondary:
|
|
98
|
+
"border border-gray-500 text-gray-700 bg-gray-50 dark:border-gray-400 dark:text-gray-300 dark:bg-gray-800",
|
|
99
|
+
success:
|
|
100
|
+
"border border-green-600 text-green-600 bg-green-50 dark:border-green-500 dark:text-green-500 dark:bg-green-900/20",
|
|
101
|
+
danger:
|
|
102
|
+
"border border-red-600 text-red-600 bg-red-50 dark:border-red-500 dark:text-red-500 dark:bg-red-900/20",
|
|
103
|
+
warning:
|
|
104
|
+
"border border-yellow-500 text-yellow-600 bg-yellow-40 dark:border-yellow-500 dark:text-yellow-500 dark:bg-yellow-900/20",
|
|
105
|
+
info: "border border-cyan-600 text-cyan-600 bg-cyan-50 dark:border-cyan-500 dark:text-cyan-500 dark:bg-cyan-900/20",
|
|
106
|
+
};
|
|
107
|
+
return `${base} ${variants[props.severity] || variants.secondary}`;
|
|
108
|
+
} else {
|
|
109
|
+
const variants = {
|
|
110
|
+
primary: "bg-blue-600 text-white dark:bg-blue-600",
|
|
111
|
+
secondary:
|
|
112
|
+
"bg-gray-100 text-gray-800 dark:bg-neutral-700 dark:text-neutral-200",
|
|
113
|
+
success: "bg-green-600 text-white dark:bg-green-600",
|
|
114
|
+
danger: "bg-red-600 text-white dark:bg-red-600",
|
|
115
|
+
warning: "bg-yellow-600 text-white dark:bg-yellow-600",
|
|
116
|
+
info: "bg-cyan-600 text-white dark:bg-cyan-600",
|
|
117
|
+
};
|
|
118
|
+
return `${base} ${variants[props.severity] || variants.secondary}`;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Tooltip attributes
|
|
123
|
+
const tooltipClasses = computed(() => {
|
|
124
|
+
if (!props.tooltip) return "";
|
|
125
|
+
|
|
126
|
+
const placement = `[--placement:${props.tooltipPosition}]`;
|
|
127
|
+
return `hs-tooltip ${placement} inline-block`;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const tagClasses = computed(() => {
|
|
131
|
+
const baseClasses = `${sizeClasses.value} ${severityClasses.value} inline-flex items-center gap-x-1 ${props.class}`;
|
|
132
|
+
|
|
133
|
+
if (props.tooltip) {
|
|
134
|
+
return `${baseClasses} hs-tooltip-toggle`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return baseClasses;
|
|
138
|
+
});
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<template>
|
|
142
|
+
<div v-if="tooltip" :class="tooltipClasses">
|
|
143
|
+
<span :class="tagClasses">
|
|
144
|
+
<!-- Icon Left -->
|
|
145
|
+
<component
|
|
146
|
+
v-if="icon && iconPosition === 'left'"
|
|
147
|
+
:is="icon"
|
|
148
|
+
:class="`${iconSizeClasses} ${iconClass}`"
|
|
149
|
+
/>
|
|
150
|
+
|
|
151
|
+
<!-- Tag Text -->
|
|
152
|
+
<slot name="text">
|
|
153
|
+
<span :class="textClass">{{ text }}</span>
|
|
154
|
+
</slot>
|
|
155
|
+
|
|
156
|
+
<!-- Icon Right -->
|
|
157
|
+
<component
|
|
158
|
+
v-if="icon && iconPosition === 'right'"
|
|
159
|
+
:is="icon"
|
|
160
|
+
:class="`${iconSizeClasses} ${iconClass}`"
|
|
161
|
+
/>
|
|
162
|
+
|
|
163
|
+
<!-- Tooltip Content -->
|
|
164
|
+
<span
|
|
165
|
+
class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-gray-900 text-xs font-medium text-white rounded-md shadow-2xs dark:bg-neutral-700"
|
|
166
|
+
role="tooltip"
|
|
167
|
+
>
|
|
168
|
+
{{ tooltip }}
|
|
169
|
+
</span>
|
|
170
|
+
</span>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<span v-else :class="tagClasses">
|
|
174
|
+
<!-- Icon Left -->
|
|
175
|
+
<component
|
|
176
|
+
v-if="icon && iconPosition === 'left'"
|
|
177
|
+
:is="icon"
|
|
178
|
+
:class="`${iconSizeClasses} ${iconClass}`"
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
<!-- Tag Text -->
|
|
182
|
+
<slot name="text">
|
|
183
|
+
<span :class="textClass">{{ text }}</span>
|
|
184
|
+
</slot>
|
|
185
|
+
|
|
186
|
+
<!-- Icon Right -->
|
|
187
|
+
<component
|
|
188
|
+
v-if="icon && iconPosition === 'right'"
|
|
189
|
+
:is="icon"
|
|
190
|
+
:class="`${iconSizeClasses} ${iconClass}`"
|
|
191
|
+
/>
|
|
192
|
+
</span>
|
|
193
|
+
</template>
|