@innertia-solutions/nuxt-theme-spark 0.1.65 → 0.1.67

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.
@@ -2,43 +2,80 @@
2
2
  import { IconSearch, IconAdjustmentsHorizontal, IconLayoutColumns, IconGripVertical } from '@tabler/icons-vue'
3
3
 
4
4
  const props = defineProps({
5
- endpoint: { type: String, required: true },
6
- columns: { type: Array, required: true },
7
- name: { type: String, required: true },
8
- params: { type: Object, default: () => ({}) },
9
- checkable: { type: Boolean, default: false },
10
- cached: { type: Boolean, default: true },
11
- showReloadButton: { type: Boolean, default: true },
12
- clickRowToOpen: { type: Boolean, default: false },
13
- searchPlaceholder: { type: String, default: 'Buscar...' },
14
- showSearch: { type: Boolean, default: true },
15
- showFilters: { type: Boolean, default: true },
16
- showExport: { type: Boolean, default: true },
5
+ endpoint: { type: String, required: true },
6
+ columns: { type: Array, required: true },
7
+ name: { type: String, required: true },
8
+ params: { type: Object, default: () => ({}) },
9
+ checkable: { type: Boolean, default: false },
10
+ cached: { type: Boolean, default: true },
11
+ showReloadButton: { type: Boolean, default: true },
12
+ clickRowToOpen: { type: Boolean, default: false },
13
+ searchPlaceholder: { type: String, default: 'Buscar...' },
14
+ showSearch: { type: Boolean, default: true },
15
+ showFilters: { type: Boolean, default: true },
16
+ showExport: { type: Boolean, default: true },
17
+ filters: { type: Array, default: () => [] },
18
+ splitRatio: { type: Number, default: 60 },
17
19
  })
18
20
 
19
21
  const emit = defineEmits(['row-click', 'loaded'])
22
+ const slots = useSlots()
20
23
 
21
- const search = ref('')
22
- const filters = ref({})
24
+ const search = ref('')
25
+ const activeFilters = ref({})
23
26
  const showFilterPanel = ref(false)
24
- const tableRef = ref(null)
27
+ const tableRef = ref(null)
25
28
 
26
- const hasFilterableColumns = computed(() =>
27
- props.columns.some(c => c.filterType)
29
+ // ─── Filter config ─────────────────────────────────────────────────────────────
30
+ const filtersConfig = computed(() =>
31
+ props.filters?.length ? props.filters : props.columns.filter(c => c.filterType)
28
32
  )
29
33
 
34
+ const hasFilterableColumns = computed(() => filtersConfig.value.length > 0)
35
+
30
36
  const activeFilterCount = computed(() =>
31
- Object.values(filters.value).filter(v => v !== null && v !== undefined && v !== '').length
37
+ Object.values(activeFilters.value).filter(v => v !== null && v !== undefined && v !== '').length
32
38
  )
33
39
 
34
40
  const mergedParams = computed(() => ({
35
41
  ...props.params,
36
- ...filters.value,
42
+ ...activeFilters.value,
37
43
  }))
38
44
 
45
+ // ─── Preview panel ─────────────────────────────────────────────────────────────
46
+ const previewRow = ref(null)
47
+ const currentRatio = ref(props.splitRatio)
48
+ const containerRef = ref(null)
49
+ const closePreview = () => { previewRow.value = null }
50
+
51
+ const handleRowClick = (row) => {
52
+ previewRow.value = previewRow.value?.id === row.id ? null : row
53
+ if (!slots.preview) emit('row-click', row)
54
+ }
55
+
56
+ const startResize = (e) => {
57
+ e.preventDefault()
58
+ const onMove = (ev) => {
59
+ if (!containerRef.value) return
60
+ const rect = containerRef.value.getBoundingClientRect()
61
+ const ratio = ((ev.clientX - rect.left) / rect.width) * 100
62
+ currentRatio.value = Math.min(80, Math.max(25, ratio))
63
+ }
64
+ const onUp = () => {
65
+ window.removeEventListener('mousemove', onMove)
66
+ window.removeEventListener('mouseup', onUp)
67
+ }
68
+ window.addEventListener('mousemove', onMove)
69
+ window.addEventListener('mouseup', onUp)
70
+ }
71
+
72
+ const onEsc = (e) => { if (e.key === 'Escape' && previewRow.value) closePreview() }
73
+ onMounted(() => window.addEventListener('keydown', onEsc))
74
+ onBeforeUnmount(() => window.removeEventListener('keydown', onEsc))
75
+
39
76
  // ─── Column panel ─────────────────────────────────────────────────────────────
40
77
  const showColumnPanel = ref(false)
41
- const columnPanelRef = ref(null)
78
+ const columnPanelRef = ref(null)
42
79
 
43
80
  const orderedColumns = computed(() => {
44
81
  if (!tableRef.value) return props.columns.filter(c => c.label)
@@ -50,17 +87,16 @@ let draggedKey = null
50
87
  const dragOverKey = ref(null)
51
88
 
52
89
  const onDragStart = (key) => { draggedKey = key }
53
- const onDragOver = (e, key) => { e.preventDefault(); dragOverKey.value = key }
90
+ const onDragOver = (e, key) => { e.preventDefault(); dragOverKey.value = key }
54
91
  const onDragLeave = () => { dragOverKey.value = null }
55
92
  const onDrop = (key) => {
56
93
  if (!draggedKey || draggedKey === key) return
57
94
  const ids = tableRef.value?.table.getAllLeafColumns().map(c => c.id) ?? []
58
95
  const from = ids.indexOf(draggedKey)
59
- const to = ids.indexOf(key)
96
+ const to = ids.indexOf(key)
60
97
  if (from < 0 || to < 0) return
61
98
  ids.splice(from, 1)
62
99
  ids.splice(to, 0, draggedKey)
63
- // keep 'select' pinned first
64
100
  const selIdx = ids.indexOf('select')
65
101
  if (selIdx > 0) { ids.splice(selIdx, 1); ids.unshift('select') }
66
102
  tableRef.value?.setColumnOrder(ids)
@@ -81,111 +117,146 @@ watch(showColumnPanel, (v) => {
81
117
 
82
118
  // ─── Expose ───────────────────────────────────────────────────────────────────
83
119
  const getSelectedRows = () => tableRef.value?.getSelectedRows()
84
- const reload = () => tableRef.value?.reload()
85
- const clearCache = () => tableRef.value?.clearCache()
86
- const exportTable = (format, allPages, filteredRows) => tableRef.value?.exportTable(format, allPages, filteredRows)
120
+ const reload = () => tableRef.value?.reload()
121
+ const clearCache = () => tableRef.value?.clearCache()
122
+ const exportTable = (format, allPages, filteredRows) => tableRef.value?.exportTable(format, allPages, filteredRows)
87
123
 
88
124
  defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef })
89
125
  </script>
90
126
 
91
127
  <template>
92
- <div class="relative">
93
- <!-- Card -->
94
- <div class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-sm overflow-hidden">
95
- <!-- Toolbar -->
96
- <div class="flex flex-wrap items-center gap-3 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
97
- <!-- Search -->
98
- <div v-if="showSearch" class="flex-1 min-w-48">
99
- <Forms.Input
100
- v-model="search"
101
- type="search"
102
- :placeholder="searchPlaceholder"
103
- :icon-left="IconSearch"
128
+ <div class="relative" ref="containerRef">
129
+ <div :class="previewRow && $slots.preview ? 'flex items-stretch gap-3' : ''">
130
+
131
+ <!-- Card -->
132
+ <div
133
+ class="relative bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-sm overflow-hidden"
134
+ :style="previewRow && $slots.preview ? { width: currentRatio + '%', minWidth: 0, flexShrink: 0 } : {}"
135
+ >
136
+ <!-- Toolbar -->
137
+ <div class="flex flex-wrap items-center gap-3 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
138
+ <!-- Search -->
139
+ <div v-if="showSearch" class="flex-1 min-w-48">
140
+ <Forms.Input
141
+ v-model="search"
142
+ type="search"
143
+ :placeholder="searchPlaceholder"
144
+ :icon-left="IconSearch"
145
+ />
146
+ </div>
147
+
148
+ <!-- Filter toggle -->
149
+ <button
150
+ v-if="showFilters && hasFilterableColumns"
151
+ type="button"
152
+ @click="showFilterPanel = !showFilterPanel"
153
+ :class="[
154
+ 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
155
+ showFilterPanel || activeFilterCount > 0
156
+ ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
157
+ : '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'
158
+ ]"
159
+ >
160
+ <IconAdjustmentsHorizontal class="size-4" stroke="1.5" />
161
+ Filtros
162
+ <span
163
+ v-if="activeFilterCount > 0"
164
+ class="inline-flex items-center justify-center size-5 rounded-full bg-blue-600 text-white text-xs font-bold"
165
+ >{{ activeFilterCount }}</span>
166
+ </button>
167
+
168
+ <slot name="actions" />
169
+
170
+ <!-- Column visibility toggle -->
171
+ <button
172
+ type="button"
173
+ @click="showColumnPanel = !showColumnPanel"
174
+ :class="[
175
+ 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
176
+ showColumnPanel
177
+ ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
178
+ : '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'
179
+ ]"
180
+ >
181
+ <IconLayoutColumns class="size-4" />
182
+ Columnas
183
+ </button>
184
+
185
+ <!-- Export -->
186
+ <TableExportable
187
+ v-if="showExport"
188
+ :table-ref="tableRef"
189
+ :name="name"
190
+ :columns="columns"
104
191
  />
105
192
  </div>
106
193
 
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
- ]"
194
+ <!-- Filter panel -->
195
+ <Transition
196
+ enter-active-class="transition ease-out duration-150"
197
+ enter-from-class="opacity-0 -translate-y-2"
198
+ enter-to-class="opacity-100 translate-y-0"
199
+ leave-active-class="transition ease-in duration-100"
200
+ leave-from-class="opacity-100 translate-y-0"
201
+ leave-to-class="opacity-0 -translate-y-2"
139
202
  >
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"
203
+ <div
204
+ v-if="showFilterPanel && hasFilterableColumns"
205
+ class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50"
206
+ >
207
+ <TableFilter v-model="activeFilters" :columns="filtersConfig" />
208
+ </div>
209
+ </Transition>
210
+
211
+ <!-- Table -->
212
+ <Table
213
+ ref="tableRef"
214
+ :endpoint="endpoint"
149
215
  :columns="columns"
150
- />
216
+ :name="name"
217
+ :params="mergedParams"
218
+ :search="search"
219
+ :checkable="checkable"
220
+ :cached="cached"
221
+ :show-reload-button="showReloadButton"
222
+ :click-row-to-open="clickRowToOpen"
223
+ :preview-row-id="previewRow?.id ?? null"
224
+ @row-click="handleRowClick"
225
+ @loaded="emit('loaded', $event)"
226
+ >
227
+ <template v-for="(_, name) in $slots" #[name]="slotProps">
228
+ <slot :name="name" v-bind="slotProps ?? {}" />
229
+ </template>
230
+ </Table>
231
+ </div>
232
+
233
+ <!-- Resize handle -->
234
+ <div
235
+ v-if="previewRow && $slots.preview"
236
+ class="w-3 flex items-center justify-center cursor-col-resize shrink-0 group"
237
+ @mousedown="startResize"
238
+ >
239
+ <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" />
151
240
  </div>
152
241
 
153
- <!-- Filter panel -->
242
+ <!-- Preview panel -->
154
243
  <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"
244
+ enter-active-class="transition ease-out duration-300"
245
+ enter-from-class="opacity-0 translate-x-4"
246
+ enter-to-class="opacity-100 translate-x-0"
247
+ leave-active-class="transition ease-in duration-200"
248
+ leave-from-class="opacity-100 translate-x-0"
249
+ leave-to-class="opacity-0 translate-x-4"
161
250
  >
162
251
  <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"
252
+ v-if="previewRow && $slots.preview"
253
+ class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-sm overflow-hidden flex flex-col"
254
+ :style="{ width: (100 - currentRatio) + '%', minWidth: 0, flexShrink: 0 }"
165
255
  >
166
- <TableFilter v-model="filters" :columns="columns" />
256
+ <slot name="preview" :row="previewRow" :close="closePreview" />
167
257
  </div>
168
258
  </Transition>
169
259
 
170
- <!-- Table -->
171
- <Table
172
- ref="tableRef"
173
- :endpoint="endpoint"
174
- :columns="columns"
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>
189
260
  </div>
190
261
 
191
262
  <!-- Column panel — outside overflow-hidden so never clipped -->
@@ -19,7 +19,8 @@ const props = defineProps({
19
19
  showReloadButton: { type: Boolean, default: true },
20
20
  viewMode: { type: String, default: 'table' }, // 'table' | 'grid'
21
21
  gridClass: { type: String, default: 'grid grid-cols-2 lg:grid-cols-3 gap-4' },
22
- clickRowToOpen: { type: Boolean, default: false },
22
+ clickRowToOpen: { type: Boolean, default: false },
23
+ previewRowId: { type: [String, Number], default: null },
23
24
  })
24
25
 
25
26
  const emit = defineEmits(['update:search', 'row-click', 'loaded'])
@@ -630,6 +631,7 @@ defineExpose({
630
631
  :class="{
631
632
  'cursor-pointer': isRowClickEnabled,
632
633
  'bg-indigo-50/40 dark:bg-indigo-900/10 hover:bg-indigo-50/60': row.getIsSelected(),
634
+ '!bg-indigo-50 dark:!bg-indigo-900/20 ring-1 ring-inset ring-indigo-200 dark:ring-indigo-700': previewRowId && row.original.id === previewRowId,
633
635
  }"
634
636
  >
635
637
  <td
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innertia-solutions/nuxt-theme-spark",
3
- "version": "0.1.65",
3
+ "version": "0.1.67",
4
4
  "description": "Innertia Solutions — Spark theme: backoffice, landing and mobile components and layouts",
5
5
  "keywords": [
6
6
  "nuxt",