@innertia-solutions/innertia-nuxt 0.1.9 → 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.
@@ -0,0 +1,323 @@
1
+ <script setup>
2
+ /**
3
+ * <DataPreview> — Panel de preview overlay compartido por Table.Standard y Tree.Standard.
4
+ *
5
+ * Encapsula:
6
+ * - Panel absoluto a la derecha del container padre (position:relative)
7
+ * - Resize por drag del handle vertical en el borde izquierdo
8
+ * - Header con título + acciones + botones (abrir, eliminar, minimizar, cerrar)
9
+ * - Tabs inferior cuando se pasa `historyEndpoint` (Datos / Bitácora)
10
+ * - Cache de la fila previewada en sessionStorage cuando `cached`
11
+ * - Chip flotante (Teleport to body) compartido vía `useDockedPreviews` Pinia store
12
+ *
13
+ * El parent es dueño de `row` (v-model) — DataPreview lo seteoa a null al cerrar.
14
+ * El parent también maneja el click fuera + ESC porque conoce su containerRef.
15
+ *
16
+ * Slots:
17
+ * - header ({ row, close }) — título del panel
18
+ * - actions ({ row, close }) — botones extra al lado de los built-in
19
+ * - default ({ row, close }) — body del preview
20
+ */
21
+
22
+ import {
23
+ IconExternalLink, IconTrash, IconMinus, IconX, IconMaximize,
24
+ } from '@tabler/icons-vue'
25
+
26
+ const props = defineProps({
27
+ /** Activa el preview (típicamente derivado de !!slots.preview en el parent). */
28
+ enabled: { type: Boolean, default: false },
29
+ /** Nombre único para cache + dock id. */
30
+ name: { type: String, default: 'default' },
31
+ /** Si true, persiste la fila previewada en sessionStorage entre navegaciones. */
32
+ cached: { type: Boolean, default: false },
33
+ /** URL fija o (row) => url para botón "abrir externa". */
34
+ previewHref: { type: [String, Function], default: null },
35
+ /** Muestra botón eliminar (emite @delete). */
36
+ previewDeletable: { type: Boolean, default: false },
37
+ /** Porcentaje inicial de la tabla (0-100). */
38
+ splitRatio: { type: Number, default: 55 },
39
+ /** Endpoint del historial. Cuando se pasa, muestra tabs Datos/Bitácora. */
40
+ historyEndpoint: { type: String, default: null },
41
+ /** Container ref (HTMLElement) para medir el width al hacer resize. */
42
+ containerRef: { type: Object, default: null },
43
+ })
44
+
45
+ const emit = defineEmits(['delete'])
46
+
47
+ // v-model:row → parent es dueño de la fila activa.
48
+ const row = defineModel('row', { default: null })
49
+
50
+ const previewFromCache = ref(false)
51
+ const currentRatio = ref(props.splitRatio)
52
+ const panelRef = ref(null)
53
+ const previewTab = ref('datos')
54
+
55
+ const cacheKey = computed(() => `data-preview-${props.name}`)
56
+
57
+ const resolvedPreviewHref = computed(() => {
58
+ if (!props.previewHref || !row.value) return null
59
+ return typeof props.previewHref === 'function'
60
+ ? props.previewHref(row.value)
61
+ : props.previewHref
62
+ })
63
+
64
+ const hasHistory = computed(() => !!props.historyEndpoint)
65
+
66
+ // Resetear tab al cambiar de fila + reinit tooltips de Preline
67
+ watch(row, async (r) => {
68
+ previewTab.value = 'datos'
69
+ if (r) {
70
+ await nextTick()
71
+ if (typeof window !== 'undefined') window.HSStaticMethods?.autoInit?.(['Tooltip'])
72
+ }
73
+ })
74
+
75
+ // Persistir fila en sessionStorage
76
+ watch(row, (r) => {
77
+ if (!props.cached) return
78
+ if (r) sessionStorage.setItem(cacheKey.value, JSON.stringify(r))
79
+ else sessionStorage.removeItem(cacheKey.value)
80
+ })
81
+
82
+ const close = () => { row.value = null }
83
+
84
+ const startResize = (e) => {
85
+ e.preventDefault()
86
+ const onMove = (ev) => {
87
+ const el = props.containerRef
88
+ if (!el) return
89
+ const rect = el.getBoundingClientRect()
90
+ const ratio = ((ev.clientX - rect.left) / rect.width) * 100
91
+ currentRatio.value = Math.min(80, Math.max(25, ratio))
92
+ }
93
+ const onUp = () => {
94
+ window.removeEventListener('mousemove', onMove)
95
+ window.removeEventListener('mouseup', onUp)
96
+ }
97
+ window.addEventListener('mousemove', onMove)
98
+ window.addEventListener('mouseup', onUp)
99
+ }
100
+
101
+ // ─── Dock (chip flotante) ─────────────────────────────────────────────────────
102
+ const {
103
+ docked,
104
+ dock, undock: undockItem,
105
+ activeDockId, activeDockRect,
106
+ collapseDock,
107
+ } = useDockedPreviews()
108
+ const route = useRoute()
109
+
110
+ const minimize = () => {
111
+ if (!row.value) return
112
+ const label = row.value.name ?? row.value.title ?? row.value.email ?? String(row.value.id)
113
+ const subtitle = row.value.email ?? row.value.description ?? null
114
+ dock({
115
+ id: `${props.name}-${row.value.id}`,
116
+ label,
117
+ subtitle,
118
+ row: { ...row.value },
119
+ tableName: props.name,
120
+ route: route.path,
121
+ })
122
+ close()
123
+ }
124
+
125
+ const floatingItem = computed(() =>
126
+ activeDockId.value
127
+ ? docked.value.find(d => d.id === activeDockId.value && d.tableName === props.name) ?? null
128
+ : null
129
+ )
130
+
131
+ const floatingPanelStyle = computed(() => {
132
+ const rect = activeDockRect.value
133
+ const panelW = 384
134
+ const bottom = 52
135
+ if (!rect || typeof window === 'undefined') return { bottom: bottom + 'px', right: '16px' }
136
+ const tabCenter = rect.left + rect.width / 2
137
+ let right = window.innerWidth - tabCenter - panelW / 2
138
+ right = Math.max(8, Math.min(right, window.innerWidth - panelW - 8))
139
+ return { bottom: bottom + 'px', right: right + 'px' }
140
+ })
141
+
142
+ const expandToFull = (item) => {
143
+ row.value = item.row
144
+ undockItem(item.id)
145
+ }
146
+
147
+ // Restore from cache on mount
148
+ onMounted(async () => {
149
+ if (props.cached && props.enabled) {
150
+ try {
151
+ const raw = sessionStorage.getItem(cacheKey.value)
152
+ if (raw) {
153
+ previewFromCache.value = true
154
+ row.value = JSON.parse(raw)
155
+ await nextTick()
156
+ previewFromCache.value = false
157
+ }
158
+ } catch {}
159
+ }
160
+ })
161
+
162
+ defineExpose({ panelRef })
163
+ </script>
164
+
165
+ <template>
166
+ <!-- Panel overlay (absolute right, dentro del container del parent) -->
167
+ <Transition
168
+ :enter-active-class="previewFromCache ? '' : 'transition ease-out duration-200'"
169
+ :enter-from-class="previewFromCache ? '' : 'opacity-0 translate-x-6'"
170
+ :enter-to-class="previewFromCache ? '' : 'opacity-100 translate-x-0'"
171
+ leave-active-class="transition ease-in duration-150"
172
+ leave-from-class="opacity-100 translate-x-0"
173
+ leave-to-class="opacity-0 translate-x-6"
174
+ >
175
+ <div
176
+ v-if="row && enabled"
177
+ ref="panelRef"
178
+ class="absolute top-0 right-0 bottom-0 z-30 flex flex-col bg-card border border-card-line rounded-card shadow-xl overflow-hidden"
179
+ :style="{ width: (100 - currentRatio) + '%' }"
180
+ >
181
+ <!-- Resize handle -->
182
+ <div
183
+ class="absolute left-1 top-1/2 -translate-y-1/2 h-12 w-1 cursor-col-resize rounded-full bg-border hover:bg-primary/50 transition-colors z-10"
184
+ @mousedown="startResize"
185
+ />
186
+
187
+ <!-- Header -->
188
+ <div class="shrink-0 flex items-center gap-3 px-5 py-4 border-b border-card-line">
189
+ <div class="flex-1 min-w-0">
190
+ <slot name="header" :row="row" :close="close" />
191
+ </div>
192
+
193
+ <div class="flex items-center gap-0.5 shrink-0">
194
+ <slot name="actions" :row="row" :close="close" />
195
+
196
+ <!-- Abrir externa -->
197
+ <div v-if="resolvedPreviewHref" class="hs-tooltip [--placement:top] inline-block">
198
+ <NuxtLink
199
+ :to="resolvedPreviewHref"
200
+ class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-control text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
201
+ >
202
+ <IconExternalLink class="size-3.5" />
203
+ </NuxtLink>
204
+ <span 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-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-popover shadow-2xs" role="tooltip">Abrir</span>
205
+ </div>
206
+
207
+ <!-- Eliminar -->
208
+ <div v-if="previewDeletable" class="hs-tooltip [--placement:top] inline-block">
209
+ <button
210
+ type="button"
211
+ class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-control text-muted-foreground hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
212
+ @click.stop="emit('delete', row)"
213
+ >
214
+ <IconTrash class="size-3.5" />
215
+ </button>
216
+ <span 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-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-popover shadow-2xs" role="tooltip">Eliminar</span>
217
+ </div>
218
+
219
+ <!-- Minimizar -->
220
+ <div class="hs-tooltip [--placement:top] inline-block">
221
+ <button
222
+ type="button"
223
+ class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-control text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
224
+ @click.stop="minimize"
225
+ >
226
+ <IconMinus class="size-3.5" />
227
+ </button>
228
+ <span 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-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-popover shadow-2xs" role="tooltip">Minimizar</span>
229
+ </div>
230
+
231
+ <!-- Cerrar -->
232
+ <div class="hs-tooltip [--placement:top] inline-block">
233
+ <button
234
+ type="button"
235
+ class="hs-tooltip-toggle inline-flex items-center justify-center size-7 rounded-control text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors"
236
+ @click.stop="close"
237
+ >
238
+ <IconX class="size-3.5" />
239
+ </button>
240
+ <span 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-tooltip border border-tooltip-line text-xs font-medium text-tooltip-foreground rounded-popover shadow-2xs" role="tooltip">Cerrar</span>
241
+ </div>
242
+ </div>
243
+ </div>
244
+
245
+ <!-- Body -->
246
+ <div class="flex-1 overflow-y-auto min-h-0">
247
+ <slot v-if="previewTab === 'datos'" :row="row" :close="close" />
248
+ <TablePreviewTimeline
249
+ v-else-if="previewTab === 'bitacora' && historyEndpoint"
250
+ :endpoint="historyEndpoint"
251
+ />
252
+ </div>
253
+
254
+ <!-- Tabs (cuando hay historyEndpoint) -->
255
+ <div v-if="hasHistory" class="shrink-0 flex border-t border-card-line">
256
+ <button
257
+ type="button"
258
+ @click="previewTab = 'datos'"
259
+ :class="[
260
+ 'flex-1 py-2.5 text-xs font-semibold transition-colors border-r border-card-line border-t-2 -mt-px',
261
+ previewTab === 'datos'
262
+ ? 'border-t-card text-foreground'
263
+ : 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
264
+ ]"
265
+ >
266
+ Datos
267
+ </button>
268
+ <button
269
+ type="button"
270
+ @click="previewTab = 'bitacora'"
271
+ :class="[
272
+ 'flex-1 py-2.5 text-xs font-semibold transition-colors border-t-2 -mt-px',
273
+ previewTab === 'bitacora'
274
+ ? 'border-t-card text-foreground'
275
+ : 'border-t-transparent text-muted-foreground hover:text-foreground hover:bg-muted-hover'
276
+ ]"
277
+ >
278
+ Bitácora
279
+ </button>
280
+ </div>
281
+ </div>
282
+ </Transition>
283
+
284
+ <!-- Floating chip (dock minimizado) — siempre teleportado a body -->
285
+ <Teleport to="body">
286
+ <Transition
287
+ enter-active-class="transition ease-out duration-200"
288
+ enter-from-class="opacity-0 translate-y-4"
289
+ enter-to-class="opacity-100 translate-y-0"
290
+ leave-active-class="transition ease-in duration-150"
291
+ leave-from-class="opacity-100 translate-y-0"
292
+ leave-to-class="opacity-0 translate-y-4"
293
+ >
294
+ <div
295
+ v-if="floatingItem"
296
+ class="fixed z-[60] w-96 flex flex-col bg-card border border-card-line rounded-t-card shadow-2xl overflow-hidden"
297
+ :style="{ ...floatingPanelStyle, maxHeight: 'min(480px, calc(100vh - 60px))' }"
298
+ >
299
+ <div class="flex items-center gap-2 px-3 py-2.5 border-b border-card-line shrink-0 bg-surface select-none">
300
+ <span class="size-6 rounded-avatar bg-primary flex items-center justify-center text-[10px] font-bold text-primary-foreground shrink-0">
301
+ {{ (floatingItem.label?.[0] ?? '?').toUpperCase() }}
302
+ </span>
303
+ <div class="flex-1 min-w-0">
304
+ <p class="text-sm font-semibold text-foreground truncate leading-tight">{{ floatingItem.label }}</p>
305
+ <p v-if="floatingItem.subtitle" class="text-xs text-muted-foreground truncate">{{ floatingItem.subtitle }}</p>
306
+ </div>
307
+ <button type="button" title="Expandir" class="inline-flex items-center justify-center size-6 rounded-control text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors" @click.stop="expandToFull(floatingItem)">
308
+ <IconMaximize class="size-3.5" />
309
+ </button>
310
+ <button type="button" title="Minimizar" class="inline-flex items-center justify-center size-6 rounded-control text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors" @click.stop="collapseDock()">
311
+ <IconMinus class="size-3.5" />
312
+ </button>
313
+ <button type="button" title="Cerrar" class="inline-flex items-center justify-center size-6 rounded-control text-muted-foreground hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors" @click.stop="undockItem(floatingItem.id)">
314
+ <IconX class="size-3.5" />
315
+ </button>
316
+ </div>
317
+ <div class="flex-1 overflow-y-auto min-h-0">
318
+ <slot :row="floatingItem.row" :close="() => undockItem(floatingItem.id)" />
319
+ </div>
320
+ </div>
321
+ </Transition>
322
+ </Teleport>
323
+ </template>
@@ -436,7 +436,7 @@ const handleRowKeydown = (row, event) => {
436
436
  type="checkbox"
437
437
  :checked="isAllVisibleSelected"
438
438
  @change="toggleSelectAll"
439
- class="mx-2 shrink-0 border-gray-300 rounded-sm text-blue-900 focus:ring-blue-900 dark:bg-card border-card-line"
439
+ class="mx-2 shrink-0 border-gray-300 rounded-control text-blue-900 focus:ring-blue-900 dark:bg-card border-card-line"
440
440
  />
441
441
  </th>
442
442
  <th
@@ -538,7 +538,7 @@ const handleRowKeydown = (row, event) => {
538
538
  <!-- Empty overlays -->
539
539
  <div
540
540
  v-if="!loading && data.length === 0 && !search"
541
- class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-card/60 transition-all rounded-xl"
541
+ class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-card/60 transition-all rounded-card"
542
542
  >
543
543
  <slot name="empty">
544
544
  <p class="text-muted-foreground text-lg font-medium italic">No hay registros</p>
@@ -547,7 +547,7 @@ const handleRowKeydown = (row, event) => {
547
547
 
548
548
  <div
549
549
  v-if="!loading && data.length === 0 && search"
550
- class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-card/60 transition-all rounded-xl"
550
+ class="absolute inset-0 z-10 pointer-events-none flex items-center justify-center backdrop-blur-sm bg-card/60 transition-all rounded-card"
551
551
  >
552
552
  <slot name="empty-search">
553
553
  <p class="text-muted-foreground text-lg font-medium italic">No hay registros en la búsqueda</p>
@@ -561,7 +561,7 @@ const handleRowKeydown = (row, event) => {
561
561
  <div v-if="loading" :class="gridClass">
562
562
  <div v-for="(_, index) in skeletonRows" :key="'grid-skeleton-' + index" class="animate-pulse">
563
563
  <slot name="grid-skeleton">
564
- <div class="bg-card rounded-lg border border-card-line p-4">
564
+ <div class="bg-card rounded-card border border-card-line p-4">
565
565
  <div class="space-y-3">
566
566
  <div class="h-4 bg-surface-1 rounded w-3/4"></div>
567
567
  <div class="h-4 bg-surface-1 rounded w-1/2"></div>
@@ -583,7 +583,7 @@ const handleRowKeydown = (row, event) => {
583
583
  :checkable="checkable"
584
584
  :toggleRow="() => toggleRow(row)"
585
585
  >
586
- <div class="bg-card rounded-lg border border-card-line p-4 hover:shadow-md transition-shadow relative">
586
+ <div class="bg-card rounded-card border border-card-line p-4 hover:shadow-md transition-shadow relative">
587
587
  <div v-if="checkable" class="absolute top-2 left-2 z-10">
588
588
  <input
589
589
  type="checkbox"
@@ -636,11 +636,11 @@ const handleRowKeydown = (row, event) => {
636
636
  <p class="text-sm text-foreground font-medium">{{ meta.total }} registros</p>
637
637
 
638
638
  <div v-if="isDataFromCache && cached" class="group relative flex items-center">
639
- <div class="flex items-center gap-x-1.5 py-1 px-2.5 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 rounded-lg cursor-help transition-colors hover:bg-emerald-500/20">
639
+ <div class="flex items-center gap-x-1.5 py-1 px-2.5 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 rounded-badge cursor-help transition-colors hover:bg-emerald-500/20">
640
640
  <IconBolt class="size-3.5 fill-current" />
641
641
  <span class="text-[10px] font-bold uppercase tracking-wider">Instant</span>
642
642
  </div>
643
- <div class="absolute bottom-full mb-2 left-0 hidden group-hover:block w-48 p-2.5 bg-slate-900 text-white text-[11px] leading-relaxed rounded-xl shadow-2xl z-50 animate-in fade-in zoom-in duration-200">
643
+ <div class="absolute bottom-full mb-2 left-0 hidden group-hover:block w-48 p-2.5 bg-slate-900 text-white text-[11px] leading-relaxed rounded-popover shadow-2xl z-50 animate-in fade-in zoom-in duration-200">
644
644
  <div class="font-bold mb-1 flex items-center gap-x-1.5 text-emerald-400">
645
645
  <IconBolt class="size-3" />
646
646
  Datos en Caché
@@ -659,7 +659,7 @@ const handleRowKeydown = (row, event) => {
659
659
  v-if="!isCustomPerPage"
660
660
  :value="perPage"
661
661
  @change="(e) => handlePerPageChange(e.target.value)"
662
- class="bg-surface border-none text-[11px] font-bold text-muted-foreground-1 rounded-lg focus:ring-0 cursor-pointer py-1 pl-2 pr-8"
662
+ class="bg-surface border-none text-[11px] font-bold text-muted-foreground-1 rounded-control focus:ring-0 cursor-pointer py-1 pl-2 pr-8"
663
663
  >
664
664
  <option :value="10">10</option>
665
665
  <option :value="25">25</option>
@@ -673,7 +673,7 @@ const handleRowKeydown = (row, event) => {
673
673
  v-model.number="perPage"
674
674
  min="1"
675
675
  max="500"
676
- class="w-14 bg-surface border-none text-[11px] font-bold text-muted-foreground-1 rounded-lg focus:ring-2 focus:ring-indigo-500/20 py-1 px-2"
676
+ class="w-14 bg-surface border-none text-[11px] font-bold text-muted-foreground-1 rounded-control focus:ring-2 focus:ring-indigo-500/20 py-1 px-2"
677
677
  @blur="perPage = perPage || 10"
678
678
  />
679
679
  <button @click="resetPerPage" class="text-[10px] text-indigo-500 font-bold hover:underline">Volver</button>
@@ -683,7 +683,7 @@ const handleRowKeydown = (row, event) => {
683
683
  <nav class="flex justify-end items-center gap-x-1" aria-label="Pagination">
684
684
  <button
685
685
  type="button"
686
- class="size-8 flex items-center justify-center rounded-lg text-foreground hover:bg-muted-hover disabled:opacity-30"
686
+ class="size-8 flex items-center justify-center rounded-control text-foreground hover:bg-muted-hover disabled:opacity-30"
687
687
  :disabled="page <= 1"
688
688
  @click="goToPreviousPage"
689
689
  >
@@ -692,13 +692,13 @@ const handleRowKeydown = (row, event) => {
692
692
  </svg>
693
693
  </button>
694
694
  <div class="flex items-center gap-x-1 mx-2">
695
- <span class="size-8 flex items-center justify-center text-xs font-bold rounded-lg bg-surface text-foreground">{{ meta.current_page }}</span>
695
+ <span class="size-8 flex items-center justify-center text-xs font-bold rounded-control bg-surface text-foreground">{{ meta.current_page }}</span>
696
696
  <span class="text-[10px] font-bold text-muted-foreground uppercase mx-1">de</span>
697
697
  <span class="text-[10px] font-bold text-muted-foreground">{{ meta.last_page }}</span>
698
698
  </div>
699
699
  <button
700
700
  type="button"
701
- class="size-8 flex items-center justify-center rounded-lg text-foreground hover:bg-muted-hover disabled:opacity-30"
701
+ class="size-8 flex items-center justify-center rounded-control text-foreground hover:bg-muted-hover disabled:opacity-30"
702
702
  :disabled="page >= meta.last_page"
703
703
  @click="goToNextPage"
704
704
  >
@@ -180,76 +180,101 @@ const clear = () => {
180
180
  <template>
181
181
  <div class="relative w-full">
182
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
183
  <input
188
184
  ref="inputEl"
189
185
  type="text"
190
186
  :value="internalValue"
191
187
  :disabled="disabled"
192
188
  :placeholder="placeholder"
193
- class="w-full py-2 ps-10 pe-10 border border-gray-300 rounded-lg text-sm dark:bg-card dark:border-card-line 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"
189
+ class="innertia-field pe-10 cursor-pointer"
194
190
  readonly
195
191
  />
196
192
 
197
- <!-- Clear button -->
193
+ <!-- Clear button (cuando hay valor) o ícono calendario (cuando está vacío) — ambos a la derecha -->
198
194
  <button
199
195
  v-if="clearable && internalValue && !disabled"
200
196
  @click.stop="clear"
201
197
  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"
198
+ class="absolute inset-y-0 end-0 flex items-center z-20 pe-3 text-muted-foreground hover:text-destructive transition-colors focus:outline-none"
203
199
  >
204
200
  <IconX class="size-4" />
205
201
  </button>
202
+ <div
203
+ v-else
204
+ class="absolute inset-y-0 end-0 flex items-center pointer-events-none z-20 pe-3"
205
+ >
206
+ <IconCalendar class="size-4 text-muted-foreground-2 group-focus-within:text-primary transition-colors" />
207
+ </div>
206
208
  </div>
207
209
  </div>
208
210
  </template>
209
211
 
210
212
  <style>
211
213
  /*
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;
214
+ * Vanilla Calendar v3 compila Tailwind directo en sus themes. Overridemos
215
+ * solo los acentos del brand color para que sigan los tokens del DS.
216
+ *
217
+ * Light y dark usan las mismas variables — `--color-primary` ya cambia
218
+ * automáticamente entre claro/oscuro a través de la cascada del tema.
219
+ *
220
+ * Las clases de selected / today / hover aplican tanto en
221
+ * `[data-vc-theme=light]` como `[data-vc-theme=dark]`.
222
+ */
223
+
224
+ /* Día seleccionado / mes seleccionado / año seleccionado */
225
+ .vc-months__month[data-vc-months-month-selected],
226
+ .vc-years__year[data-vc-years-year-selected],
227
+ .vc-date[data-vc-date-selected=middle][data-vc-date-selected] .vc-date__btn,
228
+ .vc-date[data-vc-date-selected] .vc-date__btn {
229
+ background-color: var(--color-primary) !important;
230
+ color: var(--color-primary-foreground) !important;
231
+ border-radius: var(--radius-control) !important;
223
232
  }
224
233
 
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 */
234
+ .vc-months__month[data-vc-months-month-selected]:hover,
235
+ .vc-years__year[data-vc-years-year-selected]:hover,
236
+ .vc-date[data-vc-date-selected=middle][data-vc-date-selected] .vc-date__btn:hover,
237
+ .vc-date[data-vc-date-selected] .vc-date__btn:hover {
238
+ background-color: var(--color-primary-hover) !important;
230
239
  }
231
240
 
232
- [data-vc-theme=light] .vc-date[data-vc-date-today] .vc-date__btn {
233
- color: #2563eb !important;
241
+ /* Hoy (sin seleccionar): color del texto en brand */
242
+ .vc-date[data-vc-date-today] .vc-date__btn {
243
+ color: var(--color-primary) !important;
244
+ font-weight: 600;
234
245
  }
235
246
 
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;
247
+ /* Hover de cualquier día (no seleccionado) */
248
+ .vc-date:not([data-vc-date-selected]) .vc-date__btn:hover,
249
+ .vc-months__month:not([data-vc-months-month-selected]):hover,
250
+ .vc-years__year:not([data-vc-years-year-selected]):hover {
251
+ background-color: var(--color-muted-hover) !important;
252
+ border-radius: var(--radius-control) !important;
253
+ }
254
+
255
+ /* Días del mes anterior / siguiente — más opacos */
256
+ .vc-date[data-vc-date-month=prev] .vc-date__btn,
257
+ .vc-date[data-vc-date-month=next] .vc-date__btn {
258
+ color: var(--color-muted-foreground-2) !important;
259
+ }
260
+
261
+ /* Botones de navegación (mes anterior / siguiente) */
262
+ .vc-arrow {
263
+ color: var(--color-foreground) !important;
264
+ }
265
+ .vc-arrow:hover {
266
+ background-color: var(--color-muted-hover) !important;
267
+ border-radius: var(--radius-control) !important;
243
268
  }
244
269
 
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 */
270
+ /* Header del mes/año en el centro */
271
+ .vc-month,
272
+ .vc-year {
273
+ color: var(--color-foreground) !important;
250
274
  }
251
275
 
252
- [data-vc-theme=dark] .vc-date[data-vc-date-today] .vc-date__btn {
253
- color: #3b82f6 !important;
276
+ /* Nombres de los días (Lun, Mar, …) */
277
+ .vc-week__day {
278
+ color: var(--color-muted-foreground) !important;
254
279
  }
255
280
  </style>
@@ -10,6 +10,7 @@ const props = defineProps<{
10
10
  label?: string
11
11
  hint?: string
12
12
  iconLeft?: object | Function | null
13
+ iconRight?: object | Function | null
13
14
  autocomplete?: string
14
15
  size?: 'sm' | 'md'
15
16
  }>()
@@ -24,7 +25,7 @@ const inputType = computed(() => {
24
25
  })
25
26
 
26
27
  const baseClasses = computed(() =>
27
- `${props.size === 'sm' ? 'py-1.5' : 'py-2'} px-3 block w-full rounded-lg text-sm text-slate-800 border border-card-line focus:ring-0 focus:border-gray-400 focus:outline-none disabled:opacity-50 dark:bg-transparent dark:text-muted-foreground-1 transition-colors placeholder:text-muted-foreground dark:placeholder:text-muted-foreground`
28
+ `innertia-field${props.size === 'sm' ? ' innertia-field-sm' : ''}`
28
29
  )
29
30
  </script>
30
31
 
@@ -49,11 +50,16 @@ const baseClasses = computed(() =>
49
50
  :class="[
50
51
  baseClasses,
51
52
  iconLeft ? 'ps-9' : '',
52
- type === 'password' ? 'pe-10' : '',
53
+ (iconRight || type === 'password') ? 'pe-10' : '',
53
54
  error ? '!border-red-400 dark:!border-red-500' : '',
54
55
  ]"
55
56
  />
56
57
 
58
+ <!-- Ícono derecho (no se renderiza si type=password — ahí va el toggle) -->
59
+ <div v-if="iconRight && type !== 'password'" class="absolute inset-y-0 end-0 flex items-center pe-3 pointer-events-none text-muted-foreground">
60
+ <component :is="iconRight" class="size-4" />
61
+ </div>
62
+
57
63
  <!-- Toggle contraseña -->
58
64
  <button
59
65
  v-if="type === 'password'"