@r2digisolutions/ui 0.34.5 → 0.34.6
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.
|
@@ -58,28 +58,25 @@
|
|
|
58
58
|
);
|
|
59
59
|
|
|
60
60
|
// =========================
|
|
61
|
-
// CONTEXT MENU
|
|
61
|
+
// CONTEXT MENU (FIX)
|
|
62
62
|
// =========================
|
|
63
63
|
let contextPopover = $state<HTMLDivElement | null>(null);
|
|
64
64
|
let contextRow = $state<T | null>(null);
|
|
65
65
|
|
|
66
|
-
//
|
|
66
|
+
// Punto preferido: cursor o botón (client coords)
|
|
67
67
|
let contextPos = $state<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
68
68
|
|
|
69
|
-
// ✅
|
|
69
|
+
// ✅ Posición final (clamp + flip) en left/top reales
|
|
70
70
|
let contextRender = $state<{ left: number; top: number }>({ left: 0, top: 0 });
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
const CONTEXT_GAP = 8; // separación visual del cursor/botón
|
|
72
|
+
const CONTEXT_MARGIN = 12;
|
|
73
|
+
const CONTEXT_GAP = 8;
|
|
75
74
|
|
|
76
75
|
let openRows = $state<Set<string>>(new Set());
|
|
77
76
|
|
|
78
77
|
const contextOpen = $derived(contextRow !== null);
|
|
79
78
|
|
|
80
|
-
// =========================
|
|
81
79
|
// GRID / STICKY
|
|
82
|
-
// =========================
|
|
83
80
|
$effect(() => {
|
|
84
81
|
const parts: string[] = [];
|
|
85
82
|
if (controller.multiSelect) parts.push('40px');
|
|
@@ -89,7 +86,7 @@
|
|
|
89
86
|
parts.push(`${w}px`);
|
|
90
87
|
});
|
|
91
88
|
|
|
92
|
-
//
|
|
89
|
+
// Columna de acciones
|
|
93
90
|
parts.push('64px');
|
|
94
91
|
gridTemplate = parts.join(' ');
|
|
95
92
|
|
|
@@ -101,60 +98,65 @@
|
|
|
101
98
|
let accLeft = controller.multiSelect ? 40 : 0;
|
|
102
99
|
controller.mainColumns.forEach((col) => {
|
|
103
100
|
const w = controller.getColumnWidth(col.id as keyof T);
|
|
104
|
-
if (col.sticky === 'left')
|
|
101
|
+
if (col.sticky === 'left') {
|
|
102
|
+
offsets[col.id as keyof T] = { left: accLeft };
|
|
103
|
+
}
|
|
105
104
|
accLeft += w;
|
|
106
105
|
});
|
|
107
|
-
|
|
108
106
|
stickyOffsets = offsets;
|
|
109
107
|
});
|
|
110
108
|
|
|
111
109
|
// CHECK ALL
|
|
112
110
|
$effect(() => {
|
|
113
111
|
if (!controller.multiSelect || !selectAllEl) return;
|
|
114
|
-
|
|
115
112
|
if (!controller.currentRows.length) {
|
|
116
113
|
selectAllEl.checked = false;
|
|
117
114
|
selectAllEl.indeterminate = false;
|
|
118
115
|
return;
|
|
119
116
|
}
|
|
120
|
-
|
|
121
117
|
selectAllEl.checked = controller.allVisibleSelected;
|
|
122
118
|
selectAllEl.indeterminate = controller.someVisibleSelected;
|
|
123
119
|
});
|
|
124
120
|
|
|
125
|
-
// CERRAR CONTEXT MENU
|
|
121
|
+
// CERRAR CONTEXT MENU (click fuera)
|
|
126
122
|
$effect(() => {
|
|
127
123
|
function handleDocumentClick(event: MouseEvent) {
|
|
128
124
|
if (!contextOpen) return;
|
|
129
125
|
const target = event.target as HTMLElement;
|
|
130
|
-
if (!target.closest('[data-context-host="true"]'))
|
|
126
|
+
if (!target.closest('[data-context-host="true"]')) {
|
|
127
|
+
closeContext();
|
|
128
|
+
}
|
|
131
129
|
}
|
|
132
|
-
|
|
133
130
|
if (contextOpen) document.addEventListener('click', handleDocumentClick);
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
return () => {
|
|
132
|
+
document.removeEventListener('click', handleDocumentClick);
|
|
133
|
+
};
|
|
136
134
|
});
|
|
137
135
|
|
|
138
|
-
// ✅ Reposicionar
|
|
136
|
+
// ✅ Reposicionar al hacer scroll/resize mientras está abierto
|
|
139
137
|
$effect(() => {
|
|
140
138
|
if (!contextOpen) return;
|
|
141
139
|
|
|
142
|
-
const
|
|
143
|
-
|
|
140
|
+
const handler = () => {
|
|
141
|
+
// si se mueve el viewport o scroll, recalculamos
|
|
142
|
+
positionContext();
|
|
144
143
|
};
|
|
145
144
|
|
|
146
|
-
window.addEventListener('resize',
|
|
147
|
-
window.addEventListener('scroll',
|
|
145
|
+
window.addEventListener('resize', handler);
|
|
146
|
+
window.addEventListener('scroll', handler, { passive: true, capture: true });
|
|
147
|
+
|
|
148
|
+
// visualViewport es clave en móviles / zoom / barras
|
|
149
|
+
window.visualViewport?.addEventListener('resize', handler);
|
|
150
|
+
window.visualViewport?.addEventListener('scroll', handler);
|
|
148
151
|
|
|
149
152
|
return () => {
|
|
150
|
-
window.removeEventListener('resize',
|
|
151
|
-
window.removeEventListener('scroll',
|
|
153
|
+
window.removeEventListener('resize', handler);
|
|
154
|
+
window.removeEventListener('scroll', handler, true as any);
|
|
155
|
+
window.visualViewport?.removeEventListener('resize', handler);
|
|
156
|
+
window.visualViewport?.removeEventListener('scroll', handler);
|
|
152
157
|
};
|
|
153
158
|
});
|
|
154
159
|
|
|
155
|
-
// =========================
|
|
156
|
-
// RESIZE COLUMNS
|
|
157
|
-
// =========================
|
|
158
160
|
let resizingId: keyof T | null = null;
|
|
159
161
|
let startX = 0;
|
|
160
162
|
let startWidth = 0;
|
|
@@ -172,7 +174,8 @@
|
|
|
172
174
|
function onResizeMove(event: MouseEvent) {
|
|
173
175
|
if (!resizingId) return;
|
|
174
176
|
const dx = event.clientX - startX;
|
|
175
|
-
|
|
177
|
+
const width = startWidth + dx;
|
|
178
|
+
controller.resizeColumn(resizingId, width);
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
function onResizeUp() {
|
|
@@ -181,9 +184,6 @@
|
|
|
181
184
|
window.removeEventListener('mouseup', onResizeUp);
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
// =========================
|
|
185
|
-
// HELPERS
|
|
186
|
-
// =========================
|
|
187
187
|
function rowIdFor(row: T, index: number) {
|
|
188
188
|
return controller.getRowId(row, index);
|
|
189
189
|
}
|
|
@@ -216,12 +216,13 @@
|
|
|
216
216
|
|
|
217
217
|
function handleToggleAll(e: Event) {
|
|
218
218
|
const input = e.currentTarget as HTMLInputElement;
|
|
219
|
-
|
|
219
|
+
const checked = input.checked;
|
|
220
|
+
if (checked) controller.selectAllCurrentPage();
|
|
220
221
|
else controller.unselectAllCurrentPage();
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
// =========================
|
|
224
|
-
// ✅
|
|
225
|
+
// ✅ Positioning helpers
|
|
225
226
|
// =========================
|
|
226
227
|
function clamp(n: number, min: number, max: number) {
|
|
227
228
|
return Math.max(min, Math.min(max, n));
|
|
@@ -231,70 +232,81 @@
|
|
|
231
232
|
return new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
232
233
|
}
|
|
233
234
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
function getViewport() {
|
|
236
|
+
const vv = window.visualViewport;
|
|
237
|
+
if (vv) {
|
|
238
|
+
return {
|
|
239
|
+
left: vv.offsetLeft,
|
|
240
|
+
top: vv.offsetTop,
|
|
241
|
+
width: vv.width,
|
|
242
|
+
height: vv.height
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
left: 0,
|
|
247
|
+
top: 0,
|
|
248
|
+
width: window.innerWidth,
|
|
249
|
+
height: window.innerHeight
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
239
253
|
function computeContextPosition(preferred: { x: number; y: number }, pop: DOMRect) {
|
|
240
|
-
const
|
|
241
|
-
const vh = window.innerHeight;
|
|
254
|
+
const vp = getViewport();
|
|
242
255
|
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
const tooTall = pop.height > maxHeightAllowed;
|
|
256
|
+
const minLeft = vp.left + CONTEXT_MARGIN;
|
|
257
|
+
const minTop = vp.top + CONTEXT_MARGIN;
|
|
246
258
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// - vertical: abajo del punto
|
|
250
|
-
let left = preferred.x - pop.width; // izquierda
|
|
251
|
-
let top = preferred.y + CONTEXT_GAP; // abajo
|
|
259
|
+
const maxLeft = vp.left + vp.width - CONTEXT_MARGIN - pop.width;
|
|
260
|
+
const maxTop = vp.top + vp.height - CONTEXT_MARGIN - pop.height;
|
|
252
261
|
|
|
253
|
-
//
|
|
254
|
-
|
|
262
|
+
// Si es demasiado alto, lo pegamos arriba (y el popover scrollea)
|
|
263
|
+
const tooTall = pop.height > vp.height - CONTEXT_MARGIN * 2;
|
|
264
|
+
|
|
265
|
+
// Preferencia UX original: izquierda + abajo
|
|
266
|
+
let left = preferred.x - pop.width;
|
|
267
|
+
let top = preferred.y + CONTEXT_GAP;
|
|
268
|
+
|
|
269
|
+
// Flip horizontal si no cabe a la izquierda
|
|
270
|
+
if (left < minLeft) {
|
|
255
271
|
left = preferred.x + CONTEXT_GAP;
|
|
256
272
|
}
|
|
257
273
|
|
|
258
|
-
//
|
|
274
|
+
// Flip vertical si no cabe abajo
|
|
259
275
|
if (!tooTall) {
|
|
260
276
|
const bottomIfDown = top + pop.height;
|
|
261
|
-
const
|
|
262
|
-
if (
|
|
263
|
-
top = preferred.y - CONTEXT_GAP - pop.height;
|
|
277
|
+
const bottomLimit = vp.top + vp.height - CONTEXT_MARGIN;
|
|
278
|
+
if (bottomIfDown > bottomLimit) {
|
|
279
|
+
top = preferred.y - CONTEXT_GAP - pop.height;
|
|
264
280
|
}
|
|
281
|
+
} else {
|
|
282
|
+
top = minTop;
|
|
265
283
|
}
|
|
266
284
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Clamp final dentro del viewport
|
|
273
|
-
// (si lo ponemos a la derecha pero no cabe, clamp lo corrige)
|
|
274
|
-
left = clamp(left, CONTEXT_MARGIN, vw - CONTEXT_MARGIN - pop.width);
|
|
275
|
-
top = clamp(top, CONTEXT_MARGIN, vh - CONTEXT_MARGIN - Math.min(pop.height, maxHeightAllowed));
|
|
285
|
+
// Clamp final
|
|
286
|
+
left = clamp(left, minLeft, Math.max(minLeft, maxLeft));
|
|
287
|
+
top = clamp(top, minTop, Math.max(minTop, maxTop));
|
|
276
288
|
|
|
277
289
|
return { left, top };
|
|
278
290
|
}
|
|
279
291
|
|
|
280
|
-
|
|
281
|
-
* Posiciona midiendo tamaño real.
|
|
282
|
-
* RAF suele ser más fiable que tick() con Popover API.
|
|
283
|
-
*/
|
|
284
|
-
async function positionContext(retries = 3) {
|
|
292
|
+
async function positionContext() {
|
|
285
293
|
if (!contextPopover || !contextRow) return;
|
|
286
294
|
|
|
295
|
+
// Asegura layout real (Popover API a veces “late layout”)
|
|
287
296
|
await raf();
|
|
288
297
|
|
|
289
|
-
|
|
290
|
-
|
|
298
|
+
const rect1 = contextPopover.getBoundingClientRect();
|
|
299
|
+
if (!rect1.width || !rect1.height) return;
|
|
291
300
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
301
|
+
// 1ª pasada
|
|
302
|
+
contextRender = computeContextPosition(contextPos, rect1);
|
|
303
|
+
|
|
304
|
+
// 2ª pasada (por si cambia alto/ancho por scrollbar)
|
|
305
|
+
await raf();
|
|
306
|
+
const rect2 = contextPopover.getBoundingClientRect();
|
|
307
|
+
if (!rect2.width || !rect2.height) return;
|
|
296
308
|
|
|
297
|
-
contextRender = computeContextPosition(contextPos,
|
|
309
|
+
contextRender = computeContextPosition(contextPos, rect2);
|
|
298
310
|
}
|
|
299
311
|
|
|
300
312
|
async function openContextAt(event: MouseEvent, row: T) {
|
|
@@ -306,8 +318,9 @@
|
|
|
306
318
|
|
|
307
319
|
if (contextPopover) contextPopover.showPopover();
|
|
308
320
|
|
|
321
|
+
// Espera render + posiciona
|
|
309
322
|
await tick();
|
|
310
|
-
await positionContext(
|
|
323
|
+
await positionContext();
|
|
311
324
|
}
|
|
312
325
|
|
|
313
326
|
async function openContextFromButton(event: MouseEvent, row: T) {
|
|
@@ -315,14 +328,15 @@
|
|
|
315
328
|
event.stopPropagation();
|
|
316
329
|
|
|
317
330
|
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
318
|
-
|
|
319
331
|
contextRow = row;
|
|
332
|
+
|
|
333
|
+
// Punto preferido: esquina inferior derecha
|
|
320
334
|
contextPos = { x: rect.right, y: rect.bottom };
|
|
321
335
|
|
|
322
336
|
if (contextPopover) contextPopover.showPopover();
|
|
323
337
|
|
|
324
338
|
await tick();
|
|
325
|
-
await positionContext(
|
|
339
|
+
await positionContext();
|
|
326
340
|
}
|
|
327
341
|
|
|
328
342
|
function closeContext() {
|
|
@@ -462,6 +476,7 @@
|
|
|
462
476
|
{@const id = rowIdFor(row, index)}
|
|
463
477
|
|
|
464
478
|
<div class="group relative">
|
|
479
|
+
<!-- Fila principal -->
|
|
465
480
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
466
481
|
<div
|
|
467
482
|
role="row"
|
|
@@ -495,7 +510,7 @@
|
|
|
495
510
|
{@const value = col.accessor ? col.accessor(row) : (row as any)[col.id]}
|
|
496
511
|
{@const sticky = stickyOffsets[col.id as keyof T]}
|
|
497
512
|
<div
|
|
498
|
-
class={`flex items-center border-r border-neutral-200/60 px-3
|
|
513
|
+
class={`flex items-center border-r border-neutral-200/60 px-3 text-black dark:text-neutral-50${
|
|
499
514
|
density === 'compact' ? 'py-1.5' : 'py-2.5'
|
|
500
515
|
} dark:border-neutral-800/70 ${
|
|
501
516
|
col.sticky === 'left'
|
|
@@ -507,7 +522,12 @@
|
|
|
507
522
|
: ''}
|
|
508
523
|
>
|
|
509
524
|
{#if cell}
|
|
510
|
-
{@render cell({
|
|
525
|
+
{@render cell({
|
|
526
|
+
row,
|
|
527
|
+
column: col,
|
|
528
|
+
value,
|
|
529
|
+
index
|
|
530
|
+
})}
|
|
511
531
|
{:else}
|
|
512
532
|
<span
|
|
513
533
|
class={`line-clamp-2 text-black dark:text-neutral-50 ${
|
|
@@ -601,10 +621,28 @@
|
|
|
601
621
|
{/each}
|
|
602
622
|
</div>
|
|
603
623
|
{:else}
|
|
604
|
-
<!-- GRID VIEW
|
|
624
|
+
<!-- GRID VIEW -->
|
|
605
625
|
<div class="grid gap-3 p-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
606
626
|
{#each controller.currentRows as row, index (rowIdFor(row, index))}
|
|
607
|
-
|
|
627
|
+
{@const id = rowIdFor(row, index)}
|
|
628
|
+
{@const cols = controller.mainColumns as any[]}
|
|
629
|
+
{@const firstCol = cols[0]}
|
|
630
|
+
{@const firstValue = firstCol
|
|
631
|
+
? firstCol.accessor
|
|
632
|
+
? firstCol.accessor(row)
|
|
633
|
+
: (row as any)[firstCol.id]
|
|
634
|
+
: null}
|
|
635
|
+
{@const restCols = cols.slice(1)}
|
|
636
|
+
<div
|
|
637
|
+
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 ${
|
|
638
|
+
controller.selectedIds.has(id)
|
|
639
|
+
? 'bg-purple-50/70 ring-1 ring-purple-400/70 dark:bg-purple-950/20'
|
|
640
|
+
: ''
|
|
641
|
+
}`}
|
|
642
|
+
oncontextmenu={(e) => openContextAt(e, row)}
|
|
643
|
+
>
|
|
644
|
+
<!-- ... tu grid view (igual que antes) ... -->
|
|
645
|
+
</div>
|
|
608
646
|
{/each}
|
|
609
647
|
</div>
|
|
610
648
|
{/if}
|
|
@@ -618,12 +656,12 @@
|
|
|
618
656
|
|
|
619
657
|
<DataTableFooter />
|
|
620
658
|
|
|
621
|
-
<!-- ✅ CONTEXT POPOVER
|
|
659
|
+
<!-- ✅ CONTEXT POPOVER (FIXED) -->
|
|
622
660
|
<div
|
|
623
661
|
bind:this={contextPopover}
|
|
624
662
|
popover="manual"
|
|
625
663
|
data-context-host="true"
|
|
626
|
-
class="z-[1300] max-h-[calc(100vh-
|
|
664
|
+
class="z-[1300] max-h-[calc(100vh-24px)] max-w-xs min-w-[190px] overflow-auto 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"
|
|
627
665
|
style={`position: fixed; left: ${contextRender.left}px; top: ${contextRender.top}px;`}
|
|
628
666
|
onbeforetoggle={(e) => {
|
|
629
667
|
if ((e as any).newState === 'closed') contextRow = null;
|