@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.
Files changed (155) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +91 -2
  3. package/dist/color.d.ts +21 -0
  4. package/dist/color.js +32 -0
  5. package/dist/components/Accordion.svelte +70 -0
  6. package/dist/components/Accordion.svelte.d.ts +15 -0
  7. package/dist/components/Alert.svelte +46 -0
  8. package/dist/components/Alert.svelte.d.ts +15 -0
  9. package/dist/components/Avatar.svelte +32 -0
  10. package/dist/components/Avatar.svelte.d.ts +9 -0
  11. package/dist/components/AvatarGroup.svelte +20 -0
  12. package/dist/components/AvatarGroup.svelte.d.ts +11 -0
  13. package/dist/components/Badge.svelte +27 -0
  14. package/dist/components/Badge.svelte.d.ts +9 -0
  15. package/dist/components/Breadcrumbs.svelte +38 -0
  16. package/dist/components/Breadcrumbs.svelte.d.ts +12 -0
  17. package/dist/components/Button.svelte +119 -0
  18. package/dist/components/Button.svelte.d.ts +16 -0
  19. package/dist/components/ButtonGroup.svelte +23 -0
  20. package/dist/components/ButtonGroup.svelte.d.ts +9 -0
  21. package/dist/components/Calendar.svelte +166 -0
  22. package/dist/components/Calendar.svelte.d.ts +19 -0
  23. package/dist/components/Callout.svelte +50 -0
  24. package/dist/components/Callout.svelte.d.ts +14 -0
  25. package/dist/components/Card.svelte +50 -0
  26. package/dist/components/Card.svelte.d.ts +15 -0
  27. package/dist/components/Checkbox.svelte +37 -0
  28. package/dist/components/Checkbox.svelte.d.ts +16 -0
  29. package/dist/components/ColorPicker.svelte +164 -0
  30. package/dist/components/ColorPicker.svelte.d.ts +14 -0
  31. package/dist/components/Combobox.svelte +131 -0
  32. package/dist/components/Combobox.svelte.d.ts +19 -0
  33. package/dist/components/ContextMenu.svelte +51 -0
  34. package/dist/components/ContextMenu.svelte.d.ts +10 -0
  35. package/dist/components/DataTable.svelte +657 -0
  36. package/dist/components/DataTable.svelte.d.ts +76 -0
  37. package/dist/components/DatePicker.svelte +70 -0
  38. package/dist/components/DatePicker.svelte.d.ts +15 -0
  39. package/dist/components/DateRangePicker.svelte +92 -0
  40. package/dist/components/DateRangePicker.svelte.d.ts +15 -0
  41. package/dist/components/DateTimePicker.svelte +62 -0
  42. package/dist/components/DateTimePicker.svelte.d.ts +19 -0
  43. package/dist/components/DateTimeRangePicker.svelte +167 -0
  44. package/dist/components/DateTimeRangePicker.svelte.d.ts +22 -0
  45. package/dist/components/Dialog.svelte +70 -0
  46. package/dist/components/Dialog.svelte.d.ts +14 -0
  47. package/dist/components/Divider.svelte +16 -0
  48. package/dist/components/Divider.svelte.d.ts +8 -0
  49. package/dist/components/Drawer.svelte +64 -0
  50. package/dist/components/Drawer.svelte.d.ts +13 -0
  51. package/dist/components/DurationInput.svelte +112 -0
  52. package/dist/components/DurationInput.svelte.d.ts +19 -0
  53. package/dist/components/EditableText.svelte +73 -0
  54. package/dist/components/EditableText.svelte.d.ts +14 -0
  55. package/dist/components/EmptyState.svelte +30 -0
  56. package/dist/components/EmptyState.svelte.d.ts +12 -0
  57. package/dist/components/FileUpload.svelte +143 -0
  58. package/dist/components/FileUpload.svelte.d.ts +18 -0
  59. package/dist/components/FormGroup.svelte +27 -0
  60. package/dist/components/FormGroup.svelte.d.ts +13 -0
  61. package/dist/components/Input.svelte +62 -0
  62. package/dist/components/Input.svelte.d.ts +15 -0
  63. package/dist/components/InputAddon.svelte +21 -0
  64. package/dist/components/InputAddon.svelte.d.ts +9 -0
  65. package/dist/components/InputGroup.svelte +70 -0
  66. package/dist/components/InputGroup.svelte.d.ts +16 -0
  67. package/dist/components/Menu.svelte +95 -0
  68. package/dist/components/Menu.svelte.d.ts +15 -0
  69. package/dist/components/MenuButton.svelte +45 -0
  70. package/dist/components/MenuButton.svelte.d.ts +16 -0
  71. package/dist/components/MultiSelect.svelte +129 -0
  72. package/dist/components/MultiSelect.svelte.d.ts +18 -0
  73. package/dist/components/Navbar.svelte +17 -0
  74. package/dist/components/Navbar.svelte.d.ts +10 -0
  75. package/dist/components/NumberInput.svelte +66 -0
  76. package/dist/components/NumberInput.svelte.d.ts +16 -0
  77. package/dist/components/OptionList.svelte +74 -0
  78. package/dist/components/OptionList.svelte.d.ts +15 -0
  79. package/dist/components/Popover.svelte +87 -0
  80. package/dist/components/Popover.svelte.d.ts +14 -0
  81. package/dist/components/ProgressBar.svelte +35 -0
  82. package/dist/components/ProgressBar.svelte.d.ts +10 -0
  83. package/dist/components/RadioGroup.svelte +39 -0
  84. package/dist/components/RadioGroup.svelte.d.ts +14 -0
  85. package/dist/components/Section.svelte +56 -0
  86. package/dist/components/Section.svelte.d.ts +15 -0
  87. package/dist/components/SegmentedControl.svelte +56 -0
  88. package/dist/components/SegmentedControl.svelte.d.ts +17 -0
  89. package/dist/components/Select.svelte +204 -0
  90. package/dist/components/Select.svelte.d.ts +41 -0
  91. package/dist/components/Shell.svelte +143 -0
  92. package/dist/components/Shell.svelte.d.ts +36 -0
  93. package/dist/components/Sidebar.svelte +84 -0
  94. package/dist/components/Sidebar.svelte.d.ts +24 -0
  95. package/dist/components/Skeleton.svelte +25 -0
  96. package/dist/components/Skeleton.svelte.d.ts +9 -0
  97. package/dist/components/Slider.svelte +121 -0
  98. package/dist/components/Slider.svelte.d.ts +16 -0
  99. package/dist/components/Spinner.svelte +30 -0
  100. package/dist/components/Spinner.svelte.d.ts +9 -0
  101. package/dist/components/SplitButton.svelte +113 -0
  102. package/dist/components/SplitButton.svelte.d.ts +14 -0
  103. package/dist/components/SplitPane.svelte +106 -0
  104. package/dist/components/SplitPane.svelte.d.ts +16 -0
  105. package/dist/components/Stepper.svelte +73 -0
  106. package/dist/components/Stepper.svelte.d.ts +26 -0
  107. package/dist/components/Switch.svelte +21 -0
  108. package/dist/components/Switch.svelte.d.ts +10 -0
  109. package/dist/components/Tabs.svelte +74 -0
  110. package/dist/components/Tabs.svelte.d.ts +18 -0
  111. package/dist/components/Tag.svelte +49 -0
  112. package/dist/components/Tag.svelte.d.ts +16 -0
  113. package/dist/components/TagInput.svelte +72 -0
  114. package/dist/components/TagInput.svelte.d.ts +17 -0
  115. package/dist/components/Textarea.svelte +37 -0
  116. package/dist/components/Textarea.svelte.d.ts +11 -0
  117. package/dist/components/TimeList.svelte +76 -0
  118. package/dist/components/TimeList.svelte.d.ts +10 -0
  119. package/dist/components/TimePicker.svelte +79 -0
  120. package/dist/components/TimePicker.svelte.d.ts +15 -0
  121. package/dist/components/TimeRangePicker.svelte +44 -0
  122. package/dist/components/TimeRangePicker.svelte.d.ts +17 -0
  123. package/dist/components/Timeline.svelte +38 -0
  124. package/dist/components/Timeline.svelte.d.ts +17 -0
  125. package/dist/components/Toaster.svelte +35 -0
  126. package/dist/components/Toaster.svelte.d.ts +18 -0
  127. package/dist/components/Tooltip.svelte +66 -0
  128. package/dist/components/Tooltip.svelte.d.ts +11 -0
  129. package/dist/components/Tree.svelte +147 -0
  130. package/dist/components/Tree.svelte.d.ts +16 -0
  131. package/dist/date.d.ts +19 -0
  132. package/dist/date.js +43 -0
  133. package/dist/duration.d.ts +11 -0
  134. package/dist/duration.js +44 -0
  135. package/dist/fonts/LICENSE-Inter.txt +93 -0
  136. package/dist/fonts/LICENSE-JetBrainsMono.txt +93 -0
  137. package/dist/fonts/inter-latin-wght-normal.woff2 +0 -0
  138. package/dist/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  139. package/dist/icons/Icon.svelte +30 -0
  140. package/dist/icons/Icon.svelte.d.ts +9 -0
  141. package/dist/icons/paths.d.ts +39 -0
  142. package/dist/icons/paths.js +43 -0
  143. package/dist/index.d.ts +69 -0
  144. package/dist/index.js +65 -0
  145. package/dist/styles/base.css +71 -0
  146. package/dist/styles/fonts.css +21 -0
  147. package/dist/styles/index.css +3 -0
  148. package/dist/styles/tokens.css +143 -0
  149. package/dist/time.d.ts +7 -0
  150. package/dist/time.js +30 -0
  151. package/dist/toast.svelte.d.ts +22 -0
  152. package/dist/toast.svelte.js +19 -0
  153. package/dist/types.d.ts +70 -0
  154. package/dist/types.js +1 -0
  155. 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;