@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.
- package/components/Table/Standard.vue +180 -109
- package/components/Table.vue +3 -1
- package/package.json +1 -1
|
@@ -2,43 +2,80 @@
|
|
|
2
2
|
import { IconSearch, IconAdjustmentsHorizontal, IconLayoutColumns, IconGripVertical } from '@tabler/icons-vue'
|
|
3
3
|
|
|
4
4
|
const props = defineProps({
|
|
5
|
-
endpoint:
|
|
6
|
-
columns:
|
|
7
|
-
name:
|
|
8
|
-
params:
|
|
9
|
-
checkable:
|
|
10
|
-
cached:
|
|
11
|
-
showReloadButton:
|
|
12
|
-
clickRowToOpen:
|
|
13
|
-
searchPlaceholder: { type: String,
|
|
14
|
-
showSearch:
|
|
15
|
-
showFilters:
|
|
16
|
-
showExport:
|
|
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
|
|
22
|
-
const
|
|
24
|
+
const search = ref('')
|
|
25
|
+
const activeFilters = ref({})
|
|
23
26
|
const showFilterPanel = ref(false)
|
|
24
|
-
const tableRef
|
|
27
|
+
const tableRef = ref(null)
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
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(
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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
|
|
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
|
|
85
|
-
const clearCache
|
|
86
|
-
const exportTable
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
<!--
|
|
96
|
-
<div
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
<!--
|
|
242
|
+
<!-- Preview panel -->
|
|
154
243
|
<Transition
|
|
155
|
-
enter-active-class="transition ease-out duration-
|
|
156
|
-
enter-from-class="opacity-0
|
|
157
|
-
enter-to-class="opacity-100 translate-
|
|
158
|
-
leave-active-class="transition ease-in duration-
|
|
159
|
-
leave-from-class="opacity-100 translate-
|
|
160
|
-
leave-to-class="opacity-0
|
|
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="
|
|
164
|
-
class="
|
|
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
|
-
<
|
|
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 -->
|
package/components/Table.vue
CHANGED
|
@@ -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:
|
|
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
|