@r2digisolutions/ui 0.24.1 → 0.24.2

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,8 +1,33 @@
1
1
  <script lang="ts">
2
2
  import type { TContextMenuEntry } from '../core/types.js';
3
- import { portal } from '../utils/portal.js';
4
- import { clampToViewport } from '../utils/position.js';
5
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 ---
6
31
  type Props = {
7
32
  items?: TContextMenuEntry[];
8
33
  x?: number;
@@ -11,7 +36,6 @@
11
36
  title?: string;
12
37
  searchable?: boolean;
13
38
  context?: any;
14
- // por si algún día quieres desactivar el portal:
15
39
  portalTarget?: HTMLElement | null;
16
40
  };
17
41
  let {
@@ -25,15 +49,17 @@
25
49
  portalTarget = null
26
50
  }: Props = $props();
27
51
 
52
+ // --- Estado interno ---
28
53
  let stack = $state<{ label: string; items: TContextMenuEntry[] }[]>([]);
29
54
  let q = $state('');
30
-
31
55
  const current = $derived(stack.length ? stack[stack.length - 1] : { label: title, items });
32
56
 
33
57
  function close() {
34
58
  open = false;
35
59
  stack = [];
36
60
  q = '';
61
+ // opcional si quieres limpiar coords:
62
+ // x = 0; y = 0;
37
63
  }
38
64
 
39
65
  function hasChildren(it: TContextMenuEntry) {
@@ -66,6 +92,7 @@
66
92
  const query = q.trim().toLowerCase();
67
93
  let arr = query ? list.filter((it) => matches(it, query)) : list.slice();
68
94
 
95
+ // limpiar divisores consecutivos y extremos
69
96
  const out: TContextMenuEntry[] = [];
70
97
  let prevDiv = false;
71
98
  for (const it of arr) {
@@ -98,6 +125,10 @@
98
125
  }
99
126
  }
100
127
 
128
+ // --- Ref al elemento del menú para detectar click-fuera ---
129
+ let menuEl: HTMLDivElement | null = $state(null);
130
+
131
+ // Teclado (Escape / Backspace)
101
132
  $effect(() => {
102
133
  if (!open) return;
103
134
  const handler = (e: KeyboardEvent) => onKey(e);
@@ -105,17 +136,43 @@
105
136
  return () => document.removeEventListener('keydown', handler);
106
137
  });
107
138
 
108
- // --- CLAMP dinámico al abrir o al cambiar x/y ---
109
- let menuEl: HTMLDivElement | null = $state(null);
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
+ };
167
+ });
110
168
 
169
+ // Clamp de posición tras abrir o cambiar coords
111
170
  $effect(() => {
112
171
  if (!open || !menuEl) return;
113
- // siguiente frame para medir dimensiones reales
114
172
  requestAnimationFrame(() => {
115
173
  if (!menuEl) return;
116
174
  const rect = menuEl.getBoundingClientRect();
117
175
  const { x: nx, y: ny } = clampToViewport(x, y, rect.width, rect.height, 8);
118
- // solo si cambian, re-ubica
119
176
  if (nx !== x || ny !== y) {
120
177
  x = nx;
121
178
  y = ny;
@@ -125,7 +182,7 @@
125
182
  </script>
126
183
 
127
184
  {#if open}
128
- <!-- BACKDROP: va al body por el portal también -->
185
+ <!-- BACKDROP (portaleado al body) -->
129
186
  <div
130
187
  use:portal={portalTarget}
131
188
  role="dialog"
@@ -137,7 +194,7 @@
137
194
  style="pointer-events:auto"
138
195
  />
139
196
 
140
- <!-- MENU: fijado al viewport y portaleado al body -->
197
+ <!-- MENÚ (portaleado al body) -->
141
198
  <div
142
199
  bind:this={menuEl}
143
200
  use:portal={portalTarget}
@@ -161,10 +218,8 @@
161
218
  stroke="currentColor"
162
219
  stroke-width="2"
163
220
  stroke-linecap="round"
164
- stroke-linejoin="round"
221
+ stroke-linejoin="round"><polyline points="15 18 9 12 15 6" /></svg
165
222
  >
166
- <polyline points="15 18 9 12 15 6" />
167
- </svg>
168
223
  </button>
169
224
  {/if}
170
225
  <div class="min-w-0 flex-1 truncate px-1 text-xs font-medium opacity-70">
@@ -211,10 +266,8 @@
211
266
  stroke="currentColor"
212
267
  stroke-width="2"
213
268
  stroke-linecap="round"
214
- stroke-linejoin="round"
269
+ stroke-linejoin="round"><polyline points="9 18 15 12 9 6" /></svg
215
270
  >
216
- <polyline points="9 18 15 12 9 6" />
217
- </svg>
218
271
  {/if}
219
272
  </span>
220
273
  </button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/ui",
3
- "version": "0.24.1",
3
+ "version": "0.24.2",
4
4
  "private": false,
5
5
  "packageManager": "bun@1.2.23",
6
6
  "publishConfig": {