@r2digisolutions/ui 0.34.6 → 0.34.8

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.
@@ -8,6 +8,7 @@
8
8
  import DataTableToolbar from './components/DataTableToolbar.svelte';
9
9
  import DataTableFooter from './components/DataTableFooter.svelte';
10
10
  import ContextMenu from './components/ContextMenu.svelte';
11
+ import { portal } from '../DataTable/utils/portal.js';
11
12
 
12
13
  interface CellContext<T> {
13
14
  row: T;
@@ -57,23 +58,23 @@
57
58
  {} as Record<keyof T, { left?: number; right?: number }>
58
59
  );
59
60
 
60
- // =========================
61
- // CONTEXT MENU (FIX)
62
- // =========================
61
+ // ✅ el contenedor que scrollea (tu overflow-auto)
62
+ let scrollEl = $state<HTMLDivElement | null>(null);
63
+
64
+ // CONTEXT
63
65
  let contextPopover = $state<HTMLDivElement | null>(null);
64
66
  let contextRow = $state<T | null>(null);
65
67
 
66
- // Punto preferido: cursor o botón (client coords)
68
+ // punto donde “quieres” abrirlo (coords de viewport)
67
69
  let contextPos = $state<{ x: number; y: number }>({ x: 0, y: 0 });
68
70
 
69
- // ✅ Posición final (clamp + flip) en left/top reales
71
+ // ✅ left/top final (sin transform)
70
72
  let contextRender = $state<{ left: number; top: number }>({ left: 0, top: 0 });
71
73
 
72
74
  const CONTEXT_MARGIN = 12;
73
75
  const CONTEXT_GAP = 8;
74
76
 
75
77
  let openRows = $state<Set<string>>(new Set());
76
-
77
78
  const contextOpen = $derived(contextRow !== null);
78
79
 
79
80
  // GRID / STICKY
@@ -86,7 +87,6 @@
86
87
  parts.push(`${w}px`);
87
88
  });
88
89
 
89
- // Columna de acciones
90
90
  parts.push('64px');
91
91
  gridTemplate = parts.join(' ');
92
92
 
@@ -98,9 +98,7 @@
98
98
  let accLeft = controller.multiSelect ? 40 : 0;
99
99
  controller.mainColumns.forEach((col) => {
100
100
  const w = controller.getColumnWidth(col.id as keyof T);
101
- if (col.sticky === 'left') {
102
- offsets[col.id as keyof T] = { left: accLeft };
103
- }
101
+ if (col.sticky === 'left') offsets[col.id as keyof T] = { left: accLeft };
104
102
  accLeft += w;
105
103
  });
106
104
  stickyOffsets = offsets;
@@ -118,45 +116,35 @@
118
116
  selectAllEl.indeterminate = controller.someVisibleSelected;
119
117
  });
120
118
 
121
- // CERRAR CONTEXT MENU (click fuera)
119
+ // Cerrar por click fuera
122
120
  $effect(() => {
123
121
  function handleDocumentClick(event: MouseEvent) {
124
122
  if (!contextOpen) return;
125
123
  const target = event.target as HTMLElement;
126
- if (!target.closest('[data-context-host="true"]')) {
127
- closeContext();
128
- }
124
+ if (!target.closest('[data-context-host="true"]')) closeContext();
129
125
  }
130
- if (contextOpen) document.addEventListener('click', handleDocumentClick);
131
- return () => {
132
- document.removeEventListener('click', handleDocumentClick);
133
- };
126
+ if (contextOpen) document.addEventListener('mousedown', handleDocumentClick, true);
127
+ return () => document.removeEventListener('mousedown', handleDocumentClick, true);
134
128
  });
135
129
 
136
- // ✅ Reposicionar al hacer scroll/resize mientras está abierto
130
+ // ✅ Reposicionar mientras está abierto: window resize + scroll del contenedor REAL
137
131
  $effect(() => {
138
132
  if (!contextOpen) return;
139
133
 
140
- const handler = () => {
141
- // si se mueve el viewport o scroll, recalculamos
142
- positionContext();
143
- };
134
+ const handler = () => void positionContext();
144
135
 
145
136
  window.addEventListener('resize', handler);
146
- window.addEventListener('scroll', handler, { passive: true, capture: true });
147
137
 
148
- // visualViewport es clave en móviles / zoom / barras
149
- window.visualViewport?.addEventListener('resize', handler);
150
- window.visualViewport?.addEventListener('scroll', handler);
138
+ // 👇 ESTE ES el scroll que te estaba faltando
139
+ scrollEl?.addEventListener('scroll', handler, { passive: true });
151
140
 
152
141
  return () => {
153
142
  window.removeEventListener('resize', handler);
154
- window.removeEventListener('scroll', handler, true as any);
155
- window.visualViewport?.removeEventListener('resize', handler);
156
- window.visualViewport?.removeEventListener('scroll', handler);
143
+ scrollEl?.removeEventListener('scroll', handler as any);
157
144
  };
158
145
  });
159
146
 
147
+ // RESIZE columns
160
148
  let resizingId: keyof T | null = null;
161
149
  let startX = 0;
162
150
  let startWidth = 0;
@@ -174,8 +162,7 @@
174
162
  function onResizeMove(event: MouseEvent) {
175
163
  if (!resizingId) return;
176
164
  const dx = event.clientX - startX;
177
- const width = startWidth + dx;
178
- controller.resizeColumn(resizingId, width);
165
+ controller.resizeColumn(resizingId, startWidth + dx);
179
166
  }
180
167
 
181
168
  function onResizeUp() {
@@ -216,13 +203,12 @@
216
203
 
217
204
  function handleToggleAll(e: Event) {
218
205
  const input = e.currentTarget as HTMLInputElement;
219
- const checked = input.checked;
220
- if (checked) controller.selectAllCurrentPage();
206
+ if (input.checked) controller.selectAllCurrentPage();
221
207
  else controller.unselectAllCurrentPage();
222
208
  }
223
209
 
224
210
  // =========================
225
- // ✅ Positioning helpers
211
+ // ✅ POSICIONAMIENTO REAL (sin transform)
226
212
  // =========================
227
213
  function clamp(n: number, min: number, max: number) {
228
214
  return Math.max(min, Math.min(max, n));
@@ -234,20 +220,8 @@
234
220
 
235
221
  function getViewport() {
236
222
  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
- };
223
+ if (vv) return { left: vv.offsetLeft, top: vv.offsetTop, width: vv.width, height: vv.height };
224
+ return { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight };
251
225
  }
252
226
 
253
227
  function computeContextPosition(preferred: { x: number; y: number }, pop: DOMRect) {
@@ -259,30 +233,30 @@
259
233
  const maxLeft = vp.left + vp.width - CONTEXT_MARGIN - pop.width;
260
234
  const maxTop = vp.top + vp.height - CONTEXT_MARGIN - pop.height;
261
235
 
262
- // Si es demasiado alto, lo pegamos arriba (y el popover scrollea)
263
236
  const tooTall = pop.height > vp.height - CONTEXT_MARGIN * 2;
264
237
 
265
- // Preferencia UX original: izquierda + abajo
238
+ // default: left + down
266
239
  let left = preferred.x - pop.width;
267
240
  let top = preferred.y + CONTEXT_GAP;
268
241
 
269
- // Flip horizontal si no cabe a la izquierda
270
- if (left < minLeft) {
271
- left = preferred.x + CONTEXT_GAP;
272
- }
242
+ // flip horizontal
243
+ if (left < minLeft) left = preferred.x + CONTEXT_GAP;
273
244
 
274
- // Flip vertical si no cabe abajo
275
- if (!tooTall) {
245
+ if (tooTall) {
246
+ // si no cabe en altura, pegado arriba
247
+ top = minTop;
248
+ } else {
249
+ // flip vertical si se sale abajo
276
250
  const bottomIfDown = top + pop.height;
277
251
  const bottomLimit = vp.top + vp.height - CONTEXT_MARGIN;
278
252
  if (bottomIfDown > bottomLimit) {
279
- top = preferred.y - CONTEXT_GAP - pop.height;
253
+ top = preferred.y - CONTEXT_GAP - pop.height; // arriba
280
254
  }
281
- } else {
282
- top = minTop;
255
+
256
+ // si arriba se sale, pegado abajo
257
+ if (top < minTop) top = maxTop;
283
258
  }
284
259
 
285
- // Clamp final
286
260
  left = clamp(left, minLeft, Math.max(minLeft, maxLeft));
287
261
  top = clamp(top, minTop, Math.max(minTop, maxTop));
288
262
 
@@ -292,21 +266,18 @@
292
266
  async function positionContext() {
293
267
  if (!contextPopover || !contextRow) return;
294
268
 
295
- // Asegura layout real (Popover API a veces “late layout”)
296
269
  await raf();
270
+ const r1 = contextPopover.getBoundingClientRect();
271
+ if (!r1.width || !r1.height) return;
297
272
 
298
- const rect1 = contextPopover.getBoundingClientRect();
299
- if (!rect1.width || !rect1.height) return;
300
-
301
- // 1ª pasada
302
- contextRender = computeContextPosition(contextPos, rect1);
273
+ contextRender = computeContextPosition(contextPos, r1);
303
274
 
304
- // pasada (por si cambia alto/ancho por scrollbar)
275
+ // segunda pasada por scrollbar
305
276
  await raf();
306
- const rect2 = contextPopover.getBoundingClientRect();
307
- if (!rect2.width || !rect2.height) return;
277
+ const r2 = contextPopover.getBoundingClientRect();
278
+ if (!r2.width || !r2.height) return;
308
279
 
309
- contextRender = computeContextPosition(contextPos, rect2);
280
+ contextRender = computeContextPosition(contextPos, r2);
310
281
  }
311
282
 
312
283
  async function openContextAt(event: MouseEvent, row: T) {
@@ -316,9 +287,6 @@
316
287
  contextRow = row;
317
288
  contextPos = { x: event.clientX, y: event.clientY };
318
289
 
319
- if (contextPopover) contextPopover.showPopover();
320
-
321
- // Espera render + posiciona
322
290
  await tick();
323
291
  await positionContext();
324
292
  }
@@ -330,23 +298,21 @@
330
298
  const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
331
299
  contextRow = row;
332
300
 
333
- // Punto preferido: esquina inferior derecha
301
+ // punto: esquina inferior derecha del botón
334
302
  contextPos = { x: rect.right, y: rect.bottom };
335
303
 
336
- if (contextPopover) contextPopover.showPopover();
337
-
338
304
  await tick();
339
305
  await positionContext();
340
306
  }
341
307
 
342
308
  function closeContext() {
343
- if (contextPopover) contextPopover.hidePopover();
344
309
  contextRow = null;
345
310
  }
346
311
  </script>
347
312
 
313
+ <!-- ✅ wrapper principal relativo (para z-index) -->
348
314
  <div
349
- 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"
315
+ class="relative 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"
350
316
  >
351
317
  <DataTableToolbar
352
318
  {density}
@@ -366,7 +332,8 @@
366
332
  </div>
367
333
  {/if}
368
334
 
369
- <div class="relative max-h-[70vh] flex-1 overflow-auto">
335
+ <!-- ESTE es el scroll container, le hacemos bind -->
336
+ <div bind:this={scrollEl} class="relative max-h-[70vh] flex-1 overflow-auto">
370
337
  {#if controller.loading}
371
338
  <div class="pointer-events-none absolute inset-0 z-20 bg-neutral-900/30 backdrop-blur-md">
372
339
  <div class="flex h-full items-center justify-center">
@@ -476,8 +443,6 @@
476
443
  {@const id = rowIdFor(row, index)}
477
444
 
478
445
  <div class="group relative">
479
- <!-- Fila principal -->
480
- <!-- svelte-ignore a11y_click_events_have_key_events -->
481
446
  <div
482
447
  role="row"
483
448
  tabindex="0"
@@ -522,22 +487,9 @@
522
487
  : ''}
523
488
  >
524
489
  {#if cell}
525
- {@render cell({
526
- row,
527
- column: col,
528
- value,
529
- index
530
- })}
490
+ {@render cell({ row, column: col, value, index })}
531
491
  {:else}
532
- <span
533
- class={`line-clamp-2 text-black dark:text-neutral-50 ${
534
- col.align === 'right'
535
- ? 'ml-auto text-right'
536
- : col.align === 'center'
537
- ? 'mx-auto text-center'
538
- : ''
539
- }`}
540
- >
492
+ <span class="line-clamp-2 text-black dark:text-neutral-50">
541
493
  {formatValue(col, value, row)}
542
494
  </span>
543
495
  {/if}
@@ -551,33 +503,30 @@
551
503
  data-stop-row-toggle="true"
552
504
  >
553
505
  {#if actions.length}
554
- {#if rowActions}
555
- {@render rowActions(row, actions)}
556
- {:else}
557
- <div class="flex items-center gap-1.5">
558
- {#if rowCollapse}
559
- <button
560
- type="button"
561
- onclick={(e) => {
562
- e.stopPropagation();
563
- toggleRow(row, index);
564
- }}
565
- 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 ${
566
- openRows.has(id) ? 'rotate-180' : ''
567
- }`}
568
- >
569
- <ChevronDown class="h-3.5 w-3.5" />
570
- </button>
571
- {/if}
506
+ <div class="flex items-center gap-1.5">
507
+ {#if rowCollapse}
572
508
  <button
573
509
  type="button"
574
- onclick={(e) => openContextFromButton(e, row)}
575
- 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"
510
+ onclick={(e) => {
511
+ e.stopPropagation();
512
+ toggleRow(row, index);
513
+ }}
514
+ 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 ${
515
+ openRows.has(id) ? 'rotate-180' : ''
516
+ }`}
576
517
  >
577
- <EllipsisVertical class="h-4 w-4" />
518
+ <ChevronDown class="h-3.5 w-3.5" />
578
519
  </button>
579
- </div>
580
- {/if}
520
+ {/if}
521
+
522
+ <button
523
+ type="button"
524
+ onclick={(e) => openContextFromButton(e, row)}
525
+ 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"
526
+ >
527
+ <EllipsisVertical class="h-4 w-4" />
528
+ </button>
529
+ </div>
581
530
  {/if}
582
531
  </div>
583
532
  </div>
@@ -621,27 +570,13 @@
621
570
  {/each}
622
571
  </div>
623
572
  {:else}
624
- <!-- GRID VIEW -->
625
573
  <div class="grid gap-3 p-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
626
574
  {#each controller.currentRows as row, index (rowIdFor(row, index))}
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
575
  <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
- }`}
576
+ 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"
642
577
  oncontextmenu={(e) => openContextAt(e, row)}
643
578
  >
644
- <!-- ... tu grid view (igual que antes) ... -->
579
+ <!-- tu grid content -->
645
580
  </div>
646
581
  {/each}
647
582
  </div>
@@ -655,22 +590,22 @@
655
590
  </div>
656
591
 
657
592
  <DataTableFooter />
658
-
659
- <!-- ✅ CONTEXT POPOVER (FIXED) -->
660
- <div
661
- bind:this={contextPopover}
662
- popover="manual"
663
- data-context-host="true"
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"
665
- style={`position: fixed; left: ${contextRender.left}px; top: ${contextRender.top}px;`}
666
- onbeforetoggle={(e) => {
667
- if ((e as any).newState === 'closed') contextRow = null;
668
- }}
669
- >
670
- {#if contextRow && actions.length}
671
- <ContextMenu {actions} row={contextRow} onClose={closeContext} />
672
- {:else}
673
- <div class="flex flex-col gap-2">No hay acciones disponibles</div>
674
- {/if}
675
- </div>
676
593
  </div>
594
+ <!-- ✅ HOST fuera del overflow-auto (ya no lo recorta) -->
595
+ {#if contextOpen}
596
+ {#key contextRow}
597
+ <div
598
+ bind:this={contextPopover}
599
+ use:portal
600
+ data-context-host="true"
601
+ class="pointer-events-auto fixed z-[999999] 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"
602
+ style={`left:${contextRender.left}px; top:${contextRender.top}px;`}
603
+ >
604
+ {#if contextRow && actions.length}
605
+ <ContextMenu {actions} row={contextRow} onClose={closeContext} />
606
+ {:else}
607
+ <div class="flex flex-col gap-2">No hay acciones disponibles</div>
608
+ {/if}
609
+ </div>
610
+ {/key}
611
+ {/if}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/ui",
3
- "version": "0.34.6",
3
+ "version": "0.34.8",
4
4
  "private": false,
5
5
  "packageManager": "bun@1.3.9",
6
6
  "publishConfig": {