@innertia-solutions/nuxt-theme-spark 0.1.32 → 0.1.34

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.
@@ -1,7 +1,6 @@
1
1
  <script setup>
2
- import { IconSearch, IconAdjustmentsHorizontal, IconLayoutColumns } from '@tabler/icons-vue'
2
+ import { IconSearch, IconAdjustmentsHorizontal, IconLayoutColumns, IconGripVertical, IconBolt, IconReload } from '@tabler/icons-vue'
3
3
 
4
- // Standard admin table: search + filters + Table (TanStack) + export
5
4
  const props = defineProps({
6
5
  endpoint: { type: String, required: true },
7
6
  columns: { type: Array, required: true },
@@ -32,12 +31,52 @@ const activeFilterCount = computed(() =>
32
31
  Object.values(filters.value).filter(v => v !== null && v !== undefined && v !== '').length
33
32
  )
34
33
 
35
- // Merge filters into params
36
34
  const mergedParams = computed(() => ({
37
35
  ...props.params,
38
36
  ...filters.value,
39
37
  }))
40
38
 
39
+ // ─── Column panel ─────────────────────────────────────────────────────────────
40
+ const showColumnPanel = ref(false)
41
+ const columnPanelRef = ref(null)
42
+
43
+ const orderedColumns = computed(() => {
44
+ if (!tableRef.value) return props.columns
45
+ const ids = tableRef.value.table.getAllLeafColumns().map(c => c.id)
46
+ return ids.map(id => props.columns.find(c => c.key === id)).filter(Boolean)
47
+ })
48
+
49
+ let draggedKey = null
50
+ const dragOverKey = ref(null)
51
+
52
+ const onDragStart = (key) => { draggedKey = key }
53
+ const onDragOver = (e, key) => { e.preventDefault(); dragOverKey.value = key }
54
+ const onDragLeave = () => { dragOverKey.value = null }
55
+ const onDrop = (key) => {
56
+ if (!draggedKey || draggedKey === key) return
57
+ const ids = tableRef.value?.table.getAllLeafColumns().map(c => c.id) ?? []
58
+ const from = ids.indexOf(draggedKey)
59
+ const to = ids.indexOf(key)
60
+ if (from < 0 || to < 0) return
61
+ ids.splice(from, 1)
62
+ ids.splice(to, 0, draggedKey)
63
+ tableRef.value?.setColumnOrder(ids)
64
+ draggedKey = null
65
+ dragOverKey.value = null
66
+ }
67
+
68
+ const onPanelOutsideClick = (e) => {
69
+ if (columnPanelRef.value && !columnPanelRef.value.contains(e.target)) {
70
+ showColumnPanel.value = false
71
+ }
72
+ }
73
+
74
+ watch(showColumnPanel, (v) => {
75
+ if (v) document.addEventListener('mousedown', onPanelOutsideClick)
76
+ else document.removeEventListener('mousedown', onPanelOutsideClick)
77
+ })
78
+
79
+ // ─── Expose ───────────────────────────────────────────────────────────────────
41
80
  const getSelectedRows = () => tableRef.value?.getSelectedRows()
42
81
  const reload = () => tableRef.value?.reload()
43
82
  const clearCache = () => tableRef.value?.clearCache()
@@ -47,103 +86,174 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
47
86
  </script>
48
87
 
49
88
  <template>
50
- <div class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
51
- <!-- Toolbar -->
52
- <div class="flex flex-wrap items-center gap-3 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
53
- <!-- Search -->
54
- <div v-if="showSearch" class="relative flex-1 min-w-48">
55
- <div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
56
- <IconSearch class="size-4 text-slate-400" stroke="1.5" />
89
+ <div class="relative">
90
+ <!-- Card -->
91
+ <div class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
92
+ <!-- Toolbar -->
93
+ <div class="flex flex-wrap items-center gap-3 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
94
+ <!-- Search -->
95
+ <div v-if="showSearch" class="relative flex-1 min-w-48">
96
+ <div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
97
+ <IconSearch class="size-4 text-slate-400" stroke="1.5" />
98
+ </div>
99
+ <input
100
+ v-model="search"
101
+ type="search"
102
+ :placeholder="searchPlaceholder"
103
+ class="block w-full rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-white py-1.5 ps-9 pe-4 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
104
+ />
105
+ </div>
106
+
107
+ <!-- Filter toggle -->
108
+ <button
109
+ v-if="showFilters && hasFilterableColumns"
110
+ type="button"
111
+ @click="showFilterPanel = !showFilterPanel"
112
+ :class="[
113
+ 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
114
+ showFilterPanel || activeFilterCount > 0
115
+ ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
116
+ : 'border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700'
117
+ ]"
118
+ >
119
+ <IconAdjustmentsHorizontal class="size-4" stroke="1.5" />
120
+ Filtros
121
+ <span
122
+ v-if="activeFilterCount > 0"
123
+ class="inline-flex items-center justify-center size-5 rounded-full bg-blue-600 text-white text-xs font-bold"
124
+ >{{ activeFilterCount }}</span>
125
+ </button>
126
+
127
+ <slot name="actions" />
128
+
129
+ <!-- Reload -->
130
+ <button
131
+ type="button"
132
+ @click="reload()"
133
+ class="py-1.5 px-2.5 inline-flex items-center gap-2 text-sm font-medium rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
134
+ title="Recargar"
135
+ >
136
+ <IconReload class="size-4" />
137
+ </button>
138
+
139
+ <!-- Cache badge -->
140
+ <div v-if="tableRef?.isDataFromCache && tableRef?.cached" class="group relative flex items-center">
141
+ <div class="flex items-center gap-x-1.5 py-1.5 px-2.5 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 rounded-lg cursor-help hover:bg-emerald-500/20 transition-colors border border-emerald-200 dark:border-emerald-800">
142
+ <IconBolt class="size-3.5 fill-current" />
143
+ <span class="text-[10px] font-bold uppercase tracking-wider">Instant</span>
144
+ </div>
145
+ <div class="absolute top-full mt-2 right-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">
146
+ <div class="font-bold mb-1 flex items-center gap-x-1.5 text-emerald-400">
147
+ <IconBolt class="size-3" /> Datos en Caché
148
+ </div>
149
+ Cargados desde memoria local. Recarga para sincronizar.
150
+ <div class="absolute bottom-full right-4 mb-[-1px] border-4 border-transparent border-b-slate-900"></div>
151
+ </div>
57
152
  </div>
58
- <input
59
- v-model="search"
60
- type="search"
61
- :placeholder="searchPlaceholder"
62
- class="block w-full rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-white py-1.5 ps-9 pe-4 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
153
+
154
+ <!-- Column visibility toggle -->
155
+ <button
156
+ type="button"
157
+ @click="showColumnPanel = !showColumnPanel"
158
+ :class="[
159
+ 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
160
+ showColumnPanel
161
+ ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
162
+ : 'border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700'
163
+ ]"
164
+ >
165
+ <IconLayoutColumns class="size-4" />
166
+ Columnas
167
+ </button>
168
+
169
+ <!-- Export -->
170
+ <TableExportable
171
+ v-if="showExport"
172
+ :table-ref="tableRef"
173
+ :name="name"
174
+ :columns="columns"
63
175
  />
64
176
  </div>
65
177
 
66
- <!-- Filter toggle -->
67
- <button
68
- v-if="showFilters && hasFilterableColumns"
69
- type="button"
70
- @click="showFilterPanel = !showFilterPanel"
71
- :class="[
72
- 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
73
- showFilterPanel || activeFilterCount > 0
74
- ? 'border-indigo-500 bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:border-indigo-500 dark:text-indigo-300'
75
- : 'border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700'
76
- ]"
178
+ <!-- Filter panel -->
179
+ <Transition
180
+ enter-active-class="transition ease-out duration-150"
181
+ enter-from-class="opacity-0 -translate-y-2"
182
+ enter-to-class="opacity-100 translate-y-0"
183
+ leave-active-class="transition ease-in duration-100"
184
+ leave-from-class="opacity-100 translate-y-0"
185
+ leave-to-class="opacity-0 -translate-y-2"
77
186
  >
78
- <IconAdjustmentsHorizontal class="size-4" stroke="1.5" />
79
- Filtros
80
- <span
81
- v-if="activeFilterCount > 0"
82
- class="inline-flex items-center justify-center size-5 rounded-full bg-indigo-600 text-white text-xs font-bold"
83
- >{{ activeFilterCount }}</span>
84
- </button>
85
-
86
- <slot name="actions" />
87
-
88
- <!-- Column visibility -->
89
- <button
90
- type="button"
91
- @click="tableRef?.toggleColumnPanel()"
92
- :class="[
93
- 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
94
- tableRef?.showColumnPanel
95
- ? 'border-indigo-500 bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:border-indigo-500 dark:text-indigo-300'
96
- : 'border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700'
97
- ]"
98
- >
99
- <IconLayoutColumns class="size-4" />
100
- Columnas
101
- </button>
102
-
103
- <!-- Export -->
104
- <TableExportable
105
- v-if="showExport"
106
- :table-ref="tableRef"
107
- :name="name"
187
+ <div
188
+ v-if="showFilterPanel && hasFilterableColumns"
189
+ class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50"
190
+ >
191
+ <TableFilter v-model="filters" :columns="columns" />
192
+ </div>
193
+ </Transition>
194
+
195
+ <!-- Table -->
196
+ <Table
197
+ ref="tableRef"
198
+ :endpoint="endpoint"
108
199
  :columns="columns"
109
- />
200
+ :name="name"
201
+ :params="mergedParams"
202
+ :search="search"
203
+ :checkable="checkable"
204
+ :cached="cached"
205
+ :show-reload-button="showReloadButton"
206
+ :click-row-to-open="clickRowToOpen"
207
+ @row-click="emit('row-click', $event)"
208
+ @loaded="emit('loaded', $event)"
209
+ >
210
+ <template v-for="(_, name) in $slots" #[name]="slotProps">
211
+ <slot :name="name" v-bind="slotProps ?? {}" />
212
+ </template>
213
+ </Table>
110
214
  </div>
111
215
 
112
- <!-- Filter panel -->
216
+ <!-- Column panel — outside overflow-hidden so never clipped -->
113
217
  <Transition
114
218
  enter-active-class="transition ease-out duration-150"
115
- enter-from-class="opacity-0 -translate-y-2"
116
- enter-to-class="opacity-100 translate-y-0"
219
+ enter-from-class="opacity-0 translate-y-1 scale-95"
220
+ enter-to-class="opacity-100 translate-y-0 scale-100"
117
221
  leave-active-class="transition ease-in duration-100"
118
- leave-from-class="opacity-100 translate-y-0"
119
- leave-to-class="opacity-0 -translate-y-2"
222
+ leave-from-class="opacity-100 translate-y-0 scale-100"
223
+ leave-to-class="opacity-0 translate-y-1 scale-95"
120
224
  >
121
225
  <div
122
- v-if="showFilterPanel && hasFilterableColumns"
123
- class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50"
226
+ v-if="showColumnPanel"
227
+ ref="columnPanelRef"
228
+ class="absolute top-12 right-0 z-50 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-2xl p-3 min-w-56 max-h-80 overflow-y-auto"
124
229
  >
125
- <TableFilter v-model="filters" :columns="columns" />
230
+ <p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-2 px-1">
231
+ Columnas visibles
232
+ </p>
233
+ <div
234
+ v-for="col in orderedColumns"
235
+ :key="col.key"
236
+ draggable="true"
237
+ @dragstart="onDragStart(col.key)"
238
+ @dragover="(e) => onDragOver(e, col.key)"
239
+ @dragleave="onDragLeave"
240
+ @drop="onDrop(col.key)"
241
+ class="flex items-center gap-2 py-1.5 px-2 rounded-lg select-none transition-colors"
242
+ :class="dragOverKey === col.key
243
+ ? 'bg-blue-50 dark:bg-blue-900/20 ring-1 ring-blue-300 dark:ring-blue-700'
244
+ : 'hover:bg-slate-50 dark:hover:bg-slate-700 cursor-grab'"
245
+ >
246
+ <IconGripVertical class="size-4 text-slate-300 dark:text-slate-600 shrink-0" />
247
+ <input
248
+ type="checkbox"
249
+ :checked="tableRef?.table.getColumn(col.key)?.getIsVisible() ?? true"
250
+ @change="tableRef?.table.getColumn(col.key)?.toggleVisibility()"
251
+ @click.stop
252
+ class="rounded border-gray-300 dark:bg-slate-700 dark:border-slate-600 shrink-0 cursor-pointer"
253
+ />
254
+ <span class="text-sm text-slate-700 dark:text-slate-200 truncate">{{ col.label }}</span>
255
+ </div>
126
256
  </div>
127
257
  </Transition>
128
-
129
- <!-- Table -->
130
- <Table
131
- ref="tableRef"
132
- :endpoint="endpoint"
133
- :columns="columns"
134
- :name="name"
135
- :params="mergedParams"
136
- :search="search"
137
- :checkable="checkable"
138
- :cached="cached"
139
- :show-reload-button="showReloadButton"
140
- :click-row-to-open="clickRowToOpen"
141
- @row-click="emit('row-click', $event)"
142
- @loaded="emit('loaded', $event)"
143
- >
144
- <template v-for="(_, name) in $slots" #[name]="slotProps">
145
- <slot :name="name" v-bind="slotProps ?? {}" />
146
- </template>
147
- </Table>
148
258
  </div>
149
259
  </template>
@@ -6,8 +6,6 @@ import {
6
6
  IconSortDescendingSmallBig,
7
7
  IconReload,
8
8
  IconBolt,
9
- IconLayoutColumns,
10
- IconGripVertical,
11
9
  } from '@tabler/icons-vue'
12
10
 
13
11
  const props = defineProps({
@@ -285,47 +283,7 @@ onBeforeUnmount(() => {
285
283
  })
286
284
 
287
285
  // ─── Column settings panel ────────────────────────────────────────────────────
288
- const showColumnPanel = ref(false)
289
- const columnPanelRef = ref(null)
290
-
291
- const orderedColumns = computed(() => {
292
- if (!columnOrder.value.length) return props.columns
293
- return [...props.columns].sort((a, b) => {
294
- const ia = columnOrder.value.indexOf(a.key)
295
- const ib = columnOrder.value.indexOf(b.key)
296
- return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib)
297
- })
298
- })
299
-
300
- let draggedPanelKey = null
301
- const dragOverPanelKey = ref(null)
302
-
303
- const onPanelDragStart = (key) => { draggedPanelKey = key }
304
- const onPanelDragOver = (e, key) => { e.preventDefault(); dragOverPanelKey.value = key }
305
- const onPanelDragLeave = () => { dragOverPanelKey.value = null }
306
- const onPanelDrop = (key) => {
307
- if (!draggedPanelKey || draggedPanelKey === key) return
308
- const order = [...columnOrder.value]
309
- const from = order.indexOf(draggedPanelKey)
310
- const to = order.indexOf(key)
311
- if (from < 0 || to < 0) return
312
- order.splice(from, 1)
313
- order.splice(to, 0, draggedPanelKey)
314
- columnOrder.value = order
315
- draggedPanelKey = null
316
- dragOverPanelKey.value = null
317
- }
318
-
319
- const handlePanelOutsideClick = (e) => {
320
- if (columnPanelRef.value && !columnPanelRef.value.contains(e.target)) {
321
- showColumnPanel.value = false
322
- }
323
- }
324
-
325
- watch(showColumnPanel, (v) => {
326
- if (v) document.addEventListener('mousedown', handlePanelOutsideClick)
327
- else document.removeEventListener('mousedown', handlePanelOutsideClick)
328
- })
286
+ const setColumnOrder = (order) => { columnOrder.value = order }
329
287
 
330
288
  // ─── Header drag reorder ──────────────────────────────────────────────────────
331
289
  let draggedHeaderId = null
@@ -446,8 +404,6 @@ const reloadTable = () => {
446
404
  fetchData()
447
405
  }
448
406
 
449
- const toggleColumnPanel = () => { showColumnPanel.value = !showColumnPanel.value }
450
-
451
407
  defineExpose({
452
408
  getSelectedRows,
453
409
  loading,
@@ -455,57 +411,15 @@ defineExpose({
455
411
  reload: reloadTable,
456
412
  clearCache,
457
413
  table,
458
- showColumnPanel,
459
- toggleColumnPanel,
414
+ setColumnOrder,
415
+ isDataFromCache,
416
+ cached: computed(() => props.cached),
460
417
  })
461
418
  </script>
462
419
 
463
420
  <template>
464
421
  <div class="relative">
465
422
 
466
- <!-- Column settings panel -->
467
- <Transition
468
- enter-active-class="transition ease-out duration-150"
469
- enter-from-class="opacity-0 translate-y-1 scale-95"
470
- enter-to-class="opacity-100 translate-y-0 scale-100"
471
- leave-active-class="transition ease-in duration-100"
472
- leave-from-class="opacity-100 translate-y-0 scale-100"
473
- leave-to-class="opacity-0 translate-y-1 scale-95"
474
- >
475
- <div
476
- v-if="showColumnPanel"
477
- ref="columnPanelRef"
478
- class="absolute top-0 right-0 z-50 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-2xl p-3 min-w-56 max-h-80 overflow-y-auto"
479
- >
480
- <p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-2 px-1">
481
- Columnas visibles
482
- </p>
483
- <div
484
- v-for="col in orderedColumns"
485
- :key="col.key"
486
- draggable="true"
487
- @dragstart="onPanelDragStart(col.key)"
488
- @dragover="(e) => onPanelDragOver(e, col.key)"
489
- @dragleave="onPanelDragLeave"
490
- @drop="onPanelDrop(col.key)"
491
- class="flex items-center gap-2 py-1.5 px-2 rounded-lg select-none transition-colors"
492
- :class="dragOverPanelKey === col.key
493
- ? 'bg-indigo-50 dark:bg-indigo-900/20 ring-1 ring-indigo-300 dark:ring-indigo-700'
494
- : 'hover:bg-slate-50 dark:hover:bg-slate-700 cursor-grab'"
495
- >
496
- <IconGripVertical class="size-4 text-slate-300 dark:text-slate-600 shrink-0" />
497
- <input
498
- type="checkbox"
499
- :checked="table.getColumn(col.key)?.getIsVisible() ?? true"
500
- @change="table.getColumn(col.key)?.toggleVisibility()"
501
- @click.stop
502
- class="rounded border-gray-300 dark:bg-slate-700 dark:border-slate-600 shrink-0 cursor-pointer"
503
- />
504
- <span class="text-sm text-slate-700 dark:text-slate-200 truncate">{{ col.label }}</span>
505
- </div>
506
- </div>
507
- </Transition>
508
-
509
423
  <!-- Table view -->
510
424
  <div v-if="!isGridView" class="overflow-x-auto relative">
511
425
  <table
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innertia-solutions/nuxt-theme-spark",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Innertia Solutions — Spark theme: backoffice, landing and mobile components and layouts",
5
5
  "keywords": [
6
6
  "nuxt",