@r2digisolutions/ui 0.24.0 → 0.24.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,7 @@
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';
3
5
 
4
6
  type Props = {
5
7
  items?: TContextMenuEntry[];
@@ -9,6 +11,8 @@
9
11
  title?: string;
10
12
  searchable?: boolean;
11
13
  context?: any;
14
+ // por si algún día quieres desactivar el portal:
15
+ portalTarget?: HTMLElement | null;
12
16
  };
13
17
  let {
14
18
  items = [],
@@ -17,7 +21,8 @@
17
21
  open = $bindable(false),
18
22
  title = '',
19
23
  searchable = true,
20
- context = null
24
+ context = null,
25
+ portalTarget = null
21
26
  }: Props = $props();
22
27
 
23
28
  let stack = $state<{ label: string; items: TContextMenuEntry[] }[]>([]);
@@ -61,7 +66,6 @@
61
66
  const query = q.trim().toLowerCase();
62
67
  let arr = query ? list.filter((it) => matches(it, query)) : list.slice();
63
68
 
64
- // limpiar divisores (sin duplicados, ni al principio/fin)
65
69
  const out: TContextMenuEntry[] = [];
66
70
  let prevDiv = false;
67
71
  for (const it of arr) {
@@ -100,20 +104,44 @@
100
104
  document.addEventListener('keydown', handler);
101
105
  return () => document.removeEventListener('keydown', handler);
102
106
  });
107
+
108
+ // --- CLAMP dinámico al abrir o al cambiar x/y ---
109
+ let menuEl: HTMLDivElement | null = $state(null);
110
+
111
+ $effect(() => {
112
+ if (!open || !menuEl) return;
113
+ // siguiente frame para medir dimensiones reales
114
+ requestAnimationFrame(() => {
115
+ if (!menuEl) return;
116
+ const rect = menuEl.getBoundingClientRect();
117
+ const { x: nx, y: ny } = clampToViewport(x, y, rect.width, rect.height, 8);
118
+ // solo si cambian, re-ubica
119
+ if (nx !== x || ny !== y) {
120
+ x = nx;
121
+ y = ny;
122
+ }
123
+ });
124
+ });
103
125
  </script>
104
126
 
105
127
  {#if open}
128
+ <!-- BACKDROP: va al body por el portal también -->
106
129
  <div
130
+ use:portal={portalTarget}
107
131
  role="dialog"
108
- class="fixed inset-0 z-40"
132
+ class="fixed inset-0 z-[2147483646]"
109
133
  onclick={() => close()}
110
134
  oncontextmenu={(e) => e.preventDefault()}
111
135
  aria-modal="true"
112
136
  tabindex="0"
137
+ style="pointer-events:auto"
113
138
  />
114
139
 
140
+ <!-- MENU: fijado al viewport y portaleado al body -->
115
141
  <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"
142
+ bind:this={menuEl}
143
+ use:portal={portalTarget}
144
+ class="fixed z-[2147483647] w-72 rounded-2xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900"
117
145
  style={`left:${x}px; top:${y}px`}
118
146
  oncontextmenu={(e) => e.preventDefault()}
119
147
  >
@@ -133,8 +161,10 @@
133
161
  stroke="currentColor"
134
162
  stroke-width="2"
135
163
  stroke-linecap="round"
136
- stroke-linejoin="round"><polyline points="15 18 9 12 15 6" /></svg
164
+ stroke-linejoin="round"
137
165
  >
166
+ <polyline points="15 18 9 12 15 6" />
167
+ </svg>
138
168
  </button>
139
169
  {/if}
140
170
  <div class="min-w-0 flex-1 truncate px-1 text-xs font-medium opacity-70">
@@ -181,8 +211,10 @@
181
211
  stroke="currentColor"
182
212
  stroke-width="2"
183
213
  stroke-linecap="round"
184
- stroke-linejoin="round"><polyline points="9 18 15 12 9 6" /></svg
214
+ stroke-linejoin="round"
185
215
  >
216
+ <polyline points="9 18 15 12 9 6" />
217
+ </svg>
186
218
  {/if}
187
219
  </span>
188
220
  </button>
@@ -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.1",
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": {