@r2digisolutions/ui 0.24.1 → 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,7 +1,5 @@
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
 
6
4
  type Props = {
7
5
  items?: TContextMenuEntry[];
@@ -11,8 +9,6 @@
11
9
  title?: string;
12
10
  searchable?: boolean;
13
11
  context?: any;
14
- // por si algún día quieres desactivar el portal:
15
- portalTarget?: HTMLElement | null;
16
12
  };
17
13
  let {
18
14
  items = [],
@@ -21,19 +17,22 @@
21
17
  open = $bindable(false),
22
18
  title = '',
23
19
  searchable = true,
24
- context = null,
25
- portalTarget = null
20
+ context = null
26
21
  }: Props = $props();
27
22
 
28
23
  let stack = $state<{ label: string; items: TContextMenuEntry[] }[]>([]);
29
24
  let q = $state('');
30
-
31
25
  const current = $derived(stack.length ? stack[stack.length - 1] : { label: title, items });
32
26
 
27
+ let menuEl: HTMLDivElement | null = $state(null);
28
+
33
29
  function close() {
30
+ if (menuEl) menuEl.hidePopover();
34
31
  open = false;
35
32
  stack = [];
36
33
  q = '';
34
+ x = 0;
35
+ y = 0;
37
36
  }
38
37
 
39
38
  function hasChildren(it: TContextMenuEntry) {
@@ -65,7 +64,6 @@
65
64
  const list = current.items ?? [];
66
65
  const query = q.trim().toLowerCase();
67
66
  let arr = query ? list.filter((it) => matches(it, query)) : list.slice();
68
-
69
67
  const out: TContextMenuEntry[] = [];
70
68
  let prevDiv = false;
71
69
  for (const it of arr) {
@@ -98,58 +96,54 @@
98
96
  }
99
97
  }
100
98
 
99
+ // Manage popover open/close
101
100
  $effect(() => {
102
- if (!open) return;
103
- const handler = (e: KeyboardEvent) => onKey(e);
104
- document.addEventListener('keydown', handler);
105
- return () => document.removeEventListener('keydown', handler);
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
+ }
106
109
  });
107
110
 
108
- // --- CLAMP dinámico al abrir o al cambiar x/y ---
109
- let menuEl: HTMLDivElement | null = $state(null);
110
-
111
+ // Clamp position to viewport
111
112
  $effect(() => {
112
113
  if (!open || !menuEl) return;
113
- // siguiente frame para medir dimensiones reales
114
114
  requestAnimationFrame(() => {
115
115
  if (!menuEl) return;
116
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
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);
119
124
  if (nx !== x || ny !== y) {
120
125
  x = nx;
121
126
  y = ny;
127
+ menuEl.style.setProperty('--popover-x', `${nx}px`);
128
+ menuEl.style.setProperty('--popover-y', `${ny}px`);
122
129
  }
123
130
  });
124
131
  });
125
132
  </script>
126
133
 
127
134
  {#if open}
128
- <!-- BACKDROP: va al body por el portal también -->
129
- <div
130
- use:portal={portalTarget}
131
- role="dialog"
132
- class="fixed inset-0 z-[2147483646]"
133
- onclick={() => close()}
134
- oncontextmenu={(e) => e.preventDefault()}
135
- aria-modal="true"
136
- tabindex="0"
137
- style="pointer-events:auto"
138
- />
139
-
140
- <!-- MENU: fijado al viewport y portaleado al body -->
141
135
  <div
142
136
  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"
145
- 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);"
146
140
  oncontextmenu={(e) => e.preventDefault()}
141
+ onkeydown={onKey}
147
142
  >
148
143
  <div class="flex items-center gap-1 px-1 py-1">
149
144
  {#if stack.length > 0}
150
145
  <button
151
146
  class="rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-800"
152
- role="dialog"
153
147
  aria-label="Atrás"
154
148
  onclick={back}
155
149
  >
@@ -190,16 +184,15 @@
190
184
  {:else}
191
185
  <button
192
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"
193
- role="dialog"
194
187
  disabled={it.disabled}
195
188
  onclick={() => clickItem(it)}
196
189
  >
197
190
  <span class="truncate">{it.label}</span>
198
191
  <span class="flex items-center gap-2">
199
192
  {#if it.shortcut}
200
- <kbd class="rounded bg-gray-100 px-1 text-[10px] dark:bg-gray-800"
201
- >{it.shortcut}</kbd
202
- >
193
+ <kbd class="rounded bg-gray-100 px-1 text-[10px] dark:bg-gray-800">
194
+ {it.shortcut}
195
+ </kbd>
203
196
  {/if}
204
197
  {#if hasChildren(it)}
205
198
  <svg
@@ -223,3 +216,12 @@
223
216
  </div>
224
217
  </div>
225
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.1",
3
+ "version": "0.24.3",
4
4
  "private": false,
5
5
  "packageManager": "bun@1.2.23",
6
6
  "publishConfig": {