@ruckc/truss-ui 0.0.0-stage → 1.0.0
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/LICENSE +24 -0
- package/README.md +91 -2
- package/dist/color.d.ts +21 -0
- package/dist/color.js +32 -0
- package/dist/components/Accordion.svelte +70 -0
- package/dist/components/Accordion.svelte.d.ts +15 -0
- package/dist/components/Alert.svelte +46 -0
- package/dist/components/Alert.svelte.d.ts +15 -0
- package/dist/components/Avatar.svelte +32 -0
- package/dist/components/Avatar.svelte.d.ts +9 -0
- package/dist/components/AvatarGroup.svelte +20 -0
- package/dist/components/AvatarGroup.svelte.d.ts +11 -0
- package/dist/components/Badge.svelte +27 -0
- package/dist/components/Badge.svelte.d.ts +9 -0
- package/dist/components/Breadcrumbs.svelte +38 -0
- package/dist/components/Breadcrumbs.svelte.d.ts +12 -0
- package/dist/components/Button.svelte +119 -0
- package/dist/components/Button.svelte.d.ts +16 -0
- package/dist/components/ButtonGroup.svelte +23 -0
- package/dist/components/ButtonGroup.svelte.d.ts +9 -0
- package/dist/components/Calendar.svelte +166 -0
- package/dist/components/Calendar.svelte.d.ts +19 -0
- package/dist/components/Callout.svelte +50 -0
- package/dist/components/Callout.svelte.d.ts +14 -0
- package/dist/components/Card.svelte +50 -0
- package/dist/components/Card.svelte.d.ts +15 -0
- package/dist/components/Checkbox.svelte +37 -0
- package/dist/components/Checkbox.svelte.d.ts +16 -0
- package/dist/components/ColorPicker.svelte +164 -0
- package/dist/components/ColorPicker.svelte.d.ts +14 -0
- package/dist/components/Combobox.svelte +131 -0
- package/dist/components/Combobox.svelte.d.ts +19 -0
- package/dist/components/ContextMenu.svelte +51 -0
- package/dist/components/ContextMenu.svelte.d.ts +10 -0
- package/dist/components/DataTable.svelte +657 -0
- package/dist/components/DataTable.svelte.d.ts +76 -0
- package/dist/components/DatePicker.svelte +70 -0
- package/dist/components/DatePicker.svelte.d.ts +15 -0
- package/dist/components/DateRangePicker.svelte +92 -0
- package/dist/components/DateRangePicker.svelte.d.ts +15 -0
- package/dist/components/DateTimePicker.svelte +62 -0
- package/dist/components/DateTimePicker.svelte.d.ts +19 -0
- package/dist/components/DateTimeRangePicker.svelte +167 -0
- package/dist/components/DateTimeRangePicker.svelte.d.ts +22 -0
- package/dist/components/Dialog.svelte +70 -0
- package/dist/components/Dialog.svelte.d.ts +14 -0
- package/dist/components/Divider.svelte +16 -0
- package/dist/components/Divider.svelte.d.ts +8 -0
- package/dist/components/Drawer.svelte +64 -0
- package/dist/components/Drawer.svelte.d.ts +13 -0
- package/dist/components/DurationInput.svelte +112 -0
- package/dist/components/DurationInput.svelte.d.ts +19 -0
- package/dist/components/EditableText.svelte +73 -0
- package/dist/components/EditableText.svelte.d.ts +14 -0
- package/dist/components/EmptyState.svelte +30 -0
- package/dist/components/EmptyState.svelte.d.ts +12 -0
- package/dist/components/FileUpload.svelte +143 -0
- package/dist/components/FileUpload.svelte.d.ts +18 -0
- package/dist/components/FormGroup.svelte +27 -0
- package/dist/components/FormGroup.svelte.d.ts +13 -0
- package/dist/components/Input.svelte +62 -0
- package/dist/components/Input.svelte.d.ts +15 -0
- package/dist/components/InputAddon.svelte +21 -0
- package/dist/components/InputAddon.svelte.d.ts +9 -0
- package/dist/components/InputGroup.svelte +70 -0
- package/dist/components/InputGroup.svelte.d.ts +16 -0
- package/dist/components/Menu.svelte +95 -0
- package/dist/components/Menu.svelte.d.ts +15 -0
- package/dist/components/MenuButton.svelte +45 -0
- package/dist/components/MenuButton.svelte.d.ts +16 -0
- package/dist/components/MultiSelect.svelte +129 -0
- package/dist/components/MultiSelect.svelte.d.ts +18 -0
- package/dist/components/Navbar.svelte +17 -0
- package/dist/components/Navbar.svelte.d.ts +10 -0
- package/dist/components/NumberInput.svelte +66 -0
- package/dist/components/NumberInput.svelte.d.ts +16 -0
- package/dist/components/OptionList.svelte +74 -0
- package/dist/components/OptionList.svelte.d.ts +15 -0
- package/dist/components/Popover.svelte +87 -0
- package/dist/components/Popover.svelte.d.ts +14 -0
- package/dist/components/ProgressBar.svelte +35 -0
- package/dist/components/ProgressBar.svelte.d.ts +10 -0
- package/dist/components/RadioGroup.svelte +39 -0
- package/dist/components/RadioGroup.svelte.d.ts +14 -0
- package/dist/components/Section.svelte +56 -0
- package/dist/components/Section.svelte.d.ts +15 -0
- package/dist/components/SegmentedControl.svelte +56 -0
- package/dist/components/SegmentedControl.svelte.d.ts +17 -0
- package/dist/components/Select.svelte +204 -0
- package/dist/components/Select.svelte.d.ts +41 -0
- package/dist/components/Shell.svelte +143 -0
- package/dist/components/Shell.svelte.d.ts +36 -0
- package/dist/components/Sidebar.svelte +84 -0
- package/dist/components/Sidebar.svelte.d.ts +24 -0
- package/dist/components/Skeleton.svelte +25 -0
- package/dist/components/Skeleton.svelte.d.ts +9 -0
- package/dist/components/Slider.svelte +121 -0
- package/dist/components/Slider.svelte.d.ts +16 -0
- package/dist/components/Spinner.svelte +30 -0
- package/dist/components/Spinner.svelte.d.ts +9 -0
- package/dist/components/SplitButton.svelte +113 -0
- package/dist/components/SplitButton.svelte.d.ts +14 -0
- package/dist/components/SplitPane.svelte +106 -0
- package/dist/components/SplitPane.svelte.d.ts +16 -0
- package/dist/components/Stepper.svelte +73 -0
- package/dist/components/Stepper.svelte.d.ts +26 -0
- package/dist/components/Switch.svelte +21 -0
- package/dist/components/Switch.svelte.d.ts +10 -0
- package/dist/components/Tabs.svelte +74 -0
- package/dist/components/Tabs.svelte.d.ts +18 -0
- package/dist/components/Tag.svelte +49 -0
- package/dist/components/Tag.svelte.d.ts +16 -0
- package/dist/components/TagInput.svelte +72 -0
- package/dist/components/TagInput.svelte.d.ts +17 -0
- package/dist/components/Textarea.svelte +37 -0
- package/dist/components/Textarea.svelte.d.ts +11 -0
- package/dist/components/TimeList.svelte +76 -0
- package/dist/components/TimeList.svelte.d.ts +10 -0
- package/dist/components/TimePicker.svelte +79 -0
- package/dist/components/TimePicker.svelte.d.ts +15 -0
- package/dist/components/TimeRangePicker.svelte +44 -0
- package/dist/components/TimeRangePicker.svelte.d.ts +17 -0
- package/dist/components/Timeline.svelte +38 -0
- package/dist/components/Timeline.svelte.d.ts +17 -0
- package/dist/components/Toaster.svelte +35 -0
- package/dist/components/Toaster.svelte.d.ts +18 -0
- package/dist/components/Tooltip.svelte +66 -0
- package/dist/components/Tooltip.svelte.d.ts +11 -0
- package/dist/components/Tree.svelte +147 -0
- package/dist/components/Tree.svelte.d.ts +16 -0
- package/dist/date.d.ts +19 -0
- package/dist/date.js +43 -0
- package/dist/duration.d.ts +11 -0
- package/dist/duration.js +44 -0
- package/dist/fonts/LICENSE-Inter.txt +93 -0
- package/dist/fonts/LICENSE-JetBrainsMono.txt +93 -0
- package/dist/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/dist/icons/Icon.svelte +30 -0
- package/dist/icons/Icon.svelte.d.ts +9 -0
- package/dist/icons/paths.d.ts +39 -0
- package/dist/icons/paths.js +43 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +65 -0
- package/dist/styles/base.css +71 -0
- package/dist/styles/fonts.css +21 -0
- package/dist/styles/index.css +3 -0
- package/dist/styles/tokens.css +143 -0
- package/dist/time.d.ts +7 -0
- package/dist/time.js +30 -0
- package/dist/toast.svelte.d.ts +22 -0
- package/dist/toast.svelte.js +19 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.js +1 -0
- package/package.json +76 -5
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import Drawer from './Drawer.svelte';
|
|
4
|
+
|
|
5
|
+
export type ShellApi = {
|
|
6
|
+
/** Narrow viewport: the sidebar lives in a drawer. */
|
|
7
|
+
narrow: boolean;
|
|
8
|
+
toggleSidebar: () => void;
|
|
9
|
+
theme: 'system' | 'light' | 'dark';
|
|
10
|
+
toggleTheme: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
header,
|
|
15
|
+
sidebar,
|
|
16
|
+
aside,
|
|
17
|
+
footer,
|
|
18
|
+
children,
|
|
19
|
+
sidebarCollapsed = $bindable(false),
|
|
20
|
+
sidebarOpen = $bindable(false),
|
|
21
|
+
asideOpen = $bindable(true),
|
|
22
|
+
asideWidth = $bindable(320),
|
|
23
|
+
theme = $bindable('system'),
|
|
24
|
+
breakpoint = 840,
|
|
25
|
+
navLabel = 'Navigation',
|
|
26
|
+
asideLabel = 'Details'
|
|
27
|
+
}: {
|
|
28
|
+
/** Top bar; usually a `<Navbar>`. */
|
|
29
|
+
header?: Snippet<[api: ShellApi]>;
|
|
30
|
+
/** Left navigation; usually a `<Sidebar bind:collapsed>`. On narrow screens it renders in a drawer. */
|
|
31
|
+
sidebar?: Snippet<[api: ShellApi & { collapsed: boolean }]>;
|
|
32
|
+
/** Right inspector panel. Resizable by its left edge. */
|
|
33
|
+
aside?: Snippet<[api: ShellApi]>;
|
|
34
|
+
/** Status bar. */
|
|
35
|
+
footer?: Snippet<[api: ShellApi]>;
|
|
36
|
+
children: Snippet;
|
|
37
|
+
sidebarCollapsed?: boolean;
|
|
38
|
+
/** Drawer state on narrow screens. */
|
|
39
|
+
sidebarOpen?: boolean;
|
|
40
|
+
asideOpen?: boolean;
|
|
41
|
+
/** px */
|
|
42
|
+
asideWidth?: number;
|
|
43
|
+
/** 'system' follows the OS; otherwise forces data-theme on <html>. */
|
|
44
|
+
theme?: 'system' | 'light' | 'dark';
|
|
45
|
+
/** px below which the sidebar becomes a drawer. */
|
|
46
|
+
breakpoint?: number;
|
|
47
|
+
navLabel?: string;
|
|
48
|
+
asideLabel?: string;
|
|
49
|
+
} = $props();
|
|
50
|
+
|
|
51
|
+
let narrow = $state(false);
|
|
52
|
+
$effect(() => {
|
|
53
|
+
const mq = matchMedia(`(max-width: ${breakpoint}px)`);
|
|
54
|
+
const sync = () => { narrow = mq.matches; if (narrow) asideOpen = false; else sidebarOpen = false; };
|
|
55
|
+
sync();
|
|
56
|
+
mq.addEventListener('change', sync);
|
|
57
|
+
return () => mq.removeEventListener('change', sync);
|
|
58
|
+
});
|
|
59
|
+
$effect(() => {
|
|
60
|
+
const el = document.documentElement;
|
|
61
|
+
if (theme === 'system') delete el.dataset.theme;
|
|
62
|
+
else el.dataset.theme = theme;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const toggleSidebar = () => (narrow ? (sidebarOpen = !sidebarOpen) : (sidebarCollapsed = !sidebarCollapsed));
|
|
66
|
+
const toggleTheme = () => {
|
|
67
|
+
const dark = theme === 'dark' || (theme === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
|
|
68
|
+
theme = dark ? 'light' : 'dark';
|
|
69
|
+
};
|
|
70
|
+
const api = $derived<ShellApi>({ narrow, toggleSidebar, theme, toggleTheme });
|
|
71
|
+
|
|
72
|
+
let resizing = $state(false);
|
|
73
|
+
function startResize(e: PointerEvent) {
|
|
74
|
+
const el = e.currentTarget as HTMLElement;
|
|
75
|
+
el.setPointerCapture(e.pointerId);
|
|
76
|
+
resizing = true;
|
|
77
|
+
const x0 = e.clientX, w0 = asideWidth;
|
|
78
|
+
const move = (ev: PointerEvent) => (asideWidth = Math.round(Math.min(720, Math.max(240, w0 - (ev.clientX - x0)))));
|
|
79
|
+
const up = () => { resizing = false; el.removeEventListener('pointermove', move); el.removeEventListener('pointerup', up); };
|
|
80
|
+
el.addEventListener('pointermove', move);
|
|
81
|
+
el.addEventListener('pointerup', up);
|
|
82
|
+
}
|
|
83
|
+
function resizeKey(e: KeyboardEvent) {
|
|
84
|
+
const d = e.key === 'ArrowLeft' ? 16 : e.key === 'ArrowRight' ? -16 : 0;
|
|
85
|
+
if (!d) return;
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
asideWidth = Math.min(720, Math.max(240, asideWidth + d));
|
|
88
|
+
}
|
|
89
|
+
const hasAside = $derived(!!aside && asideOpen && !narrow);
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<div class="shell" data-narrow={narrow} data-resizing={resizing} style:--aside-w="{asideWidth}px">
|
|
93
|
+
<a class="skip" href="#tr-main">Skip to content</a>
|
|
94
|
+
|
|
95
|
+
{#if header}<div class="hd">{@render header(api)}</div>{/if}
|
|
96
|
+
|
|
97
|
+
{#if sidebar && !narrow}
|
|
98
|
+
<div class="sb" role="presentation">{@render sidebar({ ...api, collapsed: sidebarCollapsed })}</div>
|
|
99
|
+
{/if}
|
|
100
|
+
|
|
101
|
+
<main id="tr-main" tabindex="-1">{@render children()}</main>
|
|
102
|
+
|
|
103
|
+
{#if aside && asideOpen}
|
|
104
|
+
{#if narrow}
|
|
105
|
+
<Drawer open={true} title={asideLabel} position="bottom" onclose={() => (asideOpen = false)}>{@render aside(api)}</Drawer>
|
|
106
|
+
{:else}
|
|
107
|
+
<aside class="as" aria-label={asideLabel}>
|
|
108
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
109
|
+
<div class="grip" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Resize {asideLabel.toLowerCase()} panel" aria-valuenow={asideWidth} aria-valuemin={240} aria-valuemax={720} onpointerdown={startResize} onkeydown={resizeKey}></div>
|
|
110
|
+
<div class="as-body">{@render aside(api)}</div>
|
|
111
|
+
</aside>
|
|
112
|
+
{/if}
|
|
113
|
+
{/if}
|
|
114
|
+
|
|
115
|
+
{#if footer}<footer class="ft">{@render footer(api)}</footer>{/if}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{#if sidebar && narrow}
|
|
119
|
+
<Drawer bind:open={sidebarOpen} title={navLabel} position="left" size="sm">{@render sidebar({ ...api, collapsed: false })}</Drawer>
|
|
120
|
+
{/if}
|
|
121
|
+
|
|
122
|
+
<style>
|
|
123
|
+
.shell {
|
|
124
|
+
display: grid; height: 100vh; height: 100dvh; min-height: 0; background: var(--tr-surface); color: var(--tr-on-surface);
|
|
125
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
126
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
127
|
+
grid-template-areas: 'hd hd hd' 'sb main as' 'ft ft ft';
|
|
128
|
+
}
|
|
129
|
+
.shell[data-narrow='true'] { grid-template-columns: minmax(0, 1fr); grid-template-areas: 'hd' 'main' 'ft'; }
|
|
130
|
+
.hd { grid-area: hd; min-width: 0; }
|
|
131
|
+
.sb { grid-area: sb; min-height: 0; overflow: hidden; }
|
|
132
|
+
main { grid-area: main; min-width: 0; min-height: 0; overflow: auto; outline: none; }
|
|
133
|
+
.as { grid-area: as; position: relative; width: var(--aside-w); min-height: 0; background: var(--tr-surface-container-low); border-left: 1px solid var(--tr-outline-variant); }
|
|
134
|
+
.as-body { height: 100%; overflow: auto; padding: 12px; }
|
|
135
|
+
.ft { grid-area: ft; min-width: 0; display: flex; align-items: center; gap: 12px; min-height: 24px; padding: 0 12px; background: var(--tr-surface-container); border-top: 1px solid var(--tr-outline-variant); font-size: var(--tr-text-sm); color: var(--tr-on-surface-variant); }
|
|
136
|
+
.grip { position: absolute; left: -4px; top: 0; bottom: 0; width: 8px; cursor: col-resize; z-index: 5; touch-action: none; }
|
|
137
|
+
.grip::after { content: ''; position: absolute; left: 3px; top: 0; bottom: 0; width: 2px; background: transparent; transition: background-color var(--tr-dur) var(--tr-ease); }
|
|
138
|
+
.grip:hover::after, .grip:focus-visible::after, .shell[data-resizing='true'] .grip::after { background: var(--tr-primary); }
|
|
139
|
+
.grip:focus-visible { outline: none; }
|
|
140
|
+
.shell[data-resizing='true'] { user-select: none; }
|
|
141
|
+
.skip { position: absolute; z-index: 400; left: 8px; top: -48px; padding: 8px 14px; border-radius: var(--tr-radius-md); background: var(--tr-primary); color: var(--tr-on-primary); font-weight: 600; text-decoration: none; transition: top var(--tr-dur) var(--tr-ease); }
|
|
142
|
+
.skip:focus { top: 8px; outline: none; box-shadow: var(--tr-focus-ring); }
|
|
143
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type ShellApi = {
|
|
3
|
+
/** Narrow viewport: the sidebar lives in a drawer. */
|
|
4
|
+
narrow: boolean;
|
|
5
|
+
toggleSidebar: () => void;
|
|
6
|
+
theme: 'system' | 'light' | 'dark';
|
|
7
|
+
toggleTheme: () => void;
|
|
8
|
+
};
|
|
9
|
+
type $$ComponentProps = {
|
|
10
|
+
/** Top bar; usually a `<Navbar>`. */
|
|
11
|
+
header?: Snippet<[api: ShellApi]>;
|
|
12
|
+
/** Left navigation; usually a `<Sidebar bind:collapsed>`. On narrow screens it renders in a drawer. */
|
|
13
|
+
sidebar?: Snippet<[api: ShellApi & {
|
|
14
|
+
collapsed: boolean;
|
|
15
|
+
}]>;
|
|
16
|
+
/** Right inspector panel. Resizable by its left edge. */
|
|
17
|
+
aside?: Snippet<[api: ShellApi]>;
|
|
18
|
+
/** Status bar. */
|
|
19
|
+
footer?: Snippet<[api: ShellApi]>;
|
|
20
|
+
children: Snippet;
|
|
21
|
+
sidebarCollapsed?: boolean;
|
|
22
|
+
/** Drawer state on narrow screens. */
|
|
23
|
+
sidebarOpen?: boolean;
|
|
24
|
+
asideOpen?: boolean;
|
|
25
|
+
/** px */
|
|
26
|
+
asideWidth?: number;
|
|
27
|
+
/** 'system' follows the OS; otherwise forces data-theme on <html>. */
|
|
28
|
+
theme?: 'system' | 'light' | 'dark';
|
|
29
|
+
/** px below which the sidebar becomes a drawer. */
|
|
30
|
+
breakpoint?: number;
|
|
31
|
+
navLabel?: string;
|
|
32
|
+
asideLabel?: string;
|
|
33
|
+
};
|
|
34
|
+
declare const Shell: import("svelte").Component<$$ComponentProps, {}, "sidebarCollapsed" | "sidebarOpen" | "asideOpen" | "asideWidth" | "theme">;
|
|
35
|
+
type Shell = ReturnType<typeof Shell>;
|
|
36
|
+
export default Shell;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Icon from '../icons/Icon.svelte';
|
|
3
|
+
import Tooltip from './Tooltip.svelte';
|
|
4
|
+
import Badge from './Badge.svelte';
|
|
5
|
+
import type { IconName } from '../icons/paths.js';
|
|
6
|
+
|
|
7
|
+
type Entry =
|
|
8
|
+
| { heading: string }
|
|
9
|
+
| { id: string; label: string; icon?: IconName; href?: string; badge?: string | number; children?: { id: string; label: string; href?: string }[] };
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
items,
|
|
13
|
+
active = $bindable(),
|
|
14
|
+
collapsed = $bindable(false),
|
|
15
|
+
label = 'Primary'
|
|
16
|
+
}: { items: Entry[]; active?: string; collapsed?: boolean; label?: string } = $props();
|
|
17
|
+
|
|
18
|
+
let expanded = $state<string[]>([]);
|
|
19
|
+
const toggle = (id: string) => (expanded = expanded.includes(id) ? expanded.filter((x) => x !== id) : [...expanded, id]);
|
|
20
|
+
const holdsActive = (e: Entry) => 'children' in e && !!e.children?.some((c) => c.id === active);
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<nav class="sb" aria-label={label} data-collapsed={collapsed}>
|
|
24
|
+
<ul>
|
|
25
|
+
{#each items as e, i}
|
|
26
|
+
{#if 'heading' in e}
|
|
27
|
+
<li class="head" role="presentation">{#if !collapsed}{e.heading}{:else}<span class="rule"></span>{/if}</li>
|
|
28
|
+
{:else}
|
|
29
|
+
{@const open = expanded.includes(e.id) || holdsActive(e)}
|
|
30
|
+
<li>
|
|
31
|
+
{#snippet row()}
|
|
32
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
33
|
+
<svelte:element
|
|
34
|
+
this={e.href && !e.children ? 'a' : 'button'} class="item"
|
|
35
|
+
href={e.href && !e.children ? e.href : undefined} type={e.href && !e.children ? undefined : 'button'}
|
|
36
|
+
aria-current={active === e.id ? 'page' : undefined} aria-expanded={e.children ? open : undefined} aria-label={collapsed ? e.label : undefined}
|
|
37
|
+
onclick={() => (e.children ? toggle(e.id) : (active = e.id))}
|
|
38
|
+
>
|
|
39
|
+
{#if e.icon}<Icon name={e.icon} size={20} />{/if}
|
|
40
|
+
{#if !collapsed}
|
|
41
|
+
<span class="lbl">{e.label}</span>
|
|
42
|
+
{#if e.badge != null}<Badge dot={false} intent="primary">{String(e.badge)}</Badge>{/if}
|
|
43
|
+
{#if e.children}<Icon name={open ? 'chevron-down' : 'chevron-right'} />{/if}
|
|
44
|
+
{/if}
|
|
45
|
+
</svelte:element>
|
|
46
|
+
{/snippet}
|
|
47
|
+
{#if collapsed}<Tooltip text={e.label} placement="right">{@render row()}</Tooltip>{:else}{@render row()}{/if}
|
|
48
|
+
{#if e.children && open && !collapsed}
|
|
49
|
+
<ul class="sub">
|
|
50
|
+
{#each e.children as c}
|
|
51
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
52
|
+
<li><svelte:element this={c.href ? 'a' : 'button'} class="item child" href={c.href} type={c.href ? undefined : 'button'} aria-current={active === c.id ? 'page' : undefined} onclick={() => (active = c.id)}><span class="lbl">{c.label}</span></svelte:element></li>
|
|
53
|
+
{/each}
|
|
54
|
+
</ul>
|
|
55
|
+
{/if}
|
|
56
|
+
</li>
|
|
57
|
+
{/if}
|
|
58
|
+
{/each}
|
|
59
|
+
</ul>
|
|
60
|
+
<button type="button" class="collapse" aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'} onclick={() => (collapsed = !collapsed)}>
|
|
61
|
+
<Icon name={collapsed ? 'chevrons-right' : 'chevrons-left'} />{#if !collapsed}<span>Collapse</span>{/if}
|
|
62
|
+
</button>
|
|
63
|
+
</nav>
|
|
64
|
+
|
|
65
|
+
<style>
|
|
66
|
+
.sb { display: flex; flex-direction: column; width: 232px; height: 100%; padding: 8px; background: var(--tr-surface-container-low); border-right: 1px solid var(--tr-outline-variant); transition: width var(--tr-dur) var(--tr-ease); overflow: hidden; }
|
|
67
|
+
.sb[data-collapsed='true'] { width: 56px; }
|
|
68
|
+
ul { margin: 0; padding: 0; list-style: none; }
|
|
69
|
+
.sb > ul { flex: 1; overflow-y: auto; }
|
|
70
|
+
.head { padding: 12px 10px 4px; font-size: var(--tr-text-xs); font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--tr-on-surface-variant); }
|
|
71
|
+
.rule { display: block; height: 1px; background: var(--tr-outline-variant); margin: 4px 4px; }
|
|
72
|
+
.item { display: flex; align-items: center; gap: 10px; width: 100%; height: 36px; padding: 0 10px; border: 0; border-radius: var(--tr-radius-pill); background: none; color: var(--tr-on-surface-variant); font: 500 var(--tr-text-md) var(--tr-font-sans); text-decoration: none; text-align: left; cursor: pointer; }
|
|
73
|
+
.sb[data-collapsed='true'] .item { justify-content: center; padding: 0; width: 40px; }
|
|
74
|
+
.sb[data-collapsed='true'] li { display: flex; justify-content: center; }
|
|
75
|
+
.item:hover { background: var(--tr-surface-container-high); color: var(--tr-on-surface); }
|
|
76
|
+
.item[aria-current='page'] { background: var(--tr-primary-container); color: var(--tr-on-primary-container); font-weight: 700; }
|
|
77
|
+
.item:focus-visible { outline: none; box-shadow: var(--tr-focus-ring); }
|
|
78
|
+
.lbl { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
79
|
+
.sub { margin: 2px 0 2px 20px; padding-left: 8px; border-left: 1px solid var(--tr-outline-variant); }
|
|
80
|
+
.child { height: 30px; font-size: var(--tr-text-sm); }
|
|
81
|
+
.collapse { display: flex; align-items: center; gap: 10px; height: 36px; padding: 0 10px; border: 0; border-top: 1px solid var(--tr-outline-variant); border-radius: 0; background: none; color: var(--tr-on-surface-variant); font: 500 var(--tr-text-sm) var(--tr-font-sans); cursor: pointer; }
|
|
82
|
+
.collapse:hover { color: var(--tr-on-surface); }
|
|
83
|
+
.collapse:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--tr-primary); }
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IconName } from '../icons/paths.js';
|
|
2
|
+
type Entry = {
|
|
3
|
+
heading: string;
|
|
4
|
+
} | {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: IconName;
|
|
8
|
+
href?: string;
|
|
9
|
+
badge?: string | number;
|
|
10
|
+
children?: {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
href?: string;
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
16
|
+
type $$ComponentProps = {
|
|
17
|
+
items: Entry[];
|
|
18
|
+
active?: string;
|
|
19
|
+
collapsed?: boolean;
|
|
20
|
+
label?: string;
|
|
21
|
+
};
|
|
22
|
+
declare const Sidebar: import("svelte").Component<$$ComponentProps, {}, "active" | "collapsed">;
|
|
23
|
+
type Sidebar = ReturnType<typeof Sidebar>;
|
|
24
|
+
export default Sidebar;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
variant = 'text',
|
|
4
|
+
lines = 1,
|
|
5
|
+
width,
|
|
6
|
+
height
|
|
7
|
+
}: { variant?: 'text' | 'rect' | 'circle'; lines?: number; width?: string | number; height?: string | number } = $props();
|
|
8
|
+
const px = (v?: string | number) => (typeof v === 'number' ? `${v}px` : v);
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<span class="sk-wrap" aria-hidden="true">
|
|
12
|
+
{#each Array.from({ length: variant === 'text' ? lines : 1 }) as _, i}
|
|
13
|
+
<span class="sk" data-variant={variant} style:width={px(width) ?? (variant === 'text' && lines > 1 && i === lines - 1 ? '60%' : undefined)} style:height={px(height)}></span>
|
|
14
|
+
{/each}
|
|
15
|
+
</span>
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
.sk-wrap { display: flex; flex-direction: column; gap: 8px; }
|
|
19
|
+
.sk { display: block; background: linear-gradient(90deg, var(--tr-surface-container-high) 25%, var(--tr-surface-container-highest) 50%, var(--tr-surface-container-high) 75%); background-size: 200% 100%; animation: shimmer 1.4s linear infinite; border-radius: var(--tr-radius-sm); }
|
|
20
|
+
.sk[data-variant='text'] { height: 12px; }
|
|
21
|
+
.sk[data-variant='rect'] { height: 80px; border-radius: var(--tr-radius-md); }
|
|
22
|
+
.sk[data-variant='circle'] { width: 40px; height: 40px; border-radius: 50%; }
|
|
23
|
+
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
|
24
|
+
@media (prefers-reduced-motion: reduce) { .sk { animation: none; } }
|
|
25
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
variant?: 'text' | 'rect' | 'circle';
|
|
3
|
+
lines?: number;
|
|
4
|
+
width?: string | number;
|
|
5
|
+
height?: string | number;
|
|
6
|
+
};
|
|
7
|
+
declare const Skeleton: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type Skeleton = ReturnType<typeof Skeleton>;
|
|
9
|
+
export default Skeleton;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
value = $bindable(0),
|
|
4
|
+
min = 0,
|
|
5
|
+
max = 100,
|
|
6
|
+
step = 1,
|
|
7
|
+
label,
|
|
8
|
+
ariaLabel,
|
|
9
|
+
format = (n: number) => String(n),
|
|
10
|
+
marks = false,
|
|
11
|
+
disabled = false
|
|
12
|
+
}: {
|
|
13
|
+
/** A number for a single thumb, or [low, high] for a range. */
|
|
14
|
+
value?: number | [number, number];
|
|
15
|
+
min?: number;
|
|
16
|
+
max?: number;
|
|
17
|
+
step?: number;
|
|
18
|
+
label?: string;
|
|
19
|
+
ariaLabel?: string;
|
|
20
|
+
format?: (n: number) => string;
|
|
21
|
+
/** Show a tick per step (when there are ≤ 50). */
|
|
22
|
+
marks?: boolean;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
} = $props();
|
|
25
|
+
|
|
26
|
+
const id = $props.id();
|
|
27
|
+
const isRange = $derived(Array.isArray(value));
|
|
28
|
+
const vals = $derived<number[]>(Array.isArray(value) ? value : [value]);
|
|
29
|
+
let track: HTMLDivElement;
|
|
30
|
+
let thumbs = $state<HTMLElement[]>([]);
|
|
31
|
+
let active = $state<number | null>(null);
|
|
32
|
+
let dragging = $state(false);
|
|
33
|
+
|
|
34
|
+
const pct = (v: number) => ((v - min) / (max - min || 1)) * 100;
|
|
35
|
+
const decimals = $derived(String(step).split('.')[1]?.length ?? 0);
|
|
36
|
+
const snap = (v: number) => Number(Math.min(max, Math.max(min, Math.round((v - min) / step) * step + min)).toFixed(decimals));
|
|
37
|
+
const ticks = $derived(marks && (max - min) / step <= 50 ? Array.from({ length: Math.floor((max - min) / step) + 1 }, (_, i) => min + i * step) : []);
|
|
38
|
+
|
|
39
|
+
function setThumb(i: number, raw: number) {
|
|
40
|
+
const v = snap(raw);
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
const [a, b] = value;
|
|
43
|
+
value = i === 0 ? [Math.min(v, b), b] : [a, Math.max(v, a)];
|
|
44
|
+
} else value = v;
|
|
45
|
+
}
|
|
46
|
+
const fromX = (x: number) => {
|
|
47
|
+
const r = track.getBoundingClientRect();
|
|
48
|
+
return min + Math.min(1, Math.max(0, (x - r.left) / r.width)) * (max - min);
|
|
49
|
+
};
|
|
50
|
+
function down(e: PointerEvent) {
|
|
51
|
+
if (disabled) return;
|
|
52
|
+
const v = fromX(e.clientX);
|
|
53
|
+
let i = 0;
|
|
54
|
+
if (vals.length > 1) i = vals[0] === vals[1] ? (v < vals[0] ? 0 : 1) : Math.abs(v - vals[0]) <= Math.abs(v - vals[1]) ? 0 : 1;
|
|
55
|
+
active = i; dragging = true;
|
|
56
|
+
setThumb(i, v);
|
|
57
|
+
track.setPointerCapture(e.pointerId);
|
|
58
|
+
thumbs[i]?.focus();
|
|
59
|
+
}
|
|
60
|
+
const move = (e: PointerEvent) => dragging && active != null && setThumb(active, fromX(e.clientX));
|
|
61
|
+
const up = () => (dragging = false);
|
|
62
|
+
|
|
63
|
+
function key(e: KeyboardEvent, i: number) {
|
|
64
|
+
const big = Math.max(step, (max - min) / 10);
|
|
65
|
+
const d: Record<string, number> = { ArrowRight: step, ArrowUp: step, ArrowLeft: -step, ArrowDown: -step, PageUp: big, PageDown: -big };
|
|
66
|
+
if (e.key in d) setThumb(i, vals[i] + d[e.key]);
|
|
67
|
+
else if (e.key === 'Home') setThumb(i, min);
|
|
68
|
+
else if (e.key === 'End') setThumb(i, max);
|
|
69
|
+
else return;
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
}
|
|
72
|
+
const from = $derived(vals.length > 1 ? pct(vals[0]) : 0);
|
|
73
|
+
const to = $derived(pct(vals.at(-1)!));
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<div class="sl" data-disabled={disabled}>
|
|
77
|
+
{#if label}
|
|
78
|
+
<div class="head">
|
|
79
|
+
<span class="tr-label" id="{id}-label">{label}</span>
|
|
80
|
+
<span class="val">{vals.map(format).join(' – ')}</span>
|
|
81
|
+
</div>
|
|
82
|
+
{/if}
|
|
83
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
84
|
+
<div class="track" bind:this={track} onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}>
|
|
85
|
+
<div class="rail"></div>
|
|
86
|
+
<div class="fill" style:left="{from}%" style:width="{to - from}%"></div>
|
|
87
|
+
{#each ticks as t}<span class="tick" style:left="{pct(t)}%"></span>{/each}
|
|
88
|
+
{#each vals as v, i}
|
|
89
|
+
<div
|
|
90
|
+
class="thumb" role="slider" tabindex={disabled ? -1 : 0} bind:this={thumbs[i]}
|
|
91
|
+
style:left="{pct(v)}%" data-active={dragging && active === i}
|
|
92
|
+
aria-valuemin={isRange ? (i === 0 ? min : vals[0]) : min} aria-valuemax={isRange ? (i === 0 ? vals[1] : max) : max}
|
|
93
|
+
aria-valuenow={v} aria-valuetext={format(v)} aria-disabled={disabled}
|
|
94
|
+
aria-label={isRange ? `${label ?? ariaLabel ?? 'Range'} ${i === 0 ? 'minimum' : 'maximum'}` : (label ? undefined : ariaLabel)}
|
|
95
|
+
aria-labelledby={!isRange && label ? `${id}-label` : undefined}
|
|
96
|
+
onkeydown={(e) => !disabled && key(e, i)}
|
|
97
|
+
>
|
|
98
|
+
<span class="bubble">{format(v)}</span>
|
|
99
|
+
</div>
|
|
100
|
+
{/each}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<style>
|
|
105
|
+
.sl { padding: 2px 8px; }
|
|
106
|
+
.sl[data-disabled='true'] { opacity: 0.5; }
|
|
107
|
+
.head { display: flex; justify-content: space-between; margin: 0 -8px 4px; }
|
|
108
|
+
.head .tr-label { margin: 0; }
|
|
109
|
+
.val { font: 600 var(--tr-text-sm) var(--tr-font-sans); font-variant-numeric: tabular-nums; color: var(--tr-primary); }
|
|
110
|
+
.track { position: relative; height: 24px; cursor: pointer; touch-action: none; }
|
|
111
|
+
.sl[data-disabled='true'] .track { cursor: not-allowed; }
|
|
112
|
+
.rail, .fill { position: absolute; top: 10px; height: 4px; border-radius: 2px; }
|
|
113
|
+
.rail { left: 0; right: 0; background: var(--tr-surface-container-highest); box-shadow: inset 0 0 0 1px var(--tr-outline-variant); }
|
|
114
|
+
.fill { background: var(--tr-primary); }
|
|
115
|
+
.tick { position: absolute; top: 6px; width: 1px; height: 12px; background: var(--tr-outline-variant); transform: translateX(-0.5px); }
|
|
116
|
+
.thumb { position: absolute; top: 4px; width: 16px; height: 16px; margin-left: -8px; border-radius: 50%; background: var(--tr-primary); border: 2px solid var(--tr-surface); box-shadow: var(--tr-elevation-1), 0 0 0 1px var(--tr-primary); outline: none; transition: box-shadow var(--tr-dur) var(--tr-ease), transform var(--tr-dur-fast) var(--tr-ease); }
|
|
117
|
+
.thumb:hover { transform: scale(1.1); }
|
|
118
|
+
.thumb:focus-visible { box-shadow: var(--tr-elevation-1), 0 0 0 1px var(--tr-primary), var(--tr-focus-ring); }
|
|
119
|
+
.bubble { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%) scale(0.9); padding: 2px 8px; border-radius: var(--tr-radius-pill); background: var(--tr-on-surface); color: var(--tr-surface); font: 600 var(--tr-text-xs) var(--tr-font-sans); font-variant-numeric: tabular-nums; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity var(--tr-dur-fast) var(--tr-ease), transform var(--tr-dur-fast) var(--tr-ease); }
|
|
120
|
+
.thumb:focus-visible .bubble, .thumb[data-active='true'] .bubble { opacity: 1; transform: translateX(-50%) scale(1); }
|
|
121
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
/** A number for a single thumb, or [low, high] for a range. */
|
|
3
|
+
value?: number | [number, number];
|
|
4
|
+
min?: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
step?: number;
|
|
7
|
+
label?: string;
|
|
8
|
+
ariaLabel?: string;
|
|
9
|
+
format?: (n: number) => string;
|
|
10
|
+
/** Show a tick per step (when there are ≤ 50). */
|
|
11
|
+
marks?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
};
|
|
14
|
+
declare const Slider: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
15
|
+
type Slider = ReturnType<typeof Slider>;
|
|
16
|
+
export default Slider;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
size = 24,
|
|
4
|
+
value,
|
|
5
|
+
intent = 'primary',
|
|
6
|
+
label = 'Loading'
|
|
7
|
+
}: { size?: number; /** 0–100 for determinate; omit to spin. */ value?: number; intent?: 'primary' | 'success' | 'warning' | 'danger' | 'neutral'; label?: string } = $props();
|
|
8
|
+
const r = 10;
|
|
9
|
+
const c = 2 * Math.PI * r;
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<span class="sp" data-intent={intent} role="progressbar" aria-label={label} aria-valuemin={value == null ? undefined : 0} aria-valuemax={value == null ? undefined : 100} aria-valuenow={value}>
|
|
13
|
+
<svg width={size} height={size} viewBox="0 0 24 24" data-spin={value == null}>
|
|
14
|
+
<circle class="track" cx="12" cy="12" {r} />
|
|
15
|
+
<circle class="arc" cx="12" cy="12" {r} stroke-dasharray={c} stroke-dashoffset={c * (1 - (value ?? 25) / 100)} />
|
|
16
|
+
</svg>
|
|
17
|
+
</span>
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
.sp { display: inline-flex; --c: var(--tr-primary); }
|
|
21
|
+
.sp[data-intent='success'] { --c: var(--tr-success); }
|
|
22
|
+
.sp[data-intent='warning'] { --c: var(--tr-warning); }
|
|
23
|
+
.sp[data-intent='danger'] { --c: var(--tr-danger); }
|
|
24
|
+
.sp[data-intent='neutral'] { --c: var(--tr-on-surface-variant); }
|
|
25
|
+
circle { fill: none; stroke-width: 3; }
|
|
26
|
+
.track { stroke: var(--tr-surface-container-highest); }
|
|
27
|
+
.arc { stroke: var(--c); stroke-linecap: round; transform: rotate(-90deg); transform-origin: center; transition: stroke-dashoffset var(--tr-dur) var(--tr-ease); }
|
|
28
|
+
svg[data-spin='true'] { animation: spin 0.8s linear infinite; }
|
|
29
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
30
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
size?: number; /** 0–100 for determinate; omit to spin. */
|
|
3
|
+
value?: number;
|
|
4
|
+
intent?: 'primary' | 'success' | 'warning' | 'danger' | 'neutral';
|
|
5
|
+
label?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const Spinner: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type Spinner = ReturnType<typeof Spinner>;
|
|
9
|
+
export default Spinner;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import Button from './Button.svelte';
|
|
4
|
+
import Icon from '../icons/Icon.svelte';
|
|
5
|
+
|
|
6
|
+
import type { MenuItem } from '../types.js';
|
|
7
|
+
import type { IconName } from '../icons/paths.js';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
label,
|
|
11
|
+
icon,
|
|
12
|
+
items,
|
|
13
|
+
onclick,
|
|
14
|
+
variant = 'outlined',
|
|
15
|
+
intent = 'primary',
|
|
16
|
+
size = 'md'
|
|
17
|
+
}: {
|
|
18
|
+
label: string;
|
|
19
|
+
icon?: IconName;
|
|
20
|
+
items: MenuItem[];
|
|
21
|
+
onclick?: (e: MouseEvent) => void;
|
|
22
|
+
variant?: 'filled' | 'tonal' | 'outlined';
|
|
23
|
+
intent?: 'primary' | 'neutral' | 'success' | 'warning' | 'danger';
|
|
24
|
+
size?: 'sm' | 'md' | 'lg';
|
|
25
|
+
} = $props();
|
|
26
|
+
|
|
27
|
+
let open = $state(false);
|
|
28
|
+
let root: HTMLDivElement;
|
|
29
|
+
let menu = $state<HTMLUListElement>();
|
|
30
|
+
const uid = $props.id();
|
|
31
|
+
|
|
32
|
+
const enabled = () => [...(menu?.querySelectorAll<HTMLButtonElement>('button:not(:disabled)') ?? [])];
|
|
33
|
+
|
|
34
|
+
async function toggle(force?: boolean) {
|
|
35
|
+
open = force ?? !open;
|
|
36
|
+
if (open) {
|
|
37
|
+
await tick();
|
|
38
|
+
enabled()[0]?.focus();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function onMenuKey(e: KeyboardEvent) {
|
|
43
|
+
const els = enabled();
|
|
44
|
+
const i = els.indexOf(document.activeElement as HTMLButtonElement);
|
|
45
|
+
if (e.key === 'ArrowDown') els[(i + 1) % els.length]?.focus();
|
|
46
|
+
else if (e.key === 'ArrowUp') els[(i - 1 + els.length) % els.length]?.focus();
|
|
47
|
+
else if (e.key === 'Home') els[0]?.focus();
|
|
48
|
+
else if (e.key === 'End') els.at(-1)?.focus();
|
|
49
|
+
else if (e.key === 'Escape') {
|
|
50
|
+
open = false;
|
|
51
|
+
root.querySelector<HTMLButtonElement>('[data-trigger]')?.focus();
|
|
52
|
+
} else if (e.key === 'Tab') open = false;
|
|
53
|
+
else return;
|
|
54
|
+
if (e.key !== 'Tab') e.preventDefault();
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<svelte:window onpointerdown={(e) => open && !root.contains(e.target as Node) && (open = false)} />
|
|
59
|
+
|
|
60
|
+
<div class="tr-split" bind:this={root} data-variant={variant} data-intent={intent}>
|
|
61
|
+
<Button {variant} {intent} {size} {icon} {onclick} class="tr-split-action">{label}</Button>
|
|
62
|
+
<Button
|
|
63
|
+
{variant}
|
|
64
|
+
{intent}
|
|
65
|
+
{size}
|
|
66
|
+
rightIcon="caret-down"
|
|
67
|
+
class="tr-split-trigger"
|
|
68
|
+
data-trigger
|
|
69
|
+
aria-label="More {label} options"
|
|
70
|
+
aria-haspopup="menu"
|
|
71
|
+
aria-expanded={open}
|
|
72
|
+
aria-controls="{uid}-menu"
|
|
73
|
+
onclick={() => toggle()}
|
|
74
|
+
/>
|
|
75
|
+
{#if open}
|
|
76
|
+
<ul class="menu" id="{uid}-menu" role="menu" bind:this={menu} onkeydown={onMenuKey}>
|
|
77
|
+
{#each items as item}
|
|
78
|
+
<li role="none">
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
role="menuitem"
|
|
82
|
+
disabled={item.disabled}
|
|
83
|
+
onclick={() => { open = false; item.onselect?.(); }}
|
|
84
|
+
>
|
|
85
|
+
{#if item.icon}<Icon name={item.icon} />{/if}{item.label}
|
|
86
|
+
</button>
|
|
87
|
+
</li>
|
|
88
|
+
{/each}
|
|
89
|
+
</ul>
|
|
90
|
+
{/if}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<style>
|
|
94
|
+
.tr-split { position: relative; display: inline-flex; }
|
|
95
|
+
.tr-split :global(.tr-split-action) { border-top-right-radius: 0; border-bottom-right-radius: 0; }
|
|
96
|
+
.tr-split :global(.tr-split-trigger) { border-top-left-radius: 0; border-bottom-left-radius: 0; padding: 0; width: var(--tr-control-h); }
|
|
97
|
+
/* Seam: Blueprint-style hairline divider between action and trigger */
|
|
98
|
+
.tr-split :global(.tr-split-trigger) { margin-left: -1px; box-shadow: inset 1px 0 0 color-mix(in srgb, currentColor 30%, transparent); }
|
|
99
|
+
.tr-split[data-variant='outlined'] :global(.tr-split-trigger) { border-left-color: var(--tr-outline); }
|
|
100
|
+
.menu {
|
|
101
|
+
position: absolute; top: calc(100% + 4px); right: 0; z-index: 50; min-width: 100%;
|
|
102
|
+
margin: 0; padding: 4px; list-style: none;
|
|
103
|
+
background: var(--tr-surface-container-low); border: 1px solid var(--tr-outline-variant);
|
|
104
|
+
border-radius: var(--tr-radius-md); box-shadow: var(--tr-elevation-2);
|
|
105
|
+
}
|
|
106
|
+
.menu button {
|
|
107
|
+
display: flex; align-items: center; gap: 8px; width: 100%; height: var(--tr-control-h);
|
|
108
|
+
padding: 0 10px; border: 0; border-radius: var(--tr-radius-sm); background: none;
|
|
109
|
+
color: var(--tr-on-surface); font: inherit; text-align: left; white-space: nowrap; cursor: pointer;
|
|
110
|
+
}
|
|
111
|
+
.menu button:hover:not(:disabled), .menu button:focus-visible { outline: none; background: var(--tr-surface-container-highest); }
|
|
112
|
+
.menu button:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
113
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MenuItem } from '../types.js';
|
|
2
|
+
import type { IconName } from '../icons/paths.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: IconName;
|
|
6
|
+
items: MenuItem[];
|
|
7
|
+
onclick?: (e: MouseEvent) => void;
|
|
8
|
+
variant?: 'filled' | 'tonal' | 'outlined';
|
|
9
|
+
intent?: 'primary' | 'neutral' | 'success' | 'warning' | 'danger';
|
|
10
|
+
size?: 'sm' | 'md' | 'lg';
|
|
11
|
+
};
|
|
12
|
+
declare const SplitButton: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type SplitButton = ReturnType<typeof SplitButton>;
|
|
14
|
+
export default SplitButton;
|