@w5-ui/svelte 0.7.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.7.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",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@w5-ui/assets": "^0.2.1",
48
- "@w5-ui/tokens": "^0.7.0"
48
+ "@w5-ui/tokens": "^0.8.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "svelte": "^5.56.4",
@@ -33,7 +33,7 @@
33
33
  // Status-warning is yellow in both motifs; near-black text stays
34
34
  // legible against it. `text-neutral-10` resolves to `#0a0a0b`.
35
35
  warn: 'bg-status-warning text-neutral-10',
36
- info: 'bg-brand-05 text-white',
36
+ info: 'bg-primary-fill text-primary-fill-fg',
37
37
  success: 'bg-status-success text-white',
38
38
  neutral: 'bg-status-neutral text-white',
39
39
  };
@@ -47,7 +47,7 @@
47
47
  // ghost: transparent surface with motif-aware text/border + hover.
48
48
  // danger: semantic status-danger text + faded border.
49
49
  const VARIANT: Record<NonNullable<Props['variant']>, string> = {
50
- primary: 'bg-brand-05 text-white hover:bg-brand-06 border-transparent',
50
+ primary: 'bg-primary-fill text-primary-fill-fg hover:bg-primary-fill-hover active:bg-primary-fill-press border-transparent',
51
51
  ghost:
52
52
  'bg-transparent text-text-2 border-border-2 hover:bg-row-hover hover:text-text-1',
53
53
  danger: 'bg-transparent text-status-danger border-status-danger/30',
@@ -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);
@@ -135,7 +135,7 @@
135
135
  if (outside) cls += ' text-text-4';
136
136
  else cls += ' text-text-2';
137
137
  if (isToday && !isSelected) cls += ' ring-1 ring-inset ring-brand-05';
138
- if (isSelected) cls += ' bg-brand-05 text-white hover:bg-brand-06';
138
+ if (isSelected) cls += ' bg-primary-fill text-primary-fill-fg hover:bg-primary-fill-hover';
139
139
  return cls;
140
140
  }
141
141
  </script>
@@ -137,7 +137,7 @@
137
137
  if (outside) cls += ' text-text-4';
138
138
  else cls += ' text-text-2';
139
139
  if (isTodayDay && !start && !end) cls += ' ring-1 ring-inset ring-brand-05';
140
- if (start || end) cls += ' bg-brand-05 text-white hover:bg-brand-06';
140
+ if (start || end) cls += ' bg-primary-fill text-primary-fill-fg hover:bg-primary-fill-hover';
141
141
  else if (inRange) cls += ' bg-brand-05/20 text-text-1';
142
142
  return cls;
143
143
  }
@@ -114,7 +114,7 @@
114
114
  <h2 id={titleId} class="m-0 flex items-center gap-2 text-15 font-semibold text-text-1">
115
115
  Notifications
116
116
  {#if unread > 0}
117
- <span class="inline-flex h-5 min-w-[20px] items-center justify-center rounded-full bg-brand-05 px-1.5 text-11 font-bold text-white tabular-nums" aria-label={`${unread} unread`}>{unread}</span>
117
+ <span class="inline-flex h-5 min-w-[20px] items-center justify-center rounded-full bg-primary-fill px-1.5 text-11 font-bold text-primary-fill-fg tabular-nums" aria-label={`${unread} unread`}>{unread}</span>
118
118
  {/if}
119
119
  </h2>
120
120
  <div class="flex items-center gap-1.5">
@@ -29,7 +29,7 @@
29
29
 
30
30
  const BTN_BASE =
31
31
  'inline-flex h-[30px] min-w-[30px] cursor-pointer items-center justify-center rounded-md border border-transparent bg-transparent px-1.5 text-13 font-medium leading-none text-text-3 transition-[background-color,color,border-color] duration-100 hover:bg-row-hover hover:text-text-1 hover:border-border-2 disabled:cursor-not-allowed disabled:opacity-35 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05';
32
- const BTN_ACTIVE = 'bg-brand-05 text-white border-brand-05 hover:bg-brand-06 hover:border-brand-06';
32
+ const BTN_ACTIVE = 'bg-primary-fill text-primary-fill-fg border-primary-fill hover:bg-primary-fill-hover hover:border-primary-fill-hover';
33
33
  </script>
34
34
 
35
35
  {#if totalPages > 1}
@@ -60,7 +60,7 @@
60
60
  {/if}
61
61
  {#if it.pill !== undefined}
62
62
  <span
63
- class="ml-auto rounded-full bg-brand-05 px-1.5 py-px text-[10px] font-semibold text-white"
63
+ class="ml-auto rounded-full bg-primary-fill px-1.5 py-px text-[10px] font-semibold text-primary-fill-fg"
64
64
  >{it.pill}<span class="sr-only"> alert{it.pill !== 1 ? 's' : ''}</span></span>
65
65
  {/if}
66
66
  </button>
@@ -9,7 +9,7 @@
9
9
  </script>
10
10
 
11
11
  <a
12
- class="absolute left-0 top-0 z-[100] -translate-y-full rounded-br-md bg-brand-05 px-3 py-2 text-13 font-semibold text-white no-underline focus:translate-y-0 focus:outline-2 focus:-outline-offset-4 focus:outline-white
12
+ class="absolute left-0 top-0 z-[100] -translate-y-full rounded-br-md bg-primary-fill px-3 py-2 text-13 font-semibold text-primary-fill-fg no-underline focus:translate-y-0 focus:outline-2 focus:-outline-offset-4 focus:outline-white
13
13
  {className}"
14
14
  {href}
15
15
  >
@@ -77,7 +77,7 @@
77
77
 
78
78
  // Pre-composed variant chrome so Tailwind's static scanner sees the literals.
79
79
  const VARIANT: Record<NonNullable<Props['variant']>, string> = {
80
- primary: 'bg-brand-05 text-white hover:bg-brand-06 border-brand-05',
80
+ primary: 'bg-primary-fill text-primary-fill-fg hover:bg-primary-fill-hover active:bg-primary-fill-press border-primary-fill',
81
81
  ghost: 'bg-transparent text-text-2 border-border-2 hover:bg-row-hover hover:text-text-1',
82
82
  danger: 'bg-transparent text-status-danger border-status-danger/30',
83
83
  };
@@ -20,7 +20,7 @@
20
20
  // - label: colour + weight
21
21
  // - connector line (::before via Tailwind arbitrary): bg colour
22
22
  const NUM: Record<'active' | 'done' | 'upcoming', string> = {
23
- active: 'border-brand-05 bg-brand-05 text-white',
23
+ active: 'border-primary-fill bg-primary-fill text-primary-fill-fg',
24
24
  done: 'border-brand-05 bg-brand-05/[0.18] text-status-info',
25
25
  upcoming: 'border-bg-3 bg-bg-1 text-text-4',
26
26
  };
@@ -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
+ }