@r2digisolutions/ui 0.21.3 → 0.22.0
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/dist/components/container/DataTable/DataTable.svelte +380 -0
- package/dist/components/container/DataTable/DataTable.svelte.d.ts +51 -0
- package/dist/components/container/DataTable/components/Cell.svelte +90 -0
- package/dist/components/container/DataTable/components/Cell.svelte.d.ts +30 -0
- package/dist/components/container/DataTable/components/ColumnVisibilityToggle.svelte +118 -0
- package/dist/components/container/DataTable/components/ColumnVisibilityToggle.svelte.d.ts +10 -0
- package/dist/components/container/DataTable/components/ContextMenu.svelte +203 -0
- package/dist/components/container/DataTable/components/ContextMenu.svelte.d.ts +21 -0
- package/dist/components/container/DataTable/components/FilterPanel.svelte +195 -0
- package/dist/components/container/DataTable/components/FilterPanel.svelte.d.ts +10 -0
- package/dist/components/container/DataTable/components/Pagination.svelte +59 -0
- package/dist/components/container/DataTable/components/Pagination.svelte.d.ts +11 -0
- package/dist/components/container/DataTable/components/QuickFilters.svelte +38 -0
- package/dist/components/container/DataTable/components/QuickFilters.svelte.d.ts +8 -0
- package/dist/components/container/DataTable/core/DataTableManager.svelte.d.ts +39 -0
- package/dist/components/container/DataTable/core/DataTableManager.svelte.js +245 -0
- package/dist/components/container/DataTable/core/filters/types.d.ts +18 -0
- package/dist/components/container/DataTable/core/filters/types.js +1 -0
- package/dist/components/container/DataTable/core/filters/utils.d.ts +3 -0
- package/dist/components/container/DataTable/core/filters/utils.js +43 -0
- package/dist/components/container/DataTable/core/types.d.ts +101 -0
- package/dist/components/container/DataTable/core/types.js +1 -0
- package/dist/components/container/DataTable/core/utils.d.ts +5 -0
- package/dist/components/container/DataTable/core/utils.js +66 -0
- package/dist/components/container/index.d.ts +2 -0
- package/dist/components/container/index.js +2 -0
- package/dist/components/ui/Card/Card.svelte +8 -8
- package/dist/components/ui/Card/CardContent.svelte +11 -3
- package/dist/components/ui/Card/CardContent.svelte.d.ts +8 -10
- package/dist/components/ui/Card/CardFooter.svelte +12 -2
- package/dist/components/ui/Card/CardFooter.svelte.d.ts +7 -3
- package/dist/components/ui/Card/CardHeader.svelte +10 -2
- package/dist/components/ui/Card/CardHeader.svelte.d.ts +7 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +27 -27
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends { id?: any }">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { tick } from 'svelte';
|
|
4
|
+
import type { CellContext, ColumnDef, TableOptions } from './core/types.js';
|
|
5
|
+
import type { Entry } from './components/ContextMenu.svelte';
|
|
6
|
+
import type { FilterField } from './core/filters/types.js';
|
|
7
|
+
import { DataTableManager } from './core/DataTableManager.svelte';
|
|
8
|
+
import FilterPanel from './components/FilterPanel.svelte';
|
|
9
|
+
import ColumnVisibilityToggle from './components/ColumnVisibilityToggle.svelte';
|
|
10
|
+
import Pagination from './components/Pagination.svelte';
|
|
11
|
+
import ContextMenu from './components/ContextMenu.svelte';
|
|
12
|
+
import Cell from './components/Cell.svelte';
|
|
13
|
+
import { ChevronDown, ChevronRight } from 'lucide-svelte';
|
|
14
|
+
|
|
15
|
+
interface Props<T> {
|
|
16
|
+
filters?: Snippet;
|
|
17
|
+
options: TableOptions<T>;
|
|
18
|
+
rowId?: (row: T) => any;
|
|
19
|
+
actions?: (rows: T[], ctx?: CellContext<T> | null) => Entry[];
|
|
20
|
+
rowActions?: (row: T) => any;
|
|
21
|
+
onRowClick?: (row: T) => void;
|
|
22
|
+
density?: 'compact' | 'normal' | 'comfortable';
|
|
23
|
+
stickyHeader?: boolean;
|
|
24
|
+
showColumnToggle?: boolean;
|
|
25
|
+
expandIconPosition?: 'start' | 'end';
|
|
26
|
+
filterFields?: FilterField<T>[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
filters,
|
|
31
|
+
options,
|
|
32
|
+
rowId = (r: any) => r.id,
|
|
33
|
+
actions,
|
|
34
|
+
rowActions,
|
|
35
|
+
onRowClick,
|
|
36
|
+
density = 'normal',
|
|
37
|
+
stickyHeader = true,
|
|
38
|
+
showColumnToggle = true,
|
|
39
|
+
expandIconPosition = 'start',
|
|
40
|
+
filterFields
|
|
41
|
+
}: Props<T> = $props();
|
|
42
|
+
|
|
43
|
+
const CHECK_W = 64;
|
|
44
|
+
const ACTION_W = 56;
|
|
45
|
+
const EXPAND_W = 40;
|
|
46
|
+
|
|
47
|
+
const manager = new DataTableManager<T>(options);
|
|
48
|
+
|
|
49
|
+
let filterValues = $state<Record<string, any>>({});
|
|
50
|
+
let container: HTMLDivElement | null = $state(null);
|
|
51
|
+
let rightMenu = $state<{ open: boolean; x: number; y: number }>({ open: false, x: 0, y: 0 });
|
|
52
|
+
let rightClickContext = $state<CellContext<T> | null>(null);
|
|
53
|
+
let measuring = $state(true);
|
|
54
|
+
|
|
55
|
+
const sizeRow = $derived.by(() =>
|
|
56
|
+
density === 'compact' ? 'py-2' : density === 'comfortable' ? 'py-4' : 'py-3'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
await manager.load();
|
|
60
|
+
|
|
61
|
+
$effect(() => {
|
|
62
|
+
const hasActions = !!rowActions;
|
|
63
|
+
const reserved =
|
|
64
|
+
CHECK_W +
|
|
65
|
+
(expandIconPosition === 'end' && !hasActions ? EXPAND_W : 0) +
|
|
66
|
+
(hasActions ? ACTION_W : 0);
|
|
67
|
+
manager.setReservedWidth(reserved);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// reflow ancho
|
|
71
|
+
$effect(() => {
|
|
72
|
+
if (!container) return;
|
|
73
|
+
const ro = new ResizeObserver((entries) => {
|
|
74
|
+
const w = Math.floor(entries[0].contentRect.width);
|
|
75
|
+
manager.reflowForWidth(w);
|
|
76
|
+
});
|
|
77
|
+
ro.observe(container);
|
|
78
|
+
return () => ro.disconnect();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// medir DOM
|
|
82
|
+
const SAMPLE_ROWS = 10;
|
|
83
|
+
async function measureColumns() {
|
|
84
|
+
await tick();
|
|
85
|
+
if (!container) return;
|
|
86
|
+
const widths: Record<string, number> = {};
|
|
87
|
+
for (const c of manager.columns) {
|
|
88
|
+
const head = container.querySelector(`[data-dt-head="${c.id}"]`) as HTMLElement | null;
|
|
89
|
+
let maxW = head ? head.offsetWidth : 0;
|
|
90
|
+
const cells = Array.from(
|
|
91
|
+
container.querySelectorAll(`[data-dt-cell="1"][data-col-id="${c.id}"]`)
|
|
92
|
+
).slice(0, SAMPLE_ROWS) as HTMLElement[];
|
|
93
|
+
for (const el of cells) maxW = Math.max(maxW, el.offsetWidth);
|
|
94
|
+
if (c.minWidth != null) maxW = Math.max(maxW, c.minWidth);
|
|
95
|
+
if (c.width != null) maxW = Math.max(maxW, c.width);
|
|
96
|
+
widths[c.id] = Math.ceil(maxW + 16);
|
|
97
|
+
}
|
|
98
|
+
manager.setMeasuredWidths(widths);
|
|
99
|
+
const rect = container.getBoundingClientRect();
|
|
100
|
+
manager.reflowForWidth(Math.floor(rect.width));
|
|
101
|
+
measuring = false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
$effect(() => {
|
|
105
|
+
if (!manager.state.ready) return;
|
|
106
|
+
measuring = true;
|
|
107
|
+
measureColumns();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function headerClick(c: ColumnDef<T>) {
|
|
111
|
+
if (!c.sortable) return;
|
|
112
|
+
manager.setSort(c.id);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function onCellContext(
|
|
116
|
+
e: MouseEvent,
|
|
117
|
+
row: T | null,
|
|
118
|
+
columnId: string | null,
|
|
119
|
+
rowIndex: number | null
|
|
120
|
+
) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
const columnIndex = columnId ? manager.state.visibleColumns.indexOf(columnId) : null;
|
|
123
|
+
rightMenu = { open: true, x: e.clientX, y: e.clientY };
|
|
124
|
+
rightClickContext = {
|
|
125
|
+
row,
|
|
126
|
+
rowIndex,
|
|
127
|
+
columnId,
|
|
128
|
+
columnIndex,
|
|
129
|
+
event: e,
|
|
130
|
+
column: columnId ? manager.getColumn(columnId) : null
|
|
131
|
+
} as CellContext<T>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function selectedRows(): T[] {
|
|
135
|
+
const ids = manager.state.selected;
|
|
136
|
+
return manager.state.items.filter((r) => ids.has(rowId(r)));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// tracks
|
|
140
|
+
function colTrack(cId: string, measuring: boolean) {
|
|
141
|
+
if (measuring) return 'max-content';
|
|
142
|
+
const c = manager.getColumn(cId);
|
|
143
|
+
const w = manager.measured[cId] ?? c.width ?? c.minWidth ?? 160;
|
|
144
|
+
return `${Math.max(40, Math.ceil(Number(w)))}px`;
|
|
145
|
+
}
|
|
146
|
+
function headerTemplateCols(visible: string[], endExtras: boolean) {
|
|
147
|
+
const tracks = [
|
|
148
|
+
`${CHECK_W}px`,
|
|
149
|
+
...visible.map((id) => colTrack(id, measuring)),
|
|
150
|
+
...(rowActions ? [`${ACTION_W}px`] : endExtras ? [`${EXPAND_W}px`] : [])
|
|
151
|
+
];
|
|
152
|
+
return tracks.join(' ');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const colsForRender = $derived(
|
|
156
|
+
measuring ? manager.columns.map((c) => c.id) : manager.state.visibleColumns
|
|
157
|
+
);
|
|
158
|
+
const endExtras = $derived(
|
|
159
|
+
expandIconPosition === 'end' && !rowActions && manager.state.hiddenColumns.length > 0
|
|
160
|
+
);
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<div class={`space-y-3 ${measuring ? 'overflow-x-hidden' : ''}`} bind:this={container}>
|
|
164
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
165
|
+
{@render filters?.()}
|
|
166
|
+
{#if filterFields && filterFields.length}
|
|
167
|
+
<FilterPanel
|
|
168
|
+
fields={filterFields}
|
|
169
|
+
values={filterValues}
|
|
170
|
+
onapply={(defs) => manager.setFilters(defs)}
|
|
171
|
+
onclear={() => manager.clearFilters()}
|
|
172
|
+
/>
|
|
173
|
+
{/if}
|
|
174
|
+
{#if showColumnToggle}
|
|
175
|
+
<ColumnVisibilityToggle
|
|
176
|
+
columns={manager.columns}
|
|
177
|
+
visible={manager.state.visibleColumns}
|
|
178
|
+
onToggle={(id, show) => manager.setColumnVisibility(id, show)}
|
|
179
|
+
/>
|
|
180
|
+
{/if}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div class="rounded-2xl border border-gray-200 shadow-sm dark:border-gray-800">
|
|
184
|
+
<!-- HEADER -->
|
|
185
|
+
<div
|
|
186
|
+
class={`grid items-center border-b border-gray-200 text-sm font-medium dark:border-gray-800 ${stickyHeader ? 'sticky top-0 z-10 bg-white/90 backdrop-blur dark:bg-gray-950/80' : ''}`}
|
|
187
|
+
style={`grid-template-columns:${headerTemplateCols(colsForRender, endExtras)};`}
|
|
188
|
+
>
|
|
189
|
+
<div class="flex h-12 items-center px-3">
|
|
190
|
+
<input
|
|
191
|
+
type="checkbox"
|
|
192
|
+
checked={manager.state.items.length > 0 &&
|
|
193
|
+
manager.state.items.every((r) => manager.state.selected.has(rowId(r)))}
|
|
194
|
+
onclick={(e) =>
|
|
195
|
+
(e.currentTarget as HTMLInputElement).checked
|
|
196
|
+
? manager.selectAllCurrentPage(rowId)
|
|
197
|
+
: manager.clearSelection()}
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{#each colsForRender as cid}
|
|
202
|
+
<div
|
|
203
|
+
data-dt-head={cid}
|
|
204
|
+
class="flex h-12 items-center px-3 select-none"
|
|
205
|
+
class:cursor-pointer={manager.getColumn(cid).sortable}
|
|
206
|
+
onclick={() => headerClick(manager.getColumn(cid))}
|
|
207
|
+
oncontextmenu={(e) => onCellContext(e, null, cid, null)}
|
|
208
|
+
>
|
|
209
|
+
<div class="truncate">
|
|
210
|
+
{manager.getColumn(cid).header}
|
|
211
|
+
{#if manager.state.sortBy === cid}
|
|
212
|
+
<span class="ml-1 text-xs opacity-60">
|
|
213
|
+
{manager.state.sortDir === 'asc'
|
|
214
|
+
? '▲'
|
|
215
|
+
: manager.state.sortDir === 'desc'
|
|
216
|
+
? '▼'
|
|
217
|
+
: ''}
|
|
218
|
+
</span>
|
|
219
|
+
{/if}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
{/each}
|
|
223
|
+
|
|
224
|
+
{#if rowActions}
|
|
225
|
+
<div class="h-12 px-3"></div>
|
|
226
|
+
{:else if endExtras}
|
|
227
|
+
<div class="h-12 px-3"></div>
|
|
228
|
+
{/if}
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<!-- BODY -->
|
|
232
|
+
<div>
|
|
233
|
+
{#if manager.state.loading}
|
|
234
|
+
<div class="p-6 text-center opacity-70">Cargando…</div>
|
|
235
|
+
{:else if manager.state.error}
|
|
236
|
+
<div class="p-6 text-center text-red-600">{manager.state.error}</div>
|
|
237
|
+
{:else if manager.state.items.length === 0}
|
|
238
|
+
<div class="p-6 text-center opacity-70">Sin resultados</div>
|
|
239
|
+
{:else}
|
|
240
|
+
{#each manager.state.items as row, i (rowId(row))}
|
|
241
|
+
<!-- ROW -->
|
|
242
|
+
<div
|
|
243
|
+
class={`grid items-center border-b border-gray-100 last:border-b-0 dark:border-gray-900 ${sizeRow}`}
|
|
244
|
+
style={`grid-template-columns:${headerTemplateCols(colsForRender, endExtras)};`}
|
|
245
|
+
>
|
|
246
|
+
<!-- col 0: check + expand -->
|
|
247
|
+
<div class="px-3 py-2">
|
|
248
|
+
<div class="flex items-center gap-2">
|
|
249
|
+
<input
|
|
250
|
+
type="checkbox"
|
|
251
|
+
checked={manager.state.selected.has(rowId(row))}
|
|
252
|
+
onclick={() => manager.toggleSelect(rowId(row))}
|
|
253
|
+
oncontextmenu={(e) => onCellContext(e, row, '_check', i)}
|
|
254
|
+
/>
|
|
255
|
+
{#if manager.state.hiddenColumns.length > 0}
|
|
256
|
+
{#if expandIconPosition === 'start'}
|
|
257
|
+
<button
|
|
258
|
+
class="rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
259
|
+
title={manager.isExpanded(rowId(row)) ? 'Ocultar detalles' : 'Ver detalles'}
|
|
260
|
+
onclick={() => manager.toggleExpand(rowId(row))}
|
|
261
|
+
>
|
|
262
|
+
{#if manager.isExpanded(rowId(row))}<ChevronDown
|
|
263
|
+
class="h-4 w-4"
|
|
264
|
+
/>{:else}<ChevronRight class="h-4 w-4" />{/if}
|
|
265
|
+
</button>
|
|
266
|
+
{/if}
|
|
267
|
+
{/if}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<!-- data cells -->
|
|
272
|
+
{#each colsForRender as cid}
|
|
273
|
+
{@const col = manager.getColumn(cid)}
|
|
274
|
+
<div
|
|
275
|
+
onkeydown={(e) => console.log('KEYDOWN', e)}
|
|
276
|
+
tabindex="0"
|
|
277
|
+
role="button"
|
|
278
|
+
data-dt-cell="1"
|
|
279
|
+
data-col-id={cid}
|
|
280
|
+
data-row-index={i}
|
|
281
|
+
class="px-3"
|
|
282
|
+
onclick={() => onRowClick?.(row)}
|
|
283
|
+
oncontextmenu={(e) => onCellContext(e, row, cid, i)}
|
|
284
|
+
>
|
|
285
|
+
{#if col.renderCell}
|
|
286
|
+
{@render col.renderCell(row)}
|
|
287
|
+
{:else}
|
|
288
|
+
<Cell column={col} {row} {measuring} />
|
|
289
|
+
{/if}
|
|
290
|
+
</div>
|
|
291
|
+
{/each}
|
|
292
|
+
|
|
293
|
+
<!-- actions / expand-end button -->
|
|
294
|
+
{#if rowActions}
|
|
295
|
+
<div class="px-3 text-right">
|
|
296
|
+
<div class="inline-flex items-center gap-2">
|
|
297
|
+
{#if expandIconPosition === 'end' && manager.state.hiddenColumns.length > 0}
|
|
298
|
+
<button
|
|
299
|
+
class="rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
300
|
+
title={manager.isExpanded(rowId(row)) ? 'Ocultar detalles' : 'Ver detalles'}
|
|
301
|
+
onclick={() => manager.toggleExpand(rowId(row))}
|
|
302
|
+
>
|
|
303
|
+
{#if manager.isExpanded(rowId(row))}<ChevronDown
|
|
304
|
+
class="h-4 w-4"
|
|
305
|
+
/>{:else}<ChevronRight class="h-4 w-4" />{/if}
|
|
306
|
+
</button>
|
|
307
|
+
{/if}
|
|
308
|
+
{@html rowActions(row)}
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
{:else if endExtras}
|
|
312
|
+
<div class="px-3 text-right">
|
|
313
|
+
<button
|
|
314
|
+
class="rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
315
|
+
title={manager.isExpanded(rowId(row)) ? 'Ocultar detalles' : 'Ver detalles'}
|
|
316
|
+
onclick={() => manager.toggleExpand(rowId(row))}
|
|
317
|
+
>
|
|
318
|
+
{#if manager.isExpanded(rowId(row))}
|
|
319
|
+
<ChevronDown class="h-4 w-4" />
|
|
320
|
+
{:else}
|
|
321
|
+
<ChevronRight class="h-4 w-4" />
|
|
322
|
+
{/if}
|
|
323
|
+
</button>
|
|
324
|
+
</div>
|
|
325
|
+
{/if}
|
|
326
|
+
|
|
327
|
+
<!-- COLLAPSE: fila nueva de ancho completo (evita solapes) -->
|
|
328
|
+
{#if manager.isExpanded(rowId(row))}
|
|
329
|
+
<div class="col-span-full px-3 pt-1 pb-3">
|
|
330
|
+
<div class="grid gap-3 sm:grid-cols-2 md:grid-cols-3">
|
|
331
|
+
{#each manager.state.hiddenColumns as hid}
|
|
332
|
+
{#key hid}
|
|
333
|
+
{@const col = manager.columns.find((cc) => cc.id === hid)}
|
|
334
|
+
{#if col}
|
|
335
|
+
<div class="rounded-xl border p-3 dark:border-gray-800">
|
|
336
|
+
<div class="mb-1 text-[11px] tracking-wide uppercase opacity-60">
|
|
337
|
+
{col.responsiveLabel ?? col.header}
|
|
338
|
+
</div>
|
|
339
|
+
<div class="text-sm">
|
|
340
|
+
{#if col.renderCollapsed}
|
|
341
|
+
{@render col.renderCollapsed(row)}
|
|
342
|
+
{:else if col.renderCell}
|
|
343
|
+
{@render col.renderCell(row)}
|
|
344
|
+
{:else}
|
|
345
|
+
<Cell column={col} {row} measuring={false} />
|
|
346
|
+
{/if}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
{/if}
|
|
350
|
+
{/key}
|
|
351
|
+
{/each}
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
{/if}
|
|
355
|
+
</div>
|
|
356
|
+
{/each}
|
|
357
|
+
{/if}
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<Pagination
|
|
362
|
+
page={manager.state.page}
|
|
363
|
+
perPage={manager.state.perPage}
|
|
364
|
+
total={manager.state.total}
|
|
365
|
+
perPageOptions={options.perPageOptions}
|
|
366
|
+
onchange={(p) => manager.setPage(p)}
|
|
367
|
+
onperpage={(n) => manager.setPerPage(n)}
|
|
368
|
+
/>
|
|
369
|
+
|
|
370
|
+
<ContextMenu
|
|
371
|
+
bind:open={rightMenu.open}
|
|
372
|
+
x={rightMenu.x}
|
|
373
|
+
y={rightMenu.y}
|
|
374
|
+
context={rightClickContext}
|
|
375
|
+
items={(actions?.(selectedRows(), rightClickContext) ?? []).map((a) => ({
|
|
376
|
+
...a,
|
|
377
|
+
onClick: a.onClick
|
|
378
|
+
}))}
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { CellContext, TableOptions } from './core/types.js';
|
|
3
|
+
import type { Entry } from './components/ContextMenu.svelte';
|
|
4
|
+
import type { FilterField } from './core/filters/types.js';
|
|
5
|
+
interface Props<T> {
|
|
6
|
+
filters?: Snippet;
|
|
7
|
+
options: TableOptions<T>;
|
|
8
|
+
rowId?: (row: T) => any;
|
|
9
|
+
actions?: (rows: T[], ctx?: CellContext<T> | null) => Entry[];
|
|
10
|
+
rowActions?: (row: T) => any;
|
|
11
|
+
onRowClick?: (row: T) => void;
|
|
12
|
+
density?: 'compact' | 'normal' | 'comfortable';
|
|
13
|
+
stickyHeader?: boolean;
|
|
14
|
+
showColumnToggle?: boolean;
|
|
15
|
+
expandIconPosition?: 'start' | 'end';
|
|
16
|
+
filterFields?: FilterField<T>[];
|
|
17
|
+
}
|
|
18
|
+
declare function $$render<T extends {
|
|
19
|
+
id?: any;
|
|
20
|
+
}>(): Promise<{
|
|
21
|
+
props: Props<T>;
|
|
22
|
+
exports: {};
|
|
23
|
+
bindings: "";
|
|
24
|
+
slots: {};
|
|
25
|
+
events: {};
|
|
26
|
+
}>;
|
|
27
|
+
declare class __sveltets_Render<T extends {
|
|
28
|
+
id?: any;
|
|
29
|
+
}> {
|
|
30
|
+
props(): Awaited<ReturnType<typeof $$render<T>>>['props'];
|
|
31
|
+
events(): Awaited<ReturnType<typeof $$render<T>>>['events'];
|
|
32
|
+
slots(): Awaited<ReturnType<typeof $$render<T>>>['slots'];
|
|
33
|
+
bindings(): "";
|
|
34
|
+
exports(): Promise<{}>;
|
|
35
|
+
}
|
|
36
|
+
interface $$IsomorphicComponent {
|
|
37
|
+
new <T extends {
|
|
38
|
+
id?: any;
|
|
39
|
+
}>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
40
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
41
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
42
|
+
<T extends {
|
|
43
|
+
id?: any;
|
|
44
|
+
}>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
45
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
46
|
+
}
|
|
47
|
+
declare const DataTable: $$IsomorphicComponent;
|
|
48
|
+
type DataTable<T extends {
|
|
49
|
+
id?: any;
|
|
50
|
+
}> = InstanceType<typeof DataTable<T>>;
|
|
51
|
+
export default DataTable;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import type { ColumnDef } from '../core/types';
|
|
3
|
+
import { Check, X, ExternalLink } from 'lucide-svelte';
|
|
4
|
+
|
|
5
|
+
interface Props<T> {
|
|
6
|
+
column: ColumnDef<T>;
|
|
7
|
+
row: T;
|
|
8
|
+
measuring?: boolean;
|
|
9
|
+
}
|
|
10
|
+
const { column, row, measuring = false }: Props<T> = $props();
|
|
11
|
+
|
|
12
|
+
const raw = column.accessor ? column.accessor(row) : (row as any)[column.id];
|
|
13
|
+
const align = column.align ?? 'left';
|
|
14
|
+
|
|
15
|
+
function fmt(val: any) {
|
|
16
|
+
if (val == null) return '';
|
|
17
|
+
switch (column.type) {
|
|
18
|
+
case 'number':
|
|
19
|
+
return typeof val === 'number' ? val.toLocaleString() : val;
|
|
20
|
+
case 'currency':
|
|
21
|
+
return typeof val === 'number'
|
|
22
|
+
? val.toLocaleString(undefined, {
|
|
23
|
+
style: 'currency',
|
|
24
|
+
currency: 'EUR',
|
|
25
|
+
...(column.format ?? {})
|
|
26
|
+
})
|
|
27
|
+
: val;
|
|
28
|
+
case 'date':
|
|
29
|
+
try {
|
|
30
|
+
return new Date(val).toLocaleDateString(undefined, column.format);
|
|
31
|
+
} catch {
|
|
32
|
+
return val;
|
|
33
|
+
}
|
|
34
|
+
case 'datetime':
|
|
35
|
+
try {
|
|
36
|
+
return new Date(val).toLocaleString(undefined, column.format);
|
|
37
|
+
} catch {
|
|
38
|
+
return val;
|
|
39
|
+
}
|
|
40
|
+
case 'code':
|
|
41
|
+
return String(val);
|
|
42
|
+
default:
|
|
43
|
+
return String(val);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
{#if column.type === 'boolean'}
|
|
49
|
+
<div class="inline-flex items-center gap-1" style={`text-align:${align}`}>
|
|
50
|
+
{#if !!raw}
|
|
51
|
+
<Check class="h-4 w-4" />
|
|
52
|
+
<span class="text-sm">{column.trueLabel ?? 'Sí'}</span>
|
|
53
|
+
{:else}
|
|
54
|
+
<X class="h-4 w-4 opacity-60" />
|
|
55
|
+
<span class="text-sm opacity-70">{column.falseLabel ?? 'No'}</span>
|
|
56
|
+
{/if}
|
|
57
|
+
</div>
|
|
58
|
+
{:else if column.type === 'link'}
|
|
59
|
+
<a
|
|
60
|
+
class={`inline-flex items-center gap-1 ${measuring ? '' : 'truncate'}`}
|
|
61
|
+
style={`text-align:${align}`}
|
|
62
|
+
href={String(raw)}
|
|
63
|
+
target="_blank"
|
|
64
|
+
rel="noreferrer"
|
|
65
|
+
title={String(raw)}
|
|
66
|
+
>
|
|
67
|
+
<span>{String(raw)}</span>
|
|
68
|
+
<ExternalLink class="h-3 w-3 opacity-60" />
|
|
69
|
+
</a>
|
|
70
|
+
{:else if column.type === 'badge'}
|
|
71
|
+
<span
|
|
72
|
+
class={`inline-block rounded-full px-2 py-0.5 text-xs ${measuring ? '' : 'truncate'}`}
|
|
73
|
+
style={`text-align:${align}`}
|
|
74
|
+
title={fmt(raw)}>{fmt(raw)}</span
|
|
75
|
+
>
|
|
76
|
+
{:else if column.type === 'code'}
|
|
77
|
+
<code
|
|
78
|
+
class={`rounded bg-gray-100 px-1 py-0.5 text-[12px] dark:bg-gray-800 ${measuring ? '' : 'truncate'}`}
|
|
79
|
+
style={`text-align:${align}`}
|
|
80
|
+
title={fmt(raw)}>{fmt(raw)}</code
|
|
81
|
+
>
|
|
82
|
+
{:else}
|
|
83
|
+
<div
|
|
84
|
+
class={`${measuring ? '' : 'truncate'} text-sm`}
|
|
85
|
+
style={`text-align:${align}`}
|
|
86
|
+
title={fmt(raw)}
|
|
87
|
+
>
|
|
88
|
+
{fmt(raw)}
|
|
89
|
+
</div>
|
|
90
|
+
{/if}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ColumnDef } from '../core/types';
|
|
2
|
+
interface Props<T> {
|
|
3
|
+
column: ColumnDef<T>;
|
|
4
|
+
row: T;
|
|
5
|
+
measuring?: boolean;
|
|
6
|
+
}
|
|
7
|
+
declare function $$render<T>(): {
|
|
8
|
+
props: Props<T>;
|
|
9
|
+
exports: {};
|
|
10
|
+
bindings: "";
|
|
11
|
+
slots: {};
|
|
12
|
+
events: {};
|
|
13
|
+
};
|
|
14
|
+
declare class __sveltets_Render<T> {
|
|
15
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
16
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
17
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
18
|
+
bindings(): "";
|
|
19
|
+
exports(): {};
|
|
20
|
+
}
|
|
21
|
+
interface $$IsomorphicComponent {
|
|
22
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
23
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
24
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
25
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
26
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
27
|
+
}
|
|
28
|
+
declare const Cell: $$IsomorphicComponent;
|
|
29
|
+
type Cell<T> = InstanceType<typeof Cell<T>>;
|
|
30
|
+
export default Cell;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ColumnDef } from '../core/types';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
columns?: ColumnDef<any>[];
|
|
6
|
+
visible?: string[];
|
|
7
|
+
onToggle: (id: string, show: boolean) => void;
|
|
8
|
+
buttonText?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { columns = [], visible = [], onToggle, buttonText = 'Columnas' }: Props = $props();
|
|
12
|
+
|
|
13
|
+
let open = $state(false);
|
|
14
|
+
let q = $state('');
|
|
15
|
+
let anchor: HTMLButtonElement | null = $state(null);
|
|
16
|
+
let pop: HTMLDivElement | null = $state(null);
|
|
17
|
+
|
|
18
|
+
const sorted = $derived([...columns].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0)));
|
|
19
|
+
const filtered = $derived(
|
|
20
|
+
q.trim() ? sorted.filter((c) => c.header.toLowerCase().includes(q.toLowerCase())) : sorted
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
function toggleAll(show: boolean) {
|
|
24
|
+
for (const c of filtered) onToggle(c.id, show);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
$effect(() => {
|
|
28
|
+
function onDoc(e: MouseEvent) {
|
|
29
|
+
if (!open) return;
|
|
30
|
+
const t = e.target as Node;
|
|
31
|
+
if (pop?.contains(t) || anchor?.contains(t)) return;
|
|
32
|
+
open = false;
|
|
33
|
+
}
|
|
34
|
+
document.addEventListener('click', onDoc);
|
|
35
|
+
return () => document.removeEventListener('click', onDoc);
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<div class="relative">
|
|
40
|
+
<button
|
|
41
|
+
bind:this={anchor}
|
|
42
|
+
class="inline-flex cursor-pointer items-center gap-2 rounded-xl border border-gray-200 px-3 py-2 text-sm hover:bg-gray-50 dark:border-gray-800 dark:hover:bg-gray-900"
|
|
43
|
+
onclick={() => (open = !open)}
|
|
44
|
+
aria-haspopup="listbox"
|
|
45
|
+
aria-expanded={open}
|
|
46
|
+
>
|
|
47
|
+
<svg
|
|
48
|
+
width="16"
|
|
49
|
+
height="16"
|
|
50
|
+
viewBox="0 0 24 24"
|
|
51
|
+
fill="none"
|
|
52
|
+
stroke="currentColor"
|
|
53
|
+
stroke-width="2"
|
|
54
|
+
><rect x="3" y="4" width="18" height="4"></rect><rect x="3" y="10" width="18" height="4"
|
|
55
|
+
></rect><rect x="3" y="16" width="18" height="4"></rect></svg
|
|
56
|
+
>
|
|
57
|
+
{buttonText}
|
|
58
|
+
<span class="rounded bg-gray-100 px-1 text-[11px] dark:bg-gray-800"
|
|
59
|
+
>{visible.length}/{columns.length}</span
|
|
60
|
+
>
|
|
61
|
+
<svg width="14" height="14" viewBox="0 0 24 24" class="opacity-60"
|
|
62
|
+
><polyline
|
|
63
|
+
points="6 9 12 15 18 9"
|
|
64
|
+
fill="none"
|
|
65
|
+
stroke="currentColor"
|
|
66
|
+
stroke-width="2"
|
|
67
|
+
stroke-linecap="round"
|
|
68
|
+
stroke-linejoin="round"
|
|
69
|
+
/></svg
|
|
70
|
+
>
|
|
71
|
+
</button>
|
|
72
|
+
|
|
73
|
+
{#if open}
|
|
74
|
+
<div
|
|
75
|
+
bind:this={pop}
|
|
76
|
+
class="absolute z-50 mt-2 w-64 rounded-2xl border border-gray-200 bg-white p-2 shadow-xl ring-1 ring-black/5 dark:border-gray-800 dark:bg-gray-900"
|
|
77
|
+
style={`right:0;`}
|
|
78
|
+
role="listbox"
|
|
79
|
+
>
|
|
80
|
+
<div class="flex items-center gap-2 p-2">
|
|
81
|
+
<input
|
|
82
|
+
class="w-full rounded-xl border border-gray-200 px-3 py-2 text-sm dark:border-gray-800 dark:bg-gray-950"
|
|
83
|
+
placeholder="Filtrar columnas…"
|
|
84
|
+
bind:value={q}
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="flex items-center justify-between gap-2 px-2 pb-2">
|
|
88
|
+
<button
|
|
89
|
+
class="text-xs underline opacity-80 hover:opacity-100"
|
|
90
|
+
onclick={() => toggleAll(true)}>Mostrar todas</button
|
|
91
|
+
>
|
|
92
|
+
<button
|
|
93
|
+
class="text-xs underline opacity-80 hover:opacity-100"
|
|
94
|
+
onclick={() => toggleAll(false)}>Ocultar todas</button
|
|
95
|
+
>
|
|
96
|
+
</div>
|
|
97
|
+
<div
|
|
98
|
+
class="max-h-64 overflow-auto rounded-xl border border-gray-200 p-1 dark:border-gray-800 dark:bg-gray-950"
|
|
99
|
+
>
|
|
100
|
+
{#each filtered as c (c.id)}
|
|
101
|
+
<label
|
|
102
|
+
class="flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1.5 hover:bg-gray-50 dark:hover:bg-gray-800"
|
|
103
|
+
>
|
|
104
|
+
<input
|
|
105
|
+
type="checkbox"
|
|
106
|
+
checked={visible.includes(c.id)}
|
|
107
|
+
onclick={(e) => onToggle(c.id, (e.target as HTMLInputElement).checked)}
|
|
108
|
+
/>
|
|
109
|
+
<span class="text-sm">{c.header}</span>
|
|
110
|
+
</label>
|
|
111
|
+
{/each}
|
|
112
|
+
{#if filtered.length === 0}
|
|
113
|
+
<div class="p-3 text-center text-xs opacity-60">Sin coincidencias</div>
|
|
114
|
+
{/if}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
{/if}
|
|
118
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ColumnDef } from '../core/types';
|
|
2
|
+
interface Props {
|
|
3
|
+
columns?: ColumnDef<any>[];
|
|
4
|
+
visible?: string[];
|
|
5
|
+
onToggle: (id: string, show: boolean) => void;
|
|
6
|
+
buttonText?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const ColumnVisibilityToggle: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ColumnVisibilityToggle = ReturnType<typeof ColumnVisibilityToggle>;
|
|
10
|
+
export default ColumnVisibilityToggle;
|