@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,64 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import Button from './Button.svelte';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
open = $bindable(false),
|
|
7
|
+
title,
|
|
8
|
+
position = 'right',
|
|
9
|
+
size = 'md',
|
|
10
|
+
children,
|
|
11
|
+
footer,
|
|
12
|
+
onclose
|
|
13
|
+
}: {
|
|
14
|
+
open?: boolean;
|
|
15
|
+
title: string;
|
|
16
|
+
position?: 'left' | 'right' | 'bottom';
|
|
17
|
+
size?: 'sm' | 'md' | 'lg';
|
|
18
|
+
children: Snippet;
|
|
19
|
+
footer?: Snippet;
|
|
20
|
+
onclose?: () => void;
|
|
21
|
+
} = $props();
|
|
22
|
+
|
|
23
|
+
let el: HTMLDialogElement;
|
|
24
|
+
const uid = $props.id();
|
|
25
|
+
|
|
26
|
+
$effect(() => {
|
|
27
|
+
if (open && !el.open) el.showModal();
|
|
28
|
+
else if (!open && el.open) el.close();
|
|
29
|
+
});
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<dialog
|
|
33
|
+
bind:this={el} class="tr-drawer" data-pos={position} data-size={size} aria-labelledby="{uid}-t"
|
|
34
|
+
onclose={() => { open = false; onclose?.(); }}
|
|
35
|
+
onclick={(e) => e.target === el && (open = false)}
|
|
36
|
+
>
|
|
37
|
+
<div class="frame">
|
|
38
|
+
<header>
|
|
39
|
+
<h2 id="{uid}-t">{title}</h2>
|
|
40
|
+
<Button variant="minimal" intent="neutral" size="sm" icon="cross" aria-label="Close drawer" onclick={() => (open = false)} />
|
|
41
|
+
</header>
|
|
42
|
+
<div class="body">{@render children()}</div>
|
|
43
|
+
{#if footer}<footer>{@render footer()}</footer>{/if}
|
|
44
|
+
</div>
|
|
45
|
+
</dialog>
|
|
46
|
+
|
|
47
|
+
<style>
|
|
48
|
+
.tr-drawer { margin: 0; padding: 0; border: 1px solid var(--tr-outline-variant); background: var(--tr-surface-container-low); color: var(--tr-on-surface); box-shadow: var(--tr-elevation-4); overflow: hidden; max-width: 100vw; max-height: 100vh; }
|
|
49
|
+
.tr-drawer::backdrop { background: var(--tr-scrim); backdrop-filter: blur(3px); }
|
|
50
|
+
.tr-drawer[data-pos='right'], .tr-drawer[data-pos='left'] { top: 0; bottom: 0; height: 100vh; width: var(--w, 420px); border-radius: var(--tr-radius-lg) 0 0 var(--tr-radius-lg); }
|
|
51
|
+
.tr-drawer[data-pos='right'] { left: auto; right: 0; animation: in-r var(--tr-dur) var(--tr-ease); }
|
|
52
|
+
.tr-drawer[data-pos='left'] { left: 0; right: auto; border-radius: 0 var(--tr-radius-lg) var(--tr-radius-lg) 0; animation: in-l var(--tr-dur) var(--tr-ease); }
|
|
53
|
+
.tr-drawer[data-pos='bottom'] { top: auto; bottom: 0; left: 0; right: 0; width: 100vw; height: var(--h, 320px); border-radius: var(--tr-radius-lg) var(--tr-radius-lg) 0 0; animation: in-b var(--tr-dur) var(--tr-ease); }
|
|
54
|
+
.tr-drawer[data-size='sm'] { --w: 320px; --h: 220px; }
|
|
55
|
+
.tr-drawer[data-size='lg'] { --w: 640px; --h: 480px; }
|
|
56
|
+
.frame { display: flex; flex-direction: column; height: 100%; }
|
|
57
|
+
header { display: flex; align-items: center; justify-content: space-between; height: 44px; flex: none; padding: 0 8px 0 16px; background: var(--tr-surface-container); border-bottom: 1px solid var(--tr-outline-variant); }
|
|
58
|
+
h2 { margin: 0; font-size: var(--tr-text-lg); }
|
|
59
|
+
.body { flex: 1; padding: 16px; overflow: auto; }
|
|
60
|
+
footer { display: flex; justify-content: flex-end; gap: 8px; flex: none; padding: 10px 16px; background: var(--tr-surface-container); border-top: 1px solid var(--tr-outline-variant); }
|
|
61
|
+
@keyframes in-r { from { transform: translateX(24px); opacity: 0; } }
|
|
62
|
+
@keyframes in-l { from { transform: translateX(-24px); opacity: 0; } }
|
|
63
|
+
@keyframes in-b { from { transform: translateY(24px); opacity: 0; } }
|
|
64
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
title: string;
|
|
5
|
+
position?: 'left' | 'right' | 'bottom';
|
|
6
|
+
size?: 'sm' | 'md' | 'lg';
|
|
7
|
+
children: Snippet;
|
|
8
|
+
footer?: Snippet;
|
|
9
|
+
onclose?: () => void;
|
|
10
|
+
};
|
|
11
|
+
declare const Drawer: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
12
|
+
type Drawer = ReturnType<typeof Drawer>;
|
|
13
|
+
export default Drawer;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from './Button.svelte';
|
|
3
|
+
import NumberInput from './NumberInput.svelte';
|
|
4
|
+
import Popover from './Popover.svelte';
|
|
5
|
+
import { decompose, formatDuration, parseDuration, UNIT_NAMES, UNIT_SECONDS, type DurationUnit } from '../duration.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
value = $bindable(null),
|
|
9
|
+
label,
|
|
10
|
+
ariaLabel,
|
|
11
|
+
units = ['d', 'h', 'm'],
|
|
12
|
+
min = 0,
|
|
13
|
+
max,
|
|
14
|
+
presets = [900, 1800, 3600, 4 * 3600, 86400],
|
|
15
|
+
placeholder,
|
|
16
|
+
disabled = false,
|
|
17
|
+
error
|
|
18
|
+
}: {
|
|
19
|
+
/** Seconds. */
|
|
20
|
+
value?: number | null;
|
|
21
|
+
label?: string;
|
|
22
|
+
/** Accessible name when no visible `label` is shown. */
|
|
23
|
+
ariaLabel?: string;
|
|
24
|
+
/** Enabled units, any of d/h/m/s. A bare number is read in the smallest one. */
|
|
25
|
+
units?: DurationUnit[];
|
|
26
|
+
min?: number;
|
|
27
|
+
max?: number;
|
|
28
|
+
presets?: number[];
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
error?: string;
|
|
32
|
+
} = $props();
|
|
33
|
+
|
|
34
|
+
const id = $props.id();
|
|
35
|
+
const order: DurationUnit[] = ['d', 'h', 'm', 's'];
|
|
36
|
+
const active = $derived(order.filter((u) => units.includes(u)));
|
|
37
|
+
let open = $state(false);
|
|
38
|
+
let text = $state('');
|
|
39
|
+
let bad = $state(false);
|
|
40
|
+
let input: HTMLInputElement;
|
|
41
|
+
|
|
42
|
+
$effect(() => { text = value == null ? '' : formatDuration(value, active); bad = false; });
|
|
43
|
+
|
|
44
|
+
const clamp = (s: number) => Math.min(max ?? Infinity, Math.max(min, s));
|
|
45
|
+
function commit() {
|
|
46
|
+
const t = text.trim();
|
|
47
|
+
if (!t) { value = null; bad = false; return; }
|
|
48
|
+
const s = parseDuration(t, active);
|
|
49
|
+
bad = s == null;
|
|
50
|
+
if (s != null) { value = clamp(s); text = formatDuration(value, active); }
|
|
51
|
+
}
|
|
52
|
+
const parts = $derived(decompose(value ?? 0, active));
|
|
53
|
+
function setPart(u: DurationUnit, n: number | null) {
|
|
54
|
+
const next = { ...parts, [u]: Math.max(0, n ?? 0) };
|
|
55
|
+
value = clamp(active.reduce((a, k) => a + next[k] * UNIT_SECONDS[k], 0));
|
|
56
|
+
}
|
|
57
|
+
/** A unit's cap is the size of the next larger enabled unit (so 60 min rolls into hours). */
|
|
58
|
+
const cap = (u: DurationUnit) => {
|
|
59
|
+
const i = active.indexOf(u);
|
|
60
|
+
return i > 0 ? UNIT_SECONDS[active[i - 1]] / UNIT_SECONDS[u] - 1 : undefined;
|
|
61
|
+
};
|
|
62
|
+
const msg = $derived(error ?? (bad ? 'Try 1h 30m, 90m, 1:30 or PT1H30M' : undefined));
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<div class="tr-field">
|
|
66
|
+
{#if label}<label class="tr-label" for={id}>{label}</label>{/if}
|
|
67
|
+
<div class="tr-anchor">
|
|
68
|
+
<div class="tr-control" data-trail="true" data-invalid={!!msg} data-disabled={disabled}>
|
|
69
|
+
<input
|
|
70
|
+
class="tr-bare" {id} aria-label={label ? undefined : ariaLabel} bind:this={input} bind:value={text} {disabled} autocomplete="off"
|
|
71
|
+
placeholder={placeholder ?? `e.g. ${formatDuration(5400, active)}`} aria-haspopup="dialog" aria-expanded={open}
|
|
72
|
+
aria-invalid={!!msg || undefined} onblur={commit}
|
|
73
|
+
onkeydown={(e) => {
|
|
74
|
+
if (e.key === 'Enter') commit();
|
|
75
|
+
else if (e.key === 'ArrowDown' && e.altKey) open = true;
|
|
76
|
+
else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
const stepS = UNIT_SECONDS[active.at(-1)!] * (active.at(-1) === 'm' ? 5 : 1);
|
|
79
|
+
value = clamp((value ?? 0) + (e.key === 'ArrowUp' ? stepS : -stepS));
|
|
80
|
+
}
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
<Button variant="minimal" intent="neutral" size="sm" icon="clock" aria-label="Edit duration" {disabled} onclick={() => (open = !open)} />
|
|
84
|
+
</div>
|
|
85
|
+
<Popover bind:open onclose={(r) => r && input.focus()}>
|
|
86
|
+
<div class="panel">
|
|
87
|
+
<div class="fields">
|
|
88
|
+
{#each active as u (u)}
|
|
89
|
+
<NumberInput label={UNIT_NAMES[u]} min={0} max={cap(u)} bind:value={() => parts[u], (n) => setPart(u, n)} />
|
|
90
|
+
{/each}
|
|
91
|
+
</div>
|
|
92
|
+
{#if presets.length}
|
|
93
|
+
<div class="presets" role="group" aria-label="Presets">
|
|
94
|
+
{#each presets as p}<Button size="sm" variant={value === p ? 'tonal' : 'outlined'} intent="neutral" onclick={() => (value = clamp(p))}>{formatDuration(p, active)}</Button>{/each}
|
|
95
|
+
</div>
|
|
96
|
+
{/if}
|
|
97
|
+
<div class="foot">
|
|
98
|
+
<Button variant="minimal" size="sm" onclick={() => (value = null)}>Clear</Button>
|
|
99
|
+
<Button variant="tonal" size="sm" onclick={() => { open = false; input.focus(); }}>Done</Button>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</Popover>
|
|
103
|
+
</div>
|
|
104
|
+
{#if msg}<div class="tr-hint" data-error="true">{msg}</div>{/if}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<style>
|
|
108
|
+
.panel { width: 320px; max-width: calc(100vw - 32px); padding: 10px; }
|
|
109
|
+
.fields { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 8px; }
|
|
110
|
+
.presets { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 10px; }
|
|
111
|
+
.foot { display: flex; justify-content: space-between; margin-top: 10px; padding-top: 6px; border-top: 1px solid var(--tr-outline-variant); }
|
|
112
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type DurationUnit } from '../duration.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** Seconds. */
|
|
4
|
+
value?: number | null;
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Accessible name when no visible `label` is shown. */
|
|
7
|
+
ariaLabel?: string;
|
|
8
|
+
/** Enabled units, any of d/h/m/s. A bare number is read in the smallest one. */
|
|
9
|
+
units?: DurationUnit[];
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
presets?: number[];
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
error?: string;
|
|
16
|
+
};
|
|
17
|
+
declare const DurationInput: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
18
|
+
type DurationInput = ReturnType<typeof DurationInput>;
|
|
19
|
+
export default DurationInput;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import Icon from '../icons/Icon.svelte';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
value = $bindable(''),
|
|
7
|
+
placeholder = 'Click to edit',
|
|
8
|
+
multiline = false,
|
|
9
|
+
selectAll = true,
|
|
10
|
+
disabled = false,
|
|
11
|
+
label = 'Edit text',
|
|
12
|
+
onconfirm
|
|
13
|
+
}: {
|
|
14
|
+
value?: string;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
/** Enter inserts a newline; Ctrl/⌘+Enter commits. */
|
|
17
|
+
multiline?: boolean;
|
|
18
|
+
selectAll?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** Accessible name of the edit control. */
|
|
21
|
+
label?: string;
|
|
22
|
+
onconfirm?: (value: string) => void;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
let editing = $state(false);
|
|
26
|
+
let draft = $state('');
|
|
27
|
+
let field = $state<HTMLInputElement | HTMLTextAreaElement>();
|
|
28
|
+
|
|
29
|
+
async function start() {
|
|
30
|
+
if (disabled) return;
|
|
31
|
+
draft = value;
|
|
32
|
+
editing = true;
|
|
33
|
+
await tick();
|
|
34
|
+
field?.focus();
|
|
35
|
+
if (selectAll) field?.select();
|
|
36
|
+
}
|
|
37
|
+
function commit() {
|
|
38
|
+
if (!editing) return;
|
|
39
|
+
editing = false;
|
|
40
|
+
if (draft !== value) { value = draft; onconfirm?.(draft); }
|
|
41
|
+
}
|
|
42
|
+
const cancel = () => (editing = false);
|
|
43
|
+
function onKey(e: KeyboardEvent) {
|
|
44
|
+
if (e.key === 'Escape') { e.preventDefault(); cancel(); }
|
|
45
|
+
else if (e.key === 'Enter' && (!multiline || e.ctrlKey || e.metaKey)) { e.preventDefault(); commit(); }
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
{#if editing}
|
|
50
|
+
{#if multiline}
|
|
51
|
+
<textarea class="ed" bind:this={field} bind:value={draft} rows="3" aria-label={label} onkeydown={onKey} onblur={commit}></textarea>
|
|
52
|
+
{:else}
|
|
53
|
+
<input class="ed" bind:this={field} bind:value={draft} aria-label={label} onkeydown={onKey} onblur={commit} />
|
|
54
|
+
{/if}
|
|
55
|
+
{:else}
|
|
56
|
+
<button type="button" class="view" {disabled} aria-label="{label}: {value || placeholder}" onclick={start}>
|
|
57
|
+
<span class="tx" data-empty={!value}>{value || placeholder}</span>
|
|
58
|
+
{#if !disabled}<Icon name="edit" />{/if}
|
|
59
|
+
</button>
|
|
60
|
+
{/if}
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.view { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; min-height: 28px; padding: 0 6px; margin: 0 -6px; border: 0; border-radius: var(--tr-radius-sm); background: none; color: inherit; font: inherit; text-align: left; cursor: text; }
|
|
64
|
+
.view:disabled { cursor: default; }
|
|
65
|
+
.view:hover:not(:disabled) { background: var(--tr-surface-container-high); }
|
|
66
|
+
.view:focus-visible { outline: none; box-shadow: var(--tr-focus-ring); }
|
|
67
|
+
.view :global(svg) { opacity: 0; color: var(--tr-on-surface-variant); flex: none; transition: opacity var(--tr-dur) var(--tr-ease); }
|
|
68
|
+
.view:hover :global(svg), .view:focus-visible :global(svg) { opacity: 1; }
|
|
69
|
+
.tx { white-space: pre-wrap; overflow-wrap: break-word; }
|
|
70
|
+
.tx[data-empty='true'] { color: var(--tr-placeholder); font-style: italic; }
|
|
71
|
+
.ed { width: 100%; min-height: 28px; padding: 2px 6px; margin: 0 -6px; box-sizing: content-box; border: 1px solid var(--tr-primary); border-radius: var(--tr-radius-sm); background: var(--tr-surface-container); color: var(--tr-on-surface); font: inherit; outline: none; box-shadow: inset 0 -2px 0 var(--tr-primary), var(--tr-focus-ring); }
|
|
72
|
+
textarea.ed { resize: vertical; line-height: 1.4; }
|
|
73
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value?: string;
|
|
3
|
+
placeholder?: string;
|
|
4
|
+
/** Enter inserts a newline; Ctrl/⌘+Enter commits. */
|
|
5
|
+
multiline?: boolean;
|
|
6
|
+
selectAll?: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
/** Accessible name of the edit control. */
|
|
9
|
+
label?: string;
|
|
10
|
+
onconfirm?: (value: string) => void;
|
|
11
|
+
};
|
|
12
|
+
declare const EditableText: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
13
|
+
type EditableText = ReturnType<typeof EditableText>;
|
|
14
|
+
export default EditableText;
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
description,
|
|
9
|
+
icon = 'search',
|
|
10
|
+
intent = 'neutral',
|
|
11
|
+
actions
|
|
12
|
+
}: { title: string; description?: string; icon?: IconName; intent?: 'neutral' | 'danger'; actions?: Snippet } = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="es" data-intent={intent}>
|
|
16
|
+
<span class="ic"><Icon name={icon} /></span>
|
|
17
|
+
<div class="t">{title}</div>
|
|
18
|
+
{#if description}<p>{description}</p>{/if}
|
|
19
|
+
{#if actions}<div class="a">{@render actions()}</div>{/if}
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<style>
|
|
23
|
+
.es { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 32px 16px; text-align: center; color: var(--tr-on-surface-variant); }
|
|
24
|
+
.ic { display: grid; place-items: center; width: 64px; height: 64px; margin-bottom: 6px; border-radius: 50%; background: var(--tr-surface-container-high); }
|
|
25
|
+
.ic :global(svg) { width: 32px; height: 32px; stroke-width: 1; }
|
|
26
|
+
.es[data-intent='danger'] .ic { background: var(--tr-danger-container); color: var(--tr-on-danger-container); }
|
|
27
|
+
.t { font-size: var(--tr-text-lg); font-weight: 700; color: var(--tr-on-surface); }
|
|
28
|
+
p { margin: 0; max-width: 360px; line-height: 1.45; }
|
|
29
|
+
.a { display: flex; gap: 8px; margin-top: 10px; }
|
|
30
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { IconName } from '../icons/paths.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
icon?: IconName;
|
|
7
|
+
intent?: 'neutral' | 'danger';
|
|
8
|
+
actions?: Snippet;
|
|
9
|
+
};
|
|
10
|
+
declare const EmptyState: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type EmptyState = ReturnType<typeof EmptyState>;
|
|
12
|
+
export default EmptyState;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from './Button.svelte';
|
|
3
|
+
import Icon from '../icons/Icon.svelte';
|
|
4
|
+
import ProgressBar from './ProgressBar.svelte';
|
|
5
|
+
import type { UploadItem } from '../types.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
files = $bindable([]),
|
|
9
|
+
accept,
|
|
10
|
+
multiple = true,
|
|
11
|
+
maxSize,
|
|
12
|
+
maxFiles,
|
|
13
|
+
upload,
|
|
14
|
+
label = 'Upload files',
|
|
15
|
+
hint,
|
|
16
|
+
disabled = false
|
|
17
|
+
}: {
|
|
18
|
+
files?: UploadItem[];
|
|
19
|
+
/** e.g. ".csv,image/*,application/pdf" */
|
|
20
|
+
accept?: string;
|
|
21
|
+
multiple?: boolean;
|
|
22
|
+
/** Bytes. */
|
|
23
|
+
maxSize?: number;
|
|
24
|
+
maxFiles?: number;
|
|
25
|
+
/** Your uploader. Report progress 0–100; throw to mark the file failed. Omit to just collect files. */
|
|
26
|
+
upload?: (file: File, onProgress: (pct: number) => void, signal: AbortSignal) => Promise<void>;
|
|
27
|
+
label?: string;
|
|
28
|
+
hint?: string;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
let input: HTMLInputElement;
|
|
33
|
+
let over = $state(false);
|
|
34
|
+
let depth = 0;
|
|
35
|
+
const ctrls = new Map<string, AbortController>();
|
|
36
|
+
|
|
37
|
+
const fmt = (n: number) => (n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1048576).toFixed(1)} MB`);
|
|
38
|
+
function accepted(f: File) {
|
|
39
|
+
if (!accept) return true;
|
|
40
|
+
return accept.split(',').map((a) => a.trim().toLowerCase()).filter(Boolean).some((a) =>
|
|
41
|
+
a.startsWith('.') ? f.name.toLowerCase().endsWith(a) : a.endsWith('/*') ? f.type.toLowerCase().startsWith(a.slice(0, -1)) : f.type.toLowerCase() === a);
|
|
42
|
+
}
|
|
43
|
+
const patch = (id: string, p: Partial<UploadItem>) => (files = files.map((x) => (x.id === id ? { ...x, ...p } : x)));
|
|
44
|
+
|
|
45
|
+
async function run(it: UploadItem) {
|
|
46
|
+
if (!upload) return;
|
|
47
|
+
const c = new AbortController();
|
|
48
|
+
ctrls.set(it.id, c);
|
|
49
|
+
patch(it.id, { status: 'uploading', progress: 0, error: undefined });
|
|
50
|
+
try {
|
|
51
|
+
await upload(it.file, (pct) => patch(it.id, { progress: Math.min(100, Math.max(0, pct)) }), c.signal);
|
|
52
|
+
patch(it.id, { status: 'done', progress: 100 });
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (!c.signal.aborted) patch(it.id, { status: 'error', error: e instanceof Error ? e.message : 'Upload failed' });
|
|
55
|
+
} finally { ctrls.delete(it.id); }
|
|
56
|
+
}
|
|
57
|
+
function add(list: FileList | File[]) {
|
|
58
|
+
let incoming = [...list];
|
|
59
|
+
if (!multiple) { files = []; incoming = incoming.slice(0, 1); }
|
|
60
|
+
const fresh: UploadItem[] = incoming.map((file) => {
|
|
61
|
+
const id = crypto.randomUUID();
|
|
62
|
+
let error: string | undefined;
|
|
63
|
+
if (!accepted(file)) error = 'File type not allowed';
|
|
64
|
+
else if (maxSize != null && file.size > maxSize) error = `Larger than ${fmt(maxSize)}`;
|
|
65
|
+
else if (maxFiles != null && files.filter((f) => f.status !== 'error').length + incoming.indexOf(file) >= maxFiles) error = `Limit of ${maxFiles} files`;
|
|
66
|
+
return { id, file, status: error ? 'error' : 'ready', progress: 0, error };
|
|
67
|
+
});
|
|
68
|
+
files = [...files, ...fresh];
|
|
69
|
+
fresh.filter((f) => f.status === 'ready').forEach(run);
|
|
70
|
+
}
|
|
71
|
+
function remove(it: UploadItem) {
|
|
72
|
+
ctrls.get(it.id)?.abort();
|
|
73
|
+
files = files.filter((f) => f.id !== it.id);
|
|
74
|
+
}
|
|
75
|
+
function drop(e: DragEvent) {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
over = false; depth = 0;
|
|
78
|
+
if (!disabled && e.dataTransfer?.files.length) add(e.dataTransfer.files);
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<div class="fu">
|
|
83
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
84
|
+
<div
|
|
85
|
+
class="zone" role="button" tabindex={disabled ? -1 : 0} aria-label={label} aria-disabled={disabled} data-over={over}
|
|
86
|
+
onclick={() => !disabled && input.click()}
|
|
87
|
+
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && !disabled && (e.preventDefault(), input.click())}
|
|
88
|
+
ondragenter={(e) => { e.preventDefault(); depth++; over = true; }}
|
|
89
|
+
ondragover={(e) => e.preventDefault()}
|
|
90
|
+
ondragleave={() => { if (--depth <= 0) { over = false; depth = 0; } }}
|
|
91
|
+
ondrop={drop}
|
|
92
|
+
>
|
|
93
|
+
<span class="ic"><Icon name="upload" size={20} /></span>
|
|
94
|
+
<div><strong>Drop files here</strong> or <span class="link">browse</span></div>
|
|
95
|
+
{#if hint || accept || maxSize}
|
|
96
|
+
<div class="hint">{hint ?? [accept && `Accepted: ${accept}`, maxSize != null && `Max ${fmt(maxSize)}`].filter(Boolean).join(' · ')}</div>
|
|
97
|
+
{/if}
|
|
98
|
+
<input bind:this={input} type="file" class="tr-visually-hidden" {accept} {multiple} {disabled} tabindex="-1" aria-hidden="true"
|
|
99
|
+
onchange={(e) => { if (e.currentTarget.files) add(e.currentTarget.files); e.currentTarget.value = ''; }} />
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{#if files.length}
|
|
103
|
+
<ul class="list" aria-label="Files">
|
|
104
|
+
{#each files as it (it.id)}
|
|
105
|
+
<li data-status={it.status}>
|
|
106
|
+
<span class="fi"><Icon name={it.status === 'done' ? 'success' : it.status === 'error' ? 'error' : 'file'} /></span>
|
|
107
|
+
<div class="meta">
|
|
108
|
+
<div class="name"><span>{it.file.name}</span><span class="size">{fmt(it.file.size)}</span></div>
|
|
109
|
+
{#if it.status === 'uploading'}<ProgressBar value={it.progress} size="sm" />
|
|
110
|
+
{:else if it.status === 'error'}<div class="err">{it.error}</div>{/if}
|
|
111
|
+
</div>
|
|
112
|
+
{#if it.status === 'error' && upload && accepted(it.file) && !it.error?.startsWith('Larger') && !it.error?.startsWith('Limit')}
|
|
113
|
+
<Button size="sm" variant="minimal" icon="refresh" aria-label="Retry {it.file.name}" onclick={() => run(it)} />
|
|
114
|
+
{/if}
|
|
115
|
+
<Button size="sm" variant="minimal" intent="neutral" icon={it.status === 'uploading' ? 'cross' : 'trash'} aria-label="{it.status === 'uploading' ? 'Cancel' : 'Remove'} {it.file.name}" onclick={() => remove(it)} />
|
|
116
|
+
</li>
|
|
117
|
+
{/each}
|
|
118
|
+
</ul>
|
|
119
|
+
{/if}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<style>
|
|
123
|
+
.zone { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 24px 16px; text-align: center; color: var(--tr-on-surface-variant); background: var(--tr-surface-container-low); border: 1.5px dashed var(--tr-outline); border-radius: var(--tr-radius-md); cursor: pointer; transition: background-color var(--tr-dur) var(--tr-ease), border-color var(--tr-dur) var(--tr-ease); }
|
|
124
|
+
.zone:hover:not([aria-disabled='true']) { background: var(--tr-surface-container); }
|
|
125
|
+
.zone[data-over='true'] { border-color: var(--tr-primary); background: color-mix(in srgb, var(--tr-primary-container) 55%, transparent); border-style: solid; }
|
|
126
|
+
.zone:focus-visible { outline: none; border-color: var(--tr-primary); box-shadow: var(--tr-focus-ring); }
|
|
127
|
+
.zone[aria-disabled='true'] { opacity: 0.5; cursor: not-allowed; }
|
|
128
|
+
strong { color: var(--tr-on-surface); }
|
|
129
|
+
.link { color: var(--tr-primary); font-weight: 600; text-decoration: underline; }
|
|
130
|
+
.ic { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; background: var(--tr-primary-container); color: var(--tr-on-primary-container); }
|
|
131
|
+
.hint { font-size: var(--tr-text-sm); }
|
|
132
|
+
.list { margin: 8px 0 0; padding: 0; list-style: none; display: grid; gap: 6px; }
|
|
133
|
+
li { display: flex; align-items: center; gap: 10px; padding: 6px 6px 6px 10px; background: var(--tr-surface-container-low); border: 1px solid var(--tr-outline-variant); border-radius: var(--tr-radius-md); }
|
|
134
|
+
li[data-status='error'] { border-color: var(--tr-danger); }
|
|
135
|
+
.fi { color: var(--tr-on-surface-variant); }
|
|
136
|
+
li[data-status='done'] .fi { color: var(--tr-success); }
|
|
137
|
+
li[data-status='error'] .fi { color: var(--tr-danger); }
|
|
138
|
+
.meta { flex: 1; min-width: 0; }
|
|
139
|
+
.name { display: flex; gap: 8px; justify-content: space-between; }
|
|
140
|
+
.name span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
|
|
141
|
+
.size { color: var(--tr-on-surface-variant); font-size: var(--tr-text-sm); font-variant-numeric: tabular-nums; flex: none; }
|
|
142
|
+
.err { color: var(--tr-danger); font-size: var(--tr-text-sm); }
|
|
143
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { UploadItem } from '../types.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
files?: UploadItem[];
|
|
4
|
+
/** e.g. ".csv,image/*,application/pdf" */
|
|
5
|
+
accept?: string;
|
|
6
|
+
multiple?: boolean;
|
|
7
|
+
/** Bytes. */
|
|
8
|
+
maxSize?: number;
|
|
9
|
+
maxFiles?: number;
|
|
10
|
+
/** Your uploader. Report progress 0–100; throw to mark the file failed. Omit to just collect files. */
|
|
11
|
+
upload?: (file: File, onProgress: (pct: number) => void, signal: AbortSignal) => Promise<void>;
|
|
12
|
+
label?: string;
|
|
13
|
+
hint?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
};
|
|
16
|
+
declare const FileUpload: import("svelte").Component<$$ComponentProps, {}, "files">;
|
|
17
|
+
type FileUpload = ReturnType<typeof FileUpload>;
|
|
18
|
+
export default FileUpload;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
label,
|
|
6
|
+
labelFor,
|
|
7
|
+
helper,
|
|
8
|
+
error,
|
|
9
|
+
required = false,
|
|
10
|
+
inline = false,
|
|
11
|
+
children
|
|
12
|
+
}: { label: string; labelFor?: string; helper?: string; error?: string; required?: boolean; inline?: boolean; children: Snippet } = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="fg" data-inline={inline}>
|
|
16
|
+
<label class="tr-label" for={labelFor}>{label}{#if required}<span class="req" aria-hidden="true"> *</span>{/if}</label>
|
|
17
|
+
<div class="ctl">
|
|
18
|
+
{@render children()}
|
|
19
|
+
{#if error}<div class="tr-hint" data-error="true" role="alert">{error}</div>{:else if helper}<div class="tr-hint">{helper}</div>{/if}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
.fg[data-inline='true'] { display: grid; grid-template-columns: 160px minmax(0, 1fr); gap: 12px; align-items: start; }
|
|
25
|
+
.fg[data-inline='true'] .tr-label { margin: 0; padding-top: 7px; }
|
|
26
|
+
.req { color: var(--tr-danger); }
|
|
27
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
label: string;
|
|
4
|
+
labelFor?: string;
|
|
5
|
+
helper?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
inline?: boolean;
|
|
9
|
+
children: Snippet;
|
|
10
|
+
};
|
|
11
|
+
declare const FormGroup: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type FormGroup = ReturnType<typeof FormGroup>;
|
|
13
|
+
export default FormGroup;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
4
|
+
import Icon from '../icons/Icon.svelte';
|
|
5
|
+
import type { IconName } from '../icons/paths.js';
|
|
6
|
+
|
|
7
|
+
type Props = Omit<HTMLInputAttributes, 'size'> & {
|
|
8
|
+
value?: string | number | null;
|
|
9
|
+
label?: string;
|
|
10
|
+
hint?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
icon?: IconName;
|
|
13
|
+
right?: Snippet;
|
|
14
|
+
size?: 'sm' | 'md' | 'lg';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let { value = $bindable(), label, hint, error, icon, right, size = 'md', class: klass = '', ...rest }: Props = $props();
|
|
18
|
+
const uid = $props.id();
|
|
19
|
+
const id = $derived(rest.id ?? uid);
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class="tr-field {klass}">
|
|
23
|
+
{#if label}<label class="tr-label" for={id}>{label}</label>{/if}
|
|
24
|
+
<div class="control" data-size={size} data-invalid={!!error} data-disabled={rest.disabled}>
|
|
25
|
+
{#if icon}<span class="slot left"><Icon name={icon} /></span>{/if}
|
|
26
|
+
<input {...rest} {id} bind:value aria-invalid={!!error || undefined} aria-describedby={error || hint ? `${id}-msg` : undefined} />
|
|
27
|
+
{#if right}<span class="slot right">{@render right()}</span>{/if}
|
|
28
|
+
</div>
|
|
29
|
+
{#if error || hint}<div class="tr-hint" id="{id}-msg" data-error={!!error}>{error ?? hint}</div>{/if}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<style>
|
|
33
|
+
.control {
|
|
34
|
+
--ring: var(--tr-primary);
|
|
35
|
+
display: flex; align-items: center; gap: 6px;
|
|
36
|
+
height: var(--tr-control-h); padding: 0 10px;
|
|
37
|
+
background: var(--tr-surface-container-low);
|
|
38
|
+
border: 1px solid var(--tr-outline-variant);
|
|
39
|
+
border-radius: var(--tr-radius-sm);
|
|
40
|
+
color: var(--tr-on-surface-variant);
|
|
41
|
+
transition: background-color var(--tr-dur) var(--tr-ease), box-shadow var(--tr-dur) var(--tr-ease), border-color var(--tr-dur) var(--tr-ease);
|
|
42
|
+
}
|
|
43
|
+
.control[data-size='sm'] { height: var(--tr-control-h-sm); font-size: var(--tr-text-sm); }
|
|
44
|
+
.control[data-size='lg'] { height: var(--tr-control-h-lg); }
|
|
45
|
+
/* Material tonal fill on hover */
|
|
46
|
+
.control:hover:not([data-disabled='true']) { background: var(--tr-surface-container); border-color: var(--tr-outline); }
|
|
47
|
+
/* Focus: tonal fill + 2px bottom indicator + soft glow — border width never changes, so layout doesn't shift */
|
|
48
|
+
.control:focus-within {
|
|
49
|
+
background: var(--tr-surface-container);
|
|
50
|
+
border-color: var(--ring);
|
|
51
|
+
box-shadow: inset 0 -2px 0 var(--ring), var(--tr-focus-ring);
|
|
52
|
+
}
|
|
53
|
+
.control[data-invalid='true'] { --ring: var(--tr-danger); border-color: var(--tr-danger); }
|
|
54
|
+
.control[data-invalid='true']:focus-within { box-shadow: inset 0 -2px 0 var(--ring), 0 0 0 3px color-mix(in srgb, var(--tr-danger) 30%, transparent); }
|
|
55
|
+
.control[data-disabled='true'] { opacity: 0.5; cursor: not-allowed; }
|
|
56
|
+
input {
|
|
57
|
+
flex: 1; min-width: 0; height: 100%; padding: 0; border: 0; outline: 0; background: none;
|
|
58
|
+
color: var(--tr-on-surface); font: inherit;
|
|
59
|
+
}
|
|
60
|
+
input::placeholder { color: var(--tr-placeholder); opacity: 1; }
|
|
61
|
+
.slot { display: inline-flex; align-items: center; }
|
|
62
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import type { IconName } from '../icons/paths.js';
|
|
4
|
+
type Props = Omit<HTMLInputAttributes, 'size'> & {
|
|
5
|
+
value?: string | number | null;
|
|
6
|
+
label?: string;
|
|
7
|
+
hint?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
icon?: IconName;
|
|
10
|
+
right?: Snippet;
|
|
11
|
+
size?: 'sm' | 'md' | 'lg';
|
|
12
|
+
};
|
|
13
|
+
declare const Input: import("svelte").Component<Props, {}, "value">;
|
|
14
|
+
type Input = ReturnType<typeof Input>;
|
|
15
|
+
export default Input;
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
/** Static text or an icon attached to an input inside an <InputGroup>, e.g. `https://`, `$`, `.com`, `USD`. */
|
|
7
|
+
let { icon, children }: { icon?: IconName; children?: Snippet } = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<span class="addon">
|
|
11
|
+
{#if icon}<Icon name={icon} />{/if}{#if children}{@render children()}{/if}
|
|
12
|
+
</span>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.addon {
|
|
16
|
+
display: inline-flex; align-items: center; gap: 6px; box-sizing: border-box; height: var(--tr-control-h); padding: 0 10px;
|
|
17
|
+
background: var(--tr-surface-container-high); color: var(--tr-on-surface-variant);
|
|
18
|
+
border: 1px solid var(--tr-outline-variant); border-radius: var(--tr-radius-sm);
|
|
19
|
+
font: 600 var(--tr-text-md) / 1 var(--tr-font-sans); white-space: nowrap; user-select: none;
|
|
20
|
+
}
|
|
21
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { IconName } from '../icons/paths.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
icon?: IconName;
|
|
5
|
+
children?: Snippet;
|
|
6
|
+
};
|
|
7
|
+
declare const InputAddon: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type InputAddon = ReturnType<typeof InputAddon>;
|
|
9
|
+
export default InputAddon;
|