@r2digisolutions/ui 0.34.0 → 0.34.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.
@@ -1,5 +1,6 @@
1
1
  <script lang="ts" generics="T">
2
2
  import type { Snippet } from 'svelte';
3
+ import { tick } from 'svelte';
3
4
  import { EllipsisVertical, ChevronDown } from 'lucide-svelte';
4
5
  import type { ColumnDef, RowAction } from './core/types.js';
5
6
  import type { DataTableController } from './core/DataTableController.svelte';
@@ -56,10 +57,22 @@
56
57
  {} as Record<keyof T, { left?: number; right?: number }>
57
58
  );
58
59
 
60
+ // CONTEXT MENU
59
61
  let contextPopover = $state<HTMLDivElement | null>(null);
60
62
  let contextRow = $state<T | null>(null);
63
+
64
+ // punto preferido donde se abrió (cursor o botón)
61
65
  let contextPos = $state<{ x: number; y: number }>({ x: 0, y: 0 });
62
66
 
67
+ // ✅ posición final render (clamp + flip)
68
+ let contextRender = $state<{ x: number; y: number; transform: string }>({
69
+ x: 0,
70
+ y: 0,
71
+ transform: 'translate(-100%, 8px)'
72
+ });
73
+
74
+ const CONTEXT_MARGIN = 10;
75
+
63
76
  let openRows = $state<Set<string>>(new Set());
64
77
 
65
78
  const contextOpen = $derived(contextRow !== null);
@@ -123,6 +136,22 @@
123
136
  };
124
137
  });
125
138
 
139
+ // ✅ Reposicionar en resize / scroll (capture) mientras esté abierto
140
+ $effect(() => {
141
+ if (!contextOpen) return;
142
+
143
+ const onWin = () => positionContext();
144
+
145
+ window.addEventListener('resize', onWin);
146
+ // capture: true para enterarte del scroll en contenedores overflow-auto
147
+ window.addEventListener('scroll', onWin, { passive: true, capture: true });
148
+
149
+ return () => {
150
+ window.removeEventListener('resize', onWin);
151
+ window.removeEventListener('scroll', onWin, true as any);
152
+ };
153
+ });
154
+
126
155
  let resizingId: keyof T | null = null;
127
156
  let startX = 0;
128
157
  let startWidth = 0;
@@ -165,21 +194,95 @@
165
194
  return String(value);
166
195
  }
167
196
 
168
- function openContextAt(event: MouseEvent, row: T) {
197
+ // =========================
198
+ // ✅ Context positioning logic
199
+ // =========================
200
+ function clamp(n: number, min: number, max: number) {
201
+ return Math.max(min, Math.min(max, n));
202
+ }
203
+
204
+ function computeContextPosition(preferred: { x: number; y: number }, pop: DOMRect) {
205
+ const vw = window.innerWidth;
206
+ const vh = window.innerHeight;
207
+
208
+ const fitsLeft = preferred.x - pop.width - CONTEXT_MARGIN >= 0;
209
+ 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
+
213
+ // Horizontal: preferimos “a la izquierda” (como translate(-100%, ...))
214
+ const placeToRight = !fitsLeft && fitsRight;
215
+
216
+ // Vertical: preferimos “abajo”
217
+ const placeUp = !fitsDown && fitsUp;
218
+
219
+ let transformX = placeToRight ? '0%' : '-100%';
220
+ let transformY = placeUp ? 'calc(-100% - 8px)' : '8px';
221
+
222
+ let x = preferred.x;
223
+ let y = preferred.y;
224
+
225
+ // Clamp horizontal según transform
226
+ if (transformX === '-100%') {
227
+ // pop ocupa [x - w, x]
228
+ x = clamp(x, CONTEXT_MARGIN + pop.width, vw - CONTEXT_MARGIN);
229
+ } else {
230
+ // pop ocupa [x, x + w]
231
+ x = clamp(x, CONTEXT_MARGIN, vw - CONTEXT_MARGIN - pop.width);
232
+ }
233
+
234
+ // Clamp vertical según lado
235
+ if (placeUp) {
236
+ // pop ocupa [y - 8 - h, y - 8]
237
+ y = clamp(y, CONTEXT_MARGIN + pop.height + 8, vh - CONTEXT_MARGIN);
238
+ } else {
239
+ // pop ocupa [y + 8, y + 8 + h]
240
+ y = clamp(y, CONTEXT_MARGIN - 8, vh - CONTEXT_MARGIN - pop.height - 8);
241
+ }
242
+
243
+ return {
244
+ x,
245
+ y,
246
+ transform: `translate(${transformX}, ${transformY})`
247
+ };
248
+ }
249
+
250
+ async function positionContext() {
251
+ if (!contextPopover || !contextRow) return;
252
+
253
+ // Si está cerrado, evita medir
254
+ // (aunque contextRow sea truthy, por seguridad)
255
+ const rect = contextPopover.getBoundingClientRect();
256
+ if (!rect.width || !rect.height) return;
257
+
258
+ const next = computeContextPosition(contextPos, rect);
259
+ contextRender = next;
260
+ }
261
+
262
+ async function openContextAt(event: MouseEvent, row: T) {
169
263
  event.preventDefault();
170
264
  event.stopPropagation();
171
265
  contextRow = row;
172
266
  contextPos = { x: event.clientX, y: event.clientY };
173
267
  if (contextPopover) contextPopover.showPopover();
268
+
269
+ await tick();
270
+ await positionContext();
174
271
  }
175
272
 
176
- function openContextFromButton(event: MouseEvent, row: T) {
273
+ async function openContextFromButton(event: MouseEvent, row: T) {
177
274
  event.preventDefault();
178
275
  event.stopPropagation();
179
276
  const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
180
277
  contextRow = row;
278
+
279
+ // Punto preferido: esquina inferior derecha del botón
181
280
  contextPos = { x: rect.right, y: rect.bottom };
281
+
182
282
  if (contextPopover) contextPopover.showPopover();
283
+
284
+ await tick();
285
+ await positionContext();
183
286
  }
184
287
 
185
288
  function closeContext() {
@@ -613,8 +716,8 @@
613
716
  bind:this={contextPopover}
614
717
  popover="manual"
615
718
  data-context-host="true"
616
- 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"
617
- style={`position: fixed; left: ${contextPos.x}px; top: ${contextPos.y}px; transform: translate(-100%, 8px);`}
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"
720
+ style={`position: fixed; left: ${contextRender.x}px; top: ${contextRender.y}px; transform: ${contextRender.transform};`}
618
721
  onbeforetoggle={(e) => {
619
722
  if ((e as any).newState === 'closed') contextRow = null;
620
723
  }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/ui",
3
- "version": "0.34.0",
3
+ "version": "0.34.1",
4
4
  "private": false,
5
5
  "packageManager": "bun@1.3.8",
6
6
  "publishConfig": {