@innertia-solutions/nuxt-theme-spark 0.1.64 → 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.
- package/components/Admin/PageHeader.vue +3 -0
- package/components/Table/Standard.vue +186 -109
- package/components/Table.vue +3 -1
- package/package.json +1 -1
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
defineProps<{
|
|
3
3
|
title: string
|
|
4
4
|
description?: string
|
|
5
|
+
sticky?: boolean
|
|
5
6
|
}>()
|
|
6
7
|
</script>
|
|
7
8
|
|
|
8
9
|
<template>
|
|
10
|
+
<div :class="sticky ? 'sticky top-3 z-20' : ''">
|
|
9
11
|
<div class="flex items-center justify-between bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-sm px-4 py-3">
|
|
10
12
|
<div class="flex items-center gap-x-4">
|
|
11
13
|
<slot name="icon" />
|
|
@@ -26,4 +28,5 @@ defineProps<{
|
|
|
26
28
|
<slot name="actions" />
|
|
27
29
|
</div>
|
|
28
30
|
</div>
|
|
31
|
+
</div>
|
|
29
32
|
</template>
|
|
@@ -2,43 +2,86 @@
|
|
|
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 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
|
|
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
|
|
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
|
|
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
|
|
85
|
-
const clearCache
|
|
86
|
-
const exportTable
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
<!--
|
|
96
|
-
<div
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
-
]"
|
|
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
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
</
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
<!--
|
|
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-
|
|
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
|
|
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="
|
|
164
|
-
class="
|
|
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
|
-
<
|
|
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 -->
|
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
|