@innertia-solutions/nuxt-theme-spark 0.1.118 → 0.1.120

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,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>
@@ -63,6 +63,17 @@ const previewCacheKey = computed(() => `table-preview-${resolvedName.value}`)
63
63
  const previewFromCache = ref(false)
64
64
  const closePreview = () => { previewRow.value = null }
65
65
 
66
+ const previewTab = ref('datos')
67
+ const tableMeta = ref(null)
68
+
69
+ const hasHistory = computed(() => !!tableMeta.value?.has_history)
70
+ const resolvedHistoryEndpoint = computed(() => {
71
+ if (!hasHistory.value || !previewRow.value?.id || !tableMeta.value?.entity_type) return null
72
+ return `history/${tableMeta.value.entity_type}/${previewRow.value.id}`
73
+ })
74
+
75
+ watch(previewRow, () => { previewTab.value = 'datos' })
76
+
66
77
  const handleRowClick = (row) => {
67
78
  if (previewEnabled.value) {
68
79
  previewRow.value = previewRow.value?.id === row.id ? null : row
@@ -81,6 +92,7 @@ watch(previewRow, (row) => {
81
92
  // When data reloads, update previewRow with fresh data — close silently if deleted
82
93
  const handleLoaded = (res) => {
83
94
  emit('loaded', res)
95
+ if (res?.meta) tableMeta.value = res.meta
84
96
  if (previewRow.value && Array.isArray(res?.data)) {
85
97
  const fresh = res.data.find(r => r.id === previewRow.value.id)
86
98
  if (fresh) previewRow.value = fresh
@@ -329,8 +341,48 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
329
341
  @mousedown="startResize"
330
342
  />
331
343
  <!-- Preview -->
332
- <div class="flex flex-col overflow-y-auto flex-1">
333
- <slot name="preview" :row="previewRow" :close="closePreview" />
344
+ <div class="flex flex-col flex-1 overflow-hidden">
345
+
346
+ <!-- Content -->
347
+ <div class="flex-1 overflow-y-auto">
348
+ <slot v-if="previewTab === 'datos'" name="preview" :row="previewRow" :close="closePreview" />
349
+ <Table.PreviewTimeline
350
+ v-else-if="previewTab === 'bitacora' && resolvedHistoryEndpoint"
351
+ :endpoint="resolvedHistoryEndpoint"
352
+ />
353
+ </div>
354
+
355
+ <!-- Tabs — bottom -->
356
+ <div v-if="hasHistory" class="shrink-0 flex border-t border-card-line">
357
+ <button
358
+ type="button"
359
+ @click="previewTab = 'datos'"
360
+ :class="[
361
+ 'flex-1 py-2 text-xs font-semibold transition-colors border-r border-card-line',
362
+ previewTab === 'datos'
363
+ ? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/10'
364
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted-hover'
365
+ ]"
366
+ >
367
+ Datos
368
+ </button>
369
+ <button
370
+ type="button"
371
+ @click="resolvedHistoryEndpoint && (previewTab = 'bitacora')"
372
+ :disabled="!resolvedHistoryEndpoint"
373
+ :class="[
374
+ 'flex-1 py-2 text-xs font-semibold transition-colors',
375
+ !resolvedHistoryEndpoint
376
+ ? 'text-muted-foreground/40 cursor-not-allowed'
377
+ : previewTab === 'bitacora'
378
+ ? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/10'
379
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted-hover'
380
+ ]"
381
+ >
382
+ Bitácora
383
+ </button>
384
+ </div>
385
+
334
386
  </div>
335
387
  </div>
336
388
  </Transition>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innertia-solutions/nuxt-theme-spark",
3
- "version": "0.1.118",
3
+ "version": "0.1.120",
4
4
  "description": "Innertia Solutions — Spark theme: backoffice, landing and mobile components and layouts",
5
5
  "keywords": [
6
6
  "nuxt",