@r2digisolutions/ui 0.24.2 → 0.24.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.
@@ -71,7 +71,7 @@
71
71
  manager.setReservedWidth(reserved);
72
72
  });
73
73
 
74
- // reflow ancho
74
+ // Reflow ancho
75
75
  $effect(() => {
76
76
  if (!container) return;
77
77
  const ro = new ResizeObserver((entries) => {
@@ -82,7 +82,7 @@
82
82
  return () => ro.disconnect();
83
83
  });
84
84
 
85
- // medir DOM
85
+ // Medir DOM
86
86
  const SAMPLE_ROWS = 10;
87
87
  async function measureColumns() {
88
88
  await tick();
@@ -124,7 +124,15 @@
124
124
  ) {
125
125
  e.preventDefault();
126
126
  const columnIndex = columnId ? manager.state.visibleColumns.indexOf(columnId) : null;
127
- rightMenu = { open: true, x: e.clientX, y: e.clientY };
127
+ // Always set fresh coordinates and ensure menu is closed before reopening
128
+ if (rightMenu.open) {
129
+ rightMenu = { open: false, x: 0, y: 0 };
130
+ setTimeout(() => {
131
+ rightMenu = { open: true, x: e.clientX, y: e.clientY };
132
+ }, 0);
133
+ } else {
134
+ rightMenu = { open: true, x: e.clientX, y: e.clientY };
135
+ }
128
136
  rightClickContext = {
129
137
  row,
130
138
  rowIndex,
@@ -140,13 +148,14 @@
140
148
  return manager.state.items.filter((r) => ids.has(rowId(r)));
141
149
  }
142
150
 
143
- // tracks
151
+ // Tracks
144
152
  function colTrack(cId: string, measuring: boolean) {
145
153
  if (measuring) return 'max-content';
146
154
  const c = manager.getColumn(cId);
147
155
  const w = manager.measured[cId] ?? c.width ?? c.minWidth ?? 160;
148
156
  return `${Math.max(40, Math.ceil(Number(w)))}px`;
149
157
  }
158
+
150
159
  function headerTemplateCols(visible: string[], endExtras: boolean) {
151
160
  const tracks = [
152
161
  `${CHECK_W}px`,
@@ -1,33 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { TContextMenuEntry } from '../core/types.js';
3
3
 
4
- // --- Action portal (interna al componente) ---
5
- function portal(node: HTMLElement, target: HTMLElement | null = null) {
6
- const tgt = target ?? document.body;
7
- const placeholder = document.createComment('portal-placeholder');
8
- node.parentNode?.insertBefore(placeholder, node);
9
- tgt.appendChild(node);
10
- return {
11
- destroy() {
12
- node.remove();
13
- placeholder.parentNode?.insertBefore(node, placeholder);
14
- placeholder.remove();
15
- }
16
- };
17
- }
18
-
19
- // --- Util para clamping a viewport (interna) ---
20
- function clampToViewport(x: number, y: number, menuW: number, menuH: number, padding = 8) {
21
- const vw = document.documentElement.clientWidth;
22
- const vh = document.documentElement.clientHeight;
23
- let nx = x;
24
- let ny = y;
25
- if (nx + menuW + padding > vw) nx = Math.max(padding, vw - menuW - padding);
26
- if (ny + menuH + padding > vh) ny = Math.max(padding, vh - menuH - padding);
27
- return { x: nx, y: ny };
28
- }
29
-
30
- // --- Props ---
31
4
  type Props = {
32
5
  items?: TContextMenuEntry[];
33
6
  x?: number;
@@ -36,7 +9,6 @@
36
9
  title?: string;
37
10
  searchable?: boolean;
38
11
  context?: any;
39
- portalTarget?: HTMLElement | null;
40
12
  };
41
13
  let {
42
14
  items = [],
@@ -45,21 +17,22 @@
45
17
  open = $bindable(false),
46
18
  title = '',
47
19
  searchable = true,
48
- context = null,
49
- portalTarget = null
20
+ context = null
50
21
  }: Props = $props();
51
22
 
52
- // --- Estado interno ---
53
23
  let stack = $state<{ label: string; items: TContextMenuEntry[] }[]>([]);
54
24
  let q = $state('');
55
25
  const current = $derived(stack.length ? stack[stack.length - 1] : { label: title, items });
56
26
 
27
+ let menuEl: HTMLDivElement | null = $state(null);
28
+
57
29
  function close() {
30
+ if (menuEl) menuEl.hidePopover();
58
31
  open = false;
59
32
  stack = [];
60
33
  q = '';
61
- // opcional si quieres limpiar coords:
62
- // x = 0; y = 0;
34
+ x = 0;
35
+ y = 0;
63
36
  }
64
37
 
65
38
  function hasChildren(it: TContextMenuEntry) {
@@ -91,8 +64,6 @@
91
64
  const list = current.items ?? [];
92
65
  const query = q.trim().toLowerCase();
93
66
  let arr = query ? list.filter((it) => matches(it, query)) : list.slice();
94
-
95
- // limpiar divisores consecutivos y extremos
96
67
  const out: TContextMenuEntry[] = [];
97
68
  let prevDiv = false;
98
69
  for (const it of arr) {
@@ -125,88 +96,54 @@
125
96
  }
126
97
  }
127
98
 
128
- // --- Ref al elemento del menú para detectar click-fuera ---
129
- let menuEl: HTMLDivElement | null = $state(null);
130
-
131
- // Teclado (Escape / Backspace)
99
+ // Manage popover open/close
132
100
  $effect(() => {
133
- if (!open) return;
134
- const handler = (e: KeyboardEvent) => onKey(e);
135
- document.addEventListener('keydown', handler);
136
- return () => document.removeEventListener('keydown', handler);
137
- });
138
-
139
- // Cierres robustos: click-fuera en capture, scroll/resize, y bloqueo de menú nativo
140
- $effect(() => {
141
- if (!open) return;
142
-
143
- const onPointerDownCapture = (ev: PointerEvent) => {
144
- if (!menuEl) return;
145
- if (!menuEl.contains(ev.target as Node)) {
146
- close();
147
- }
148
- };
149
-
150
- const onNativeCtx = (ev: MouseEvent) => {
151
- ev.preventDefault();
152
- };
153
-
154
- const onScrollOrResize = () => close();
155
-
156
- document.addEventListener('pointerdown', onPointerDownCapture, true);
157
- document.addEventListener('contextmenu', onNativeCtx);
158
- window.addEventListener('scroll', onScrollOrResize, true);
159
- window.addEventListener('resize', onScrollOrResize, true);
160
-
161
- return () => {
162
- document.removeEventListener('pointerdown', onPointerDownCapture, true);
163
- document.removeEventListener('contextmenu', onNativeCtx);
164
- window.removeEventListener('scroll', onScrollOrResize, true);
165
- window.removeEventListener('resize', onScrollOrResize, true);
166
- };
101
+ if (!menuEl) return;
102
+ if (open) {
103
+ menuEl.style.setProperty('--popover-x', `${x}px`);
104
+ menuEl.style.setProperty('--popover-y', `${y}px`);
105
+ menuEl.showPopover();
106
+ } else {
107
+ menuEl.hidePopover();
108
+ }
167
109
  });
168
110
 
169
- // Clamp de posición tras abrir o cambiar coords
111
+ // Clamp position to viewport
170
112
  $effect(() => {
171
113
  if (!open || !menuEl) return;
172
114
  requestAnimationFrame(() => {
173
115
  if (!menuEl) return;
174
116
  const rect = menuEl.getBoundingClientRect();
175
- const { x: nx, y: ny } = clampToViewport(x, y, rect.width, rect.height, 8);
117
+ const vw = document.documentElement.clientWidth;
118
+ const vh = document.documentElement.clientHeight;
119
+ let nx = x;
120
+ let ny = y;
121
+ const padding = 8;
122
+ if (nx + rect.width + padding > vw) nx = Math.max(padding, vw - rect.width - padding);
123
+ if (ny + rect.height + padding > vh) ny = Math.max(padding, vh - rect.height - padding);
176
124
  if (nx !== x || ny !== y) {
177
125
  x = nx;
178
126
  y = ny;
127
+ menuEl.style.setProperty('--popover-x', `${nx}px`);
128
+ menuEl.style.setProperty('--popover-y', `${ny}px`);
179
129
  }
180
130
  });
181
131
  });
182
132
  </script>
183
133
 
184
134
  {#if open}
185
- <!-- BACKDROP (portaleado al body) -->
186
- <div
187
- use:portal={portalTarget}
188
- role="dialog"
189
- class="fixed inset-0 z-[2147483646]"
190
- onclick={() => close()}
191
- oncontextmenu={(e) => e.preventDefault()}
192
- aria-modal="true"
193
- tabindex="0"
194
- style="pointer-events:auto"
195
- />
196
-
197
- <!-- MENÚ (portaleado al body) -->
198
135
  <div
199
136
  bind:this={menuEl}
200
- use:portal={portalTarget}
201
- class="fixed z-[2147483647] w-72 rounded-2xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900"
202
- style={`left:${x}px; top:${y}px`}
137
+ popover="manual"
138
+ class="w-72 rounded-2xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900"
139
+ style="position: fixed; left: var(--popover-x); top: var(--popover-y);"
203
140
  oncontextmenu={(e) => e.preventDefault()}
141
+ onkeydown={onKey}
204
142
  >
205
143
  <div class="flex items-center gap-1 px-1 py-1">
206
144
  {#if stack.length > 0}
207
145
  <button
208
146
  class="rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-800"
209
- role="dialog"
210
147
  aria-label="Atrás"
211
148
  onclick={back}
212
149
  >
@@ -218,8 +155,10 @@
218
155
  stroke="currentColor"
219
156
  stroke-width="2"
220
157
  stroke-linecap="round"
221
- stroke-linejoin="round"><polyline points="15 18 9 12 15 6" /></svg
158
+ stroke-linejoin="round"
222
159
  >
160
+ <polyline points="15 18 9 12 15 6" />
161
+ </svg>
223
162
  </button>
224
163
  {/if}
225
164
  <div class="min-w-0 flex-1 truncate px-1 text-xs font-medium opacity-70">
@@ -245,16 +184,15 @@
245
184
  {:else}
246
185
  <button
247
186
  class="flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm hover:bg-gray-100 disabled:opacity-50 dark:hover:bg-gray-800"
248
- role="dialog"
249
187
  disabled={it.disabled}
250
188
  onclick={() => clickItem(it)}
251
189
  >
252
190
  <span class="truncate">{it.label}</span>
253
191
  <span class="flex items-center gap-2">
254
192
  {#if it.shortcut}
255
- <kbd class="rounded bg-gray-100 px-1 text-[10px] dark:bg-gray-800"
256
- >{it.shortcut}</kbd
257
- >
193
+ <kbd class="rounded bg-gray-100 px-1 text-[10px] dark:bg-gray-800">
194
+ {it.shortcut}
195
+ </kbd>
258
196
  {/if}
259
197
  {#if hasChildren(it)}
260
198
  <svg
@@ -266,8 +204,10 @@
266
204
  stroke="currentColor"
267
205
  stroke-width="2"
268
206
  stroke-linecap="round"
269
- stroke-linejoin="round"><polyline points="9 18 15 12 9 6" /></svg
207
+ stroke-linejoin="round"
270
208
  >
209
+ <polyline points="9 18 15 12 9 6" />
210
+ </svg>
271
211
  {/if}
272
212
  </span>
273
213
  </button>
@@ -276,3 +216,12 @@
276
216
  </div>
277
217
  </div>
278
218
  {/if}
219
+
220
+ <style>
221
+ [popover] {
222
+ margin: 0;
223
+ border: none;
224
+ padding: 0;
225
+ z-index: 2147483647;
226
+ }
227
+ </style>
@@ -7,7 +7,6 @@ type Props = {
7
7
  title?: string;
8
8
  searchable?: boolean;
9
9
  context?: any;
10
- portalTarget?: HTMLElement | null;
11
10
  };
12
11
  declare const ContextMenu: import("svelte").Component<Props, {}, "open">;
13
12
  type ContextMenu = ReturnType<typeof ContextMenu>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/ui",
3
- "version": "0.24.2",
3
+ "version": "0.24.3",
4
4
  "private": false,
5
5
  "packageManager": "bun@1.2.23",
6
6
  "publishConfig": {