@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 +2 -2
- package/src/lib/components/Badge.svelte +1 -1
- package/src/lib/components/Button.svelte +1 -1
- package/src/lib/components/ContextMenu.svelte +24 -4
- package/src/lib/components/DatePicker.svelte +1 -1
- package/src/lib/components/DateRangePicker.svelte +1 -1
- package/src/lib/components/NotificationPanel.svelte +1 -1
- package/src/lib/components/Pagination.svelte +1 -1
- package/src/lib/components/Sidebar.svelte +1 -1
- package/src/lib/components/SkipLink.svelte +1 -1
- package/src/lib/components/SplitButton.svelte +1 -1
- package/src/lib/components/Stepper.svelte +1 -1
- package/src/lib/components/menu-position.ts +38 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@w5-ui/svelte",
|
|
3
|
-
"version": "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.
|
|
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-
|
|
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-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
+
}
|