@r2digisolutions/ui 0.24.0 → 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,6 +1,33 @@
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 ---
4
31
  type Props = {
5
32
  items?: TContextMenuEntry[];
6
33
  x?: number;
@@ -9,6 +36,7 @@
9
36
  title?: string;
10
37
  searchable?: boolean;
11
38
  context?: any;
39
+ portalTarget?: HTMLElement | null;
12
40
  };
13
41
  let {
14
42
  items = [],
@@ -17,18 +45,21 @@
17
45
  open = $bindable(false),
18
46
  title = '',
19
47
  searchable = true,
20
- context = null
48
+ context = null,
49
+ portalTarget = null
21
50
  }: Props = $props();
22
51
 
52
+ // --- Estado interno ---
23
53
  let stack = $state<{ label: string; items: TContextMenuEntry[] }[]>([]);
24
54
  let q = $state('');
25
-
26
55
  const current = $derived(stack.length ? stack[stack.length - 1] : { label: title, items });
27
56
 
28
57
  function close() {
29
58
  open = false;
30
59
  stack = [];
31
60
  q = '';
61
+ // opcional si quieres limpiar coords:
62
+ // x = 0; y = 0;
32
63
  }
33
64
 
34
65
  function hasChildren(it: TContextMenuEntry) {
@@ -61,7 +92,7 @@
61
92
  const query = q.trim().toLowerCase();
62
93
  let arr = query ? list.filter((it) => matches(it, query)) : list.slice();
63
94
 
64
- // limpiar divisores (sin duplicados, ni al principio/fin)
95
+ // limpiar divisores consecutivos y extremos
65
96
  const out: TContextMenuEntry[] = [];
66
97
  let prevDiv = false;
67
98
  for (const it of arr) {
@@ -94,26 +125,80 @@
94
125
  }
95
126
  }
96
127
 
128
+ // --- Ref al elemento del menú para detectar click-fuera ---
129
+ let menuEl: HTMLDivElement | null = $state(null);
130
+
131
+ // Teclado (Escape / Backspace)
97
132
  $effect(() => {
98
133
  if (!open) return;
99
134
  const handler = (e: KeyboardEvent) => onKey(e);
100
135
  document.addEventListener('keydown', handler);
101
136
  return () => document.removeEventListener('keydown', handler);
102
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
+ };
167
+ });
168
+
169
+ // Clamp de posición tras abrir o cambiar coords
170
+ $effect(() => {
171
+ if (!open || !menuEl) return;
172
+ requestAnimationFrame(() => {
173
+ if (!menuEl) return;
174
+ const rect = menuEl.getBoundingClientRect();
175
+ const { x: nx, y: ny } = clampToViewport(x, y, rect.width, rect.height, 8);
176
+ if (nx !== x || ny !== y) {
177
+ x = nx;
178
+ y = ny;
179
+ }
180
+ });
181
+ });
103
182
  </script>
104
183
 
105
184
  {#if open}
185
+ <!-- BACKDROP (portaleado al body) -->
106
186
  <div
187
+ use:portal={portalTarget}
107
188
  role="dialog"
108
- class="fixed inset-0 z-40"
189
+ class="fixed inset-0 z-[2147483646]"
109
190
  onclick={() => close()}
110
191
  oncontextmenu={(e) => e.preventDefault()}
111
192
  aria-modal="true"
112
193
  tabindex="0"
194
+ style="pointer-events:auto"
113
195
  />
114
196
 
197
+ <!-- MENÚ (portaleado al body) -->
115
198
  <div
116
- class="fixed z-50 w-72 rounded-2xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900"
199
+ 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"
117
202
  style={`left:${x}px; top:${y}px`}
118
203
  oncontextmenu={(e) => e.preventDefault()}
119
204
  >
@@ -7,6 +7,7 @@ type Props = {
7
7
  title?: string;
8
8
  searchable?: boolean;
9
9
  context?: any;
10
+ portalTarget?: HTMLElement | null;
10
11
  };
11
12
  declare const ContextMenu: import("svelte").Component<Props, {}, "open">;
12
13
  type ContextMenu = ReturnType<typeof ContextMenu>;
@@ -0,0 +1,3 @@
1
+ export declare function portal(node: HTMLElement, target?: HTMLElement | null): {
2
+ destroy(): void;
3
+ };
@@ -0,0 +1,13 @@
1
+ export function portal(node, target = null) {
2
+ const tgt = target ?? document.body;
3
+ const placeholder = document.createComment('portal-placeholder');
4
+ node.parentNode?.insertBefore(placeholder, node);
5
+ tgt.appendChild(node);
6
+ return {
7
+ destroy() {
8
+ node.remove();
9
+ placeholder.parentNode?.insertBefore(node, placeholder);
10
+ placeholder.remove();
11
+ }
12
+ };
13
+ }
@@ -0,0 +1,4 @@
1
+ export declare function clampToViewport(x: number, y: number, menuW: number, menuH: number, padding?: number): {
2
+ x: number;
3
+ y: number;
4
+ };
@@ -0,0 +1,11 @@
1
+ export function clampToViewport(x, y, menuW, menuH, padding = 8) {
2
+ const vw = document.documentElement.clientWidth;
3
+ const vh = document.documentElement.clientHeight;
4
+ let nx = x;
5
+ let ny = y;
6
+ if (nx + menuW + padding > vw)
7
+ nx = Math.max(padding, vw - menuW - padding);
8
+ if (ny + menuH + padding > vh)
9
+ ny = Math.max(padding, vh - menuH - padding);
10
+ return { x: nx, y: ny };
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/ui",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
4
4
  "private": false,
5
5
  "packageManager": "bun@1.2.23",
6
6
  "publishConfig": {
@@ -53,20 +53,20 @@
53
53
  "@playwright/test": "^1.55.1",
54
54
  "@storybook/addon-essentials": "^8.6.14",
55
55
  "@storybook/addon-interactions": "^8.6.14",
56
- "@storybook/addon-svelte-csf": "5.0.8",
56
+ "@storybook/addon-svelte-csf": "5.0.10",
57
57
  "@storybook/blocks": "^8.6.14",
58
- "@storybook/svelte": "^9.1.8",
59
- "@storybook/sveltekit": "^9.1.8",
58
+ "@storybook/svelte": "^9.1.10",
59
+ "@storybook/sveltekit": "^9.1.10",
60
60
  "@storybook/test": "^8.6.14",
61
- "@sveltejs/adapter-static": "^3.0.9",
62
- "@sveltejs/kit": "^2.43.5",
61
+ "@sveltejs/adapter-static": "^3.0.10",
62
+ "@sveltejs/kit": "^2.44.0",
63
63
  "@sveltejs/package": "^2.5.4",
64
64
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
65
- "@tailwindcss/postcss": "^4.1.13",
65
+ "@tailwindcss/postcss": "^4.1.14",
66
66
  "@testing-library/svelte": "^5.2.8",
67
67
  "@vitest/browser": "^3.2.4",
68
68
  "changeset": "^0.2.6",
69
- "eslint": "^9.36.0",
69
+ "eslint": "^9.37.0",
70
70
  "eslint-config-prettier": "^10.1.8",
71
71
  "eslint-plugin-svelte": "^3.12.4",
72
72
  "globals": "^16.4.0",
@@ -75,14 +75,14 @@
75
75
  "prettier": "^3.6.2",
76
76
  "prettier-plugin-svelte": "^3.4.0",
77
77
  "prettier-plugin-tailwindcss": "^0.6.14",
78
- "publint": "^0.3.13",
79
- "storybook": "^9.1.8",
80
- "svelte": "^5.39.6",
78
+ "publint": "^0.3.14",
79
+ "storybook": "^9.1.10",
80
+ "svelte": "^5.39.9",
81
81
  "svelte-check": "^4.3.2",
82
- "tailwindcss": "^4.1.13",
83
- "typescript": "^5.9.2",
82
+ "tailwindcss": "^4.1.14",
83
+ "typescript": "^5.9.3",
84
84
  "typescript-eslint": "^8.45.0",
85
- "vite": "^7.1.7",
85
+ "vite": "^7.1.9",
86
86
  "vitest": "^3.2.4"
87
87
  },
88
88
  "dependencies": {