@innertia-solutions/nuxt-theme-spark 0.1.73 → 0.1.75

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.
@@ -24,6 +24,7 @@ const slots = useSlots()
24
24
  const search = ref('')
25
25
  const activeFilters = ref({})
26
26
  const showFilterPanel = ref(false)
27
+ const filterPanelRef = ref(null)
27
28
  const tableRef = ref(null)
28
29
 
29
30
  // ─── Filter config ─────────────────────────────────────────────────────────────
@@ -51,10 +52,8 @@ const previewEnabled = ref(false)
51
52
  const closePreview = () => { previewRow.value = null }
52
53
 
53
54
  const handleRowClick = (row) => {
54
- console.log('[Standard] handleRowClick | previewEnabled:', previewEnabled.value, '| row:', row?.name ?? row?.id)
55
55
  if (previewEnabled.value) {
56
56
  previewRow.value = previewRow.value?.id === row.id ? null : row
57
- console.log('[Standard] previewRow =', previewRow.value?.name ?? previewRow.value?.id)
58
57
  } else {
59
58
  emit('row-click', row)
60
59
  }
@@ -79,7 +78,6 @@ const startResize = (e) => {
79
78
  const onEsc = (e) => { if (e.key === 'Escape' && previewRow.value) closePreview() }
80
79
  onMounted(() => {
81
80
  previewEnabled.value = !!slots.preview
82
- console.log('[Standard] mounted | previewEnabled:', previewEnabled.value, '| slot keys:', Object.keys(slots))
83
81
  window.addEventListener('keydown', onEsc)
84
82
  })
85
83
  onBeforeUnmount(() => window.removeEventListener('keydown', onEsc))
@@ -115,15 +113,24 @@ const onDrop = (key) => {
115
113
  dragOverKey.value = null
116
114
  }
117
115
 
118
- const onPanelOutsideClick = (e) => {
116
+ const onColumnPanelOutsideClick = (e) => {
119
117
  if (columnPanelRef.value && !columnPanelRef.value.contains(e.target)) {
120
118
  showColumnPanel.value = false
121
119
  }
122
120
  }
121
+ const onFilterPanelOutsideClick = (e) => {
122
+ if (filterPanelRef.value && !filterPanelRef.value.contains(e.target)) {
123
+ showFilterPanel.value = false
124
+ }
125
+ }
123
126
 
124
127
  watch(showColumnPanel, (v) => {
125
- if (v) document.addEventListener('mousedown', onPanelOutsideClick)
126
- else document.removeEventListener('mousedown', onPanelOutsideClick)
128
+ if (v) document.addEventListener('mousedown', onColumnPanelOutsideClick)
129
+ else document.removeEventListener('mousedown', onColumnPanelOutsideClick)
130
+ })
131
+ watch(showFilterPanel, (v) => {
132
+ if (v) document.addEventListener('mousedown', onFilterPanelOutsideClick)
133
+ else document.removeEventListener('mousedown', onFilterPanelOutsideClick)
127
134
  })
128
135
 
129
136
  // ─── Expose ───────────────────────────────────────────────────────────────────
@@ -137,140 +144,119 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
137
144
 
138
145
  <template>
139
146
  <div class="relative" ref="containerRef">
140
- <div :class="previewRow && previewEnabled ? 'flex items-stretch gap-3' : ''">
141
147
 
142
- <!-- Card -->
143
- <div
144
- class="relative bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-sm overflow-hidden"
145
- :style="previewRow && previewEnabled ? { width: currentRatio + '%', minWidth: 0, flexShrink: 0 } : {}"
146
- >
147
- <!-- Toolbar -->
148
- <div class="flex flex-wrap items-center gap-3 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
149
- <!-- Search -->
150
- <div v-if="showSearch" class="flex-1 min-w-48">
151
- <Forms.Input
152
- v-model="search"
153
- type="search"
154
- :placeholder="searchPlaceholder"
155
- :icon-left="IconSearch"
156
- />
157
- </div>
148
+ <!-- Card único -->
149
+ <div class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-sm overflow-hidden">
158
150
 
159
- <!-- Filter toggle -->
160
- <button
161
- v-if="showFilters && hasFilterableColumns"
162
- type="button"
163
- @click="showFilterPanel = !showFilterPanel"
164
- :class="[
165
- 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
166
- showFilterPanel || activeFilterCount > 0
167
- ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
168
- : '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'
169
- ]"
170
- >
171
- <IconAdjustmentsHorizontal class="size-4" stroke="1.5" />
172
- Filtros
173
- <span
174
- v-if="activeFilterCount > 0"
175
- class="inline-flex items-center justify-center size-5 rounded-full bg-blue-600 text-white text-xs font-bold"
176
- >{{ activeFilterCount }}</span>
177
- </button>
178
-
179
- <slot name="actions" />
180
-
181
- <!-- Column visibility toggle -->
182
- <button
183
- type="button"
184
- @click="showColumnPanel = !showColumnPanel"
185
- :class="[
186
- 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
187
- showColumnPanel
188
- ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
189
- : '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'
190
- ]"
191
- >
192
- <IconLayoutColumns class="size-4" />
193
- Columnas
194
- </button>
195
-
196
- <!-- Export -->
197
- <TableExportable
198
- v-if="showExport"
199
- :table-ref="tableRef"
200
- :name="name"
201
- :columns="columns"
202
- />
151
+ <!-- Toolbar -->
152
+ <div class="flex flex-wrap items-center gap-3 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
153
+ <div v-if="showSearch" class="flex-1 min-w-48">
154
+ <Forms.Input v-model="search" type="search" :placeholder="searchPlaceholder" :icon-left="IconSearch" />
203
155
  </div>
204
156
 
205
- <!-- Filter panel -->
206
- <Transition
207
- enter-active-class="transition ease-out duration-150"
208
- enter-from-class="opacity-0 -translate-y-2"
209
- enter-to-class="opacity-100 translate-y-0"
210
- leave-active-class="transition ease-in duration-100"
211
- leave-from-class="opacity-100 translate-y-0"
212
- leave-to-class="opacity-0 -translate-y-2"
157
+ <button
158
+ v-if="showFilters && hasFilterableColumns"
159
+ type="button"
160
+ @click="showFilterPanel = !showFilterPanel"
161
+ :class="[
162
+ 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
163
+ showFilterPanel || activeFilterCount > 0
164
+ ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
165
+ : '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'
166
+ ]"
213
167
  >
214
- <div
215
- v-if="showFilterPanel && hasFilterableColumns"
216
- class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50"
217
- >
218
- <TableFilter v-model="activeFilters" :columns="filtersConfig" />
219
- </div>
220
- </Transition>
221
-
222
- <!-- Table -->
223
- <Table
224
- ref="tableRef"
225
- :endpoint="endpoint"
226
- :columns="columns"
227
- :name="name"
228
- :params="mergedParams"
229
- :search="search"
230
- :checkable="checkable"
231
- :cached="cached"
232
- :show-reload-button="showReloadButton"
233
- :click-row-to-open="clickRowToOpen"
234
- :preview-row-id="previewRow?.id ?? null"
235
- :preview-mode="!!previewEnabled"
236
- @row-click="handleRowClick"
237
- @loaded="emit('loaded', $event)"
168
+ <IconAdjustmentsHorizontal class="size-4" stroke="1.5" />
169
+ Filtros{{ activeFilterCount > 0 ? ` (${activeFilterCount})` : '' }}
170
+ </button>
171
+
172
+ <slot name="actions" />
173
+
174
+ <button
175
+ type="button"
176
+ @click="showColumnPanel = !showColumnPanel"
177
+ :class="[
178
+ 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
179
+ showColumnPanel
180
+ ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
181
+ : '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'
182
+ ]"
238
183
  >
239
- <template v-for="(_, name) in $slots" #[name]="slotProps">
240
- <slot :name="name" v-bind="slotProps ?? {}" />
241
- </template>
242
- </Table>
243
- </div>
184
+ <IconLayoutColumns class="size-4" />
185
+ Columnas
186
+ </button>
244
187
 
245
- <!-- Resize handle -->
246
- <div
247
- v-if="previewRow && previewEnabled"
248
- class="w-3 flex items-center justify-center cursor-col-resize shrink-0 group"
249
- @mousedown="startResize"
250
- >
251
- <div class="w-px h-12 bg-slate-200 dark:bg-slate-600 rounded-full group-hover:bg-indigo-400 dark:group-hover:bg-indigo-500 transition-colors" />
188
+ <TableExportable v-if="showExport" :table-ref="tableRef" :name="name" :columns="columns" />
252
189
  </div>
253
190
 
254
- <!-- Preview panel -->
255
- <Transition
256
- enter-active-class="transition ease-out duration-300"
257
- enter-from-class="opacity-0 translate-x-4"
258
- enter-to-class="opacity-100 translate-x-0"
259
- leave-active-class="transition ease-in duration-200"
260
- leave-from-class="opacity-100 translate-x-0"
261
- leave-to-class="opacity-0 translate-x-4"
262
- >
191
+ <!-- Contenido: tabla + preview en flex -->
192
+ <div :class="previewRow && previewEnabled ? 'flex items-stretch' : ''">
193
+
194
+ <!-- Tabla -->
263
195
  <div
264
- v-if="previewRow && previewEnabled"
265
- class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-sm overflow-hidden flex flex-col"
266
- :style="{ width: (100 - currentRatio) + '%', minWidth: 0, flexShrink: 0 }"
196
+ :style="previewRow && previewEnabled ? { width: currentRatio + '%', flexShrink: 0, minWidth: 0 } : {}"
267
197
  >
268
- <slot name="preview" :row="previewRow" :close="closePreview" />
198
+ <Table
199
+ ref="tableRef"
200
+ :endpoint="endpoint"
201
+ :columns="columns"
202
+ :name="name"
203
+ :params="mergedParams"
204
+ :search="search"
205
+ :checkable="checkable"
206
+ :cached="cached"
207
+ :show-reload-button="showReloadButton"
208
+ :click-row-to-open="clickRowToOpen"
209
+ :preview-row-id="previewRow?.id ?? null"
210
+ :preview-mode="!!previewEnabled"
211
+ @row-click="handleRowClick"
212
+ @loaded="emit('loaded', $event)"
213
+ >
214
+ <template v-for="(_, name) in $slots" #[name]="slotProps">
215
+ <slot :name="name" v-bind="slotProps ?? {}" />
216
+ </template>
217
+ </Table>
269
218
  </div>
270
- </Transition>
271
219
 
220
+ <!-- Divider + preview -->
221
+ <template v-if="previewRow && previewEnabled">
222
+ <!-- Resize handle -->
223
+ <div
224
+ class="w-1 shrink-0 cursor-col-resize bg-slate-100 dark:bg-slate-700/60 hover:bg-indigo-300 dark:hover:bg-indigo-600 transition-colors"
225
+ @mousedown="startResize"
226
+ />
227
+ <!-- Preview -->
228
+ <div
229
+ class="flex flex-col overflow-y-auto border-l border-slate-200 dark:border-slate-700"
230
+ :style="{ width: (100 - currentRatio) + '%', flexShrink: 0, minWidth: 0 }"
231
+ >
232
+ <slot name="preview" :row="previewRow" :close="closePreview" />
233
+ </div>
234
+ </template>
235
+
236
+ </div>
272
237
  </div>
273
238
 
239
+ <!-- Filter panel — outside overflow-hidden so never clipped -->
240
+ <Transition
241
+ enter-active-class="transition ease-out duration-150"
242
+ enter-from-class="opacity-0 translate-y-1 scale-95"
243
+ enter-to-class="opacity-100 translate-y-0 scale-100"
244
+ leave-active-class="transition ease-in duration-100"
245
+ leave-from-class="opacity-100 translate-y-0 scale-100"
246
+ leave-to-class="opacity-0 translate-y-1 scale-95"
247
+ >
248
+ <div
249
+ v-if="showFilterPanel && hasFilterableColumns"
250
+ ref="filterPanelRef"
251
+ class="absolute top-12 left-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-64 max-h-96 overflow-y-auto"
252
+ >
253
+ <p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-3 px-1">
254
+ Filtros
255
+ </p>
256
+ <TableFilter v-model="activeFilters" :columns="filtersConfig" />
257
+ </div>
258
+ </Transition>
259
+
274
260
  <!-- Column panel — outside overflow-hidden so never clipped -->
275
261
  <Transition
276
262
  enter-active-class="transition ease-out duration-150"
@@ -318,13 +318,11 @@ const measureText = (text, font) => {
318
318
 
319
319
  const autoSizeColumn = (header) => {
320
320
  const colId = header.column.id
321
- const pad = 32 // px-4 on each side
321
+ const pad = 32
322
322
 
323
- // Measure header label
324
323
  const label = header.column.columnDef.meta?.label ?? header.id
325
- let max = measureText(label, '500 12px ui-sans-serif,system-ui,sans-serif') + pad + 20 // +20 for sort icon
324
+ let max = measureText(label, '500 12px ui-sans-serif,system-ui,sans-serif') + pad + 20
326
325
 
327
- // Measure all visible data cells
328
326
  if (tableBodyRef.value) {
329
327
  tableBodyRef.value.querySelectorAll(`td[data-col-id="${colId}"]`).forEach(td => {
330
328
  const w = measureText(td.textContent?.trim(), '14px ui-sans-serif,system-ui,sans-serif') + pad
@@ -332,7 +330,7 @@ const autoSizeColumn = (header) => {
332
330
  })
333
331
  }
334
332
 
335
- header.column.setSize(Math.ceil(max))
333
+ table.setColumnSizing(prev => ({ ...prev, [colId]: Math.ceil(max) }))
336
334
  }
337
335
 
338
336
  const onHeaderDragStart = (colId) => { draggedHeaderId = colId }
@@ -1,17 +1,12 @@
1
1
  <script setup>
2
- // Reusable filter panel — reads filterType from column definitions
3
- // Column definition: { key, label, filterType: 'text' | 'select' | 'daterange', filterOptions?: [{value, label}] }
4
-
5
2
  const props = defineProps({
6
- modelValue: { type: Object, default: () => ({}) }, // { [key]: value }
7
- columns: { type: Array, required: true },
3
+ modelValue: { type: Object, default: () => ({}) },
4
+ columns: { type: Array, required: true },
8
5
  })
9
6
 
10
7
  const emit = defineEmits(['update:modelValue'])
11
8
 
12
- const filterableColumns = computed(() =>
13
- props.columns.filter(c => c.filterType)
14
- )
9
+ const filterableColumns = computed(() => props.columns.filter(c => c.filterType))
15
10
 
16
11
  const localFilters = ref({ ...props.modelValue })
17
12
 
@@ -20,7 +15,7 @@ watch(() => props.modelValue, (v) => {
20
15
  }, { deep: true })
21
16
 
22
17
  const updateFilter = (key, value) => {
23
- localFilters.value[key] = value
18
+ localFilters.value[key] = value || null
24
19
  emit('update:modelValue', { ...localFilters.value })
25
20
  }
26
21
 
@@ -35,66 +30,64 @@ const activeCount = computed(() =>
35
30
  </script>
36
31
 
37
32
  <template>
38
- <div class="flex flex-wrap items-end gap-3">
33
+ <div class="space-y-3">
39
34
  <template v-for="col in filterableColumns" :key="col.key">
40
- <!-- text filter -->
41
- <div v-if="col.filterType === 'text'" class="flex flex-col gap-1 min-w-40">
42
- <label class="text-xs text-slate-500 dark:text-slate-400">{{ col.label }}</label>
35
+
36
+ <!-- text -->
37
+ <div v-if="col.filterType === 'text'">
38
+ <label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">{{ col.label }}</label>
43
39
  <input
44
40
  type="text"
45
41
  :value="localFilters[col.key] ?? ''"
46
42
  @input="updateFilter(col.key, $event.target.value)"
47
43
  :placeholder="`Filtrar ${col.label.toLowerCase()}...`"
48
- class="rounded-lg border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white py-2 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
44
+ class="w-full rounded-lg border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white py-1.5 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
49
45
  />
50
46
  </div>
51
47
 
52
- <!-- select filter -->
53
- <div v-else-if="col.filterType === 'select'" class="flex flex-col gap-1 min-w-40">
54
- <label class="text-xs text-slate-500 dark:text-slate-400">{{ col.label }}</label>
55
- <select
56
- :value="localFilters[col.key] ?? ''"
57
- @change="updateFilter(col.key, $event.target.value || null)"
58
- class="rounded-lg border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white py-2 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
59
- >
60
- <option value="">Todos</option>
61
- <option
62
- v-for="opt in col.filterOptions ?? []"
63
- :key="opt.value"
64
- :value="opt.value"
65
- >{{ opt.label }}</option>
66
- </select>
48
+ <!-- select -->
49
+ <div v-else-if="col.filterType === 'select'">
50
+ <Forms.Select
51
+ :model-value="localFilters[col.key] ?? ''"
52
+ @update:model-value="updateFilter(col.key, $event)"
53
+ :options="[{ value: '', label: 'Todos' }, ...(col.filterOptions ?? [])]"
54
+ :label="col.label"
55
+ />
67
56
  </div>
68
57
 
69
- <!-- daterange filter -->
70
- <div v-else-if="col.filterType === 'daterange'" class="flex flex-col gap-1">
71
- <label class="text-xs text-slate-500 dark:text-slate-400">{{ col.label }}</label>
58
+ <!-- daterange -->
59
+ <div v-else-if="col.filterType === 'daterange'">
60
+ <label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">{{ col.label }}</label>
72
61
  <div class="flex items-center gap-1.5">
73
62
  <input
74
63
  type="date"
75
64
  :value="localFilters[col.key]?.from ?? ''"
76
65
  @change="updateFilter(col.key, { ...localFilters[col.key], from: $event.target.value || null })"
77
- class="rounded-lg border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white py-2 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
66
+ class="flex-1 rounded-lg border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white py-1.5 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
78
67
  />
79
- <span class="text-slate-400 text-sm">—</span>
68
+ <span class="text-slate-400 text-xs shrink-0">—</span>
80
69
  <input
81
70
  type="date"
82
71
  :value="localFilters[col.key]?.to ?? ''"
83
72
  @change="updateFilter(col.key, { ...localFilters[col.key], to: $event.target.value || null })"
84
- class="rounded-lg border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white py-2 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
73
+ class="flex-1 rounded-lg border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white py-1.5 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
85
74
  />
86
75
  </div>
87
76
  </div>
77
+
88
78
  </template>
89
79
 
90
- <button
91
- v-if="activeCount > 0"
92
- type="button"
93
- @click="clearAll"
94
- class="py-2 px-3 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors flex items-center gap-1.5 self-end"
95
- >
96
- <svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
97
- Limpiar ({{ activeCount }})
98
- </button>
80
+ <div class="pt-2 border-t border-slate-100 dark:border-slate-700/60">
81
+ <button
82
+ v-if="activeCount > 0"
83
+ type="button"
84
+ @click="clearAll"
85
+ class="w-full py-1.5 px-3 text-sm text-rose-600 dark:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-lg transition-colors flex items-center justify-center gap-1.5"
86
+ >
87
+ <svg class="size-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
88
+ Limpiar filtros
89
+ </button>
90
+ <p v-else class="text-xs text-center text-slate-400 dark:text-slate-500 py-0.5">Sin filtros activos</p>
91
+ </div>
99
92
  </div>
100
93
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innertia-solutions/nuxt-theme-spark",
3
- "version": "0.1.73",
3
+ "version": "0.1.75",
4
4
  "description": "Innertia Solutions — Spark theme: backoffice, landing and mobile components and layouts",
5
5
  "keywords": [
6
6
  "nuxt",