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

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 } 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,149 @@ 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
+ />
57
105
  </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"
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
+ <!-- Column visibility toggle -->
130
+ <button
131
+ type="button"
132
+ @click="showColumnPanel = !showColumnPanel"
133
+ :class="[
134
+ 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
135
+ showColumnPanel
136
+ ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
137
+ : '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'
138
+ ]"
139
+ >
140
+ <IconLayoutColumns class="size-4" />
141
+ Columnas
142
+ </button>
143
+
144
+ <!-- Export -->
145
+ <TableExportable
146
+ v-if="showExport"
147
+ :table-ref="tableRef"
148
+ :name="name"
149
+ :columns="columns"
63
150
  />
64
151
  </div>
65
152
 
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
- ]"
77
- >
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
- ]"
153
+ <!-- Filter panel -->
154
+ <Transition
155
+ enter-active-class="transition ease-out duration-150"
156
+ enter-from-class="opacity-0 -translate-y-2"
157
+ enter-to-class="opacity-100 translate-y-0"
158
+ leave-active-class="transition ease-in duration-100"
159
+ leave-from-class="opacity-100 translate-y-0"
160
+ leave-to-class="opacity-0 -translate-y-2"
98
161
  >
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"
162
+ <div
163
+ v-if="showFilterPanel && hasFilterableColumns"
164
+ class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50"
165
+ >
166
+ <TableFilter v-model="filters" :columns="columns" />
167
+ </div>
168
+ </Transition>
169
+
170
+ <!-- Table -->
171
+ <Table
172
+ ref="tableRef"
173
+ :endpoint="endpoint"
108
174
  :columns="columns"
109
- />
175
+ :name="name"
176
+ :params="mergedParams"
177
+ :search="search"
178
+ :checkable="checkable"
179
+ :cached="cached"
180
+ :show-reload-button="showReloadButton"
181
+ :click-row-to-open="clickRowToOpen"
182
+ @row-click="emit('row-click', $event)"
183
+ @loaded="emit('loaded', $event)"
184
+ >
185
+ <template v-for="(_, name) in $slots" #[name]="slotProps">
186
+ <slot :name="name" v-bind="slotProps ?? {}" />
187
+ </template>
188
+ </Table>
110
189
  </div>
111
190
 
112
- <!-- Filter panel -->
191
+ <!-- Column panel — outside overflow-hidden so never clipped -->
113
192
  <Transition
114
193
  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"
194
+ enter-from-class="opacity-0 translate-y-1 scale-95"
195
+ enter-to-class="opacity-100 translate-y-0 scale-100"
117
196
  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"
197
+ leave-from-class="opacity-100 translate-y-0 scale-100"
198
+ leave-to-class="opacity-0 translate-y-1 scale-95"
120
199
  >
121
200
  <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"
201
+ v-if="showColumnPanel"
202
+ ref="columnPanelRef"
203
+ 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
204
  >
125
- <TableFilter v-model="filters" :columns="columns" />
205
+ <p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-2 px-1">
206
+ Columnas visibles
207
+ </p>
208
+ <div
209
+ v-for="col in orderedColumns"
210
+ :key="col.key"
211
+ draggable="true"
212
+ @dragstart="onDragStart(col.key)"
213
+ @dragover="(e) => onDragOver(e, col.key)"
214
+ @dragleave="onDragLeave"
215
+ @drop="onDrop(col.key)"
216
+ class="flex items-center gap-2 py-1.5 px-2 rounded-lg select-none transition-colors"
217
+ :class="dragOverKey === col.key
218
+ ? 'bg-blue-50 dark:bg-blue-900/20 ring-1 ring-blue-300 dark:ring-blue-700'
219
+ : 'hover:bg-slate-50 dark:hover:bg-slate-700 cursor-grab'"
220
+ >
221
+ <IconGripVertical class="size-4 text-slate-300 dark:text-slate-600 shrink-0" />
222
+ <input
223
+ type="checkbox"
224
+ :checked="tableRef?.table.getColumn(col.key)?.getIsVisible() ?? true"
225
+ @change="tableRef?.table.getColumn(col.key)?.toggleVisibility()"
226
+ @click.stop
227
+ class="rounded border-gray-300 dark:bg-slate-700 dark:border-slate-600 shrink-0 cursor-pointer"
228
+ />
229
+ <span class="text-sm text-slate-700 dark:text-slate-200 truncate">{{ col.label }}</span>
230
+ </div>
126
231
  </div>
127
232
  </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
233
  </div>
149
234
  </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,13 @@ defineExpose({
455
411
  reload: reloadTable,
456
412
  clearCache,
457
413
  table,
458
- showColumnPanel,
459
- toggleColumnPanel,
414
+ setColumnOrder,
460
415
  })
461
416
  </script>
462
417
 
463
418
  <template>
464
419
  <div class="relative">
465
420
 
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
421
  <!-- Table view -->
510
422
  <div v-if="!isGridView" class="overflow-x-auto relative">
511
423
  <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.33",
4
4
  "description": "Innertia Solutions — Spark theme: backoffice, landing and mobile components and layouts",
5
5
  "keywords": [
6
6
  "nuxt",