@ruckc/truss-ui 0.0.0-stage → 1.1.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.
Files changed (161) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +103 -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/DialogHost.svelte +8 -0
  48. package/dist/components/DialogHost.svelte.d.ts +3 -0
  49. package/dist/components/Divider.svelte +16 -0
  50. package/dist/components/Divider.svelte.d.ts +8 -0
  51. package/dist/components/Drawer.svelte +64 -0
  52. package/dist/components/Drawer.svelte.d.ts +13 -0
  53. package/dist/components/DurationInput.svelte +112 -0
  54. package/dist/components/DurationInput.svelte.d.ts +19 -0
  55. package/dist/components/EditableText.svelte +73 -0
  56. package/dist/components/EditableText.svelte.d.ts +14 -0
  57. package/dist/components/EmptyState.svelte +30 -0
  58. package/dist/components/EmptyState.svelte.d.ts +12 -0
  59. package/dist/components/FileUpload.svelte +143 -0
  60. package/dist/components/FileUpload.svelte.d.ts +18 -0
  61. package/dist/components/FormGroup.svelte +27 -0
  62. package/dist/components/FormGroup.svelte.d.ts +13 -0
  63. package/dist/components/Input.svelte +62 -0
  64. package/dist/components/Input.svelte.d.ts +15 -0
  65. package/dist/components/InputAddon.svelte +21 -0
  66. package/dist/components/InputAddon.svelte.d.ts +9 -0
  67. package/dist/components/InputGroup.svelte +70 -0
  68. package/dist/components/InputGroup.svelte.d.ts +16 -0
  69. package/dist/components/Menu.svelte +95 -0
  70. package/dist/components/Menu.svelte.d.ts +15 -0
  71. package/dist/components/MenuButton.svelte +45 -0
  72. package/dist/components/MenuButton.svelte.d.ts +16 -0
  73. package/dist/components/MultiSelect.svelte +129 -0
  74. package/dist/components/MultiSelect.svelte.d.ts +18 -0
  75. package/dist/components/Navbar.svelte +17 -0
  76. package/dist/components/Navbar.svelte.d.ts +10 -0
  77. package/dist/components/NumberInput.svelte +66 -0
  78. package/dist/components/NumberInput.svelte.d.ts +16 -0
  79. package/dist/components/OptionList.svelte +74 -0
  80. package/dist/components/OptionList.svelte.d.ts +15 -0
  81. package/dist/components/Popover.svelte +87 -0
  82. package/dist/components/Popover.svelte.d.ts +14 -0
  83. package/dist/components/ProgressBar.svelte +35 -0
  84. package/dist/components/ProgressBar.svelte.d.ts +10 -0
  85. package/dist/components/RadioGroup.svelte +39 -0
  86. package/dist/components/RadioGroup.svelte.d.ts +14 -0
  87. package/dist/components/Section.svelte +56 -0
  88. package/dist/components/Section.svelte.d.ts +15 -0
  89. package/dist/components/SegmentedControl.svelte +56 -0
  90. package/dist/components/SegmentedControl.svelte.d.ts +17 -0
  91. package/dist/components/Select.svelte +204 -0
  92. package/dist/components/Select.svelte.d.ts +41 -0
  93. package/dist/components/Shell.svelte +143 -0
  94. package/dist/components/Shell.svelte.d.ts +36 -0
  95. package/dist/components/Sidebar.svelte +84 -0
  96. package/dist/components/Sidebar.svelte.d.ts +24 -0
  97. package/dist/components/Skeleton.svelte +25 -0
  98. package/dist/components/Skeleton.svelte.d.ts +9 -0
  99. package/dist/components/Slider.svelte +121 -0
  100. package/dist/components/Slider.svelte.d.ts +16 -0
  101. package/dist/components/Spinner.svelte +30 -0
  102. package/dist/components/Spinner.svelte.d.ts +9 -0
  103. package/dist/components/SplitButton.svelte +113 -0
  104. package/dist/components/SplitButton.svelte.d.ts +14 -0
  105. package/dist/components/SplitPane.svelte +106 -0
  106. package/dist/components/SplitPane.svelte.d.ts +16 -0
  107. package/dist/components/Stepper.svelte +73 -0
  108. package/dist/components/Stepper.svelte.d.ts +26 -0
  109. package/dist/components/Switch.svelte +21 -0
  110. package/dist/components/Switch.svelte.d.ts +10 -0
  111. package/dist/components/Tabs.svelte +74 -0
  112. package/dist/components/Tabs.svelte.d.ts +18 -0
  113. package/dist/components/Tag.svelte +49 -0
  114. package/dist/components/Tag.svelte.d.ts +16 -0
  115. package/dist/components/TagInput.svelte +72 -0
  116. package/dist/components/TagInput.svelte.d.ts +17 -0
  117. package/dist/components/Textarea.svelte +37 -0
  118. package/dist/components/Textarea.svelte.d.ts +11 -0
  119. package/dist/components/TimeList.svelte +76 -0
  120. package/dist/components/TimeList.svelte.d.ts +10 -0
  121. package/dist/components/TimePicker.svelte +79 -0
  122. package/dist/components/TimePicker.svelte.d.ts +15 -0
  123. package/dist/components/TimeRangePicker.svelte +44 -0
  124. package/dist/components/TimeRangePicker.svelte.d.ts +17 -0
  125. package/dist/components/Timeline.svelte +38 -0
  126. package/dist/components/Timeline.svelte.d.ts +17 -0
  127. package/dist/components/Toaster.svelte +35 -0
  128. package/dist/components/Toaster.svelte.d.ts +18 -0
  129. package/dist/components/Tooltip.svelte +66 -0
  130. package/dist/components/Tooltip.svelte.d.ts +11 -0
  131. package/dist/components/Tree.svelte +147 -0
  132. package/dist/components/Tree.svelte.d.ts +16 -0
  133. package/dist/components/internal/DialogRequest.svelte +99 -0
  134. package/dist/components/internal/DialogRequest.svelte.d.ts +7 -0
  135. package/dist/date.d.ts +19 -0
  136. package/dist/date.js +43 -0
  137. package/dist/dialogs.svelte.d.ts +63 -0
  138. package/dist/dialogs.svelte.js +54 -0
  139. package/dist/duration.d.ts +11 -0
  140. package/dist/duration.js +44 -0
  141. package/dist/fonts/LICENSE-Inter.txt +93 -0
  142. package/dist/fonts/LICENSE-JetBrainsMono.txt +93 -0
  143. package/dist/fonts/inter-latin-wght-normal.woff2 +0 -0
  144. package/dist/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  145. package/dist/icons/Icon.svelte +30 -0
  146. package/dist/icons/Icon.svelte.d.ts +9 -0
  147. package/dist/icons/paths.d.ts +39 -0
  148. package/dist/icons/paths.js +43 -0
  149. package/dist/index.d.ts +72 -0
  150. package/dist/index.js +67 -0
  151. package/dist/styles/base.css +71 -0
  152. package/dist/styles/fonts.css +21 -0
  153. package/dist/styles/index.css +3 -0
  154. package/dist/styles/tokens.css +143 -0
  155. package/dist/time.d.ts +7 -0
  156. package/dist/time.js +30 -0
  157. package/dist/toast.svelte.d.ts +22 -0
  158. package/dist/toast.svelte.js +19 -0
  159. package/dist/types.d.ts +70 -0
  160. package/dist/types.js +1 -0
  161. package/package.json +87 -4
package/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Curtis Ruck
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ The bundled fonts, Inter and JetBrains Mono, are licensed separately under the
24
+ SIL Open Font License 1.1; see src/lib/fonts/LICENSE-*.txt.
package/README.md CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # Truss UI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/ruckc/truss-ui/actions/workflows/ci.yml/badge.svg)](https://github.com/ruckc/truss-ui/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ A **Svelte 5 / SvelteKit design system** that blends Material 3's tonal surfaces, elevation and rounding with Blueprint's density, 32px controls and hairline structure — built from scratch, with no UI-library dependencies.
7
+
8
+ **Docs and live demo: <https://truss.ruck.io>**
9
+
10
+ - **69 components**: forms and pickers (no native `<select>`, date or time inputs), data tables with virtual scrolling, trees, menus, dialogs, an application shell and more.
11
+ - **Zero runtime dependencies** in the core (peer: `svelte`). Fonts are bundled and served by *your* build.
12
+ - **Accessible by construction**: names, ARIA, focus and complete keyboard maps, audited for every combination of every component's options.
13
+ - **Themeable**: one set of `--tr-*` CSS tokens, light and dark, drives everything, including charts, graphs and maps.
14
+ - **Tested like a library**: over 16,000 real-browser tests sweep option matrices and verify behaviour.
15
+
16
+ ## Packages
17
+
18
+ | Package | What | Dependencies |
19
+ | --- | --- | --- |
20
+ | [`@ruckc/truss-ui`](https://truss.ruck.io/docs/getting-started/) | Core: tokens, fonts, icons, components | peer: `svelte` |
21
+ | [`@ruckc/truss-chart`](https://truss.ruck.io/docs/packages/) | Line, bar, donut, histogram, sparkline (SVG) | peer: `svelte` |
22
+ | [`@ruckc/truss-graph`](https://truss.ruck.io/docs/packages/) | Node/edge editor, custom nodes, 5 edge routers, minimap | peers: `svelte`, `@ruckc/truss-ui` |
23
+ | [`@ruckc/truss-ol`](https://truss.ruck.io/docs/packages/) | OpenLayers map styled by the tokens | peers: `svelte`, `@ruckc/truss-ui`, `ol` |
24
+
25
+ ## Quick start
26
+
27
+ ```sh
28
+ npm install @ruckc/truss-ui
29
+ ```
30
+
31
+ ```svelte
32
+ <!-- +layout.svelte -->
33
+ <script lang="ts">
34
+ import '@ruckc/truss-ui/styles.css';
35
+ import { Toaster } from '@ruckc/truss-ui';
36
+ let { children } = $props();
37
+ </script>
38
+
39
+ {@render children()}
40
+ <Toaster />
41
+ ```
42
+
43
+ ```svelte
44
+ <script lang="ts">
45
+ import { Button, Input, DatePicker, toasts } from '@ruckc/truss-ui';
46
+ let name = $state('');
47
+ let due = $state<string>();
48
+ </script>
49
+
50
+ <Input label="Name" bind:value={name} />
51
+ <DatePicker label="Due" bind:value={due} />
52
+ <Button onclick={() => toasts.success(`Saved ${name}`)}>Save</Button>
53
+ ```
54
+
55
+ See **[Getting started](https://truss.ruck.io/docs/getting-started/)**, the **[component reference](https://truss.ruck.io/docs/)** (a live playground and generated API table for every component), **[Theming](https://truss.ruck.io/docs/theming/)** and **[Accessibility](https://truss.ruck.io/docs/accessibility/)**.
56
+
57
+ ## Highlights
58
+
59
+ - **DataTable**: sticky header, resizable and sortable columns, tree rows, checkbox selection, column menu, optional pagination *or* virtual scrolling with infinite loading, per-column filters (a filter row, a heading menu opened by right-click or Shift+F10, or both) and a remote mode for server-side filtering and sorting.
60
+ - **Date and time**: `Calendar`, `DatePicker`, `DateRangePicker`, `TimePicker`, `DateTimePicker`, `DateTimeRangePicker` (one combined popover), `TimeRangePicker`, `DurationInput`.
61
+ - **InputGroup**: joins inputs, selects, pickers, buttons and addons into one seamless control.
62
+ - **Graph**: custom node components, named ports on any side, edge routing (`bezier`, `straight`, `step`, `smoothstep`, `avoid`) and a minimap.
63
+ - **Charts**: horizontal, stacked, 100% stacked and negative bars; histograms with drag-to-brush; every chart has a table view and keyboard readout.
64
+
65
+ ## Development
66
+
67
+ ```sh
68
+ npm install
69
+ npm run dev # demo + docs site at http://localhost:5173
70
+ npm run check # svelte-check
71
+ npm run package:all # build all four packages into their dist/ folders
72
+ npm run build # regenerate the API docs and prerender the site (adapter-static)
73
+ npm test # the full browser suite (starts its own dev server)
74
+ ```
75
+
76
+ Repository layout:
77
+
78
+ ```
79
+ src/lib/ @ruckc/truss-ui (core)
80
+ packages/charts/ @ruckc/truss-chart
81
+ packages/graph/ @ruckc/truss-graph
82
+ packages/map/ @ruckc/truss-ol
83
+ src/routes/ the docs and demo site
84
+ src/harness/, tests/ the component matrix and behavioural specs
85
+ scripts/ API-reference and token extraction for the docs
86
+ ```
87
+
88
+ The API reference is **generated from the source** (`npm run docs`), so it can't drift from the code. The test suite is described in [Testing](https://truss.ruck.io/docs/testing/).
89
+
90
+ ## Releases
91
+
92
+ Every push to `main` that passes CI is released automatically: the version is worked out from the commit messages ([Conventional Commits](https://www.conventionalcommits.org)), all four packages are published to npm at that same version, and a GitHub release and `CHANGELOG.md` entry are created.
93
+
94
+ | Commit | Release |
95
+ | --- | --- |
96
+ | `feat: …` | minor |
97
+ | `fix:`, `perf:`, `revert:`, `docs:`, `refactor:`, `test:`, `build:`, `ci:`, `chore:`, `style:` | patch |
98
+ | any type with `!` (e.g. `feat!: …`) or a `BREAKING CHANGE:` footer | major |
99
+
100
+ Commits that don't follow the format (no `type:` prefix) don't trigger a release on their own. Publishing uses npm [trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC from the `ci.yml` workflow), so no npm token is stored anywhere. Each package's trusted publisher on npmjs.com must be for workflow `ci.yml` with the **`npm publish`** action allowed (the default is only `npm stage publish`, which npm rejects with `403 OIDC permission denied for this action`).
101
+
102
+ ## Licence
103
+
104
+ MIT © Curtis Ruck. The bundled Inter and JetBrains Mono fonts are under the SIL Open Font License 1.1.
@@ -0,0 +1,21 @@
1
+ export type RGB = {
2
+ r: number;
3
+ g: number;
4
+ b: number;
5
+ };
6
+ export type HSV = {
7
+ h: number;
8
+ s: number;
9
+ v: number;
10
+ };
11
+ declare const clamp: (n: number, lo?: number, hi?: number) => number;
12
+ /** Accepts #rgb, #rgba, #rrggbb, #rrggbbaa (with or without #). */
13
+ export declare function parseHex(text: string): (RGB & {
14
+ a: number;
15
+ }) | null;
16
+ export declare const toHex: ({ r, g, b }: RGB, a?: number) => string;
17
+ export declare function rgbToHsv({ r, g, b }: RGB): HSV;
18
+ export declare function hsvToRgb({ h, s, v }: HSV): RGB;
19
+ /** WCAG relative luminance contrast ratio. */
20
+ export declare function contrast(a: RGB, b: RGB): number;
21
+ export { clamp };
package/dist/color.js ADDED
@@ -0,0 +1,32 @@
1
+ const clamp = (n, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, n));
2
+ const h2 = (n) => Math.round(n).toString(16).padStart(2, '0');
3
+ /** Accepts #rgb, #rgba, #rrggbb, #rrggbbaa (with or without #). */
4
+ export function parseHex(text) {
5
+ let t = text.trim().replace(/^#/, '');
6
+ if (/^[0-9a-f]{3,4}$/i.test(t))
7
+ t = t.replace(/./g, '$&$&');
8
+ if (!/^([0-9a-f]{6}|[0-9a-f]{8})$/i.test(t))
9
+ return null;
10
+ const n = (i) => parseInt(t.slice(i, i + 2), 16);
11
+ return { r: n(0), g: n(2), b: n(4), a: t.length === 8 ? n(6) / 255 : 1 };
12
+ }
13
+ export const toHex = ({ r, g, b }, a) => `#${h2(r)}${h2(g)}${h2(b)}${a != null && a < 1 ? h2(a * 255) : ''}`;
14
+ export function rgbToHsv({ r, g, b }) {
15
+ const R = r / 255, G = g / 255, B = b / 255;
16
+ const max = Math.max(R, G, B), min = Math.min(R, G, B), d = max - min;
17
+ let h = 0;
18
+ if (d)
19
+ h = max === R ? ((G - B) / d) % 6 : max === G ? (B - R) / d + 2 : (R - G) / d + 4;
20
+ return { h: (h * 60 + 360) % 360, s: max ? d / max : 0, v: max };
21
+ }
22
+ export function hsvToRgb({ h, s, v }) {
23
+ const f = (n) => { const k = (n + h / 60) % 6; return v - v * s * Math.max(0, Math.min(k, 4 - k, 1)); };
24
+ return { r: Math.round(f(5) * 255), g: Math.round(f(3) * 255), b: Math.round(f(1) * 255) };
25
+ }
26
+ /** WCAG relative luminance contrast ratio. */
27
+ export function contrast(a, b) {
28
+ const L = ({ r, g, b }) => [r, g, b].map((c) => { const s = c / 255; return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; }).reduce((t, v, i) => t + v * [0.2126, 0.7152, 0.0722][i], 0);
29
+ const [x, y] = [L(a), L(b)].sort((p, q) => q - p);
30
+ return (x + 0.05) / (y + 0.05);
31
+ }
32
+ export { clamp };
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Icon from '../icons/Icon.svelte';
4
+
5
+ let {
6
+ items,
7
+ open = $bindable([]),
8
+ multiple = false,
9
+ body
10
+ }: {
11
+ items: { id: string; title: string; disabled?: boolean }[];
12
+ /** Ids of expanded items. */
13
+ open?: string[];
14
+ multiple?: boolean;
15
+ body: Snippet<[id: string]>;
16
+ } = $props();
17
+
18
+ const uid = $props.id();
19
+ let root: HTMLDivElement;
20
+
21
+ function toggle(id: string) {
22
+ if (open.includes(id)) open = open.filter((x) => x !== id);
23
+ else open = multiple ? [...open, id] : [id];
24
+ }
25
+ function onKey(e: KeyboardEvent) {
26
+ const btns = [...root.querySelectorAll<HTMLButtonElement>('.hd:not(:disabled)')];
27
+ const i = btns.indexOf(document.activeElement as HTMLButtonElement);
28
+ const to = e.key === 'ArrowDown' ? (i + 1) % btns.length : e.key === 'ArrowUp' ? (i - 1 + btns.length) % btns.length : e.key === 'Home' ? 0 : e.key === 'End' ? btns.length - 1 : -1;
29
+ if (to < 0) return;
30
+ e.preventDefault();
31
+ btns[to].focus();
32
+ }
33
+ </script>
34
+
35
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
36
+ <div class="acc" bind:this={root} onkeydown={onKey}>
37
+ {#each items as it (it.id)}
38
+ {@const isOpen = open.includes(it.id)}
39
+ <div class="item" data-open={isOpen}>
40
+ <h3>
41
+ <button type="button" class="hd" id="{uid}-h-{it.id}" aria-expanded={isOpen} aria-controls="{uid}-p-{it.id}" disabled={it.disabled} onclick={() => toggle(it.id)}>
42
+ <Icon name="chevron-right" />{it.title}
43
+ </button>
44
+ </h3>
45
+ <div class="wrap" data-open={isOpen}>
46
+ <div class="inner">
47
+ <div class="content" id="{uid}-p-{it.id}" role="region" aria-labelledby="{uid}-h-{it.id}" inert={!isOpen}>
48
+ {@render body(it.id)}
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ {/each}
54
+ </div>
55
+
56
+ <style>
57
+ .acc { border: 1px solid var(--tr-outline-variant); border-radius: var(--tr-radius-md); overflow: hidden; background: var(--tr-surface-container-low); }
58
+ .item + .item { border-top: 1px solid var(--tr-outline-variant); }
59
+ h3 { margin: 0; font-size: inherit; }
60
+ .hd { display: flex; align-items: center; gap: 8px; width: 100%; height: 36px; padding: 0 12px; border: 0; background: var(--tr-surface-container); color: var(--tr-on-surface); font: 600 var(--tr-text-md) var(--tr-font-sans); text-align: left; cursor: pointer; }
61
+ .hd:hover:not(:disabled) { background: var(--tr-surface-container-high); }
62
+ .hd:disabled { opacity: 0.45; cursor: not-allowed; }
63
+ .hd:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--tr-primary); }
64
+ .hd :global(svg) { transition: transform var(--tr-dur) var(--tr-ease); }
65
+ .item[data-open='true'] .hd :global(svg) { transform: rotate(90deg); }
66
+ .wrap { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--tr-dur) var(--tr-ease); }
67
+ .wrap[data-open='true'] { grid-template-rows: 1fr; border-top: 1px solid var(--tr-outline-variant); }
68
+ .inner { overflow: hidden; }
69
+ .content { padding: 12px; }
70
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ items: {
4
+ id: string;
5
+ title: string;
6
+ disabled?: boolean;
7
+ }[];
8
+ /** Ids of expanded items. */
9
+ open?: string[];
10
+ multiple?: boolean;
11
+ body: Snippet<[id: string]>;
12
+ };
13
+ declare const Accordion: import("svelte").Component<$$ComponentProps, {}, "open">;
14
+ type Accordion = ReturnType<typeof Accordion>;
15
+ export default Accordion;
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Button from './Button.svelte';
4
+ import Dialog from './Dialog.svelte';
5
+ import Icon from '../icons/Icon.svelte';
6
+
7
+ let {
8
+ open = $bindable(false),
9
+ title,
10
+ intent = 'primary',
11
+ confirmLabel = 'OK',
12
+ cancelLabel,
13
+ onconfirm,
14
+ oncancel,
15
+ children
16
+ }: {
17
+ open?: boolean;
18
+ title: string;
19
+ intent?: 'primary' | 'warning' | 'danger';
20
+ confirmLabel?: string;
21
+ /** Omit for a single-button acknowledgement. */
22
+ cancelLabel?: string;
23
+ onconfirm?: () => void;
24
+ oncancel?: () => void;
25
+ children: Snippet;
26
+ } = $props();
27
+ const glyph = { primary: 'info', warning: 'warning', danger: 'error' } as const;
28
+ </script>
29
+
30
+ <Dialog bind:open {title} size="sm" alert onclose={() => {}}>
31
+ <div class="row" data-intent={intent}>
32
+ <span class="ic"><Icon name={glyph[intent]} size={20} /></span>
33
+ <div>{@render children()}</div>
34
+ </div>
35
+ {#snippet footer()}
36
+ {#if cancelLabel}<Button variant="minimal" intent="neutral" onclick={() => { open = false; oncancel?.(); }}>{cancelLabel}</Button>{/if}
37
+ <Button intent={intent === 'primary' ? 'primary' : intent} onclick={() => { open = false; onconfirm?.(); }}>{confirmLabel}</Button>
38
+ {/snippet}
39
+ </Dialog>
40
+
41
+ <style>
42
+ .row { display: flex; gap: 12px; line-height: 1.45; }
43
+ .ic { flex: none; display: grid; place-items: center; width: 32px; height: 32px; border-radius: 50%; background: var(--tr-primary-container); color: var(--tr-on-primary-container); }
44
+ .row[data-intent='warning'] .ic { background: var(--tr-warning-container); color: var(--tr-on-warning-container); }
45
+ .row[data-intent='danger'] .ic { background: var(--tr-danger-container); color: var(--tr-on-danger-container); }
46
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ open?: boolean;
4
+ title: string;
5
+ intent?: 'primary' | 'warning' | 'danger';
6
+ confirmLabel?: string;
7
+ /** Omit for a single-button acknowledgement. */
8
+ cancelLabel?: string;
9
+ onconfirm?: () => void;
10
+ oncancel?: () => void;
11
+ children: Snippet;
12
+ };
13
+ declare const Alert: import("svelte").Component<$$ComponentProps, {}, "open">;
14
+ type Alert = ReturnType<typeof Alert>;
15
+ export default Alert;
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ let {
3
+ name,
4
+ src,
5
+ size = 'md',
6
+ status
7
+ }: { name: string; src?: string; size?: 'sm' | 'md' | 'lg'; status?: 'success' | 'warning' | 'danger' | 'neutral' } = $props();
8
+
9
+ const initials = $derived(name.trim().split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? '').join(''));
10
+ const hue = $derived([...name].reduce((a, c) => (a * 31 + c.charCodeAt(0)) >>> 0, 7) % 5);
11
+ let broken = $state(false);
12
+ </script>
13
+
14
+ <span class="av" data-size={size} data-hue={hue} role="img" aria-label={name} title={name}>
15
+ {#if src && !broken}<img {src} alt="" onerror={() => (broken = true)} />{:else}<span class="ini" aria-hidden="true">{initials}</span>{/if}
16
+ {#if status}<span class="st" data-status={status}></span>{/if}
17
+ </span>
18
+
19
+ <style>
20
+ .av { --cc: var(--tr-primary-container); --on: var(--tr-on-primary-container); position: relative; display: inline-grid; place-items: center; flex: none; width: 32px; height: 32px; border-radius: 50%; background: var(--cc); color: var(--on); font: 700 var(--tr-text-sm) var(--tr-font-sans); }
21
+ .av[data-size='sm'] { width: 24px; height: 24px; font-size: var(--tr-text-xs); }
22
+ .av[data-size='lg'] { width: 40px; height: 40px; font-size: var(--tr-text-md); }
23
+ .av[data-hue='1'] { --cc: var(--tr-success-container); --on: var(--tr-on-success-container); }
24
+ .av[data-hue='2'] { --cc: var(--tr-warning-container); --on: var(--tr-on-warning-container); }
25
+ .av[data-hue='3'] { --cc: var(--tr-danger-container); --on: var(--tr-on-danger-container); }
26
+ .av[data-hue='4'] { --cc: var(--tr-surface-container-highest); --on: var(--tr-on-surface); }
27
+ img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
28
+ .st { position: absolute; right: -1px; bottom: -1px; width: 10px; height: 10px; border-radius: 50%; border: 2px solid var(--tr-surface); background: var(--tr-outline); }
29
+ .st[data-status='success'] { background: var(--tr-success); }
30
+ .st[data-status='warning'] { background: var(--tr-warning); }
31
+ .st[data-status='danger'] { background: var(--tr-danger); }
32
+ </style>
@@ -0,0 +1,9 @@
1
+ type $$ComponentProps = {
2
+ name: string;
3
+ src?: string;
4
+ size?: 'sm' | 'md' | 'lg';
5
+ status?: 'success' | 'warning' | 'danger' | 'neutral';
6
+ };
7
+ declare const Avatar: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type Avatar = ReturnType<typeof Avatar>;
9
+ export default Avatar;
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import Avatar from './Avatar.svelte';
3
+ let { people, max = 4, size = 'md' }: { people: { name: string; src?: string }[]; max?: number; size?: 'sm' | 'md' | 'lg' } = $props();
4
+ const shown = $derived(people.slice(0, max));
5
+ const more = $derived(people.length - shown.length);
6
+ </script>
7
+
8
+ <span class="grp" role="group" aria-label="{people.length} people">
9
+ {#each shown as p (p.name)}<span class="slot"><Avatar {...p} {size} /></span>{/each}
10
+ {#if more > 0}<span class="slot"><span class="more" data-size={size} aria-label="{more} more">+{more}</span></span>{/if}
11
+ </span>
12
+
13
+ <style>
14
+ .grp { display: inline-flex; }
15
+ .slot { display: inline-flex; margin-left: -8px; border-radius: 50%; box-shadow: 0 0 0 2px var(--tr-surface); }
16
+ .slot:first-child { margin-left: 0; }
17
+ .more { display: inline-grid; place-items: center; width: 32px; height: 32px; border-radius: 50%; background: var(--tr-surface-container-highest); color: var(--tr-on-surface); font: 700 var(--tr-text-sm) var(--tr-font-sans); }
18
+ .more[data-size='sm'] { width: 24px; height: 24px; font-size: var(--tr-text-xs); }
19
+ .more[data-size='lg'] { width: 40px; height: 40px; }
20
+ </style>
@@ -0,0 +1,11 @@
1
+ type $$ComponentProps = {
2
+ people: {
3
+ name: string;
4
+ src?: string;
5
+ }[];
6
+ max?: number;
7
+ size?: 'sm' | 'md' | 'lg';
8
+ };
9
+ declare const AvatarGroup: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type AvatarGroup = ReturnType<typeof AvatarGroup>;
11
+ export default AvatarGroup;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ let {
5
+ intent = 'neutral',
6
+ dot = true,
7
+ children
8
+ }: { intent?: 'neutral' | 'primary' | 'success' | 'warning' | 'danger'; dot?: boolean; children: Snippet } = $props();
9
+ </script>
10
+
11
+ <span class="tr-badge" data-intent={intent}>
12
+ {#if dot}<span class="dot" aria-hidden="true"></span>{/if}{@render children()}
13
+ </span>
14
+
15
+ <style>
16
+ .tr-badge {
17
+ --c: var(--tr-on-surface-variant); --cc: var(--tr-surface-container-high); --on: var(--tr-on-surface);
18
+ display: inline-flex; align-items: center; gap: 5px; height: 20px; padding: 0 8px;
19
+ border-radius: var(--tr-radius-pill); background: var(--cc); color: var(--on);
20
+ font-size: var(--tr-text-sm); font-weight: 600; white-space: nowrap;
21
+ }
22
+ .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--c); }
23
+ .tr-badge[data-intent='primary'] { --c: var(--tr-primary); --cc: var(--tr-primary-container); --on: var(--tr-on-primary-container); }
24
+ .tr-badge[data-intent='success'] { --c: var(--tr-success); --cc: var(--tr-success-container); --on: var(--tr-on-success-container); }
25
+ .tr-badge[data-intent='warning'] { --c: var(--tr-warning); --cc: var(--tr-warning-container); --on: var(--tr-on-warning-container); }
26
+ .tr-badge[data-intent='danger'] { --c: var(--tr-danger); --cc: var(--tr-danger-container); --on: var(--tr-on-danger-container); }
27
+ </style>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ intent?: 'neutral' | 'primary' | 'success' | 'warning' | 'danger';
4
+ dot?: boolean;
5
+ children: Snippet;
6
+ };
7
+ declare const Badge: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type Badge = ReturnType<typeof Badge>;
9
+ export default Badge;
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import Icon from '../icons/Icon.svelte';
3
+ import MenuButton from './MenuButton.svelte';
4
+ import type { MenuEntry } from '../types.js';
5
+
6
+ type Crumb = { label: string; href?: string; onclick?: () => void };
7
+ let { items, max = 4 }: { items: Crumb[]; /** Collapse the middle into a “…” menu beyond this many. */ max?: number } = $props();
8
+
9
+ const collapsed = $derived(items.length > max);
10
+ const head = $derived(collapsed ? items.slice(0, 1) : items.slice(0, -1));
11
+ const hidden = $derived(collapsed ? items.slice(1, items.length - (max - 2)) : []);
12
+ const tail = $derived(collapsed ? items.slice(items.length - (max - 2), -1) : []);
13
+ const last = $derived(items.at(-1));
14
+ const menu = $derived<MenuEntry[]>(hidden.map((c) => ({ label: c.label, onselect: () => (c.href ? (location.href = c.href) : c.onclick?.()) })));
15
+ </script>
16
+
17
+ {#snippet crumb(c: Crumb)}
18
+ {#if c.href}<a href={c.href}>{c.label}</a>{:else if c.onclick}<button type="button" onclick={c.onclick}>{c.label}</button>{:else}<span>{c.label}</span>{/if}
19
+ {/snippet}
20
+
21
+ <nav aria-label="Breadcrumb">
22
+ <ol>
23
+ {#each head as c}<li>{@render crumb(c)}<Icon name="chevron-right" /></li>{/each}
24
+ {#if collapsed}<li><MenuButton label="Show hidden path" items={menu} variant="minimal" size="sm" iconOnly /><Icon name="chevron-right" /></li>{/if}
25
+ {#each tail as c}<li>{@render crumb(c)}<Icon name="chevron-right" /></li>{/each}
26
+ {#if last}<li aria-current="page"><span class="cur">{last.label}</span></li>{/if}
27
+ </ol>
28
+ </nav>
29
+
30
+ <style>
31
+ ol { display: flex; align-items: center; flex-wrap: wrap; gap: 2px; margin: 0; padding: 0; list-style: none; font-size: var(--tr-text-md); }
32
+ li { display: inline-flex; align-items: center; gap: 2px; color: var(--tr-on-surface-variant); }
33
+ a, button:not(.x) { padding: 2px 6px; border: 0; border-radius: var(--tr-radius-sm); background: none; color: var(--tr-on-surface-variant); font: inherit; text-decoration: none; cursor: pointer; }
34
+ a:hover, li > button:hover { background: var(--tr-surface-container-high); color: var(--tr-primary); }
35
+ a:focus-visible, li > button:focus-visible { outline: none; box-shadow: var(--tr-focus-ring); }
36
+ span:not(.cur) { padding: 2px 6px; }
37
+ .cur { padding: 2px 6px; color: var(--tr-on-surface); font-weight: 700; }
38
+ </style>
@@ -0,0 +1,12 @@
1
+ type Crumb = {
2
+ label: string;
3
+ href?: string;
4
+ onclick?: () => void;
5
+ };
6
+ type $$ComponentProps = {
7
+ items: Crumb[]; /** Collapse the middle into a “…” menu beyond this many. */
8
+ max?: number;
9
+ };
10
+ declare const Breadcrumbs: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
12
+ export default Breadcrumbs;
@@ -0,0 +1,119 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLButtonAttributes } from 'svelte/elements';
4
+ import Icon from '../icons/Icon.svelte';
5
+ import type { IconName } from '../icons/paths.js';
6
+
7
+ type Props = Omit<HTMLButtonAttributes, 'children'> & {
8
+ variant?: 'filled' | 'tonal' | 'outlined' | 'minimal';
9
+ intent?: 'primary' | 'neutral' | 'success' | 'warning' | 'danger';
10
+ size?: 'sm' | 'md' | 'lg';
11
+ icon?: IconName;
12
+ rightIcon?: IconName;
13
+ loading?: boolean;
14
+ href?: string;
15
+ children?: Snippet;
16
+ };
17
+
18
+ let {
19
+ variant = 'filled',
20
+ intent = 'primary',
21
+ size = 'md',
22
+ icon,
23
+ rightIcon,
24
+ loading = false,
25
+ href,
26
+ type = 'button',
27
+ disabled,
28
+ class: klass = '',
29
+ children,
30
+ ...rest
31
+ }: Props = $props();
32
+
33
+ const iconPx = $derived(size === 'lg' ? 20 : 16) as 16 | 20;
34
+ </script>
35
+
36
+ {#snippet content()}
37
+ {#if loading}<span class="spinner" aria-hidden="true"></span>{:else if icon}<Icon name={icon} size={iconPx} />{/if}
38
+ {#if children}<span class="label">{@render children()}</span>{/if}
39
+ {#if rightIcon}<Icon name={rightIcon} size={iconPx} />{/if}
40
+ {/snippet}
41
+
42
+ {#if href && !disabled && !loading}
43
+ <a class="tr-btn {klass}" data-variant={variant} data-intent={intent} data-size={size} data-icon-only={!children} {href} {...rest as Record<string, unknown>}>
44
+ {@render content()}
45
+ </a>
46
+ {:else}
47
+ <button
48
+ class="tr-btn {klass}"
49
+ data-variant={variant}
50
+ data-intent={intent}
51
+ data-size={size}
52
+ data-icon-only={!children}
53
+ {type}
54
+ disabled={disabled || loading}
55
+ aria-busy={loading || undefined}
56
+ {...rest}
57
+ >
58
+ {@render content()}
59
+ </button>
60
+ {/if}
61
+
62
+ <style>
63
+ .tr-btn {
64
+ --c: var(--tr-primary);
65
+ --on: var(--tr-on-primary);
66
+ --cc: var(--tr-primary-container);
67
+ --on-cc: var(--tr-on-primary-container);
68
+ position: relative;
69
+ display: inline-flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ gap: 6px;
73
+ height: var(--tr-control-h);
74
+ min-width: var(--tr-control-h);
75
+ padding: 0 12px;
76
+ border: 1px solid transparent;
77
+ border-radius: var(--tr-radius-sm);
78
+ font: 600 var(--tr-text-md) / 1 var(--tr-font-sans);
79
+ text-decoration: none;
80
+ white-space: nowrap;
81
+ cursor: pointer;
82
+ user-select: none;
83
+ transition:
84
+ background-color var(--tr-dur) var(--tr-ease),
85
+ box-shadow var(--tr-dur) var(--tr-ease),
86
+ transform var(--tr-dur-fast) var(--tr-ease);
87
+ }
88
+ .tr-btn[data-size='sm'] { height: var(--tr-control-h-sm); min-width: var(--tr-control-h-sm); padding: 0 8px; font-size: var(--tr-text-sm); }
89
+ .tr-btn[data-size='lg'] { height: var(--tr-control-h-lg); min-width: var(--tr-control-h-lg); padding: 0 16px; font-size: var(--tr-text-lg); }
90
+ .tr-btn[data-icon-only='true'] { padding: 0; }
91
+
92
+ .tr-btn[data-intent='neutral'] { --c: var(--tr-on-surface); --on: var(--tr-surface); --cc: var(--tr-surface-container-high); --on-cc: var(--tr-on-surface); }
93
+ .tr-btn[data-intent='success'] { --c: var(--tr-success); --on: var(--tr-on-success); --cc: var(--tr-success-container); --on-cc: var(--tr-on-success-container); }
94
+ .tr-btn[data-intent='warning'] { --c: var(--tr-warning); --on: var(--tr-on-warning); --cc: var(--tr-warning-container); --on-cc: var(--tr-on-warning-container); }
95
+ .tr-btn[data-intent='danger'] { --c: var(--tr-danger); --on: var(--tr-on-danger); --cc: var(--tr-danger-container); --on-cc: var(--tr-on-danger-container); }
96
+
97
+ /* Primary filled: solid, high contrast */
98
+ .tr-btn[data-variant='filled'] { background: var(--c); color: var(--on); box-shadow: var(--tr-elevation-1); }
99
+ .tr-btn[data-variant='filled']:hover:not(:disabled) { background: color-mix(in srgb, var(--on) 10%, var(--c)); }
100
+ /* Tonal: low-contrast container, strong label — dense toolbars */
101
+ .tr-btn[data-variant='tonal'] { background: var(--cc); color: var(--on-cc); }
102
+ .tr-btn[data-variant='tonal']:hover:not(:disabled) { background: color-mix(in srgb, var(--on-cc) 10%, var(--cc)); }
103
+ /* Outlined: precise 1px border */
104
+ .tr-btn[data-variant='outlined'] { border-color: var(--tr-outline); color: var(--c); background: transparent; }
105
+ .tr-btn[data-variant='outlined']:hover:not(:disabled) { background: color-mix(in srgb, var(--c) 8%, transparent); }
106
+ .tr-btn[data-variant='minimal'] { color: var(--c); background: transparent; }
107
+ .tr-btn[data-variant='minimal']:hover:not(:disabled) { background: color-mix(in srgb, var(--c) 10%, transparent); }
108
+
109
+ .tr-btn:active:not(:disabled) { transform: scale(0.98); }
110
+ .tr-btn:focus-visible { outline: none; box-shadow: var(--tr-focus-ring); }
111
+ .tr-btn:disabled { opacity: 0.45; cursor: not-allowed; box-shadow: none; }
112
+
113
+ .spinner {
114
+ width: 14px; height: 14px; border-radius: 50%;
115
+ border: 2px solid currentColor; border-right-color: transparent;
116
+ animation: spin 0.7s linear infinite;
117
+ }
118
+ @keyframes spin { to { transform: rotate(360deg); } }
119
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLButtonAttributes } from 'svelte/elements';
3
+ import type { IconName } from '../icons/paths.js';
4
+ type Props = Omit<HTMLButtonAttributes, 'children'> & {
5
+ variant?: 'filled' | 'tonal' | 'outlined' | 'minimal';
6
+ intent?: 'primary' | 'neutral' | 'success' | 'warning' | 'danger';
7
+ size?: 'sm' | 'md' | 'lg';
8
+ icon?: IconName;
9
+ rightIcon?: IconName;
10
+ loading?: boolean;
11
+ href?: string;
12
+ children?: Snippet;
13
+ };
14
+ declare const Button: import("svelte").Component<Props, {}, "">;
15
+ type Button = ReturnType<typeof Button>;
16
+ export default Button;