@r2digisolutions/ui 0.27.4 → 0.28.1
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/DataTableShell/DataTableShell.svelte +631 -0
- package/dist/components/container/DataTableShell/DataTableShell.svelte.d.ts +48 -0
- package/dist/components/container/DataTableShell/components/AdvancedFiltersBuilder.svelte +311 -0
- package/dist/components/container/DataTableShell/components/AdvancedFiltersBuilder.svelte.d.ts +7 -0
- package/dist/components/container/DataTableShell/components/ColumnVisibilityMenu.svelte +112 -0
- package/dist/components/container/DataTableShell/components/ColumnVisibilityMenu.svelte.d.ts +8 -0
- package/dist/components/container/DataTableShell/components/ContextMenu.svelte +70 -0
- package/dist/components/container/DataTableShell/components/ContextMenu.svelte.d.ts +30 -0
- package/dist/components/container/DataTableShell/components/DataTableFiltersSidebar.svelte +0 -0
- package/dist/components/container/DataTableShell/components/DataTableFiltersSidebar.svelte.d.ts +26 -0
- package/dist/components/container/DataTableShell/components/DataTableFooter.svelte +36 -0
- package/dist/components/container/DataTableShell/components/DataTableFooter.svelte.d.ts +18 -0
- package/dist/components/container/DataTableShell/components/DataTableToolbar.svelte +822 -0
- package/dist/components/container/DataTableShell/components/DataTableToolbar.svelte.d.ts +30 -0
- package/dist/components/container/DataTableShell/components/Pagination.svelte +117 -0
- package/dist/components/container/DataTableShell/components/Pagination.svelte.d.ts +28 -0
- package/dist/components/container/DataTableShell/components/Submenu.svelte +109 -0
- package/dist/components/container/DataTableShell/components/Submenu.svelte.d.ts +30 -0
- package/dist/components/container/DataTableShell/components/Toolbar.svelte +0 -0
- package/dist/components/container/DataTableShell/components/Toolbar.svelte.d.ts +26 -0
- package/dist/components/container/DataTableShell/core/DataTableController.svelte.d.ts +54 -0
- package/dist/components/container/DataTableShell/core/DataTableController.svelte.js +148 -0
- package/dist/components/container/DataTableShell/core/DataTableEngine.svelte.d.ts +68 -0
- package/dist/components/container/DataTableShell/core/DataTableEngine.svelte.js +319 -0
- package/dist/components/container/DataTableShell/core/DataTableInternal.svelte.d.ts +68 -0
- package/dist/components/container/DataTableShell/core/DataTableInternal.svelte.js +396 -0
- package/dist/components/container/DataTableShell/core/context.d.ts +3 -0
- package/dist/components/container/DataTableShell/core/context.js +12 -0
- package/dist/components/container/DataTableShell/core/filters-types.d.ts +14 -0
- package/dist/components/container/DataTableShell/core/filters-types.js +1 -0
- package/dist/components/container/DataTableShell/core/types.d.ts +60 -0
- package/dist/components/container/DataTableShell/core/types.js +1 -0
- package/dist/components/container/index.d.ts +3 -1
- package/dist/components/container/index.js +3 -1
- package/package.json +10 -10
|
@@ -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
|
+
|
|
351
|
+
<div class="group relative">
|
|
352
|
+
<!-- Fila principal como GRID (aquí va el background) -->
|
|
353
|
+
<div
|
|
354
|
+
role="row"
|
|
355
|
+
tabindex="0"
|
|
356
|
+
class={`relative grid items-stretch 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 ${
|
|
357
|
+
controller.selectedIds.has(id)
|
|
358
|
+
? 'bg-purple-50/60 ring-1 ring-purple-400/60 dark:bg-purple-950/25'
|
|
359
|
+
: ''
|
|
360
|
+
}`}
|
|
361
|
+
style={`grid-template-columns:${gridTemplate}`}
|
|
362
|
+
oncontextmenu={(e) => openContextAt(e, row)}
|
|
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;
|