@r2digisolutions/ui 0.27.4 → 0.28.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 (35) hide show
  1. package/dist/components/container/DataTableShell/DataTableShell.svelte +631 -0
  2. package/dist/components/container/DataTableShell/DataTableShell.svelte.d.ts +48 -0
  3. package/dist/components/container/DataTableShell/components/AdvancedFiltersBuilder.svelte +311 -0
  4. package/dist/components/container/DataTableShell/components/AdvancedFiltersBuilder.svelte.d.ts +7 -0
  5. package/dist/components/container/DataTableShell/components/ColumnVisibilityMenu.svelte +112 -0
  6. package/dist/components/container/DataTableShell/components/ColumnVisibilityMenu.svelte.d.ts +8 -0
  7. package/dist/components/container/DataTableShell/components/ContextMenu.svelte +70 -0
  8. package/dist/components/container/DataTableShell/components/ContextMenu.svelte.d.ts +30 -0
  9. package/dist/components/container/DataTableShell/components/DataTableFiltersSidebar.svelte +0 -0
  10. package/dist/components/container/DataTableShell/components/DataTableFiltersSidebar.svelte.d.ts +26 -0
  11. package/dist/components/container/DataTableShell/components/DataTableFooter.svelte +36 -0
  12. package/dist/components/container/DataTableShell/components/DataTableFooter.svelte.d.ts +18 -0
  13. package/dist/components/container/DataTableShell/components/DataTableToolbar.svelte +822 -0
  14. package/dist/components/container/DataTableShell/components/DataTableToolbar.svelte.d.ts +30 -0
  15. package/dist/components/container/DataTableShell/components/Pagination.svelte +117 -0
  16. package/dist/components/container/DataTableShell/components/Pagination.svelte.d.ts +28 -0
  17. package/dist/components/container/DataTableShell/components/Submenu.svelte +109 -0
  18. package/dist/components/container/DataTableShell/components/Submenu.svelte.d.ts +30 -0
  19. package/dist/components/container/DataTableShell/components/Toolbar.svelte +0 -0
  20. package/dist/components/container/DataTableShell/components/Toolbar.svelte.d.ts +26 -0
  21. package/dist/components/container/DataTableShell/core/DataTableController.svelte.d.ts +54 -0
  22. package/dist/components/container/DataTableShell/core/DataTableController.svelte.js +148 -0
  23. package/dist/components/container/DataTableShell/core/DataTableEngine.svelte.d.ts +68 -0
  24. package/dist/components/container/DataTableShell/core/DataTableEngine.svelte.js +319 -0
  25. package/dist/components/container/DataTableShell/core/DataTableInternal.svelte.d.ts +68 -0
  26. package/dist/components/container/DataTableShell/core/DataTableInternal.svelte.js +396 -0
  27. package/dist/components/container/DataTableShell/core/context.d.ts +3 -0
  28. package/dist/components/container/DataTableShell/core/context.js +12 -0
  29. package/dist/components/container/DataTableShell/core/filters-types.d.ts +14 -0
  30. package/dist/components/container/DataTableShell/core/filters-types.js +1 -0
  31. package/dist/components/container/DataTableShell/core/types.d.ts +60 -0
  32. package/dist/components/container/DataTableShell/core/types.js +1 -0
  33. package/dist/components/container/index.d.ts +3 -1
  34. package/dist/components/container/index.js +3 -1
  35. package/package.json +8 -8
@@ -0,0 +1,631 @@
1
+ <script lang="ts" generics="T">
2
+ import type { Snippet } from 'svelte';
3
+ import { EllipsisVertical, ChevronDown } from 'lucide-svelte';
4
+ import type { ColumnDef, RowAction } from './core/types.js';
5
+ import type { DataTableController } from './core/DataTableController.svelte';
6
+ import { provideTable } from './core/context.js';
7
+ import DataTableToolbar from './components/DataTableToolbar.svelte';
8
+ import DataTableFooter from './components/DataTableFooter.svelte';
9
+ import ContextMenu from './components/ContextMenu.svelte';
10
+
11
+ interface CellContext<T> {
12
+ row: T;
13
+ column: ColumnDef<T>;
14
+ value: unknown;
15
+ index: number;
16
+ }
17
+
18
+ interface Props<T> {
19
+ controller: DataTableController<T>;
20
+ actions?: RowAction<T>[];
21
+ cell?: Snippet<[CellContext<T>]>;
22
+ overflow?: Snippet<[T]>;
23
+ headerCell?: Snippet<[ColumnDef<T>]>;
24
+ rowActions?: Snippet<[T, RowAction<T>[]]>;
25
+ bulkActions?: Snippet<
26
+ [
27
+ {
28
+ selectedIds: string[];
29
+ clearSelection: () => void;
30
+ }
31
+ ]
32
+ >;
33
+ rowCollapse?: Snippet<[T]>;
34
+ }
35
+
36
+ const {
37
+ controller,
38
+ actions = [],
39
+ cell,
40
+ overflow,
41
+ headerCell,
42
+ rowActions,
43
+ bulkActions,
44
+ rowCollapse
45
+ }: Props<T> = $props();
46
+
47
+ provideTable(controller);
48
+
49
+ let density = $state<'comfortable' | 'compact'>('comfortable');
50
+ let viewMode = $state<'list' | 'grid'>('list');
51
+
52
+ let selectAllEl = $state<HTMLInputElement | null>(null);
53
+
54
+ let gridTemplate = $state('');
55
+ let stickyOffsets = $state<Record<keyof T, { left?: number; right?: number }>>(
56
+ {} as Record<keyof T, { left?: number; right?: number }>
57
+ );
58
+
59
+ let contextPopover = $state<HTMLDivElement | null>(null);
60
+ let contextRow = $state<T | null>(null);
61
+ let contextPos = $state<{ x: number; y: number }>({ x: 0, y: 0 });
62
+
63
+ let openRows = $state<Set<string>>(new Set());
64
+
65
+ const contextOpen = $derived(contextRow !== null);
66
+
67
+ // GRID / STICKY
68
+ $effect(() => {
69
+ const parts: string[] = [];
70
+ if (controller.multiSelect) parts.push('40px');
71
+
72
+ controller.mainColumns.forEach((col) => {
73
+ const w = controller.getColumnWidth(col.id as keyof T);
74
+ parts.push(`${w}px`);
75
+ });
76
+
77
+ // Columna de acciones
78
+ parts.push('64px');
79
+ gridTemplate = parts.join(' ');
80
+
81
+ const offsets: Record<keyof T, { left?: number; right?: number }> = {} as Record<
82
+ keyof T,
83
+ { left?: number; right?: number }
84
+ >;
85
+
86
+ let accLeft = controller.multiSelect ? 40 : 0;
87
+ controller.mainColumns.forEach((col) => {
88
+ const w = controller.getColumnWidth(col.id as keyof T);
89
+ if (col.sticky === 'left') {
90
+ offsets[col.id as keyof T] = { left: accLeft };
91
+ }
92
+ accLeft += w;
93
+ });
94
+ stickyOffsets = offsets;
95
+ });
96
+
97
+ // CHECK ALL
98
+ $effect(() => {
99
+ if (!controller.multiSelect || !selectAllEl) return;
100
+ if (!controller.currentRows.length) {
101
+ selectAllEl.checked = false;
102
+ selectAllEl.indeterminate = false;
103
+ return;
104
+ }
105
+ selectAllEl.checked = controller.allVisibleSelected;
106
+ selectAllEl.indeterminate = controller.someVisibleSelected;
107
+ });
108
+
109
+ // CERRAR CONTEXT MENU
110
+ $effect(() => {
111
+ function handleDocumentClick(event: MouseEvent) {
112
+ if (!contextOpen) return;
113
+ const target = event.target as HTMLElement;
114
+ if (!target.closest('[data-context-host="true"]')) {
115
+ closeContext();
116
+ }
117
+ }
118
+ if (contextOpen) {
119
+ document.addEventListener('click', handleDocumentClick);
120
+ }
121
+ return () => {
122
+ document.removeEventListener('click', handleDocumentClick);
123
+ };
124
+ });
125
+
126
+ let resizingId: keyof T | null = null;
127
+ let startX = 0;
128
+ let startWidth = 0;
129
+
130
+ function onResizeDown(event: MouseEvent, columnId: keyof T, el: HTMLDivElement) {
131
+ event.preventDefault();
132
+ event.stopPropagation();
133
+ resizingId = columnId;
134
+ startX = event.clientX;
135
+ startWidth = el.getBoundingClientRect().width;
136
+ window.addEventListener('mousemove', onResizeMove);
137
+ window.addEventListener('mouseup', onResizeUp);
138
+ }
139
+
140
+ function onResizeMove(event: MouseEvent) {
141
+ if (!resizingId) return;
142
+ const dx = event.clientX - startX;
143
+ const width = startWidth + dx;
144
+ controller.resizeColumn(resizingId, width);
145
+ }
146
+
147
+ function onResizeUp() {
148
+ resizingId = null;
149
+ window.removeEventListener('mousemove', onResizeMove);
150
+ window.removeEventListener('mouseup', onResizeUp);
151
+ }
152
+
153
+ function rowIdFor(row: T, index: number) {
154
+ return controller.getRowId(row, index);
155
+ }
156
+
157
+ function formatValue(col: ColumnDef<T>, value: unknown, row: T): string {
158
+ if ((col as any).format) return (col as any).format(value, row);
159
+ if (value == null) return '';
160
+ if (col.type === 'number') return String(value);
161
+ if (col.type === 'date' || col.type === 'datetime') {
162
+ if (value instanceof Date) return value.toLocaleString();
163
+ return String(value);
164
+ }
165
+ return String(value);
166
+ }
167
+
168
+ function openContextAt(event: MouseEvent, row: T) {
169
+ event.preventDefault();
170
+ event.stopPropagation();
171
+ contextRow = row;
172
+ contextPos = { x: event.clientX, y: event.clientY };
173
+ if (contextPopover) contextPopover.showPopover();
174
+ }
175
+
176
+ function openContextFromButton(event: MouseEvent, row: T) {
177
+ event.preventDefault();
178
+ event.stopPropagation();
179
+ const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
180
+ contextRow = row;
181
+ contextPos = { x: rect.right, y: rect.bottom };
182
+ if (contextPopover) contextPopover.showPopover();
183
+ }
184
+
185
+ function closeContext() {
186
+ if (contextPopover) contextPopover.hidePopover();
187
+ contextRow = null;
188
+ }
189
+
190
+ function toggleRow(row: T, index: number) {
191
+ if (!rowCollapse) return;
192
+ const id = rowIdFor(row, index);
193
+ const next = new Set(openRows);
194
+ if (next.has(id)) next.delete(id);
195
+ else next.add(id);
196
+ openRows = next;
197
+ }
198
+
199
+ function handleRowClick(event: MouseEvent, row: T, index: number) {
200
+ const target = event.target as HTMLElement;
201
+ if (target.closest('[data-stop-row-toggle="true"]')) return;
202
+ toggleRow(row, index);
203
+ }
204
+
205
+ function handleToggleAll(e: Event) {
206
+ const input = e.currentTarget as HTMLInputElement;
207
+ const checked = input.checked;
208
+ if (checked) {
209
+ controller.selectAllCurrentPage();
210
+ } else {
211
+ controller.unselectAllCurrentPage();
212
+ }
213
+ }
214
+ </script>
215
+
216
+ <div
217
+ class="flex flex-col overflow-hidden rounded-2xl border border-neutral-200/80 bg-neutral-50/70 text-xs text-neutral-900 shadow-sm backdrop-blur-2xl dark:border-neutral-800/80 dark:bg-neutral-950/70 dark:text-neutral-50"
218
+ >
219
+ <DataTableToolbar
220
+ {density}
221
+ {viewMode}
222
+ onDensityChange={(d) => (density = d)}
223
+ onViewModeChange={(m) => (viewMode = m)}
224
+ />
225
+
226
+ {#if bulkActions && controller.multiSelect && controller.selectedIds.size}
227
+ <div
228
+ class="border-b border-dashed border-neutral-200/80 bg-purple-50/70 px-3 py-2 text-[11px] text-neutral-700 dark:border-neutral-800/80 dark:bg-purple-950/40 dark:text-neutral-100"
229
+ >
230
+ {@render bulkActions({
231
+ selectedIds: Array.from(controller.selectedIds),
232
+ clearSelection: () => controller.clearSelection()
233
+ })}
234
+ </div>
235
+ {/if}
236
+
237
+ <div class="relative max-h-[70vh] flex-1 overflow-auto">
238
+ {#if controller.loading}
239
+ <div class="pointer-events-none absolute inset-0 z-20 bg-neutral-900/30 backdrop-blur-md">
240
+ <div class="flex h-full items-center justify-center">
241
+ <div
242
+ class="flex items-center gap-2 rounded-full bg-neutral-900/90 px-4 py-2 text-[11px] text-neutral-100 shadow-lg dark:bg-neutral-950/90"
243
+ >
244
+ <div class="h-2 w-2 animate-pulse rounded-full bg-purple-500"></div>
245
+ Cargando datos
246
+ </div>
247
+ </div>
248
+ </div>
249
+ {/if}
250
+
251
+ <div class="min-w-full">
252
+ <!-- HEADER -->
253
+ <div
254
+ class="sticky top-0 z-10 border-b border-neutral-200/80 bg-gradient-to-br from-neutral-100/95 via-neutral-50/95 to-neutral-100/95 text-[11px] tracking-wide text-neutral-500 uppercase backdrop-blur-xl dark:border-neutral-800/80 dark:bg-gradient-to-br dark:from-neutral-950/95 dark:via-neutral-950/95 dark:to-neutral-900/95 dark:text-neutral-400"
255
+ >
256
+ <div class="grid items-center gap-0" style={`grid-template-columns:${gridTemplate}`}>
257
+ {#if controller.multiSelect}
258
+ <div
259
+ class={`sticky top-0 left-0 z-20 flex items-center justify-center border-r border-neutral-200/60 bg-neutral-100/95 px-2 ${
260
+ density === 'compact' ? 'py-1.5' : 'py-2.5'
261
+ } backdrop-blur-xl dark:border-neutral-800/70 dark:bg-neutral-950/95`}
262
+ >
263
+ <input
264
+ type="checkbox"
265
+ bind:this={selectAllEl}
266
+ onchange={handleToggleAll}
267
+ class="h-3.5 w-3.5 rounded border-neutral-300 bg-neutral-50 text-purple-500 focus:ring-purple-500 dark:border-neutral-600 dark:bg-neutral-900"
268
+ data-stop-row-toggle="true"
269
+ />
270
+ </div>
271
+ {/if}
272
+
273
+ {#each controller.mainColumns as col (col.id)}
274
+ {@const sticky = stickyOffsets[col.id as keyof T]}
275
+ <div
276
+ role="columnheader"
277
+ tabindex="0"
278
+ class={`relative flex items-center border-r border-neutral-200/60 px-3 ${
279
+ density === 'compact' ? 'py-1.5' : 'py-2.5'
280
+ } text-left text-[11px] font-semibold text-neutral-600 dark:border-neutral-800/70 dark:text-neutral-300 ${
281
+ col.sticky === 'left'
282
+ ? 'z-10 bg-neutral-100/95 shadow-[1px_0_0_rgba(15,23,42,0.15)] backdrop-blur-xl dark:bg-neutral-950/95'
283
+ : ''
284
+ }`}
285
+ style={col.sticky === 'left' && sticky?.left !== undefined
286
+ ? `position: sticky; left: ${sticky.left}px; top: 0;`
287
+ : ''}
288
+ ondblclick={() => col.sortable && controller.toggleSort(col.id as keyof T)}
289
+ >
290
+ <button
291
+ type="button"
292
+ class="flex w-full items-center justify-between gap-1"
293
+ onclick={() => col.sortable && controller.toggleSort(col.id as keyof T)}
294
+ data-stop-row-toggle="true"
295
+ >
296
+ {#if headerCell}
297
+ {@render headerCell(col)}
298
+ {:else}
299
+ <span class="line-clamp-1">{col.label}</span>
300
+ {/if}
301
+ {#if col.sortable}
302
+ <span
303
+ class={`text-[9px] ${
304
+ controller.sortColumn === col.id
305
+ ? 'text-purple-500'
306
+ : 'text-neutral-300 dark:text-neutral-600'
307
+ }`}
308
+ >
309
+ {#if controller.sortColumn === col.id}
310
+ {controller.sortDirection === 'asc' ? '▲' : '▼'}
311
+ {:else}
312
+
313
+ {/if}
314
+ </span>
315
+ {/if}
316
+ </button>
317
+ <div
318
+ role="columnheader"
319
+ tabindex="0"
320
+ class="absolute inset-y-1 right-0 flex w-2 cursor-col-resize items-center justify-end"
321
+ onmousedown={(e) =>
322
+ onResizeDown(
323
+ e,
324
+ col.id as keyof T,
325
+ e.currentTarget.parentElement as HTMLDivElement
326
+ )}
327
+ data-stop-row-toggle="true"
328
+ >
329
+ <div
330
+ class="h-6 w-[2px] rounded-full bg-neutral-200 hover:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-400"
331
+ ></div>
332
+ </div>
333
+ </div>
334
+ {/each}
335
+
336
+ <div
337
+ class={`sticky top-0 right-0 z-20 flex items-center justify-end border-l border-neutral-200/60 bg-neutral-100/95 px-2 ${
338
+ density === 'compact' ? 'py-1.5' : 'py-2.5'
339
+ } backdrop-blur-xl dark:border-neutral-800/70 dark:bg-neutral-950/95`}
340
+ ></div>
341
+ </div>
342
+ </div>
343
+
344
+ <!-- BODY -->
345
+ {#if controller.currentRows.length}
346
+ {#if viewMode === 'list'}
347
+ <div class="divide-y divide-neutral-200/80 dark:divide-neutral-800/80">
348
+ {#each controller.currentRows as row, index (rowIdFor(row, index))}
349
+ {@const id = rowIdFor(row, index)}
350
+ <div
351
+ role="row"
352
+ tabindex="0"
353
+ class={`relative bg-neutral-50/60 text-xs text-neutral-800 transition-colors odd:bg-neutral-50/70 even:bg-neutral-100/60 hover:bg-neutral-100/90 dark:bg-neutral-950/70 dark:text-neutral-100 dark:odd:bg-neutral-950/70 dark:even:bg-neutral-900/70 dark:hover:bg-neutral-900/80 ${
354
+ controller.selectedIds.has(id)
355
+ ? 'bg-purple-50/60 ring-1 ring-purple-400/60 dark:bg-purple-950/25'
356
+ : ''
357
+ }`}
358
+ oncontextmenu={(e) => openContextAt(e, row)}
359
+ >
360
+ <div
361
+ class="grid items-stretch"
362
+ style={`grid-template-columns:${gridTemplate}`}
363
+ onclick={(e) => handleRowClick(e, row, index)}
364
+ >
365
+ {#if controller.multiSelect}
366
+ <div
367
+ class={`sticky left-0 z-10 flex items-center justify-center border-r border-neutral-200/60 bg-neutral-50/95 px-2 ${
368
+ density === 'compact' ? 'py-1.5' : 'py-2.5'
369
+ } backdrop-blur-xl dark:border-neutral-800/70 dark:bg-neutral-950/95`}
370
+ data-stop-row-toggle="true"
371
+ >
372
+ <input
373
+ type="checkbox"
374
+ checked={controller.selectedIds.has(id)}
375
+ onchange={() => controller.toggleRowSelection(id)}
376
+ class="h-3.5 w-3.5 rounded border-neutral-300 bg-neutral-50 text-purple-500 focus:ring-purple-500 dark:border-neutral-600 dark:bg-neutral-900"
377
+ />
378
+ </div>
379
+ {/if}
380
+
381
+ {#each controller.mainColumns as col (col.id)}
382
+ {@const value = col.accessor ? col.accessor(row) : (row as any)[col.id]}
383
+ {@const sticky = stickyOffsets[col.id as keyof T]}
384
+ <div
385
+ class={`flex items-center border-r border-neutral-200/60 px-3 ${
386
+ density === 'compact' ? 'py-1.5' : 'py-2.5'
387
+ } dark:border-neutral-800/70 ${
388
+ col.sticky === 'left'
389
+ ? 'z-[5] bg-neutral-50/95 shadow-[1px_0_0_rgba(15,23,42,0.10)] backdrop-blur-xl dark:bg-neutral-950/95'
390
+ : ''
391
+ }`}
392
+ style={col.sticky === 'left' && sticky?.left !== undefined
393
+ ? `position: sticky; left: ${sticky.left}px;`
394
+ : ''}
395
+ >
396
+ {#if cell}
397
+ {@render cell({
398
+ row,
399
+ column: col,
400
+ value,
401
+ index
402
+ })}
403
+ {:else}
404
+ <span
405
+ class={`line-clamp-2 ${
406
+ col.align === 'right'
407
+ ? 'ml-auto text-right'
408
+ : col.align === 'center'
409
+ ? 'mx-auto text-center'
410
+ : ''
411
+ }`}
412
+ >
413
+ {formatValue(col, value, row)}
414
+ </span>
415
+ {/if}
416
+ </div>
417
+ {/each}
418
+
419
+ <div
420
+ class={`sticky right-0 z-10 flex items-center justify-end border-l border-neutral-200/60 bg-neutral-50/95 px-2 ${
421
+ density === 'compact' ? 'py-1.5' : 'py-2.5'
422
+ } backdrop-blur-xl dark:border-neutral-800/70 dark:bg-neutral-950/95`}
423
+ data-stop-row-toggle="true"
424
+ >
425
+ {#if actions.length}
426
+ {#if rowActions}
427
+ {@render rowActions(row, actions)}
428
+ {:else}
429
+ <div class="flex items-center gap-1.5">
430
+ {#if rowCollapse}
431
+ <button
432
+ type="button"
433
+ onclick={(e) => {
434
+ e.stopPropagation();
435
+ toggleRow(row, index);
436
+ }}
437
+ class={`inline-flex h-6 w-6 items-center justify-center rounded-full text-neutral-400 transition-colors hover:bg-neutral-200/80 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-800/80 dark:hover:text-neutral-100 ${
438
+ openRows.has(id) ? 'rotate-180' : ''
439
+ }`}
440
+ >
441
+ <ChevronDown class="h-3.5 w-3.5" />
442
+ </button>
443
+ {/if}
444
+ <button
445
+ type="button"
446
+ onclick={(e) => openContextFromButton(e, row)}
447
+ class="inline-flex h-7 w-7 items-center justify-center rounded-full text-neutral-400 transition-colors hover:bg-neutral-200/80 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-800/80 dark:hover:text-neutral-100"
448
+ >
449
+ <EllipsisVertical class="h-4 w-4" />
450
+ </button>
451
+ </div>
452
+ {/if}
453
+ {/if}
454
+ </div>
455
+ </div>
456
+
457
+ {#if controller.overflowColumns.length}
458
+ <div
459
+ class="border-t border-dashed border-neutral-200/70 bg-neutral-50/80 px-3 py-2 text-[11px] text-neutral-600 dark:border-neutral-800/70 dark:bg-neutral-950/60 dark:text-neutral-300"
460
+ >
461
+ {#if overflow}
462
+ {@render overflow(row)}
463
+ {:else}
464
+ <div class="grid gap-2 md:grid-cols-3">
465
+ {#each controller.overflowColumns as colOverflow (colOverflow.id)}
466
+ {@const valueOverflow = colOverflow.accessor
467
+ ? colOverflow.accessor(row)
468
+ : (row as any)[colOverflow.id]}
469
+ <div
470
+ class="rounded-2xl border border-neutral-200/80 bg-white/80 px-2 py-1.5 text-[11px] text-neutral-800 shadow-sm backdrop-blur-md dark:border-neutral-800/80 dark:bg-neutral-900/80 dark:text-neutral-100"
471
+ >
472
+ <div
473
+ class="mb-0.5 text-[10px] font-medium tracking-wide text-neutral-400 uppercase dark:text-neutral-500"
474
+ >
475
+ {colOverflow.label}
476
+ </div>
477
+ <div>{formatValue(colOverflow, valueOverflow, row)}</div>
478
+ </div>
479
+ {/each}
480
+ </div>
481
+ {/if}
482
+ </div>
483
+ {/if}
484
+
485
+ {#if rowCollapse && openRows.has(id)}
486
+ <div
487
+ class="border-t border-dashed border-neutral-200/70 bg-neutral-50/90 px-3 py-3 text-[11px] text-neutral-700 dark:border-neutral-800/70 dark:bg-neutral-950/70 dark:text-neutral-100"
488
+ >
489
+ {@render rowCollapse(row)}
490
+ </div>
491
+ {/if}
492
+ </div>
493
+ {/each}
494
+ </div>
495
+ {:else}
496
+ <!-- GRID VIEW -->
497
+ <div class="grid gap-3 p-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
498
+ {#each controller.currentRows as row, index (rowIdFor(row, index))}
499
+ {@const id = rowIdFor(row, index)}
500
+ {@const cols = controller.mainColumns as any[]}
501
+ {@const firstCol = cols[0]}
502
+ {@const firstValue = firstCol
503
+ ? firstCol.accessor
504
+ ? firstCol.accessor(row)
505
+ : (row as any)[firstCol.id]
506
+ : null}
507
+ {@const restCols = cols.slice(1)}
508
+ <div
509
+ class={`group relative rounded-2xl border border-neutral-200/80 bg-white/80 p-3 text-[11px] text-neutral-800 shadow-sm ring-0 transition-all hover:border-purple-400/70 hover:shadow-md dark:border-neutral-800/80 dark:bg-neutral-900/80 dark:text-neutral-50 ${
510
+ controller.selectedIds.has(id)
511
+ ? 'bg-purple-50/70 ring-1 ring-purple-400/70 dark:bg-purple-950/20'
512
+ : ''
513
+ }`}
514
+ oncontextmenu={(e) => openContextAt(e, row)}
515
+ >
516
+ {#if controller.multiSelect}
517
+ <div
518
+ class="absolute top-2 left-2 z-10 rounded-full bg-neutral-900/70 p-1 backdrop-blur-md dark:bg-neutral-950/80"
519
+ data-stop-row-toggle="true"
520
+ >
521
+ <input
522
+ type="checkbox"
523
+ checked={controller.selectedIds.has(id)}
524
+ onchange={() => controller.toggleRowSelection(id)}
525
+ class="h-3.5 w-3.5 rounded border-neutral-400 bg-neutral-50 text-purple-500 focus:ring-purple-500 dark:border-neutral-500 dark:bg-neutral-900"
526
+ />
527
+ </div>
528
+ {/if}
529
+
530
+ <div class="mb-2 pr-6">
531
+ {#if cell && firstCol}
532
+ {@render cell({
533
+ row,
534
+ column: firstCol,
535
+ value: firstValue,
536
+ index
537
+ })}
538
+ {:else if firstCol}
539
+ <div
540
+ class="line-clamp-2 text-[12px] leading-snug font-semibold text-neutral-900 dark:text-neutral-50"
541
+ >
542
+ {formatValue(firstCol, firstValue, row)}
543
+ </div>
544
+ {/if}
545
+ </div>
546
+
547
+ <dl class="space-y-1.5">
548
+ {#each restCols as col (col.id)}
549
+ {@const value = col.accessor ? col.accessor(row) : (row as any)[col.id]}
550
+ <div class="flex items-start justify-between gap-2">
551
+ <dt
552
+ class="max-w-[45%] truncate text-[10px] font-medium text-neutral-400 uppercase dark:text-neutral-500"
553
+ >
554
+ {col.label}
555
+ </dt>
556
+ <dd
557
+ class="line-clamp-2 flex-1 text-right text-[11px] text-neutral-700 dark:text-neutral-200"
558
+ >
559
+ {formatValue(col, value, row)}
560
+ </dd>
561
+ </div>
562
+ {/each}
563
+ </dl>
564
+
565
+ {#if actions.length}
566
+ <div
567
+ class="mt-2 flex items-center justify-end gap-1.5"
568
+ data-stop-row-toggle="true"
569
+ >
570
+ {#if rowCollapse}
571
+ <button
572
+ type="button"
573
+ onclick={(e) => {
574
+ e.stopPropagation();
575
+ toggleRow(row, index);
576
+ }}
577
+ class={`inline-flex h-6 w-6 items-center justify-center rounded-full text-neutral-400 transition-colors hover:bg-neutral-200/80 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-800/80 dark:hover:text-neutral-100 ${
578
+ openRows.has(id) ? 'rotate-180' : ''
579
+ }`}
580
+ >
581
+ <ChevronDown class="h-3.5 w-3.5" />
582
+ </button>
583
+ {/if}
584
+ <button
585
+ type="button"
586
+ onclick={(e) => openContextFromButton(e, row)}
587
+ class="inline-flex h-7 w-7 items-center justify-center rounded-full text-neutral-400 transition-colors hover:bg-neutral-200/80 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-800/80 dark:hover:text-neutral-100"
588
+ >
589
+ <EllipsisVertical class="h-4 w-4" />
590
+ </button>
591
+ </div>
592
+ {/if}
593
+
594
+ {#if rowCollapse && openRows.has(id)}
595
+ <div
596
+ class="mt-2 rounded-2xl border border-dashed border-neutral-200/70 bg-neutral-50/80 px-2.5 py-2 text-[11px] text-neutral-700 dark:border-neutral-700/70 dark:bg-neutral-950/60 dark:text-neutral-100"
597
+ >
598
+ {@render rowCollapse(row)}
599
+ </div>
600
+ {/if}
601
+ </div>
602
+ {/each}
603
+ </div>
604
+ {/if}
605
+ {:else}
606
+ <div class="px-3 py-6 text-center text-xs text-neutral-500 dark:text-neutral-400">
607
+ No hay registros que mostrar
608
+ </div>
609
+ {/if}
610
+ </div>
611
+ </div>
612
+
613
+ <DataTableFooter />
614
+
615
+ <div
616
+ bind:this={contextPopover}
617
+ popover="manual"
618
+ data-context-host="true"
619
+ class="z-[1300] max-w-xs min-w-[190px] rounded-2xl border border-neutral-200/80 bg-neutral-50/95 p-1.5 text-xs text-neutral-900 shadow-[0_18px_50px_rgba(15,23,42,0.45)] backdrop-blur-2xl dark:border-neutral-700/80 dark:bg-neutral-900/95 dark:text-neutral-50"
620
+ style={`position: fixed; left: ${contextPos.x}px; top: ${contextPos.y}px; transform: translate(-100%, 8px);`}
621
+ onbeforetoggle={(e) => {
622
+ if ((e as any).newState === 'closed') contextRow = null;
623
+ }}
624
+ >
625
+ {#if contextRow && actions.length}
626
+ <ContextMenu {actions} row={contextRow} onClose={closeContext} />
627
+ {:else}
628
+ <div class="flex flex-col gap-2">No hay acciones disponibles</div>
629
+ {/if}
630
+ </div>
631
+ </div>
@@ -0,0 +1,48 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ColumnDef, RowAction } from './core/types.js';
3
+ import type { DataTableController } from './core/DataTableController.svelte';
4
+ interface CellContext<T> {
5
+ row: T;
6
+ column: ColumnDef<T>;
7
+ value: unknown;
8
+ index: number;
9
+ }
10
+ interface Props<T> {
11
+ controller: DataTableController<T>;
12
+ actions?: RowAction<T>[];
13
+ cell?: Snippet<[CellContext<T>]>;
14
+ overflow?: Snippet<[T]>;
15
+ headerCell?: Snippet<[ColumnDef<T>]>;
16
+ rowActions?: Snippet<[T, RowAction<T>[]]>;
17
+ bulkActions?: Snippet<[
18
+ {
19
+ selectedIds: string[];
20
+ clearSelection: () => void;
21
+ }
22
+ ]>;
23
+ rowCollapse?: Snippet<[T]>;
24
+ }
25
+ declare function $$render<T>(): {
26
+ props: Props<T>;
27
+ exports: {};
28
+ bindings: "";
29
+ slots: {};
30
+ events: {};
31
+ };
32
+ declare class __sveltets_Render<T> {
33
+ props(): ReturnType<typeof $$render<T>>['props'];
34
+ events(): ReturnType<typeof $$render<T>>['events'];
35
+ slots(): ReturnType<typeof $$render<T>>['slots'];
36
+ bindings(): "";
37
+ exports(): {};
38
+ }
39
+ interface $$IsomorphicComponent {
40
+ 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']>> & {
41
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
42
+ } & ReturnType<__sveltets_Render<T>['exports']>;
43
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
44
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
45
+ }
46
+ declare const DataTableShell: $$IsomorphicComponent;
47
+ type DataTableShell<T> = InstanceType<typeof DataTableShell<T>>;
48
+ export default DataTableShell;