@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.
- package/components/Admin/Base.vue +73 -0
- package/components/Admin/Header.vue +32 -0
- package/components/Admin/Page.vue +5 -0
- package/components/Admin/PageHeader.vue +18 -0
- package/components/App/Button.vue +59 -0
- package/components/App/Dropdown.vue +286 -0
- package/components/App/EmptyState.vue +433 -0
- package/components/App/LoadingState.vue +40 -0
- package/components/App/PageLoadingSpinner.vue +118 -0
- package/components/App/SwitchColorTheme.vue +55 -0
- package/components/App/Tag.vue +193 -0
- package/components/Forms/DatePicker.vue +255 -0
- package/components/Forms/Input.vue +72 -0
- package/components/Forms/Select.vue +100 -0
- package/components/Forms/SelectServer.vue +726 -0
- package/components/Layout/Admin.vue +32 -0
- package/components/Layout/Auth.vue +29 -0
- package/components/Modal/DeleteConfirm.vue +48 -0
- package/components/Modal.vue +103 -0
- package/components/Nav/Tabs.vue +39 -0
- package/components/Table/DownloadDropdown.vue +111 -0
- package/components/Table/FilterDropdown.vue +226 -0
- package/components/Toast/Alert.vue +113 -0
- package/components/Toast/Container.vue +34 -0
- package/components/Toast/Notification.vue +45 -0
- package/components/Toast/Process.vue +88 -0
- package/nuxt.config.ts +15 -0
- package/package.json +45 -0
- package/plugins/preline.client.ts +68 -0
- package/shared/composables/useForm.js +119 -0
- package/shared/composables/useTable.ts +84 -0
- package/shared/composables/useToast.js +69 -0
- package/shared/stores/toast.js +129 -0
- package/spark.css +207 -0
|
@@ -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>
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, watch, onMounted, onBeforeUnmount, computed } from 'vue'
|
|
3
|
+
import { IconCalendar, IconX } from '@tabler/icons-vue'
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
modelValue: {
|
|
7
|
+
type: [String, Date, Array],
|
|
8
|
+
default: null
|
|
9
|
+
},
|
|
10
|
+
mode: {
|
|
11
|
+
type: String,
|
|
12
|
+
default: 'date', // 'date' | 'datetime' | 'time' | 'range' | 'range-time'
|
|
13
|
+
},
|
|
14
|
+
placeholder: {
|
|
15
|
+
type: String,
|
|
16
|
+
default: 'Seleccionar fecha'
|
|
17
|
+
},
|
|
18
|
+
minDate: {
|
|
19
|
+
type: [String, Date],
|
|
20
|
+
default: null
|
|
21
|
+
},
|
|
22
|
+
maxDate: {
|
|
23
|
+
type: [String, Date],
|
|
24
|
+
default: null
|
|
25
|
+
},
|
|
26
|
+
disabled: {
|
|
27
|
+
type: Boolean,
|
|
28
|
+
default: false
|
|
29
|
+
},
|
|
30
|
+
clearable: {
|
|
31
|
+
type: Boolean,
|
|
32
|
+
default: true
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const emit = defineEmits(['update:modelValue', 'change'])
|
|
37
|
+
|
|
38
|
+
const inputEl = ref(null)
|
|
39
|
+
const calendarInstance = ref(null)
|
|
40
|
+
const internalValue = ref('')
|
|
41
|
+
|
|
42
|
+
const isRange = computed(() => props.mode.includes('range'))
|
|
43
|
+
const hasTime = computed(() => props.mode.includes('time'))
|
|
44
|
+
|
|
45
|
+
const formatDateToDDMMYYYY = (isoString) => {
|
|
46
|
+
if (!isoString || typeof isoString !== 'string') return isoString
|
|
47
|
+
|
|
48
|
+
// isoString comes as YYYY-MM-DD or YYYY-MM-DD HH:mm
|
|
49
|
+
const parts = isoString.split(' ')
|
|
50
|
+
const datePart = parts[0].split('-')
|
|
51
|
+
|
|
52
|
+
if (datePart.length === 3) {
|
|
53
|
+
const formattedDate = `${datePart[2]}-${datePart[1]}-${datePart[0]}`
|
|
54
|
+
if (parts[1]) {
|
|
55
|
+
return `${formattedDate} ${parts[1]}`
|
|
56
|
+
}
|
|
57
|
+
return formattedDate
|
|
58
|
+
}
|
|
59
|
+
return isoString
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const formatForDisplay = (val) => {
|
|
63
|
+
if (!val) return ''
|
|
64
|
+
|
|
65
|
+
// If it's a range, val is an array of 2 dates (YYYY-MM-DD or YYYY-MM-DD HH:mm)
|
|
66
|
+
if (Array.isArray(val) && val.length === 2) {
|
|
67
|
+
if (hasTime.value) {
|
|
68
|
+
return `${formatDateToDDMMYYYY(val[0])} - ${formatDateToDDMMYYYY(val[1])}`
|
|
69
|
+
}
|
|
70
|
+
return `${formatDateToDDMMYYYY(val[0].split(' ')[0])} - ${formatDateToDDMMYYYY(val[1].split(' ')[0])}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Single date/datetime string
|
|
74
|
+
if (typeof val === 'string') {
|
|
75
|
+
return formatDateToDDMMYYYY(val)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return val.toString()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Convert prop modelValue to internal text representation
|
|
82
|
+
watch(() => props.modelValue, (newVal) => {
|
|
83
|
+
internalValue.value = formatForDisplay(newVal)
|
|
84
|
+
}, { immediate: true })
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
onMounted(async () => {
|
|
88
|
+
// Dynamic import to avoid SSR issues
|
|
89
|
+
try {
|
|
90
|
+
const { Calendar: VanillaCalendar } = await import('vanilla-calendar-pro')
|
|
91
|
+
await import('vanilla-calendar-pro/styles/index.css')
|
|
92
|
+
|
|
93
|
+
// Prepare config options based on props.mode
|
|
94
|
+
let type = 'default'
|
|
95
|
+
let time = false
|
|
96
|
+
let selectionDatesMode = 'single'
|
|
97
|
+
|
|
98
|
+
if (props.mode === 'datetime') {
|
|
99
|
+
time = true
|
|
100
|
+
} else if (props.mode === 'time') {
|
|
101
|
+
type = 'time'
|
|
102
|
+
} else if (props.mode === 'range') {
|
|
103
|
+
selectionDatesMode = 'multiple-ranged'
|
|
104
|
+
} else if (props.mode === 'range-time') {
|
|
105
|
+
selectionDatesMode = 'multiple-ranged'
|
|
106
|
+
time = true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Set initial theme based on HTML class
|
|
110
|
+
const isDarkGlobal = useState('isDark')
|
|
111
|
+
|
|
112
|
+
let options = {
|
|
113
|
+
inputMode: true,
|
|
114
|
+
locale: 'es',
|
|
115
|
+
selectedTheme: isDarkGlobal.value ? 'dark' : 'light',
|
|
116
|
+
selectionDatesMode: selectionDatesMode,
|
|
117
|
+
selectionTimeMode: time ? 24 : false,
|
|
118
|
+
type: type,
|
|
119
|
+
onChangeToInput: (self, e) => {
|
|
120
|
+
let dates = self.context.selectedDates || []
|
|
121
|
+
let timeStr = self.context.selectedTime || ''
|
|
122
|
+
|
|
123
|
+
if (dates[0]) {
|
|
124
|
+
let res = dates[0]
|
|
125
|
+
if (dates[1]) res = [dates[0], dates[1]]
|
|
126
|
+
|
|
127
|
+
if (timeStr) {
|
|
128
|
+
if (dates[1]) {
|
|
129
|
+
res = [`${dates[0]} ${timeStr}`, `${dates[1]} ${timeStr}`]
|
|
130
|
+
} else {
|
|
131
|
+
res = `${dates[0]} ${timeStr}`
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
emit('update:modelValue', res)
|
|
135
|
+
emit('change', res)
|
|
136
|
+
|
|
137
|
+
if (!isRange.value && !hasTime.value) {
|
|
138
|
+
self.hide()
|
|
139
|
+
}
|
|
140
|
+
} else if (timeStr && type === 'time') {
|
|
141
|
+
emit('update:modelValue', timeStr)
|
|
142
|
+
emit('change', timeStr)
|
|
143
|
+
} else {
|
|
144
|
+
emit('update:modelValue', null)
|
|
145
|
+
emit('change', null)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (props.mode === 'time') {
|
|
151
|
+
options.selectionDatesMode = false
|
|
152
|
+
options.type = 'time'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (inputEl.value) {
|
|
156
|
+
calendarInstance.value = new VanillaCalendar(inputEl.value, options)
|
|
157
|
+
calendarInstance.value.init()
|
|
158
|
+
|
|
159
|
+
watch(() => isDarkGlobal.value, (newDark) => {
|
|
160
|
+
if (calendarInstance.value) {
|
|
161
|
+
calendarInstance.value.set({ selectedTheme: newDark ? 'dark' : 'light' })
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {
|
|
166
|
+
// Vanilla Calendar Pro load failed — silently continue
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
onBeforeUnmount(() => {
|
|
171
|
+
// Teardown
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const clear = () => {
|
|
175
|
+
emit('update:modelValue', null)
|
|
176
|
+
emit('change', null)
|
|
177
|
+
}
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
<template>
|
|
181
|
+
<div class="relative w-full">
|
|
182
|
+
<div class="relative group">
|
|
183
|
+
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none z-20 ps-3.5">
|
|
184
|
+
<IconCalendar class="size-4 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<input
|
|
188
|
+
ref="inputEl"
|
|
189
|
+
type="text"
|
|
190
|
+
:value="internalValue"
|
|
191
|
+
:disabled="disabled"
|
|
192
|
+
:placeholder="placeholder"
|
|
193
|
+
class="w-full py-2 ps-10 pe-10 border border-gray-300 rounded-lg text-sm dark:bg-slate-800 dark:border-slate-600 dark:text-white disabled:opacity-50 disabled:pointer-events-none cursor-pointer focus:border-blue-500 focus:ring-blue-500/20 outline-none transition-all block"
|
|
194
|
+
readonly
|
|
195
|
+
/>
|
|
196
|
+
|
|
197
|
+
<!-- Clear button -->
|
|
198
|
+
<button
|
|
199
|
+
v-if="clearable && internalValue && !disabled"
|
|
200
|
+
@click.stop="clear"
|
|
201
|
+
type="button"
|
|
202
|
+
class="absolute inset-y-0 end-0 flex items-center z-20 pe-3 text-gray-400 hover:text-red-500 transition-colors focus:outline-none"
|
|
203
|
+
>
|
|
204
|
+
<IconX class="size-4" />
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</template>
|
|
209
|
+
|
|
210
|
+
<style>
|
|
211
|
+
/*
|
|
212
|
+
Vanilla Calendar v3 compiles Tailwind classes directly into its themes.
|
|
213
|
+
We override the selected date and hover backgrounds for Light and Dark modes.
|
|
214
|
+
*/
|
|
215
|
+
|
|
216
|
+
/* LIGHT MODE OVERRIDES */
|
|
217
|
+
[data-vc-theme=light] .vc-months__month[data-vc-months-month-selected],
|
|
218
|
+
[data-vc-theme=light] .vc-years__year[data-vc-years-year-selected],
|
|
219
|
+
[data-vc-theme=light] .vc-date[data-vc-date-selected=middle][data-vc-date-selected] .vc-date__btn,
|
|
220
|
+
[data-vc-theme=light] .vc-date[data-vc-date-selected] .vc-date__btn {
|
|
221
|
+
background-color: #2563eb !important; /* blue-600 */
|
|
222
|
+
color: #ffffff !important;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
[data-vc-theme=light] .vc-months__month[data-vc-months-month-selected]:hover,
|
|
226
|
+
[data-vc-theme=light] .vc-years__year[data-vc-years-year-selected]:hover,
|
|
227
|
+
[data-vc-theme=light] .vc-date[data-vc-date-selected=middle][data-vc-date-selected] .vc-date__btn:hover,
|
|
228
|
+
[data-vc-theme=light] .vc-date[data-vc-date-selected] .vc-date__btn:hover {
|
|
229
|
+
background-color: #1d4ed8 !important; /* blue-700 */
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
[data-vc-theme=light] .vc-date[data-vc-date-today] .vc-date__btn {
|
|
233
|
+
color: #2563eb !important;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* DARK MODE OVERRIDES */
|
|
237
|
+
[data-vc-theme=dark] .vc-months__month[data-vc-months-month-selected],
|
|
238
|
+
[data-vc-theme=dark] .vc-years__year[data-vc-years-year-selected],
|
|
239
|
+
[data-vc-theme=dark] .vc-date[data-vc-date-selected=middle][data-vc-date-selected] .vc-date__btn,
|
|
240
|
+
[data-vc-theme=dark] .vc-date[data-vc-date-selected] .vc-date__btn {
|
|
241
|
+
background-color: #3b82f6 !important; /* blue-500 */
|
|
242
|
+
color: #ffffff !important;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
[data-vc-theme=dark] .vc-months__month[data-vc-months-month-selected]:hover,
|
|
246
|
+
[data-vc-theme=dark] .vc-years__year[data-vc-years-year-selected]:hover,
|
|
247
|
+
[data-vc-theme=dark] .vc-date[data-vc-date-selected=middle][data-vc-date-selected] .vc-date__btn:hover,
|
|
248
|
+
[data-vc-theme=dark] .vc-date[data-vc-date-selected] .vc-date__btn:hover {
|
|
249
|
+
background-color: #2563eb !important; /* blue-600 */
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
[data-vc-theme=dark] .vc-date[data-vc-date-today] .vc-date__btn {
|
|
253
|
+
color: #3b82f6 !important;
|
|
254
|
+
}
|
|
255
|
+
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { IconEye, IconEyeOff } from '@tabler/icons-vue'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search'
|
|
7
|
+
placeholder?: string
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
error?: string | null
|
|
10
|
+
label?: string
|
|
11
|
+
hint?: string
|
|
12
|
+
iconLeft?: object | Function | null
|
|
13
|
+
autocomplete?: string
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const modelValue = defineModel<string | number | null>({ default: '' })
|
|
17
|
+
|
|
18
|
+
const showPassword = ref(false)
|
|
19
|
+
|
|
20
|
+
const inputType = computed(() => {
|
|
21
|
+
if (props.type === 'password') return showPassword.value ? 'text' : 'password'
|
|
22
|
+
return props.type ?? 'text'
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const baseClasses = 'py-2 px-3 block w-full rounded-lg text-sm text-slate-800 border border-gray-200 dark:border-slate-700 focus:ring-0 focus:border-gray-400 focus:outline-none disabled:opacity-50 dark:bg-transparent dark:text-slate-300 transition-colors placeholder:text-slate-400 dark:placeholder:text-slate-500'
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div class="w-full">
|
|
30
|
+
<label v-if="label" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
|
31
|
+
{{ label }}
|
|
32
|
+
</label>
|
|
33
|
+
|
|
34
|
+
<div class="relative">
|
|
35
|
+
<!-- Ícono izquierdo -->
|
|
36
|
+
<div v-if="iconLeft" class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none text-slate-400">
|
|
37
|
+
<component :is="iconLeft" class="size-4" />
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<input
|
|
41
|
+
v-model="modelValue"
|
|
42
|
+
:type="inputType"
|
|
43
|
+
:placeholder="placeholder"
|
|
44
|
+
:disabled="disabled"
|
|
45
|
+
:autocomplete="autocomplete"
|
|
46
|
+
:class="[
|
|
47
|
+
baseClasses,
|
|
48
|
+
iconLeft ? 'ps-9' : '',
|
|
49
|
+
type === 'password' ? 'pe-10' : '',
|
|
50
|
+
error ? '!border-red-400 dark:!border-red-500' : '',
|
|
51
|
+
]"
|
|
52
|
+
/>
|
|
53
|
+
|
|
54
|
+
<!-- Toggle contraseña -->
|
|
55
|
+
<button
|
|
56
|
+
v-if="type === 'password'"
|
|
57
|
+
type="button"
|
|
58
|
+
tabindex="-1"
|
|
59
|
+
class="absolute inset-y-0 end-0 flex items-center px-3 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
|
60
|
+
@click="showPassword = !showPassword"
|
|
61
|
+
>
|
|
62
|
+
<component :is="showPassword ? IconEyeOff : IconEye" class="size-4" />
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Error -->
|
|
67
|
+
<p v-if="error" class="text-xs text-red-500 dark:text-red-400 mt-1">{{ error }}</p>
|
|
68
|
+
|
|
69
|
+
<!-- Hint -->
|
|
70
|
+
<p v-else-if="hint" class="text-xs text-slate-400 mt-1">{{ hint }}</p>
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{
|
|
3
|
+
options: { value: string | number; label: string }[]
|
|
4
|
+
modelValue?: string | number | null
|
|
5
|
+
label?: string
|
|
6
|
+
placeholder?: string
|
|
7
|
+
hint?: string
|
|
8
|
+
error?: string
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
'update:modelValue': [value: string | number | null]
|
|
14
|
+
change: [value: string | number | null]
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const selectRef = ref<HTMLSelectElement | null>(null)
|
|
18
|
+
|
|
19
|
+
const reinitHsSelect = async () => {
|
|
20
|
+
await nextTick()
|
|
21
|
+
const el = selectRef.value
|
|
22
|
+
if (!el) return
|
|
23
|
+
|
|
24
|
+
const instance = (window as any).HSSelect?.getInstance?.(el)
|
|
25
|
+
if (instance?.destroy) instance.destroy()
|
|
26
|
+
|
|
27
|
+
new (window as any).HSSelect(el)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Re-initialize when options change (async load)
|
|
31
|
+
watch(() => props.options, async () => {
|
|
32
|
+
await reinitHsSelect()
|
|
33
|
+
}, { deep: true })
|
|
34
|
+
|
|
35
|
+
onMounted(() => {
|
|
36
|
+
reinitHsSelect()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const handleChange = (e: Event) => {
|
|
40
|
+
const val = (e.target as HTMLSelectElement).value
|
|
41
|
+
emit('update:modelValue', val || null)
|
|
42
|
+
emit('change', val || null)
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<div class="space-y-1.5">
|
|
48
|
+
<!-- Label -->
|
|
49
|
+
<label v-if="label" class="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
50
|
+
{{ label }}
|
|
51
|
+
</label>
|
|
52
|
+
|
|
53
|
+
<!-- Select (HSSelect) -->
|
|
54
|
+
<ClientOnly>
|
|
55
|
+
<template #fallback>
|
|
56
|
+
<div class="h-[38px] bg-slate-100 dark:bg-slate-800 animate-pulse rounded-lg" />
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<div :class="['relative', error ? 'select-error' : '']">
|
|
60
|
+
<select
|
|
61
|
+
ref="selectRef"
|
|
62
|
+
class="hs-select w-full"
|
|
63
|
+
:value="modelValue ?? ''"
|
|
64
|
+
:disabled="disabled"
|
|
65
|
+
@change="handleChange"
|
|
66
|
+
data-hs-select='{
|
|
67
|
+
"placeholder": "Seleccionar...",
|
|
68
|
+
"toggleTag": "<button type=\"button\" aria-expanded=\"false\"></button>",
|
|
69
|
+
"toggleClasses": "hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative py-2 ps-4 pe-9 flex gap-x-2 text-nowrap w-full cursor-pointer bg-white border border-slate-200 rounded-lg text-start text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300 dark:focus:outline-hidden dark:focus:ring-1 dark:focus:ring-blue-600",
|
|
70
|
+
"dropdownClasses": "mt-1 z-50 w-full max-h-72 p-1 space-y-0.5 bg-white border border-slate-200 rounded-lg overflow-hidden overflow-y-auto shadow-lg dark:bg-slate-800 dark:border-slate-700",
|
|
71
|
+
"optionClasses": "py-2 px-4 w-full text-sm text-slate-800 cursor-pointer hover:bg-slate-100 rounded-lg focus:outline-hidden focus:bg-slate-100 dark:bg-slate-800 dark:hover:bg-slate-700 dark:text-slate-200 dark:focus:bg-slate-700",
|
|
72
|
+
"optionTemplate": "<div class=\"flex justify-between items-center w-full\"><span data-title></span><span class=\"hidden hs-selected:block\"><svg class=\"shrink-0 size-3.5 text-blue-600 dark:text-blue-500\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>"
|
|
73
|
+
}'
|
|
74
|
+
>
|
|
75
|
+
<option value="">{{ placeholder ?? 'Seleccionar...' }}</option>
|
|
76
|
+
<option
|
|
77
|
+
v-for="option in options"
|
|
78
|
+
:key="option.value"
|
|
79
|
+
:value="option.value"
|
|
80
|
+
>
|
|
81
|
+
{{ option.label }}
|
|
82
|
+
</option>
|
|
83
|
+
</select>
|
|
84
|
+
</div>
|
|
85
|
+
</ClientOnly>
|
|
86
|
+
|
|
87
|
+
<!-- Error -->
|
|
88
|
+
<p v-if="error" class="text-xs text-red-500 dark:text-red-400">{{ error }}</p>
|
|
89
|
+
|
|
90
|
+
<!-- Hint -->
|
|
91
|
+
<p v-else-if="hint" class="text-xs text-slate-400 dark:text-slate-500">{{ hint }}</p>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<style scoped>
|
|
96
|
+
/* Apply error border to the HSSelect toggle button when in error state */
|
|
97
|
+
.select-error :deep(button[aria-expanded]) {
|
|
98
|
+
border-color: rgb(248 113 113) !important; /* red-400 */
|
|
99
|
+
}
|
|
100
|
+
</style>
|