@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.
Files changed (36) hide show
  1. package/dist/components/container/DataTable/DataTable.svelte +380 -0
  2. package/dist/components/container/DataTable/DataTable.svelte.d.ts +51 -0
  3. package/dist/components/container/DataTable/components/Cell.svelte +90 -0
  4. package/dist/components/container/DataTable/components/Cell.svelte.d.ts +30 -0
  5. package/dist/components/container/DataTable/components/ColumnVisibilityToggle.svelte +118 -0
  6. package/dist/components/container/DataTable/components/ColumnVisibilityToggle.svelte.d.ts +10 -0
  7. package/dist/components/container/DataTable/components/ContextMenu.svelte +203 -0
  8. package/dist/components/container/DataTable/components/ContextMenu.svelte.d.ts +21 -0
  9. package/dist/components/container/DataTable/components/FilterPanel.svelte +195 -0
  10. package/dist/components/container/DataTable/components/FilterPanel.svelte.d.ts +10 -0
  11. package/dist/components/container/DataTable/components/Pagination.svelte +59 -0
  12. package/dist/components/container/DataTable/components/Pagination.svelte.d.ts +11 -0
  13. package/dist/components/container/DataTable/components/QuickFilters.svelte +38 -0
  14. package/dist/components/container/DataTable/components/QuickFilters.svelte.d.ts +8 -0
  15. package/dist/components/container/DataTable/core/DataTableManager.svelte.d.ts +39 -0
  16. package/dist/components/container/DataTable/core/DataTableManager.svelte.js +245 -0
  17. package/dist/components/container/DataTable/core/filters/types.d.ts +18 -0
  18. package/dist/components/container/DataTable/core/filters/types.js +1 -0
  19. package/dist/components/container/DataTable/core/filters/utils.d.ts +3 -0
  20. package/dist/components/container/DataTable/core/filters/utils.js +43 -0
  21. package/dist/components/container/DataTable/core/types.d.ts +101 -0
  22. package/dist/components/container/DataTable/core/types.js +1 -0
  23. package/dist/components/container/DataTable/core/utils.d.ts +5 -0
  24. package/dist/components/container/DataTable/core/utils.js +66 -0
  25. package/dist/components/container/index.d.ts +2 -0
  26. package/dist/components/container/index.js +2 -0
  27. package/dist/components/ui/Card/Card.svelte +8 -8
  28. package/dist/components/ui/Card/CardContent.svelte +11 -3
  29. package/dist/components/ui/Card/CardContent.svelte.d.ts +8 -10
  30. package/dist/components/ui/Card/CardFooter.svelte +12 -2
  31. package/dist/components/ui/Card/CardFooter.svelte.d.ts +7 -3
  32. package/dist/components/ui/Card/CardHeader.svelte +10 -2
  33. package/dist/components/ui/Card/CardHeader.svelte.d.ts +7 -3
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.js +1 -0
  36. 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;