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

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