@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,87 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ /**
5
+ * Anchored surface. By default place it inside a `position: relative` wrapper (`.tr-anchor`); it anchors to its
6
+ * parent, flips above when there is no room below, and closes on outside press or Escape.
7
+ * With `fixed` + `anchor` it positions against the viewport instead, so it can escape clipping/scrolling ancestors
8
+ * (tables, panels) — render it anywhere.
9
+ */
10
+ let {
11
+ open = $bindable(false),
12
+ onclose,
13
+ align = 'start',
14
+ fixed = false,
15
+ anchor,
16
+ children
17
+ }: {
18
+ open?: boolean;
19
+ /** `refocus` is true when closed by Escape, so the owner can return focus to its trigger. */
20
+ onclose?: (refocus: boolean) => void;
21
+ align?: 'start' | 'end';
22
+ /** Viewport-fixed positioning against `anchor`. */
23
+ fixed?: boolean;
24
+ anchor?: HTMLElement | null;
25
+ children: Snippet;
26
+ } = $props();
27
+
28
+ let el = $state<HTMLDivElement>();
29
+ let flip = $state(false);
30
+ let pos = $state({ top: 0, left: 0, minWidth: 0 });
31
+
32
+ function close(refocus: boolean) {
33
+ open = false;
34
+ onclose?.(refocus);
35
+ }
36
+
37
+ $effect(() => {
38
+ if (!open || !el) return;
39
+ const host = fixed && anchor ? anchor : el.parentElement!;
40
+ const place = () => {
41
+ const r = host.getBoundingClientRect();
42
+ const h = el!.offsetHeight, w = el!.offsetWidth;
43
+ const above = r.bottom + h + 8 > window.innerHeight && r.top > h + 8;
44
+ if (fixed) {
45
+ const left = align === 'end' ? r.right - w : r.left;
46
+ pos = { top: above ? r.top - h - 4 : r.bottom + 4, left: Math.min(Math.max(4, left), window.innerWidth - w - 4), minWidth: r.width };
47
+ }
48
+ flip = above;
49
+ };
50
+ place();
51
+ const down = (e: PointerEvent) => {
52
+ const t = e.target as Node;
53
+ if (!host.contains(t) && !el!.contains(t)) close(false);
54
+ };
55
+ const key = (e: KeyboardEvent) => { if (e.key === 'Escape' && !e.defaultPrevented) { e.preventDefault(); close(true); } };
56
+ window.addEventListener('pointerdown', down);
57
+ // Fixed popovers may live outside the anchor's subtree, so listen globally
58
+ const keyTarget: EventTarget = fixed ? window : host;
59
+ keyTarget.addEventListener('keydown', key as EventListener);
60
+ if (fixed) { window.addEventListener('scroll', place, true); window.addEventListener('resize', place); }
61
+ return () => {
62
+ window.removeEventListener('pointerdown', down);
63
+ keyTarget.removeEventListener('keydown', key as EventListener);
64
+ if (fixed) { window.removeEventListener('scroll', place, true); window.removeEventListener('resize', place); }
65
+ };
66
+ });
67
+ </script>
68
+
69
+ {#if open}
70
+ <div
71
+ class="tr-popover" bind:this={el} data-flip={flip} data-align={align} data-fixed={fixed}
72
+ style:top={fixed ? `${pos.top}px` : undefined} style:left={fixed ? `${pos.left}px` : undefined} style:min-width={fixed ? `${pos.minWidth}px` : undefined}
73
+ >{@render children()}</div>
74
+ {/if}
75
+
76
+ <style>
77
+ .tr-popover {
78
+ position: absolute; z-index: 50; top: calc(100% + 4px); left: 0; min-width: 100%;
79
+ background: var(--tr-surface-container-low); border: 1px solid var(--tr-outline-variant);
80
+ border-radius: var(--tr-radius-md); box-shadow: var(--tr-elevation-2);
81
+ animation: rise var(--tr-dur) var(--tr-ease);
82
+ }
83
+ .tr-popover[data-align='end'] { left: auto; right: 0; }
84
+ .tr-popover[data-flip='true'] { top: auto; bottom: calc(100% + 4px); }
85
+ .tr-popover[data-fixed='true'] { position: fixed; z-index: 300; right: auto; bottom: auto; }
86
+ @keyframes rise { from { opacity: 0; transform: translateY(-4px); } }
87
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ open?: boolean;
4
+ /** `refocus` is true when closed by Escape, so the owner can return focus to its trigger. */
5
+ onclose?: (refocus: boolean) => void;
6
+ align?: 'start' | 'end';
7
+ /** Viewport-fixed positioning against `anchor`. */
8
+ fixed?: boolean;
9
+ anchor?: HTMLElement | null;
10
+ children: Snippet;
11
+ };
12
+ declare const Popover: import("svelte").Component<$$ComponentProps, {}, "open">;
13
+ type Popover = ReturnType<typeof Popover>;
14
+ export default Popover;
@@ -0,0 +1,35 @@
1
+ <script lang="ts">
2
+ let {
3
+ value,
4
+ intent = 'primary',
5
+ label,
6
+ showValue = false,
7
+ size = 'md'
8
+ }: { value?: number; intent?: 'primary' | 'success' | 'warning' | 'danger' | 'neutral'; label?: string; showValue?: boolean; size?: 'sm' | 'md' } = $props();
9
+ const pct = $derived(value == null ? undefined : Math.min(100, Math.max(0, value)));
10
+ const id = $props.id();
11
+ </script>
12
+
13
+ <div class="pb" data-intent={intent} data-size={size}>
14
+ {#if label || showValue}
15
+ <div class="meta"><span id="{id}-l">{label}</span>{#if showValue && pct != null}<span class="num">{Math.round(pct)}%</span>{/if}</div>
16
+ {/if}
17
+ <div class="bar" role="progressbar" aria-labelledby={label ? `${id}-l` : undefined} aria-label={label ? undefined : 'Progress'} aria-valuemin="0" aria-valuemax="100" aria-valuenow={pct}>
18
+ <div class="fill" data-indeterminate={pct == null} style:width={pct == null ? undefined : `${pct}%`}></div>
19
+ </div>
20
+ </div>
21
+
22
+ <style>
23
+ .pb { --c: var(--tr-primary); }
24
+ .pb[data-intent='success'] { --c: var(--tr-success); }
25
+ .pb[data-intent='warning'] { --c: var(--tr-warning); }
26
+ .pb[data-intent='danger'] { --c: var(--tr-danger); }
27
+ .pb[data-intent='neutral'] { --c: var(--tr-on-surface-variant); }
28
+ .meta { display: flex; justify-content: space-between; margin-bottom: 4px; font-size: var(--tr-text-sm); font-weight: 600; color: var(--tr-on-surface-variant); }
29
+ .num { font-variant-numeric: tabular-nums; color: var(--tr-on-surface); }
30
+ .bar { position: relative; height: 6px; overflow: hidden; border-radius: 3px; background: var(--tr-surface-container-highest); }
31
+ .pb[data-size='sm'] .bar { height: 4px; }
32
+ .fill { height: 100%; border-radius: inherit; background: var(--c); transition: width var(--tr-dur) var(--tr-ease); }
33
+ .fill[data-indeterminate='true'] { position: absolute; width: 40%; animation: slide 1.2s var(--tr-ease) infinite; }
34
+ @keyframes slide { from { left: -40%; } to { left: 100%; } }
35
+ </style>
@@ -0,0 +1,10 @@
1
+ type $$ComponentProps = {
2
+ value?: number;
3
+ intent?: 'primary' | 'success' | 'warning' | 'danger' | 'neutral';
4
+ label?: string;
5
+ showValue?: boolean;
6
+ size?: 'sm' | 'md';
7
+ };
8
+ declare const ProgressBar: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type ProgressBar = ReturnType<typeof ProgressBar>;
10
+ export default ProgressBar;
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ let {
3
+ options,
4
+ value = $bindable(),
5
+ label,
6
+ orientation = 'vertical',
7
+ disabled = false
8
+ }: {
9
+ options: { value: string; label: string; disabled?: boolean }[];
10
+ value?: string;
11
+ label?: string;
12
+ orientation?: 'vertical' | 'horizontal';
13
+ disabled?: boolean;
14
+ } = $props();
15
+ const name = $props.id();
16
+ </script>
17
+
18
+ <fieldset class="rg" data-orientation={orientation} {disabled}>
19
+ {#if label}<legend class="tr-label">{label}</legend>{/if}
20
+ {#each options as o (o.value)}
21
+ <label class="opt" data-disabled={o.disabled || disabled}>
22
+ <input type="radio" class="tr-visually-hidden" {name} value={o.value} bind:group={value} disabled={o.disabled} />
23
+ <span class="dot"></span>{o.label}
24
+ </label>
25
+ {/each}
26
+ </fieldset>
27
+
28
+ <style>
29
+ .rg { margin: 0; padding: 0; border: 0; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
30
+ .rg[data-orientation='horizontal'] { flex-flow: row wrap; gap: 4px 16px; }
31
+ .rg legend { padding: 0; }
32
+ .opt { display: inline-flex; align-items: center; gap: 8px; min-height: 24px; cursor: pointer; }
33
+ .opt[data-disabled='true'] { opacity: 0.5; cursor: not-allowed; }
34
+ .dot { flex: none; width: 18px; height: 18px; border: 1px solid var(--tr-outline); border-radius: 50%; background: var(--tr-surface-container-low); display: grid; place-items: center; transition: box-shadow var(--tr-dur) var(--tr-ease); }
35
+ .dot::after { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--tr-primary); transform: scale(0); transition: transform var(--tr-dur-fast) var(--tr-ease); }
36
+ input:checked + .dot { border-color: var(--tr-primary); }
37
+ input:checked + .dot::after { transform: scale(1); }
38
+ input:focus-visible + .dot { box-shadow: var(--tr-focus-ring); }
39
+ </style>
@@ -0,0 +1,14 @@
1
+ type $$ComponentProps = {
2
+ options: {
3
+ value: string;
4
+ label: string;
5
+ disabled?: boolean;
6
+ }[];
7
+ value?: string;
8
+ label?: string;
9
+ orientation?: 'vertical' | 'horizontal';
10
+ disabled?: boolean;
11
+ };
12
+ declare const RadioGroup: import("svelte").Component<$$ComponentProps, {}, "value">;
13
+ type RadioGroup = ReturnType<typeof RadioGroup>;
14
+ export default RadioGroup;
@@ -0,0 +1,56 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Icon from '../icons/Icon.svelte';
4
+ import type { IconName } from '../icons/paths.js';
5
+
6
+ let {
7
+ title,
8
+ subtitle,
9
+ icon,
10
+ collapsible = false,
11
+ open = $bindable(true),
12
+ actions,
13
+ children
14
+ }: {
15
+ title: string;
16
+ subtitle?: string;
17
+ icon?: IconName;
18
+ collapsible?: boolean;
19
+ open?: boolean;
20
+ /** Right-aligned header controls. */
21
+ actions?: Snippet;
22
+ children: Snippet;
23
+ } = $props();
24
+ const id = $props.id();
25
+ </script>
26
+
27
+ <section class="sec" aria-labelledby="{id}-t">
28
+ <header>
29
+ {#if collapsible}
30
+ <button type="button" class="tog" aria-expanded={open} aria-controls="{id}-b" onclick={() => (open = !open)}>
31
+ <Icon name="chevron-right" />
32
+ <span class="tx"><span class="t" id="{id}-t">{title}</span>{#if subtitle}<span class="s">{subtitle}</span>{/if}</span>
33
+ </button>
34
+ {:else}
35
+ {#if icon}<Icon name={icon} size={20} />{/if}
36
+ <span class="tx"><span class="t" id="{id}-t">{title}</span>{#if subtitle}<span class="s">{subtitle}</span>{/if}</span>
37
+ {/if}
38
+ {#if actions}<div class="act">{@render actions()}</div>{/if}
39
+ </header>
40
+ {#if open}<div class="body" id="{id}-b">{@render children()}</div>{/if}
41
+ </section>
42
+
43
+ <style>
44
+ .sec { background: var(--tr-surface-container-low); border: 1px solid var(--tr-outline-variant); border-radius: var(--tr-radius-md); box-shadow: var(--tr-elevation-1); overflow: hidden; }
45
+ header { display: flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 8px 0 12px; background: var(--tr-surface-container); }
46
+ .sec:has(.body) header { border-bottom: 1px solid var(--tr-outline-variant); }
47
+ .tog { display: flex; align-items: center; gap: 8px; flex: 1; min-height: 40px; margin-left: -4px; padding: 0 4px; border: 0; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
48
+ .tog:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--tr-primary); }
49
+ .tog :global(svg) { transition: transform var(--tr-dur) var(--tr-ease); }
50
+ .tog[aria-expanded='true'] :global(svg) { transform: rotate(90deg); }
51
+ .tx { display: flex; flex-direction: column; flex: 1; line-height: 1.25; }
52
+ .t { font-weight: 700; }
53
+ .s { font-size: var(--tr-text-sm); color: var(--tr-on-surface-variant); }
54
+ .act { display: flex; gap: 4px; margin-left: auto; }
55
+ .body { padding: 16px; }
56
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { IconName } from '../icons/paths.js';
3
+ type $$ComponentProps = {
4
+ title: string;
5
+ subtitle?: string;
6
+ icon?: IconName;
7
+ collapsible?: boolean;
8
+ open?: boolean;
9
+ /** Right-aligned header controls. */
10
+ actions?: Snippet;
11
+ children: Snippet;
12
+ };
13
+ declare const Section: import("svelte").Component<$$ComponentProps, {}, "open">;
14
+ type Section = ReturnType<typeof Section>;
15
+ export default Section;
@@ -0,0 +1,56 @@
1
+ <script lang="ts">
2
+ import Icon from '../icons/Icon.svelte';
3
+ import type { IconName } from '../icons/paths.js';
4
+
5
+ let {
6
+ options,
7
+ value = $bindable(),
8
+ size = 'md',
9
+ fill = false,
10
+ label = 'Options'
11
+ }: {
12
+ options: { value: string; label?: string; /** Name for icon-only options (defaults to the value). */ ariaLabel?: string; icon?: IconName; disabled?: boolean }[];
13
+ value?: string;
14
+ size?: 'sm' | 'md';
15
+ fill?: boolean;
16
+ label?: string;
17
+ } = $props();
18
+
19
+ let root: HTMLDivElement;
20
+ const current = $derived(value ?? options.find((o) => !o.disabled)?.value);
21
+
22
+ function onKey(e: KeyboardEvent) {
23
+ const enabled = options.filter((o) => !o.disabled);
24
+ const i = enabled.findIndex((o) => o.value === current);
25
+ const d = e.key === 'ArrowRight' || e.key === 'ArrowDown' ? 1 : e.key === 'ArrowLeft' || e.key === 'ArrowUp' ? -1 : 0;
26
+ if (!d) return;
27
+ e.preventDefault();
28
+ const next = enabled[(i + d + enabled.length) % enabled.length];
29
+ value = next.value;
30
+ root.querySelector<HTMLElement>(`[data-v="${CSS.escape(next.value)}"]`)?.focus();
31
+ }
32
+ </script>
33
+
34
+ <!-- svelte-ignore a11y_interactive_supports_focus -->
35
+ <div class="seg" bind:this={root} role="radiogroup" aria-label={label} data-size={size} data-fill={fill} onkeydown={onKey}>
36
+ {#each options as o (o.value)}
37
+ <button
38
+ type="button" role="radio" data-v={o.value} aria-label={o.label ? undefined : (o.ariaLabel ?? o.value)} aria-checked={o.value === current} disabled={o.disabled}
39
+ tabindex={o.value === current ? 0 : -1} onclick={() => (value = o.value)}
40
+ >
41
+ {#if o.icon}<Icon name={o.icon} />{/if}{#if o.label}{o.label}{/if}
42
+ </button>
43
+ {/each}
44
+ </div>
45
+
46
+ <style>
47
+ .seg { display: inline-flex; padding: 2px; gap: 2px; background: var(--tr-surface-container-high); border: 1px solid var(--tr-outline-variant); border-radius: var(--tr-radius-md); }
48
+ .seg[data-fill='true'] { display: flex; }
49
+ .seg[data-fill='true'] button { flex: 1; }
50
+ button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: calc(var(--tr-control-h) - 6px); min-width: 28px; padding: 0 12px; border: 0; border-radius: var(--tr-radius-sm); background: none; color: var(--tr-on-surface-variant); font: 600 var(--tr-text-md) var(--tr-font-sans); cursor: pointer; transition: background-color var(--tr-dur) var(--tr-ease), color var(--tr-dur) var(--tr-ease); }
51
+ .seg[data-size='sm'] button { height: 20px; padding: 0 8px; font-size: var(--tr-text-sm); }
52
+ button:hover:not(:disabled):not([aria-checked='true']) { background: var(--tr-surface-container-highest); color: var(--tr-on-surface); }
53
+ button[aria-checked='true'] { background: var(--tr-surface); color: var(--tr-primary); box-shadow: var(--tr-elevation-1); }
54
+ button:focus-visible { outline: none; box-shadow: var(--tr-focus-ring); }
55
+ button:disabled { opacity: 0.45; cursor: not-allowed; }
56
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { IconName } from '../icons/paths.js';
2
+ type $$ComponentProps = {
3
+ options: {
4
+ value: string;
5
+ label?: string; /** Name for icon-only options (defaults to the value). */
6
+ ariaLabel?: string;
7
+ icon?: IconName;
8
+ disabled?: boolean;
9
+ }[];
10
+ value?: string;
11
+ size?: 'sm' | 'md';
12
+ fill?: boolean;
13
+ label?: string;
14
+ };
15
+ declare const SegmentedControl: import("svelte").Component<$$ComponentProps, {}, "value">;
16
+ type SegmentedControl = ReturnType<typeof SegmentedControl>;
17
+ export default SegmentedControl;
@@ -0,0 +1,204 @@
1
+ <script lang="ts" generics="V extends string | number">
2
+ import { tick } from 'svelte';
3
+ import Icon from '../icons/Icon.svelte';
4
+ import type { IconName } from '../icons/paths.js';
5
+
6
+ type Option = { value: V; label: string; icon?: IconName; disabled?: boolean };
7
+
8
+ let {
9
+ options,
10
+ value = $bindable(),
11
+ label,
12
+ ariaLabel,
13
+ placeholder = 'Select…',
14
+ searchable = true,
15
+ disabled = false,
16
+ error,
17
+ size = 'md'
18
+ }: {
19
+ options: Option[];
20
+ value?: V;
21
+ label?: string;
22
+ /** Accessible name when no visible `label` is shown. */
23
+ ariaLabel?: string;
24
+ placeholder?: string;
25
+ searchable?: boolean;
26
+ disabled?: boolean;
27
+ error?: string;
28
+ size?: 'sm' | 'md' | 'lg';
29
+ } = $props();
30
+
31
+ const uid = $props.id();
32
+ let open = $state(false);
33
+ let query = $state('');
34
+ let active = $state(0);
35
+ let root: HTMLDivElement;
36
+ let trigger: HTMLButtonElement;
37
+ let inputEl = $state<HTMLInputElement>();
38
+ let listEl = $state<HTMLElement>();
39
+
40
+ const filtered = $derived(options.filter((o) => o.label.toLowerCase().includes(query.trim().toLowerCase())));
41
+ const selected = $derived(options.find((o) => o.value === value));
42
+
43
+ async function show() {
44
+ if (disabled) return;
45
+ open = true;
46
+ query = '';
47
+ active = Math.max(0, options.findIndex((o) => o.value === value));
48
+ await tick();
49
+ (searchable ? inputEl : listEl)?.focus();
50
+ }
51
+ function close(refocus = true) {
52
+ open = false;
53
+ if (refocus) trigger?.focus();
54
+ }
55
+ function choose(o?: Option) {
56
+ if (!o || o.disabled) return;
57
+ value = o.value;
58
+ close();
59
+ }
60
+ function step(dir: 1 | -1) {
61
+ const n = filtered.length;
62
+ if (!n) return;
63
+ let i = active;
64
+ for (let k = 0; k < n; k++) {
65
+ i = (i + dir + n) % n;
66
+ if (!filtered[i].disabled) break;
67
+ }
68
+ active = i;
69
+ }
70
+ function onKey(e: KeyboardEvent) {
71
+ switch (e.key) {
72
+ case 'ArrowDown': step(1); break;
73
+ case 'ArrowUp': step(-1); break;
74
+ case 'Home': active = 0; break;
75
+ case 'End': active = filtered.length - 1; break;
76
+ case 'Enter': choose(filtered[active]); break;
77
+ case 'Escape': close(); break;
78
+ case 'Tab': close(false); return;
79
+ default: return;
80
+ }
81
+ e.preventDefault();
82
+ }
83
+ function onTriggerKey(e: KeyboardEvent) {
84
+ if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) {
85
+ e.preventDefault();
86
+ show();
87
+ }
88
+ }
89
+ $effect(() => {
90
+ if (open) document.getElementById(`${uid}-opt-${active}`)?.scrollIntoView({ block: 'nearest' });
91
+ });
92
+ </script>
93
+
94
+ <svelte:window onpointerdown={(e) => open && !root.contains(e.target as Node) && close(false)} />
95
+
96
+ <div class="tr-field" bind:this={root}>
97
+ {#if label}<span class="tr-label" id="{uid}-label">{label}</span>{/if}
98
+ <button
99
+ bind:this={trigger}
100
+ type="button"
101
+ class="trigger"
102
+ data-size={size}
103
+ data-invalid={!!error}
104
+ {disabled}
105
+ role="combobox"
106
+ aria-haspopup="listbox"
107
+ aria-expanded={open}
108
+ aria-controls="{uid}-list"
109
+ aria-label={label ? undefined : ariaLabel}
110
+ aria-labelledby={label ? `${uid}-label` : undefined}
111
+ onclick={() => (open ? close() : show())}
112
+ onkeydown={onTriggerKey}
113
+ >
114
+ {#if selected?.icon}<Icon name={selected.icon} />{/if}
115
+ <span class="value" data-placeholder={!selected}>{selected?.label ?? placeholder}</span>
116
+ <Icon name="chevron-down" />
117
+ </button>
118
+ {#if error}<div class="tr-hint" data-error="true">{error}</div>{/if}
119
+
120
+ {#if open}
121
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
122
+ <div class="popover" onkeydown={onKey}>
123
+ {#if searchable}
124
+ <div class="filter">
125
+ <Icon name="search" />
126
+ <input
127
+ bind:this={inputEl}
128
+ bind:value={query}
129
+ oninput={() => (active = 0)}
130
+ placeholder="Filter…"
131
+ aria-label="Filter options"
132
+ aria-controls="{uid}-list"
133
+ aria-activedescendant="{uid}-opt-{active}"
134
+ />
135
+ </div>
136
+ {/if}
137
+ <ul
138
+ id="{uid}-list"
139
+ role="listbox"
140
+ tabindex="-1"
141
+ bind:this={listEl}
142
+ aria-labelledby={label ? `${uid}-label` : undefined}
143
+ >
144
+ {#each filtered as o, i (o.value)}
145
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
146
+ <li
147
+ id="{uid}-opt-{i}"
148
+ role="option"
149
+ aria-selected={o.value === value}
150
+ aria-disabled={o.disabled}
151
+ data-active={i === active}
152
+ onpointermove={() => (active = i)}
153
+ onclick={() => choose(o)}
154
+ >
155
+ {#if o.icon}<Icon name={o.icon} />{/if}
156
+ <span>{o.label}</span>
157
+ {#if o.value === value}<Icon name="tick" />{/if}
158
+ </li>
159
+ {:else}
160
+ <li class="empty" role="presentation">No matches</li>
161
+ {/each}
162
+ </ul>
163
+ </div>
164
+ {/if}
165
+ </div>
166
+
167
+ <style>
168
+ .tr-field { position: relative; }
169
+ .trigger {
170
+ display: flex; align-items: center; gap: 6px; width: 100%;
171
+ height: var(--tr-control-h); padding: 0 10px; text-align: left;
172
+ background: var(--tr-surface-container-low); color: var(--tr-on-surface);
173
+ border: 1px solid var(--tr-outline-variant); border-radius: var(--tr-radius-sm);
174
+ font: inherit; cursor: pointer;
175
+ transition: background-color var(--tr-dur) var(--tr-ease), box-shadow var(--tr-dur) var(--tr-ease);
176
+ }
177
+ .trigger[data-size='sm'] { height: var(--tr-control-h-sm); font-size: var(--tr-text-sm); }
178
+ .trigger[data-size='lg'] { height: var(--tr-control-h-lg); }
179
+ .trigger:hover:not(:disabled) { background: var(--tr-surface-container); border-color: var(--tr-outline); }
180
+ .trigger:focus-visible, .trigger[aria-expanded='true'] {
181
+ outline: none; background: var(--tr-surface-container); border-color: var(--tr-primary);
182
+ box-shadow: inset 0 -2px 0 var(--tr-primary), var(--tr-focus-ring);
183
+ }
184
+ .trigger[data-invalid='true'] { border-color: var(--tr-danger); }
185
+ .trigger:disabled { opacity: 0.5; cursor: not-allowed; }
186
+ .value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
187
+ .value[data-placeholder='true'] { color: var(--tr-placeholder); }
188
+
189
+ .popover {
190
+ position: absolute; z-index: 50; top: calc(100% + 4px); left: 0; right: 0; min-width: 180px;
191
+ background: var(--tr-surface-container-low); border: 1px solid var(--tr-outline-variant);
192
+ border-radius: var(--tr-radius-md); box-shadow: var(--tr-elevation-2); overflow: hidden;
193
+ }
194
+ .filter { display: flex; align-items: center; gap: 6px; height: var(--tr-control-h); padding: 0 10px; border-bottom: 1px solid var(--tr-outline-variant); color: var(--tr-on-surface-variant); }
195
+ .filter input { flex: 1; min-width: 0; height: 100%; border: 0; outline: 0; background: none; color: var(--tr-on-surface); font: inherit; }
196
+ .filter input::placeholder { color: var(--tr-placeholder); }
197
+ ul { margin: 0; padding: 4px; list-style: none; max-height: 240px; overflow-y: auto; outline: none; }
198
+ li { display: flex; align-items: center; gap: 8px; height: var(--tr-control-h); padding: 0 8px; border-radius: var(--tr-radius-sm); cursor: pointer; }
199
+ li span { flex: 1; }
200
+ li[data-active='true'] { background: var(--tr-surface-container-highest); }
201
+ li[aria-selected='true'] { color: var(--tr-primary); font-weight: 600; }
202
+ li[aria-disabled='true'] { opacity: 0.45; cursor: not-allowed; }
203
+ li.empty { color: var(--tr-on-surface-variant); cursor: default; }
204
+ </style>
@@ -0,0 +1,41 @@
1
+ import type { IconName } from '../icons/paths.js';
2
+ declare function $$render<V extends string | number>(): {
3
+ props: {
4
+ options: {
5
+ value: V;
6
+ label: string;
7
+ icon?: IconName;
8
+ disabled?: boolean;
9
+ }[];
10
+ value?: V;
11
+ label?: string;
12
+ /** Accessible name when no visible `label` is shown. */
13
+ ariaLabel?: string;
14
+ placeholder?: string;
15
+ searchable?: boolean;
16
+ disabled?: boolean;
17
+ error?: string;
18
+ size?: "sm" | "md" | "lg";
19
+ };
20
+ exports: {};
21
+ bindings: "value";
22
+ slots: {};
23
+ events: {};
24
+ };
25
+ declare class __sveltets_Render<V extends string | number> {
26
+ props(): ReturnType<typeof $$render<V>>['props'];
27
+ events(): ReturnType<typeof $$render<V>>['events'];
28
+ slots(): ReturnType<typeof $$render<V>>['slots'];
29
+ bindings(): "value";
30
+ exports(): {};
31
+ }
32
+ interface $$IsomorphicComponent {
33
+ new <V extends string | number>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<V>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<V>['props']>, ReturnType<__sveltets_Render<V>['events']>, ReturnType<__sveltets_Render<V>['slots']>> & {
34
+ $$bindings?: ReturnType<__sveltets_Render<V>['bindings']>;
35
+ } & ReturnType<__sveltets_Render<V>['exports']>;
36
+ <V extends string | number>(internal: unknown, props: ReturnType<__sveltets_Render<V>['props']> & {}): ReturnType<__sveltets_Render<V>['exports']>;
37
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
38
+ }
39
+ declare const Select: $$IsomorphicComponent;
40
+ type Select<V extends string | number> = InstanceType<typeof Select<V>>;
41
+ export default Select;