@w5-ui/svelte 0.8.0 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@w5-ui/svelte",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Svelte 5 components for the Dash UI design system - mirrors the @w5-ui/react API 1:1. Part of Webber's design systems.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -26,6 +26,7 @@
26
26
  <script lang="ts">
27
27
  import { onDestroy, tick } from 'svelte';
28
28
  import Pill from './Pill.svelte';
29
+ import { computeMenuPosition } from './menu-position';
29
30
 
30
31
  const PILL_SLOT = '{pill}';
31
32
 
@@ -71,10 +72,29 @@
71
72
  }, []),
72
73
  );
73
74
 
74
- const menuW = 168;
75
- let menuH = $derived(items.length * 30 + 8);
76
- let cx = $derived(Math.min(x, (typeof window !== 'undefined' ? window.innerWidth : 1200) - menuW - 8));
77
- let cy = $derived(Math.min(y, (typeof window !== 'undefined' ? window.innerHeight : 800) - menuH - 8));
75
+ /** Space kept between the menu and every viewport edge. */
76
+ const MENU_MARGIN = 8;
77
+ // Placed from the menu's MEASURED size once it is in the DOM, by the
78
+ // effect below. Effects run before the browser paints, so the menu is
79
+ // never seen at this starting point.
80
+ let cx = $state(0);
81
+ let cy = $state(0);
82
+ $effect(() => {
83
+ void items;
84
+ if (!open || menuEl === undefined) return;
85
+ const { width, height } = menuEl.getBoundingClientRect();
86
+ const placed = computeMenuPosition({
87
+ x,
88
+ y,
89
+ menuWidth: width,
90
+ menuHeight: height,
91
+ viewportWidth: window.innerWidth,
92
+ viewportHeight: window.innerHeight,
93
+ margin: MENU_MARGIN,
94
+ });
95
+ cx = placed.left;
96
+ cy = placed.top;
97
+ });
78
98
 
79
99
  function activate(id: string) {
80
100
  onaction?.(id);
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Where a context menu opens: at the pointer, the way OS menus do.
3
+ *
4
+ * Takes the menu's MEASURED size, never a guess: menus hold labels of
5
+ * any length, and a guessed width let a long one run off the edge.
6
+ * - It opens at the pointer when it fits.
7
+ * - It flips to the pointer's left (or above it) when it would cross
8
+ * the right edge (or the bottom).
9
+ * - When neither side has room, it is clamped inside the margins, and
10
+ * a menu bigger than the viewport is pinned to the margin.
11
+ *
12
+ * Pure. The React twin is `packages/react/src/components/menu-position.ts`.
13
+ */
14
+ export interface MenuPositionInput {
15
+ /** The pointer, in viewport coordinates. */
16
+ x: number
17
+ y: number
18
+ menuWidth: number
19
+ menuHeight: number
20
+ viewportWidth: number
21
+ viewportHeight: number
22
+ /** Space kept between the menu and every viewport edge. */
23
+ margin: number
24
+ }
25
+
26
+ function along(pointer: number, size: number, viewport: number, margin: number): number {
27
+ const far = viewport - margin
28
+ if (pointer + size <= far) return Math.max(margin, pointer)
29
+ if (pointer - size >= margin) return pointer - size
30
+ return Math.max(margin, Math.min(pointer, far - size))
31
+ }
32
+
33
+ export function computeMenuPosition(input: MenuPositionInput): { left: number; top: number } {
34
+ return {
35
+ left: along(input.x, input.menuWidth, input.viewportWidth, input.margin),
36
+ top: along(input.y, input.menuHeight, input.viewportHeight, input.margin),
37
+ }
38
+ }