@r2digisolutions/ui 0.34.4 → 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.
|
@@ -57,23 +57,20 @@
|
|
|
57
57
|
{} as Record<keyof T, { left?: number; right?: number }>
|
|
58
58
|
);
|
|
59
59
|
|
|
60
|
-
//
|
|
60
|
+
// =========================
|
|
61
|
+
// CONTEXT MENU (FIX)
|
|
62
|
+
// =========================
|
|
61
63
|
let contextPopover = $state<HTMLDivElement | null>(null);
|
|
62
64
|
let contextRow = $state<T | null>(null);
|
|
63
65
|
|
|
64
|
-
// Punto
|
|
66
|
+
// Punto preferido: cursor o botón (client coords)
|
|
65
67
|
let contextPos = $state<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
66
68
|
|
|
67
|
-
// ✅
|
|
68
|
-
let contextRender = $state<{
|
|
69
|
-
x: 0,
|
|
70
|
-
y: 0,
|
|
71
|
-
transform: 'translate(-100%, 8px)'
|
|
72
|
-
});
|
|
69
|
+
// ✅ Posición final (clamp + flip) en left/top reales
|
|
70
|
+
let contextRender = $state<{ left: number; top: number }>({ left: 0, top: 0 });
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const CONTEXT_GAP = 8; // separación visual del cursor/botón
|
|
72
|
+
const CONTEXT_MARGIN = 12;
|
|
73
|
+
const CONTEXT_GAP = 8;
|
|
77
74
|
|
|
78
75
|
let openRows = $state<Set<string>>(new Set());
|
|
79
76
|
|
|
@@ -89,7 +86,7 @@
|
|
|
89
86
|
parts.push(`${w}px`);
|
|
90
87
|
});
|
|
91
88
|
|
|
92
|
-
// Columna acciones
|
|
89
|
+
// Columna de acciones
|
|
93
90
|
parts.push('64px');
|
|
94
91
|
gridTemplate = parts.join(' ');
|
|
95
92
|
|
|
@@ -101,59 +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
|
-
//
|
|
144
|
-
positionContext(
|
|
140
|
+
const handler = () => {
|
|
141
|
+
// si se mueve el viewport o scroll, recalculamos
|
|
142
|
+
positionContext();
|
|
145
143
|
};
|
|
146
144
|
|
|
147
|
-
window.addEventListener('resize',
|
|
148
|
-
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);
|
|
149
151
|
|
|
150
152
|
return () => {
|
|
151
|
-
window.removeEventListener('resize',
|
|
152
|
-
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);
|
|
153
157
|
};
|
|
154
158
|
});
|
|
155
159
|
|
|
156
|
-
// RESIZE COLUMNS
|
|
157
160
|
let resizingId: keyof T | null = null;
|
|
158
161
|
let startX = 0;
|
|
159
162
|
let startWidth = 0;
|
|
@@ -171,7 +174,8 @@
|
|
|
171
174
|
function onResizeMove(event: MouseEvent) {
|
|
172
175
|
if (!resizingId) return;
|
|
173
176
|
const dx = event.clientX - startX;
|
|
174
|
-
|
|
177
|
+
const width = startWidth + dx;
|
|
178
|
+
controller.resizeColumn(resizingId, width);
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
function onResizeUp() {
|
|
@@ -180,7 +184,6 @@
|
|
|
180
184
|
window.removeEventListener('mouseup', onResizeUp);
|
|
181
185
|
}
|
|
182
186
|
|
|
183
|
-
// HELPERS
|
|
184
187
|
function rowIdFor(row: T, index: number) {
|
|
185
188
|
return controller.getRowId(row, index);
|
|
186
189
|
}
|
|
@@ -213,12 +216,13 @@
|
|
|
213
216
|
|
|
214
217
|
function handleToggleAll(e: Event) {
|
|
215
218
|
const input = e.currentTarget as HTMLInputElement;
|
|
216
|
-
|
|
219
|
+
const checked = input.checked;
|
|
220
|
+
if (checked) controller.selectAllCurrentPage();
|
|
217
221
|
else controller.unselectAllCurrentPage();
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
// =========================
|
|
221
|
-
// ✅
|
|
225
|
+
// ✅ Positioning helpers
|
|
222
226
|
// =========================
|
|
223
227
|
function clamp(n: number, min: number, max: number) {
|
|
224
228
|
return Math.max(min, Math.min(max, n));
|
|
@@ -228,120 +232,81 @@
|
|
|
228
232
|
return new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
229
233
|
}
|
|
230
234
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
*/
|
|
239
|
-
function computeContextPosition(preferred: { x: number; y: number }, pop: DOMRect) {
|
|
240
|
-
const vw = window.innerWidth;
|
|
241
|
-
const vh = window.innerHeight;
|
|
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%, ...))
|
|
248
|
-
const fitsLeft = preferred.x - pop.width - CONTEXT_MARGIN >= 0;
|
|
249
|
-
const fitsRight = preferred.x + pop.width + CONTEXT_MARGIN <= vw;
|
|
250
|
-
|
|
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
|
|
256
|
-
const placeToRight = !fitsLeft && fitsRight;
|
|
257
|
-
|
|
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;
|
|
263
|
-
|
|
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`;
|
|
268
|
-
|
|
269
|
-
let x = preferred.x;
|
|
270
|
-
let y = preferred.y;
|
|
271
|
-
|
|
272
|
-
// Clamp horizontal según transformX
|
|
273
|
-
if (transformX === '-100%') {
|
|
274
|
-
// pop ocupa [x - w, x]
|
|
275
|
-
x = clamp(x, CONTEXT_MARGIN + pop.width, vw - CONTEXT_MARGIN);
|
|
276
|
-
} else {
|
|
277
|
-
// pop ocupa [x, x + w]
|
|
278
|
-
x = clamp(x, CONTEXT_MARGIN, vw - CONTEXT_MARGIN - pop.width);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Si tooTall, lo pegamos arriba dentro del viewport y dejamos overflow-auto
|
|
282
|
-
if (tooTall) {
|
|
235
|
+
function getViewport() {
|
|
236
|
+
const vv = window.visualViewport;
|
|
237
|
+
if (vv) {
|
|
283
238
|
return {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
239
|
+
left: vv.offsetLeft,
|
|
240
|
+
top: vv.offsetTop,
|
|
241
|
+
width: vv.width,
|
|
242
|
+
height: vv.height
|
|
287
243
|
};
|
|
288
244
|
}
|
|
245
|
+
return {
|
|
246
|
+
left: 0,
|
|
247
|
+
top: 0,
|
|
248
|
+
width: window.innerWidth,
|
|
249
|
+
height: window.innerHeight
|
|
250
|
+
};
|
|
251
|
+
}
|
|
289
252
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
}
|
|
253
|
+
function computeContextPosition(preferred: { x: number; y: number }, pop: DOMRect) {
|
|
254
|
+
const vp = getViewport();
|
|
255
|
+
|
|
256
|
+
const minLeft = vp.left + CONTEXT_MARGIN;
|
|
257
|
+
const minTop = vp.top + CONTEXT_MARGIN;
|
|
299
258
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const topIfDown = y + CONTEXT_GAP; // top real cuando está abajo
|
|
303
|
-
const bottomIfDown = topIfDown + pop.height;
|
|
259
|
+
const maxLeft = vp.left + vp.width - CONTEXT_MARGIN - pop.width;
|
|
260
|
+
const maxTop = vp.top + vp.height - CONTEXT_MARGIN - pop.height;
|
|
304
261
|
|
|
305
|
-
|
|
306
|
-
const
|
|
262
|
+
// Si es demasiado alto, lo pegamos arriba (y el popover scrollea)
|
|
263
|
+
const tooTall = pop.height > vp.height - CONTEXT_MARGIN * 2;
|
|
307
264
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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) {
|
|
271
|
+
left = preferred.x + CONTEXT_GAP;
|
|
311
272
|
}
|
|
312
273
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
274
|
+
// Flip vertical si no cabe abajo
|
|
275
|
+
if (!tooTall) {
|
|
276
|
+
const bottomIfDown = top + pop.height;
|
|
277
|
+
const bottomLimit = vp.top + vp.height - CONTEXT_MARGIN;
|
|
278
|
+
if (bottomIfDown > bottomLimit) {
|
|
279
|
+
top = preferred.y - CONTEXT_GAP - pop.height;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
top = minTop;
|
|
316
283
|
}
|
|
317
284
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
};
|
|
285
|
+
// Clamp final
|
|
286
|
+
left = clamp(left, minLeft, Math.max(minLeft, maxLeft));
|
|
287
|
+
top = clamp(top, minTop, Math.max(minTop, maxTop));
|
|
288
|
+
|
|
289
|
+
return { left, top };
|
|
323
290
|
}
|
|
324
291
|
|
|
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) {
|
|
292
|
+
async function positionContext() {
|
|
332
293
|
if (!contextPopover || !contextRow) return;
|
|
333
294
|
|
|
334
|
-
//
|
|
295
|
+
// Asegura layout real (Popover API a veces “late layout”)
|
|
335
296
|
await raf();
|
|
336
297
|
|
|
337
|
-
const
|
|
298
|
+
const rect1 = contextPopover.getBoundingClientRect();
|
|
299
|
+
if (!rect1.width || !rect1.height) return;
|
|
338
300
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
if (!rect.width || !rect.height) return;
|
|
301
|
+
// 1ª pasada
|
|
302
|
+
contextRender = computeContextPosition(contextPos, rect1);
|
|
343
303
|
|
|
344
|
-
|
|
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;
|
|
308
|
+
|
|
309
|
+
contextRender = computeContextPosition(contextPos, rect2);
|
|
345
310
|
}
|
|
346
311
|
|
|
347
312
|
async function openContextAt(event: MouseEvent, row: T) {
|
|
@@ -353,9 +318,9 @@
|
|
|
353
318
|
|
|
354
319
|
if (contextPopover) contextPopover.showPopover();
|
|
355
320
|
|
|
356
|
-
//
|
|
321
|
+
// Espera render + posiciona
|
|
357
322
|
await tick();
|
|
358
|
-
await positionContext(
|
|
323
|
+
await positionContext();
|
|
359
324
|
}
|
|
360
325
|
|
|
361
326
|
async function openContextFromButton(event: MouseEvent, row: T) {
|
|
@@ -363,16 +328,15 @@
|
|
|
363
328
|
event.stopPropagation();
|
|
364
329
|
|
|
365
330
|
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
366
|
-
|
|
367
331
|
contextRow = row;
|
|
368
332
|
|
|
369
|
-
//
|
|
333
|
+
// Punto preferido: esquina inferior derecha
|
|
370
334
|
contextPos = { x: rect.right, y: rect.bottom };
|
|
371
335
|
|
|
372
336
|
if (contextPopover) contextPopover.showPopover();
|
|
373
337
|
|
|
374
338
|
await tick();
|
|
375
|
-
await positionContext(
|
|
339
|
+
await positionContext();
|
|
376
340
|
}
|
|
377
341
|
|
|
378
342
|
function closeContext() {
|
|
@@ -546,7 +510,7 @@
|
|
|
546
510
|
{@const value = col.accessor ? col.accessor(row) : (row as any)[col.id]}
|
|
547
511
|
{@const sticky = stickyOffsets[col.id as keyof T]}
|
|
548
512
|
<div
|
|
549
|
-
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${
|
|
550
514
|
density === 'compact' ? 'py-1.5' : 'py-2.5'
|
|
551
515
|
} dark:border-neutral-800/70 ${
|
|
552
516
|
col.sticky === 'left'
|
|
@@ -558,7 +522,12 @@
|
|
|
558
522
|
: ''}
|
|
559
523
|
>
|
|
560
524
|
{#if cell}
|
|
561
|
-
{@render cell({
|
|
525
|
+
{@render cell({
|
|
526
|
+
row,
|
|
527
|
+
column: col,
|
|
528
|
+
value,
|
|
529
|
+
index
|
|
530
|
+
})}
|
|
562
531
|
{:else}
|
|
563
532
|
<span
|
|
564
533
|
class={`line-clamp-2 text-black dark:text-neutral-50 ${
|
|
@@ -652,6 +621,7 @@
|
|
|
652
621
|
{/each}
|
|
653
622
|
</div>
|
|
654
623
|
{:else}
|
|
624
|
+
<!-- GRID VIEW -->
|
|
655
625
|
<div class="grid gap-3 p-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
656
626
|
{#each controller.currentRows as row, index (rowIdFor(row, index))}
|
|
657
627
|
{@const id = rowIdFor(row, index)}
|
|
@@ -663,8 +633,6 @@
|
|
|
663
633
|
: (row as any)[firstCol.id]
|
|
664
634
|
: null}
|
|
665
635
|
{@const restCols = cols.slice(1)}
|
|
666
|
-
|
|
667
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
668
636
|
<div
|
|
669
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 ${
|
|
670
638
|
controller.selectedIds.has(id)
|
|
@@ -673,86 +641,7 @@
|
|
|
673
641
|
}`}
|
|
674
642
|
oncontextmenu={(e) => openContextAt(e, row)}
|
|
675
643
|
>
|
|
676
|
-
|
|
677
|
-
<div
|
|
678
|
-
class="absolute top-2 left-2 z-10 rounded-full bg-neutral-900/70 p-1 backdrop-blur-md dark:bg-neutral-950/80"
|
|
679
|
-
data-stop-row-toggle="true"
|
|
680
|
-
>
|
|
681
|
-
<input
|
|
682
|
-
type="checkbox"
|
|
683
|
-
checked={controller.selectedIds.has(id)}
|
|
684
|
-
onchange={() => controller.toggleRowSelection(id)}
|
|
685
|
-
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"
|
|
686
|
-
/>
|
|
687
|
-
</div>
|
|
688
|
-
{/if}
|
|
689
|
-
|
|
690
|
-
<div class="mb-2 pr-6 text-black dark:text-neutral-50">
|
|
691
|
-
{#if cell && firstCol}
|
|
692
|
-
{@render cell({ row, column: firstCol, value: firstValue, index })}
|
|
693
|
-
{:else if firstCol}
|
|
694
|
-
<div
|
|
695
|
-
class="line-clamp-2 text-[12px] leading-snug font-semibold text-neutral-900 dark:text-neutral-50"
|
|
696
|
-
>
|
|
697
|
-
{formatValue(firstCol, firstValue, row)}
|
|
698
|
-
</div>
|
|
699
|
-
{/if}
|
|
700
|
-
</div>
|
|
701
|
-
|
|
702
|
-
<dl class="space-y-1.5">
|
|
703
|
-
{#each restCols as col (col.id)}
|
|
704
|
-
{@const value = col.accessor ? col.accessor(row) : (row as any)[col.id]}
|
|
705
|
-
<div class="flex items-start justify-between gap-2">
|
|
706
|
-
<dt
|
|
707
|
-
class="max-w-[45%] truncate text-[10px] font-medium text-neutral-400 uppercase dark:text-neutral-500"
|
|
708
|
-
>
|
|
709
|
-
{col.label}
|
|
710
|
-
</dt>
|
|
711
|
-
<dd
|
|
712
|
-
class="line-clamp-2 flex-1 text-right text-[11px] text-neutral-700 dark:text-neutral-200"
|
|
713
|
-
>
|
|
714
|
-
{formatValue(col, value, row)}
|
|
715
|
-
</dd>
|
|
716
|
-
</div>
|
|
717
|
-
{/each}
|
|
718
|
-
</dl>
|
|
719
|
-
|
|
720
|
-
{#if actions.length}
|
|
721
|
-
<div
|
|
722
|
-
class="mt-2 flex items-center justify-end gap-1.5"
|
|
723
|
-
data-stop-row-toggle="true"
|
|
724
|
-
>
|
|
725
|
-
{#if rowCollapse}
|
|
726
|
-
<button
|
|
727
|
-
type="button"
|
|
728
|
-
onclick={(e) => {
|
|
729
|
-
e.stopPropagation();
|
|
730
|
-
toggleRow(row, index);
|
|
731
|
-
}}
|
|
732
|
-
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 ${
|
|
733
|
-
openRows.has(id) ? 'rotate-180' : ''
|
|
734
|
-
}`}
|
|
735
|
-
>
|
|
736
|
-
<ChevronDown class="h-3.5 w-3.5" />
|
|
737
|
-
</button>
|
|
738
|
-
{/if}
|
|
739
|
-
<button
|
|
740
|
-
type="button"
|
|
741
|
-
onclick={(e) => openContextFromButton(e, row)}
|
|
742
|
-
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"
|
|
743
|
-
>
|
|
744
|
-
<EllipsisVertical class="h-4 w-4" />
|
|
745
|
-
</button>
|
|
746
|
-
</div>
|
|
747
|
-
{/if}
|
|
748
|
-
|
|
749
|
-
{#if rowCollapse && openRows.has(id)}
|
|
750
|
-
<div
|
|
751
|
-
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"
|
|
752
|
-
>
|
|
753
|
-
{@render rowCollapse(row)}
|
|
754
|
-
</div>
|
|
755
|
-
{/if}
|
|
644
|
+
<!-- ... tu grid view (igual que antes) ... -->
|
|
756
645
|
</div>
|
|
757
646
|
{/each}
|
|
758
647
|
</div>
|
|
@@ -767,13 +656,13 @@
|
|
|
767
656
|
|
|
768
657
|
<DataTableFooter />
|
|
769
658
|
|
|
770
|
-
<!-- ✅ CONTEXT POPOVER
|
|
659
|
+
<!-- ✅ CONTEXT POPOVER (FIXED) -->
|
|
771
660
|
<div
|
|
772
661
|
bind:this={contextPopover}
|
|
773
662
|
popover="manual"
|
|
774
663
|
data-context-host="true"
|
|
775
|
-
class="z-[1300] max-h-[calc(100vh-
|
|
776
|
-
style={`position: fixed; left: ${contextRender.
|
|
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;`}
|
|
777
666
|
onbeforetoggle={(e) => {
|
|
778
667
|
if ((e as any).newState === 'closed') contextRow = null;
|
|
779
668
|
}}
|