@innertia-solutions/nuxt-theme-spark 0.1.117 → 0.1.119
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.
|
@@ -11,6 +11,7 @@ const props = defineProps<{
|
|
|
11
11
|
hint?: string
|
|
12
12
|
iconLeft?: object | Function | null
|
|
13
13
|
autocomplete?: string
|
|
14
|
+
size?: 'sm' | 'md'
|
|
14
15
|
}>()
|
|
15
16
|
|
|
16
17
|
const modelValue = defineModel<string | number | null>({ default: '' })
|
|
@@ -22,7 +23,9 @@ const inputType = computed(() => {
|
|
|
22
23
|
return props.type ?? 'text'
|
|
23
24
|
})
|
|
24
25
|
|
|
25
|
-
const baseClasses =
|
|
26
|
+
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
|
+
)
|
|
26
29
|
</script>
|
|
27
30
|
|
|
28
31
|
<template>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
endpoint: { type: String, required: true },
|
|
4
|
+
})
|
|
5
|
+
|
|
6
|
+
const config = useRuntimeConfig()
|
|
7
|
+
const baseUrl = config.public?.apiBaseUrl ?? '/api'
|
|
8
|
+
|
|
9
|
+
const url = computed(() => `${baseUrl}/${props.endpoint}`)
|
|
10
|
+
|
|
11
|
+
const { data: rawData, pending, error, refresh } = useFetch(url, {
|
|
12
|
+
watch: [url],
|
|
13
|
+
key: computed(() => `timeline-${props.endpoint}`),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const events = computed(() => {
|
|
17
|
+
const d = rawData.value
|
|
18
|
+
if (!d) return []
|
|
19
|
+
return Array.isArray(d) ? d : (d.data ?? [])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const typeConfig = {
|
|
23
|
+
created: { color: 'bg-green-500', label: 'Creado', textColor: 'text-green-600 dark:text-green-400' },
|
|
24
|
+
updated: { color: 'bg-blue-500', label: 'Actualizado', textColor: 'text-blue-600 dark:text-blue-400' },
|
|
25
|
+
deleted: { color: 'bg-red-500', label: 'Eliminado', textColor: 'text-red-600 dark:text-red-400' },
|
|
26
|
+
restored: { color: 'bg-amber-500', label: 'Restaurado', textColor: 'text-amber-600 dark:text-amber-400' },
|
|
27
|
+
default: { color: 'bg-muted-foreground', label: 'Evento', textColor: 'text-muted-foreground' },
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const getConfig = (type) => typeConfig[type] ?? typeConfig.default
|
|
31
|
+
|
|
32
|
+
const formatDate = (iso) => {
|
|
33
|
+
if (!iso) return ''
|
|
34
|
+
const d = new Date(iso)
|
|
35
|
+
return d.toLocaleDateString('es-CL', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="px-4 py-3">
|
|
41
|
+
|
|
42
|
+
<!-- Loading -->
|
|
43
|
+
<div v-if="pending" class="flex flex-col gap-3">
|
|
44
|
+
<div v-for="i in 3" :key="i" class="flex gap-3 animate-pulse">
|
|
45
|
+
<div class="flex flex-col items-center gap-1 shrink-0">
|
|
46
|
+
<div class="size-2.5 rounded-full bg-muted mt-1.5" />
|
|
47
|
+
<div class="w-px flex-1 bg-muted min-h-8" />
|
|
48
|
+
</div>
|
|
49
|
+
<div class="space-y-1.5 pb-4 flex-1">
|
|
50
|
+
<div class="h-3 w-24 bg-muted rounded" />
|
|
51
|
+
<div class="h-3 w-40 bg-muted rounded" />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Error -->
|
|
57
|
+
<div v-else-if="error" class="text-center py-6">
|
|
58
|
+
<p class="text-sm text-red-500">No se pudo cargar la bitácora</p>
|
|
59
|
+
<button @click="refresh()" class="mt-2 text-xs text-muted-foreground hover:text-foreground underline">Reintentar</button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<!-- Empty -->
|
|
63
|
+
<div v-else-if="!events.length" class="text-center py-8">
|
|
64
|
+
<p class="text-sm text-muted-foreground">Sin registros en la bitácora</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Timeline -->
|
|
68
|
+
<div v-else class="flex flex-col">
|
|
69
|
+
<div
|
|
70
|
+
v-for="(event, idx) in events"
|
|
71
|
+
:key="event.id ?? idx"
|
|
72
|
+
class="flex gap-3"
|
|
73
|
+
>
|
|
74
|
+
<!-- Dot + line -->
|
|
75
|
+
<div class="flex flex-col items-center shrink-0">
|
|
76
|
+
<div class="size-2.5 rounded-full mt-1.5 shrink-0" :class="getConfig(event.type).color" />
|
|
77
|
+
<div v-if="idx < events.length - 1" class="w-px flex-1 bg-card-line min-h-4 my-1" />
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<!-- Content -->
|
|
81
|
+
<div class="pb-4 flex-1 min-w-0">
|
|
82
|
+
<div class="flex items-baseline gap-2 flex-wrap">
|
|
83
|
+
<span class="text-xs font-semibold" :class="getConfig(event.type).textColor">
|
|
84
|
+
{{ event.action ?? getConfig(event.type).label }}
|
|
85
|
+
</span>
|
|
86
|
+
<span class="text-[10px] text-muted-foreground">{{ formatDate(event.created_at) }}</span>
|
|
87
|
+
</div>
|
|
88
|
+
<p v-if="event.description" class="text-xs text-foreground mt-0.5">{{ event.description }}</p>
|
|
89
|
+
<p v-if="event.user?.name" class="text-[10px] text-muted-foreground mt-0.5">por {{ event.user.name }}</p>
|
|
90
|
+
|
|
91
|
+
<!-- Properties diff -->
|
|
92
|
+
<div v-if="event.properties && Object.keys(event.properties).length" class="mt-1.5 space-y-0.5">
|
|
93
|
+
<div
|
|
94
|
+
v-for="(val, field) in event.properties"
|
|
95
|
+
:key="field"
|
|
96
|
+
class="flex items-center gap-1.5 text-[10px] text-muted-foreground bg-surface rounded px-1.5 py-0.5"
|
|
97
|
+
>
|
|
98
|
+
<span class="font-medium text-foreground">{{ field }}</span>
|
|
99
|
+
<span v-if="val?.old !== undefined">
|
|
100
|
+
<span class="line-through opacity-60">{{ val.old ?? '—' }}</span>
|
|
101
|
+
<span class="mx-1">→</span>
|
|
102
|
+
<span>{{ val.new ?? '—' }}</span>
|
|
103
|
+
</span>
|
|
104
|
+
<span v-else>{{ val }}</span>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
</div>
|
|
112
|
+
</template>
|
|
@@ -17,6 +17,7 @@ const props = defineProps({
|
|
|
17
17
|
showExport: { type: Boolean, default: true },
|
|
18
18
|
filters: { type: Array, default: () => [] },
|
|
19
19
|
splitRatio: { type: Number, default: 60 },
|
|
20
|
+
historyEndpoint: { type: Function, default: null },
|
|
20
21
|
})
|
|
21
22
|
|
|
22
23
|
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
@@ -63,6 +64,13 @@ const previewCacheKey = computed(() => `table-preview-${resolvedName.value}`)
|
|
|
63
64
|
const previewFromCache = ref(false)
|
|
64
65
|
const closePreview = () => { previewRow.value = null }
|
|
65
66
|
|
|
67
|
+
const previewTab = ref('datos')
|
|
68
|
+
const resolvedHistoryEndpoint = computed(() =>
|
|
69
|
+
props.historyEndpoint ? props.historyEndpoint(previewRow.value) : null
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
watch(previewRow, () => { previewTab.value = 'datos' })
|
|
73
|
+
|
|
66
74
|
const handleRowClick = (row) => {
|
|
67
75
|
if (previewEnabled.value) {
|
|
68
76
|
previewRow.value = previewRow.value?.id === row.id ? null : row
|
|
@@ -224,7 +232,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
|
|
|
224
232
|
<!-- Toolbar -->
|
|
225
233
|
<div class="flex flex-wrap items-center gap-3 px-4 py-3 border-b border-card-line">
|
|
226
234
|
<div v-if="showSearch" class="flex-1 min-w-48">
|
|
227
|
-
<Forms.Input v-model="search" type="search" :placeholder="searchPlaceholder" :icon-left="IconSearch" />
|
|
235
|
+
<Forms.Input v-model="search" type="search" :placeholder="searchPlaceholder" :icon-left="IconSearch" size="sm" />
|
|
228
236
|
</div>
|
|
229
237
|
|
|
230
238
|
<div v-if="showFilters && hasFilterableColumns" class="relative">
|
|
@@ -329,8 +337,48 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
|
|
|
329
337
|
@mousedown="startResize"
|
|
330
338
|
/>
|
|
331
339
|
<!-- Preview -->
|
|
332
|
-
<div class="flex flex-col
|
|
333
|
-
|
|
340
|
+
<div class="flex flex-col flex-1 overflow-hidden">
|
|
341
|
+
|
|
342
|
+
<!-- Content -->
|
|
343
|
+
<div class="flex-1 overflow-y-auto">
|
|
344
|
+
<slot v-if="previewTab === 'datos'" name="preview" :row="previewRow" :close="closePreview" />
|
|
345
|
+
<Table.PreviewTimeline
|
|
346
|
+
v-else-if="previewTab === 'bitacora' && resolvedHistoryEndpoint"
|
|
347
|
+
:endpoint="resolvedHistoryEndpoint"
|
|
348
|
+
/>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
<!-- Tabs — bottom -->
|
|
352
|
+
<div v-if="props.historyEndpoint" class="shrink-0 flex border-t border-card-line">
|
|
353
|
+
<button
|
|
354
|
+
type="button"
|
|
355
|
+
@click="previewTab = 'datos'"
|
|
356
|
+
:class="[
|
|
357
|
+
'flex-1 py-2 text-xs font-semibold transition-colors border-r border-card-line',
|
|
358
|
+
previewTab === 'datos'
|
|
359
|
+
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/10'
|
|
360
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
361
|
+
]"
|
|
362
|
+
>
|
|
363
|
+
Datos
|
|
364
|
+
</button>
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
@click="resolvedHistoryEndpoint && (previewTab = 'bitacora')"
|
|
368
|
+
:disabled="!resolvedHistoryEndpoint"
|
|
369
|
+
:class="[
|
|
370
|
+
'flex-1 py-2 text-xs font-semibold transition-colors',
|
|
371
|
+
!resolvedHistoryEndpoint
|
|
372
|
+
? 'text-muted-foreground/40 cursor-not-allowed'
|
|
373
|
+
: previewTab === 'bitacora'
|
|
374
|
+
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/10'
|
|
375
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted-hover'
|
|
376
|
+
]"
|
|
377
|
+
>
|
|
378
|
+
Bitácora
|
|
379
|
+
</button>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
334
382
|
</div>
|
|
335
383
|
</div>
|
|
336
384
|
</Transition>
|