@r2digisolutions/ui 0.34.1 → 0.34.3
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.
|
@@ -61,17 +61,19 @@
|
|
|
61
61
|
let contextPopover = $state<HTMLDivElement | null>(null);
|
|
62
62
|
let contextRow = $state<T | null>(null);
|
|
63
63
|
|
|
64
|
-
//
|
|
64
|
+
// Punto “preferido” (cursor o botón)
|
|
65
65
|
let contextPos = $state<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
66
66
|
|
|
67
|
-
// ✅
|
|
67
|
+
// ✅ Render final (clamp + flip + stick)
|
|
68
68
|
let contextRender = $state<{ x: number; y: number; transform: string }>({
|
|
69
69
|
x: 0,
|
|
70
70
|
y: 0,
|
|
71
71
|
transform: 'translate(-100%, 8px)'
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
// Ajusta margen si quieres “pegado” más agresivo
|
|
74
75
|
const CONTEXT_MARGIN = 10;
|
|
76
|
+
const CONTEXT_GAP = 8; // separación visual del cursor/botón
|
|
75
77
|
|
|
76
78
|
let openRows = $state<Set<string>>(new Set());
|
|
77
79
|
|
|
@@ -87,7 +89,7 @@
|
|
|
87
89
|
parts.push(`${w}px`);
|
|
88
90
|
});
|
|
89
91
|
|
|
90
|
-
// Columna
|
|
92
|
+
// Columna acciones
|
|
91
93
|
parts.push('64px');
|
|
92
94
|
gridTemplate = parts.join(' ');
|
|
93
95
|
|
|
@@ -99,51 +101,50 @@
|
|
|
99
101
|
let accLeft = controller.multiSelect ? 40 : 0;
|
|
100
102
|
controller.mainColumns.forEach((col) => {
|
|
101
103
|
const w = controller.getColumnWidth(col.id as keyof T);
|
|
102
|
-
if (col.sticky === 'left') {
|
|
103
|
-
offsets[col.id as keyof T] = { left: accLeft };
|
|
104
|
-
}
|
|
104
|
+
if (col.sticky === 'left') offsets[col.id as keyof T] = { left: accLeft };
|
|
105
105
|
accLeft += w;
|
|
106
106
|
});
|
|
107
|
+
|
|
107
108
|
stickyOffsets = offsets;
|
|
108
109
|
});
|
|
109
110
|
|
|
110
111
|
// CHECK ALL
|
|
111
112
|
$effect(() => {
|
|
112
113
|
if (!controller.multiSelect || !selectAllEl) return;
|
|
114
|
+
|
|
113
115
|
if (!controller.currentRows.length) {
|
|
114
116
|
selectAllEl.checked = false;
|
|
115
117
|
selectAllEl.indeterminate = false;
|
|
116
118
|
return;
|
|
117
119
|
}
|
|
120
|
+
|
|
118
121
|
selectAllEl.checked = controller.allVisibleSelected;
|
|
119
122
|
selectAllEl.indeterminate = controller.someVisibleSelected;
|
|
120
123
|
});
|
|
121
124
|
|
|
122
|
-
// CERRAR CONTEXT MENU
|
|
125
|
+
// CERRAR CONTEXT MENU al click fuera
|
|
123
126
|
$effect(() => {
|
|
124
127
|
function handleDocumentClick(event: MouseEvent) {
|
|
125
128
|
if (!contextOpen) return;
|
|
126
129
|
const target = event.target as HTMLElement;
|
|
127
|
-
if (!target.closest('[data-context-host="true"]'))
|
|
128
|
-
closeContext();
|
|
129
|
-
}
|
|
130
|
+
if (!target.closest('[data-context-host="true"]')) closeContext();
|
|
130
131
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return () =>
|
|
135
|
-
document.removeEventListener('click', handleDocumentClick);
|
|
136
|
-
};
|
|
132
|
+
|
|
133
|
+
if (contextOpen) document.addEventListener('click', handleDocumentClick);
|
|
134
|
+
|
|
135
|
+
return () => document.removeEventListener('click', handleDocumentClick);
|
|
137
136
|
});
|
|
138
137
|
|
|
139
|
-
// ✅ Reposicionar en resize
|
|
138
|
+
// ✅ Reposicionar en resize + scroll (capture)
|
|
140
139
|
$effect(() => {
|
|
141
140
|
if (!contextOpen) return;
|
|
142
141
|
|
|
143
|
-
const onWin = () =>
|
|
142
|
+
const onWin = () => {
|
|
143
|
+
// reintenta/ajusta por si cambia viewport, barras móviles, etc.
|
|
144
|
+
positionContext(2);
|
|
145
|
+
};
|
|
144
146
|
|
|
145
147
|
window.addEventListener('resize', onWin);
|
|
146
|
-
// capture: true para enterarte del scroll en contenedores overflow-auto
|
|
147
148
|
window.addEventListener('scroll', onWin, { passive: true, capture: true });
|
|
148
149
|
|
|
149
150
|
return () => {
|
|
@@ -152,6 +153,7 @@
|
|
|
152
153
|
};
|
|
153
154
|
});
|
|
154
155
|
|
|
156
|
+
// RESIZE COLUMNS
|
|
155
157
|
let resizingId: keyof T | null = null;
|
|
156
158
|
let startX = 0;
|
|
157
159
|
let startWidth = 0;
|
|
@@ -169,8 +171,7 @@
|
|
|
169
171
|
function onResizeMove(event: MouseEvent) {
|
|
170
172
|
if (!resizingId) return;
|
|
171
173
|
const dx = event.clientX - startX;
|
|
172
|
-
|
|
173
|
-
controller.resizeColumn(resizingId, width);
|
|
174
|
+
controller.resizeColumn(resizingId, startWidth + dx);
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
function onResizeUp() {
|
|
@@ -179,6 +180,7 @@
|
|
|
179
180
|
window.removeEventListener('mouseup', onResizeUp);
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
// HELPERS
|
|
182
184
|
function rowIdFor(row: T, index: number) {
|
|
183
185
|
return controller.getRowId(row, index);
|
|
184
186
|
}
|
|
@@ -194,35 +196,80 @@
|
|
|
194
196
|
return String(value);
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
function toggleRow(row: T, index: number) {
|
|
200
|
+
if (!rowCollapse) return;
|
|
201
|
+
const id = rowIdFor(row, index);
|
|
202
|
+
const next = new Set(openRows);
|
|
203
|
+
if (next.has(id)) next.delete(id);
|
|
204
|
+
else next.add(id);
|
|
205
|
+
openRows = next;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function handleRowClick(event: MouseEvent, row: T, index: number) {
|
|
209
|
+
const target = event.target as HTMLElement;
|
|
210
|
+
if (target.closest('[data-stop-row-toggle="true"]')) return;
|
|
211
|
+
toggleRow(row, index);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function handleToggleAll(e: Event) {
|
|
215
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
216
|
+
if (input.checked) controller.selectAllCurrentPage();
|
|
217
|
+
else controller.unselectAllCurrentPage();
|
|
218
|
+
}
|
|
219
|
+
|
|
197
220
|
// =========================
|
|
198
|
-
// ✅ Context positioning
|
|
221
|
+
// ✅ Context positioning (robusto)
|
|
199
222
|
// =========================
|
|
200
223
|
function clamp(n: number, min: number, max: number) {
|
|
201
224
|
return Math.max(min, Math.min(max, n));
|
|
202
225
|
}
|
|
203
226
|
|
|
227
|
+
function raf() {
|
|
228
|
+
return new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Decide transform y clamp final para que el popover:
|
|
233
|
+
* - no se salga del viewport
|
|
234
|
+
* - si no cabe abajo => se pega arriba
|
|
235
|
+
* - si no cabe arriba => se pega al top margin (scroll dentro)
|
|
236
|
+
* - si no cabe a la izquierda => se pone a la derecha
|
|
237
|
+
* - si no cabe a la derecha => clamp
|
|
238
|
+
*/
|
|
204
239
|
function computeContextPosition(preferred: { x: number; y: number }, pop: DOMRect) {
|
|
205
240
|
const vw = window.innerWidth;
|
|
206
241
|
const vh = window.innerHeight;
|
|
207
242
|
|
|
243
|
+
// Si es tan alto que ni arriba ni abajo, lo forzamos dentro (scroll en contenedor)
|
|
244
|
+
const tooTall = pop.height + CONTEXT_MARGIN * 2 > vh;
|
|
245
|
+
|
|
246
|
+
// Queremos aparecer “cerca” del punto preferido
|
|
247
|
+
// Horizontal por defecto: a la izquierda del punto (como tu translate(-100%, ...))
|
|
208
248
|
const fitsLeft = preferred.x - pop.width - CONTEXT_MARGIN >= 0;
|
|
209
249
|
const fitsRight = preferred.x + pop.width + CONTEXT_MARGIN <= vw;
|
|
210
|
-
const fitsDown = preferred.y + pop.height + CONTEXT_MARGIN <= vh;
|
|
211
|
-
const fitsUp = preferred.y - pop.height - CONTEXT_MARGIN >= 0;
|
|
212
250
|
|
|
213
|
-
//
|
|
251
|
+
// Vertical por defecto: abajo del punto
|
|
252
|
+
const fitsDown = preferred.y + pop.height + CONTEXT_MARGIN + CONTEXT_GAP <= vh;
|
|
253
|
+
const fitsUp = preferred.y - pop.height - CONTEXT_MARGIN - CONTEXT_GAP >= 0;
|
|
254
|
+
|
|
255
|
+
// Horizontal: si no cabe izquierda pero cabe derecha => derecha
|
|
214
256
|
const placeToRight = !fitsLeft && fitsRight;
|
|
215
257
|
|
|
216
|
-
// Vertical:
|
|
217
|
-
|
|
258
|
+
// Vertical:
|
|
259
|
+
// - preferimos abajo
|
|
260
|
+
// - si no cabe abajo y cabe arriba => arriba
|
|
261
|
+
// - si es tooTall => lo pegamos dentro del viewport
|
|
262
|
+
const placeUp = !tooTall && !fitsDown && fitsUp;
|
|
218
263
|
|
|
219
|
-
|
|
220
|
-
|
|
264
|
+
// Transform:
|
|
265
|
+
const transformX = placeToRight ? '0%' : '-100%';
|
|
266
|
+
// en vertical: abajo => +gap, arriba => -100% - gap
|
|
267
|
+
const transformY = placeUp ? `calc(-100% - ${CONTEXT_GAP}px)` : `${CONTEXT_GAP}px`;
|
|
221
268
|
|
|
222
269
|
let x = preferred.x;
|
|
223
270
|
let y = preferred.y;
|
|
224
271
|
|
|
225
|
-
// Clamp horizontal según
|
|
272
|
+
// Clamp horizontal según transformX
|
|
226
273
|
if (transformX === '-100%') {
|
|
227
274
|
// pop ocupa [x - w, x]
|
|
228
275
|
x = clamp(x, CONTEXT_MARGIN + pop.width, vw - CONTEXT_MARGIN);
|
|
@@ -231,13 +278,41 @@
|
|
|
231
278
|
x = clamp(x, CONTEXT_MARGIN, vw - CONTEXT_MARGIN - pop.width);
|
|
232
279
|
}
|
|
233
280
|
|
|
234
|
-
//
|
|
281
|
+
// Si tooTall, lo pegamos arriba dentro del viewport y dejamos overflow-auto
|
|
282
|
+
if (tooTall) {
|
|
283
|
+
return {
|
|
284
|
+
x,
|
|
285
|
+
y: CONTEXT_MARGIN,
|
|
286
|
+
transform: `translate(${transformX}, 0px)`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Clamp vertical
|
|
235
291
|
if (placeUp) {
|
|
236
|
-
// pop ocupa [y -
|
|
237
|
-
y = clamp(y, CONTEXT_MARGIN + pop.height +
|
|
292
|
+
// pop ocupa [y - gap - h, y - gap]
|
|
293
|
+
y = clamp(y, CONTEXT_MARGIN + pop.height + CONTEXT_GAP, vh - CONTEXT_MARGIN);
|
|
238
294
|
} else {
|
|
239
|
-
// pop ocupa [y +
|
|
240
|
-
|
|
295
|
+
// pop ocupa [y + gap, y + gap + h]
|
|
296
|
+
// aquí el max real del y para que no se salga por abajo:
|
|
297
|
+
y = clamp(y, CONTEXT_MARGIN - CONTEXT_GAP, vh - CONTEXT_MARGIN - pop.height - CONTEXT_GAP);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 🔥 Fallback “stick”:
|
|
301
|
+
// Si aun así por alguna razón se nos quedaría fuera (casos raros), lo pegamos a bottom/top.
|
|
302
|
+
const topIfDown = y + CONTEXT_GAP; // top real cuando está abajo
|
|
303
|
+
const bottomIfDown = topIfDown + pop.height;
|
|
304
|
+
|
|
305
|
+
const bottomIfUp = y - CONTEXT_GAP; // bottom real cuando está arriba
|
|
306
|
+
const topIfUp = bottomIfUp - pop.height;
|
|
307
|
+
|
|
308
|
+
if (!placeUp && bottomIfDown > vh - CONTEXT_MARGIN) {
|
|
309
|
+
// stick to bottom: ajusta y para que bottom quede dentro
|
|
310
|
+
y = vh - CONTEXT_MARGIN - pop.height - CONTEXT_GAP;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (placeUp && topIfUp < CONTEXT_MARGIN) {
|
|
314
|
+
// stick to top: ajusta y
|
|
315
|
+
y = CONTEXT_MARGIN + pop.height + CONTEXT_GAP;
|
|
241
316
|
}
|
|
242
317
|
|
|
243
318
|
return {
|
|
@@ -247,73 +322,63 @@
|
|
|
247
322
|
};
|
|
248
323
|
}
|
|
249
324
|
|
|
250
|
-
|
|
325
|
+
/**
|
|
326
|
+
* Posiciona con medición robusta:
|
|
327
|
+
* - espera RAF
|
|
328
|
+
* - si rect aún es 0 => retry
|
|
329
|
+
* - recalcula render
|
|
330
|
+
*/
|
|
331
|
+
async function positionContext(retries = 3) {
|
|
251
332
|
if (!contextPopover || !contextRow) return;
|
|
252
333
|
|
|
253
|
-
//
|
|
254
|
-
|
|
334
|
+
// RAF suele ser más fiable que tick() con Popover API
|
|
335
|
+
await raf();
|
|
336
|
+
|
|
255
337
|
const rect = contextPopover.getBoundingClientRect();
|
|
338
|
+
|
|
339
|
+
if ((!rect.width || !rect.height) && retries > 0) {
|
|
340
|
+
return positionContext(retries - 1);
|
|
341
|
+
}
|
|
256
342
|
if (!rect.width || !rect.height) return;
|
|
257
343
|
|
|
258
|
-
|
|
259
|
-
contextRender = next;
|
|
344
|
+
contextRender = computeContextPosition(contextPos, rect);
|
|
260
345
|
}
|
|
261
346
|
|
|
262
347
|
async function openContextAt(event: MouseEvent, row: T) {
|
|
263
348
|
event.preventDefault();
|
|
264
349
|
event.stopPropagation();
|
|
350
|
+
|
|
265
351
|
contextRow = row;
|
|
266
352
|
contextPos = { x: event.clientX, y: event.clientY };
|
|
353
|
+
|
|
267
354
|
if (contextPopover) contextPopover.showPopover();
|
|
268
355
|
|
|
356
|
+
// Svelte render + luego RAF retry para layout real
|
|
269
357
|
await tick();
|
|
270
|
-
await positionContext();
|
|
358
|
+
await positionContext(3);
|
|
271
359
|
}
|
|
272
360
|
|
|
273
361
|
async function openContextFromButton(event: MouseEvent, row: T) {
|
|
274
362
|
event.preventDefault();
|
|
275
363
|
event.stopPropagation();
|
|
364
|
+
|
|
276
365
|
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
366
|
+
|
|
277
367
|
contextRow = row;
|
|
278
368
|
|
|
279
|
-
//
|
|
369
|
+
// punto preferido: esquina inferior derecha del botón
|
|
280
370
|
contextPos = { x: rect.right, y: rect.bottom };
|
|
281
371
|
|
|
282
372
|
if (contextPopover) contextPopover.showPopover();
|
|
283
373
|
|
|
284
374
|
await tick();
|
|
285
|
-
await positionContext();
|
|
375
|
+
await positionContext(3);
|
|
286
376
|
}
|
|
287
377
|
|
|
288
378
|
function closeContext() {
|
|
289
379
|
if (contextPopover) contextPopover.hidePopover();
|
|
290
380
|
contextRow = null;
|
|
291
381
|
}
|
|
292
|
-
|
|
293
|
-
function toggleRow(row: T, index: number) {
|
|
294
|
-
if (!rowCollapse) return;
|
|
295
|
-
const id = rowIdFor(row, index);
|
|
296
|
-
const next = new Set(openRows);
|
|
297
|
-
if (next.has(id)) next.delete(id);
|
|
298
|
-
else next.add(id);
|
|
299
|
-
openRows = next;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function handleRowClick(event: MouseEvent, row: T, index: number) {
|
|
303
|
-
const target = event.target as HTMLElement;
|
|
304
|
-
if (target.closest('[data-stop-row-toggle="true"]')) return;
|
|
305
|
-
toggleRow(row, index);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function handleToggleAll(e: Event) {
|
|
309
|
-
const input = e.currentTarget as HTMLInputElement;
|
|
310
|
-
const checked = input.checked;
|
|
311
|
-
if (checked) {
|
|
312
|
-
controller.selectAllCurrentPage();
|
|
313
|
-
} else {
|
|
314
|
-
controller.unselectAllCurrentPage();
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
382
|
</script>
|
|
318
383
|
|
|
319
384
|
<div
|
|
@@ -481,7 +546,7 @@
|
|
|
481
546
|
{@const value = col.accessor ? col.accessor(row) : (row as any)[col.id]}
|
|
482
547
|
{@const sticky = stickyOffsets[col.id as keyof T]}
|
|
483
548
|
<div
|
|
484
|
-
class={`flex items-center border-r border-neutral-200/60 px-3
|
|
549
|
+
class={`flex items-center border-r border-neutral-200/60 px-3 text-black dark:text-neutral-50 ${
|
|
485
550
|
density === 'compact' ? 'py-1.5' : 'py-2.5'
|
|
486
551
|
} dark:border-neutral-800/70 ${
|
|
487
552
|
col.sticky === 'left'
|
|
@@ -493,12 +558,7 @@
|
|
|
493
558
|
: ''}
|
|
494
559
|
>
|
|
495
560
|
{#if cell}
|
|
496
|
-
{@render cell({
|
|
497
|
-
row,
|
|
498
|
-
column: col,
|
|
499
|
-
value,
|
|
500
|
-
index
|
|
501
|
-
})}
|
|
561
|
+
{@render cell({ row, column: col, value, index })}
|
|
502
562
|
{:else}
|
|
503
563
|
<span
|
|
504
564
|
class={`line-clamp-2 text-black dark:text-neutral-50 ${
|
|
@@ -592,7 +652,6 @@
|
|
|
592
652
|
{/each}
|
|
593
653
|
</div>
|
|
594
654
|
{:else}
|
|
595
|
-
<!-- GRID VIEW -->
|
|
596
655
|
<div class="grid gap-3 p-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
597
656
|
{#each controller.currentRows as row, index (rowIdFor(row, index))}
|
|
598
657
|
{@const id = rowIdFor(row, index)}
|
|
@@ -604,6 +663,7 @@
|
|
|
604
663
|
: (row as any)[firstCol.id]
|
|
605
664
|
: null}
|
|
606
665
|
{@const restCols = cols.slice(1)}
|
|
666
|
+
|
|
607
667
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
608
668
|
<div
|
|
609
669
|
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 ${
|
|
@@ -629,12 +689,7 @@
|
|
|
629
689
|
|
|
630
690
|
<div class="mb-2 pr-6 text-black dark:text-neutral-50">
|
|
631
691
|
{#if cell && firstCol}
|
|
632
|
-
{@render cell({
|
|
633
|
-
row,
|
|
634
|
-
column: firstCol,
|
|
635
|
-
value: firstValue,
|
|
636
|
-
index
|
|
637
|
-
})}
|
|
692
|
+
{@render cell({ row, column: firstCol, value: firstValue, index })}
|
|
638
693
|
{:else if firstCol}
|
|
639
694
|
<div
|
|
640
695
|
class="line-clamp-2 text-[12px] leading-snug font-semibold text-neutral-900 dark:text-neutral-50"
|
|
@@ -712,11 +767,12 @@
|
|
|
712
767
|
|
|
713
768
|
<DataTableFooter />
|
|
714
769
|
|
|
770
|
+
<!-- ✅ CONTEXT POPOVER HOST -->
|
|
715
771
|
<div
|
|
716
772
|
bind:this={contextPopover}
|
|
717
773
|
popover="manual"
|
|
718
774
|
data-context-host="true"
|
|
719
|
-
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 transition-transform duration-75 will-change-transform dark:border-neutral-700/80 dark:bg-neutral-900/95 dark:text-neutral-50"
|
|
775
|
+
class="z-[1300] max-h-[calc(100vh-20px)] 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 transition-transform duration-75 will-change-transform dark:border-neutral-700/80 dark:bg-neutral-900/95 dark:text-neutral-50"
|
|
720
776
|
style={`position: fixed; left: ${contextRender.x}px; top: ${contextRender.y}px; transform: ${contextRender.transform};`}
|
|
721
777
|
onbeforetoggle={(e) => {
|
|
722
778
|
if ((e as any).newState === 'closed') contextRow = null;
|