@innertia-solutions/nuxt-theme-spark 0.1.74 → 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 ───────────────────────────────────────────────────────────────────
@@ -159,8 +166,7 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
159
166
  ]"
160
167
  >
161
168
  <IconAdjustmentsHorizontal class="size-4" stroke="1.5" />
162
- Filtros
163
- <span v-if="activeFilterCount > 0" class="inline-flex items-center justify-center size-5 rounded-full bg-blue-600 text-white text-xs font-bold">{{ activeFilterCount }}</span>
169
+ Filtros{{ activeFilterCount > 0 ? ` (${activeFilterCount})` : '' }}
164
170
  </button>
165
171
 
166
172
  <slot name="actions" />
@@ -182,20 +188,6 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
182
188
  <TableExportable v-if="showExport" :table-ref="tableRef" :name="name" :columns="columns" />
183
189
  </div>
184
190
 
185
- <!-- Filter panel -->
186
- <Transition
187
- enter-active-class="transition ease-out duration-150"
188
- enter-from-class="opacity-0 -translate-y-2"
189
- enter-to-class="opacity-100 translate-y-0"
190
- leave-active-class="transition ease-in duration-100"
191
- leave-from-class="opacity-100 translate-y-0"
192
- leave-to-class="opacity-0 -translate-y-2"
193
- >
194
- <div v-if="showFilterPanel && hasFilterableColumns" class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50">
195
- <TableFilter v-model="activeFilters" :columns="filtersConfig" />
196
- </div>
197
- </Transition>
198
-
199
191
  <!-- Contenido: tabla + preview en flex -->
200
192
  <div :class="previewRow && previewEnabled ? 'flex items-stretch' : ''">
201
193
 
@@ -244,6 +236,27 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
244
236
  </div>
245
237
  </div>
246
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
+
247
260
  <!-- Column panel — outside overflow-hidden so never clipped -->
248
261
  <Transition
249
262
  enter-active-class="transition ease-out duration-150"
@@ -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.74",
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",