@ruckc/truss-ui 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/README.md +91 -2
- package/dist/color.d.ts +21 -0
- package/dist/color.js +32 -0
- package/dist/components/Accordion.svelte +70 -0
- package/dist/components/Accordion.svelte.d.ts +15 -0
- package/dist/components/Alert.svelte +46 -0
- package/dist/components/Alert.svelte.d.ts +15 -0
- package/dist/components/Avatar.svelte +32 -0
- package/dist/components/Avatar.svelte.d.ts +9 -0
- package/dist/components/AvatarGroup.svelte +20 -0
- package/dist/components/AvatarGroup.svelte.d.ts +11 -0
- package/dist/components/Badge.svelte +27 -0
- package/dist/components/Badge.svelte.d.ts +9 -0
- package/dist/components/Breadcrumbs.svelte +38 -0
- package/dist/components/Breadcrumbs.svelte.d.ts +12 -0
- package/dist/components/Button.svelte +119 -0
- package/dist/components/Button.svelte.d.ts +16 -0
- package/dist/components/ButtonGroup.svelte +23 -0
- package/dist/components/ButtonGroup.svelte.d.ts +9 -0
- package/dist/components/Calendar.svelte +166 -0
- package/dist/components/Calendar.svelte.d.ts +19 -0
- package/dist/components/Callout.svelte +50 -0
- package/dist/components/Callout.svelte.d.ts +14 -0
- package/dist/components/Card.svelte +50 -0
- package/dist/components/Card.svelte.d.ts +15 -0
- package/dist/components/Checkbox.svelte +37 -0
- package/dist/components/Checkbox.svelte.d.ts +16 -0
- package/dist/components/ColorPicker.svelte +164 -0
- package/dist/components/ColorPicker.svelte.d.ts +14 -0
- package/dist/components/Combobox.svelte +131 -0
- package/dist/components/Combobox.svelte.d.ts +19 -0
- package/dist/components/ContextMenu.svelte +51 -0
- package/dist/components/ContextMenu.svelte.d.ts +10 -0
- package/dist/components/DataTable.svelte +657 -0
- package/dist/components/DataTable.svelte.d.ts +76 -0
- package/dist/components/DatePicker.svelte +70 -0
- package/dist/components/DatePicker.svelte.d.ts +15 -0
- package/dist/components/DateRangePicker.svelte +92 -0
- package/dist/components/DateRangePicker.svelte.d.ts +15 -0
- package/dist/components/DateTimePicker.svelte +62 -0
- package/dist/components/DateTimePicker.svelte.d.ts +19 -0
- package/dist/components/DateTimeRangePicker.svelte +167 -0
- package/dist/components/DateTimeRangePicker.svelte.d.ts +22 -0
- package/dist/components/Dialog.svelte +70 -0
- package/dist/components/Dialog.svelte.d.ts +14 -0
- package/dist/components/Divider.svelte +16 -0
- package/dist/components/Divider.svelte.d.ts +8 -0
- package/dist/components/Drawer.svelte +64 -0
- package/dist/components/Drawer.svelte.d.ts +13 -0
- package/dist/components/DurationInput.svelte +112 -0
- package/dist/components/DurationInput.svelte.d.ts +19 -0
- package/dist/components/EditableText.svelte +73 -0
- package/dist/components/EditableText.svelte.d.ts +14 -0
- package/dist/components/EmptyState.svelte +30 -0
- package/dist/components/EmptyState.svelte.d.ts +12 -0
- package/dist/components/FileUpload.svelte +143 -0
- package/dist/components/FileUpload.svelte.d.ts +18 -0
- package/dist/components/FormGroup.svelte +27 -0
- package/dist/components/FormGroup.svelte.d.ts +13 -0
- package/dist/components/Input.svelte +62 -0
- package/dist/components/Input.svelte.d.ts +15 -0
- package/dist/components/InputAddon.svelte +21 -0
- package/dist/components/InputAddon.svelte.d.ts +9 -0
- package/dist/components/InputGroup.svelte +70 -0
- package/dist/components/InputGroup.svelte.d.ts +16 -0
- package/dist/components/Menu.svelte +95 -0
- package/dist/components/Menu.svelte.d.ts +15 -0
- package/dist/components/MenuButton.svelte +45 -0
- package/dist/components/MenuButton.svelte.d.ts +16 -0
- package/dist/components/MultiSelect.svelte +129 -0
- package/dist/components/MultiSelect.svelte.d.ts +18 -0
- package/dist/components/Navbar.svelte +17 -0
- package/dist/components/Navbar.svelte.d.ts +10 -0
- package/dist/components/NumberInput.svelte +66 -0
- package/dist/components/NumberInput.svelte.d.ts +16 -0
- package/dist/components/OptionList.svelte +74 -0
- package/dist/components/OptionList.svelte.d.ts +15 -0
- package/dist/components/Popover.svelte +87 -0
- package/dist/components/Popover.svelte.d.ts +14 -0
- package/dist/components/ProgressBar.svelte +35 -0
- package/dist/components/ProgressBar.svelte.d.ts +10 -0
- package/dist/components/RadioGroup.svelte +39 -0
- package/dist/components/RadioGroup.svelte.d.ts +14 -0
- package/dist/components/Section.svelte +56 -0
- package/dist/components/Section.svelte.d.ts +15 -0
- package/dist/components/SegmentedControl.svelte +56 -0
- package/dist/components/SegmentedControl.svelte.d.ts +17 -0
- package/dist/components/Select.svelte +204 -0
- package/dist/components/Select.svelte.d.ts +41 -0
- package/dist/components/Shell.svelte +143 -0
- package/dist/components/Shell.svelte.d.ts +36 -0
- package/dist/components/Sidebar.svelte +84 -0
- package/dist/components/Sidebar.svelte.d.ts +24 -0
- package/dist/components/Skeleton.svelte +25 -0
- package/dist/components/Skeleton.svelte.d.ts +9 -0
- package/dist/components/Slider.svelte +121 -0
- package/dist/components/Slider.svelte.d.ts +16 -0
- package/dist/components/Spinner.svelte +30 -0
- package/dist/components/Spinner.svelte.d.ts +9 -0
- package/dist/components/SplitButton.svelte +113 -0
- package/dist/components/SplitButton.svelte.d.ts +14 -0
- package/dist/components/SplitPane.svelte +106 -0
- package/dist/components/SplitPane.svelte.d.ts +16 -0
- package/dist/components/Stepper.svelte +73 -0
- package/dist/components/Stepper.svelte.d.ts +26 -0
- package/dist/components/Switch.svelte +21 -0
- package/dist/components/Switch.svelte.d.ts +10 -0
- package/dist/components/Tabs.svelte +74 -0
- package/dist/components/Tabs.svelte.d.ts +18 -0
- package/dist/components/Tag.svelte +49 -0
- package/dist/components/Tag.svelte.d.ts +16 -0
- package/dist/components/TagInput.svelte +72 -0
- package/dist/components/TagInput.svelte.d.ts +17 -0
- package/dist/components/Textarea.svelte +37 -0
- package/dist/components/Textarea.svelte.d.ts +11 -0
- package/dist/components/TimeList.svelte +76 -0
- package/dist/components/TimeList.svelte.d.ts +10 -0
- package/dist/components/TimePicker.svelte +79 -0
- package/dist/components/TimePicker.svelte.d.ts +15 -0
- package/dist/components/TimeRangePicker.svelte +44 -0
- package/dist/components/TimeRangePicker.svelte.d.ts +17 -0
- package/dist/components/Timeline.svelte +38 -0
- package/dist/components/Timeline.svelte.d.ts +17 -0
- package/dist/components/Toaster.svelte +35 -0
- package/dist/components/Toaster.svelte.d.ts +18 -0
- package/dist/components/Tooltip.svelte +66 -0
- package/dist/components/Tooltip.svelte.d.ts +11 -0
- package/dist/components/Tree.svelte +147 -0
- package/dist/components/Tree.svelte.d.ts +16 -0
- package/dist/date.d.ts +19 -0
- package/dist/date.js +43 -0
- package/dist/duration.d.ts +11 -0
- package/dist/duration.js +44 -0
- package/dist/fonts/LICENSE-Inter.txt +93 -0
- package/dist/fonts/LICENSE-JetBrainsMono.txt +93 -0
- package/dist/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/dist/icons/Icon.svelte +30 -0
- package/dist/icons/Icon.svelte.d.ts +9 -0
- package/dist/icons/paths.d.ts +39 -0
- package/dist/icons/paths.js +43 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +65 -0
- package/dist/styles/base.css +71 -0
- package/dist/styles/fonts.css +21 -0
- package/dist/styles/index.css +3 -0
- package/dist/styles/tokens.css +143 -0
- package/dist/time.d.ts +7 -0
- package/dist/time.js +30 -0
- package/dist/toast.svelte.d.ts +22 -0
- package/dist/toast.svelte.js +19 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.js +1 -0
- package/package.json +76 -5
|
@@ -0,0 +1,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;
|