@innertia-solutions/nuxt-theme-spark 0.1.35 → 0.1.37

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.
@@ -314,7 +314,7 @@ const getSelectedRows = () => {
314
314
  }
315
315
 
316
316
  // ─── Export ───────────────────────────────────────────────────────────────────
317
- const exportTable = async (format, exportAllPages, exportFilteredRows) => {
317
+ const exportTable = async (format, exportAllPages, exportFilteredRows, selectedIds = null) => {
318
318
  const { download } = useDownload()
319
319
  const id = crypto.randomUUID()
320
320
  toast.show({
@@ -328,6 +328,7 @@ const exportTable = async (format, exportAllPages, exportFilteredRows) => {
328
328
  exportType: validFormats.includes(format) ? format : 'csv',
329
329
  exportAllPages,
330
330
  exportFilteredRows,
331
+ ...(selectedIds?.length ? { selectedIds } : {}),
331
332
  }
332
333
 
333
334
  try {
@@ -7,17 +7,19 @@ import {
7
7
  IconDownload,
8
8
  } from '@tabler/icons-vue'
9
9
 
10
- // Modal with format selector, pre-filled filename, columns checkboxes
11
10
  const props = defineProps({
12
11
  tableRef: { type: Object, default: null },
13
12
  name: { type: String, default: 'export' },
14
- columns: { type: Array, default: () => [] }, // [{ key, label }]
13
+ columns: { type: Array, default: () => [] },
15
14
  })
16
15
 
17
16
  const isOpen = ref(false)
18
17
  const format = ref('xlsx')
19
18
  const filename = ref(props.name)
20
19
  const selectedColumns = ref([])
20
+ const btnRef = ref(null)
21
+ const panelRef = ref(null)
22
+ const panelStyle = ref({})
21
23
 
22
24
  watch(() => props.columns, (cols) => {
23
25
  selectedColumns.value = cols.map(c => c.key)
@@ -26,10 +28,10 @@ watch(() => props.columns, (cols) => {
26
28
  watch(() => props.name, (v) => { filename.value = v })
27
29
 
28
30
  const formats = [
29
- { value: 'xlsx', label: 'Excel', icon: 'xlsx' },
30
- { value: 'csv', label: 'CSV', icon: 'csv' },
31
- { value: 'pdf', label: 'PDF', icon: 'pdf' },
32
- { value: 'json', label: 'JSON', icon: 'json' },
31
+ { value: 'xlsx', label: 'Excel', icon: IconFileTypeXls },
32
+ { value: 'csv', label: 'CSV', icon: IconFileTypeCsv },
33
+ { value: 'pdf', label: 'PDF', icon: IconFileTypePdf },
34
+ { value: 'json', label: 'JSON', icon: IconCodeDots },
33
35
  ]
34
36
 
35
37
  const toggleColumn = (key) => {
@@ -46,158 +48,174 @@ const toggleAll = () => {
46
48
  }
47
49
 
48
50
  const allSelected = computed(() => selectedColumns.value.length === props.columns.length)
49
- const indeterminate = computed(() => selectedColumns.value.length > 0 && !allSelected.value)
51
+
52
+ const open = () => {
53
+ if (!btnRef.value) return
54
+ const rect = btnRef.value.getBoundingClientRect()
55
+ panelStyle.value = {
56
+ position: 'fixed',
57
+ top: (rect.bottom + 6) + 'px',
58
+ right: (window.innerWidth - rect.right) + 'px',
59
+ }
60
+ isOpen.value = true
61
+ }
62
+
63
+ const toggle = () => isOpen.value ? isOpen.value = false : open()
64
+
65
+ const onOutsideClick = (e) => {
66
+ if (
67
+ panelRef.value && !panelRef.value.contains(e.target) &&
68
+ btnRef.value && !btnRef.value.contains(e.target)
69
+ ) {
70
+ isOpen.value = false
71
+ }
72
+ }
73
+
74
+ watch(isOpen, (v) => {
75
+ if (v) document.addEventListener('mousedown', onOutsideClick)
76
+ else document.removeEventListener('mousedown', onOutsideClick)
77
+ })
78
+
79
+ const selectedRows = computed(() => {
80
+ if (!props.tableRef) return { meta: { all: false }, rows: [] }
81
+ return props.tableRef.getSelectedRows() ?? { meta: { all: false }, rows: [] }
82
+ })
83
+
84
+ const hasSelection = computed(() => selectedRows.value.rows.length > 0)
50
85
 
51
86
  const doExport = () => {
52
- if (props.tableRef) {
87
+ if (!props.tableRef) return
88
+ if (hasSelection.value) {
89
+ const ids = selectedRows.value.rows.map(r => r.id)
90
+ props.tableRef.exportTable(format.value, false, false, ids)
91
+ } else {
53
92
  props.tableRef.exportTable(format.value, true, true)
54
93
  }
55
94
  isOpen.value = false
56
95
  }
57
96
 
58
- const open = () => { isOpen.value = true }
59
-
60
97
  defineExpose({ open })
61
98
  </script>
62
99
 
63
100
  <template>
64
- <div>
101
+ <div class="relative">
65
102
  <button
103
+ ref="btnRef"
66
104
  type="button"
67
- @click="isOpen = true"
68
- class="py-1.5 sm:py-2 px-2.5 inline-flex items-center gap-x-1.5 text-sm font-medium rounded-lg border border-slate-200 bg-white text-slate-800 shadow-2xs hover:bg-slate-50 disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-700"
105
+ @click="toggle"
106
+ :class="[
107
+ 'py-1.5 px-3 inline-flex items-center gap-2 text-sm font-medium rounded-lg border transition-colors',
108
+ isOpen
109
+ ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:border-blue-500 dark:text-blue-300'
110
+ : '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'
111
+ ]"
69
112
  >
70
- <IconDownload class="shrink-0 size-4" stroke="1.5" />
113
+ <IconDownload class="size-4" stroke="1.5" />
71
114
  Exportar
72
115
  </button>
73
116
 
74
- <!-- Modal -->
117
+ <!-- Dropdown panel — teleported to body to avoid overflow-hidden clipping -->
75
118
  <Teleport to="body">
76
119
  <Transition
77
- enter-active-class="transition ease-out duration-200"
78
- enter-from-class="opacity-0"
79
- enter-to-class="opacity-100"
80
- leave-active-class="transition ease-in duration-150"
81
- leave-from-class="opacity-100"
82
- leave-to-class="opacity-0"
120
+ enter-active-class="transition ease-out duration-150"
121
+ enter-from-class="opacity-0 translate-y-1 scale-95"
122
+ enter-to-class="opacity-100 translate-y-0 scale-100"
123
+ leave-active-class="transition ease-in duration-100"
124
+ leave-from-class="opacity-100 translate-y-0 scale-100"
125
+ leave-to-class="opacity-0 translate-y-1 scale-95"
83
126
  >
84
127
  <div
85
128
  v-if="isOpen"
86
- class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
87
- @click.self="isOpen = false"
129
+ ref="panelRef"
130
+ :style="panelStyle"
131
+ class="z-50 w-72 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-2xl"
88
132
  >
89
- <Transition
90
- enter-active-class="transition ease-out duration-200"
91
- enter-from-class="opacity-0 scale-95"
92
- enter-to-class="opacity-100 scale-100"
93
- >
94
- <div
95
- v-if="isOpen"
96
- class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-md border border-slate-200 dark:border-slate-700"
97
- >
98
- <div class="flex items-center justify-between px-6 py-4 border-b border-slate-100 dark:border-slate-700">
99
- <h3 class="font-semibold text-slate-800 dark:text-slate-100">Exportar tabla</h3>
133
+ <div class="p-3 space-y-4">
134
+ <!-- Format -->
135
+ <div>
136
+ <p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-2">Formato</p>
137
+ <div class="grid grid-cols-4 gap-1.5">
100
138
  <button
139
+ v-for="f in formats"
140
+ :key="f.value"
101
141
  type="button"
102
- @click="isOpen = false"
103
- class="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
142
+ @click="format = f.value"
143
+ :class="[
144
+ 'flex flex-col items-center gap-1 py-2 px-1 rounded-lg border text-xs font-medium transition-colors',
145
+ format === f.value
146
+ ? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:border-blue-500 dark:text-blue-300'
147
+ : 'border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700'
148
+ ]"
104
149
  >
105
- <svg class="size-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
106
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
107
- </svg>
150
+ <component :is="f.icon" class="size-4" stroke="1.5" />
151
+ {{ f.label }}
108
152
  </button>
109
153
  </div>
154
+ </div>
110
155
 
111
- <div class="px-6 py-5 space-y-5">
112
- <!-- Format selector -->
113
- <div>
114
- <p class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-2">Formato</p>
115
- <div class="grid grid-cols-4 gap-2">
116
- <button
117
- v-for="f in formats"
118
- :key="f.value"
119
- type="button"
120
- @click="format = f.value"
121
- :class="[
122
- 'flex flex-col items-center gap-1.5 p-3 rounded-xl border text-sm font-medium transition-colors',
123
- format === f.value
124
- ? 'border-indigo-500 bg-indigo-50 text-indigo-700 dark:bg-indigo-900/30 dark:border-indigo-500 dark:text-indigo-300'
125
- : 'border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700'
126
- ]"
127
- >
128
- <IconFileTypeXls v-if="f.value === 'xlsx'" class="size-5" stroke="1.5" />
129
- <IconFileTypeCsv v-else-if="f.value === 'csv'" class="size-5" stroke="1.5" />
130
- <IconFileTypePdf v-else-if="f.value === 'pdf'" class="size-5" stroke="1.5" />
131
- <IconCodeDots v-else class="size-5" stroke="1.5" />
132
- {{ f.label }}
133
- </button>
134
- </div>
135
- </div>
136
-
137
- <!-- Filename -->
138
- <div>
139
- <label class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider block mb-2">
140
- Nombre de archivo
141
- </label>
142
- <div class="flex items-center gap-2">
143
- <input
144
- v-model="filename"
145
- type="text"
146
- class="flex-1 rounded-lg border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-900 text-slate-900 dark:text-white py-2 px-3 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
147
- />
148
- <span class="text-sm text-slate-400">.{{ format }}</span>
149
- </div>
150
- </div>
151
-
152
- <!-- Columns -->
153
- <div v-if="columns.length > 0">
154
- <div class="flex items-center justify-between mb-2">
155
- <p class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Columnas</p>
156
- <button
157
- type="button"
158
- @click="toggleAll"
159
- class="text-xs text-indigo-600 dark:text-indigo-400 hover:underline"
160
- >
161
- {{ allSelected ? 'Deseleccionar todas' : 'Seleccionar todas' }}
162
- </button>
163
- </div>
164
- <div class="grid grid-cols-2 gap-1.5 max-h-40 overflow-y-auto pr-1">
165
- <label
166
- v-for="col in columns"
167
- :key="col.key"
168
- class="flex items-center gap-2 py-1.5 px-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 cursor-pointer"
169
- >
170
- <input
171
- type="checkbox"
172
- :checked="selectedColumns.includes(col.key)"
173
- @change="toggleColumn(col.key)"
174
- class="rounded border-gray-300 dark:bg-slate-700 dark:border-slate-600 text-indigo-600"
175
- />
176
- <span class="text-sm text-slate-700 dark:text-slate-200 truncate">{{ col.label }}</span>
177
- </label>
178
- </div>
179
- </div>
156
+ <!-- Filename -->
157
+ <div>
158
+ <p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1.5">Archivo</p>
159
+ <div class="flex items-center gap-1.5">
160
+ <Forms.Input v-model="filename" type="text" placeholder="nombre" class="flex-1" />
161
+ <span class="text-sm text-slate-400 shrink-0">.{{ format }}</span>
180
162
  </div>
163
+ </div>
181
164
 
182
- <div class="flex justify-end gap-2 px-6 py-4 border-t border-slate-100 dark:border-slate-700">
183
- <button
184
- type="button"
185
- @click="isOpen = false"
186
- class="py-2 px-4 text-sm font-medium rounded-lg border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
187
- >
188
- Cancelar
165
+ <!-- Columns -->
166
+ <div v-if="columns.length > 0">
167
+ <div class="flex items-center justify-between mb-1.5">
168
+ <p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">Columnas</p>
169
+ <button type="button" @click="toggleAll" class="text-[10px] text-blue-600 dark:text-blue-400 hover:underline font-medium">
170
+ {{ allSelected ? 'Quitar todas' : 'Todas' }}
189
171
  </button>
190
- <button
191
- type="button"
192
- @click="doExport"
193
- class="py-2 px-4 text-sm font-medium rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition-colors inline-flex items-center gap-2"
172
+ </div>
173
+ <div class="grid grid-cols-2 gap-1 max-h-36 overflow-y-auto">
174
+ <label
175
+ v-for="col in columns"
176
+ :key="col.key"
177
+ class="flex items-center gap-2 py-1 px-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 cursor-pointer"
194
178
  >
195
- <IconDownload class="size-4" stroke="1.5" />
196
- Exportar
197
- </button>
179
+ <input
180
+ type="checkbox"
181
+ :checked="selectedColumns.includes(col.key)"
182
+ @change="toggleColumn(col.key)"
183
+ class="rounded border-gray-300 dark:bg-slate-700 dark:border-slate-600 text-blue-600"
184
+ />
185
+ <span class="text-xs text-slate-700 dark:text-slate-200 truncate">{{ col.label }}</span>
186
+ </label>
198
187
  </div>
199
188
  </div>
200
- </Transition>
189
+ </div>
190
+
191
+ <!-- Selection info -->
192
+ <div class="px-3 pb-2">
193
+ <p v-if="hasSelection" class="text-xs text-blue-600 dark:text-blue-400 font-medium">
194
+ Se exportarán {{ selectedRows.rows.length }} fila{{ selectedRows.rows.length !== 1 ? 's' : '' }} seleccionada{{ selectedRows.rows.length !== 1 ? 's' : '' }}
195
+ </p>
196
+ <p v-else class="text-xs text-slate-400 dark:text-slate-500">
197
+ Se exportarán todos los registros
198
+ </p>
199
+ </div>
200
+
201
+ <!-- Footer -->
202
+ <div class="flex gap-2 px-3 pb-3">
203
+ <button
204
+ type="button"
205
+ @click="isOpen = false"
206
+ class="flex-1 py-1.5 text-sm font-medium rounded-lg border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
207
+ >
208
+ Cancelar
209
+ </button>
210
+ <button
211
+ type="button"
212
+ @click="doExport"
213
+ class="flex-1 py-1.5 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors inline-flex items-center justify-center gap-1.5"
214
+ >
215
+ <IconDownload class="size-4" stroke="1.5" />
216
+ Exportar
217
+ </button>
218
+ </div>
201
219
  </div>
202
220
  </Transition>
203
221
  </Teleport>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innertia-solutions/nuxt-theme-spark",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "description": "Innertia Solutions — Spark theme: backoffice, landing and mobile components and layouts",
5
5
  "keywords": [
6
6
  "nuxt",