@dorsk/tsumikit 0.1.0 → 0.2.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/README.md +4 -2
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +30 -0
- package/dist/components/atoms/Badge.svelte +65 -3
- package/dist/components/atoms/Badge.svelte.d.ts +4 -0
- package/dist/components/atoms/Button.svelte +80 -8
- package/dist/components/atoms/Button.svelte.d.ts +4 -0
- package/dist/components/atoms/Checkbox.svelte +7 -1
- package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
- package/dist/components/atoms/Dot.svelte +67 -0
- package/dist/components/atoms/Dot.svelte.d.ts +12 -0
- package/dist/components/atoms/Input.svelte +28 -2
- package/dist/components/atoms/Input.svelte.d.ts +5 -1
- package/dist/components/atoms/Select.svelte +9 -2
- package/dist/components/atoms/Select.svelte.d.ts +2 -0
- package/dist/components/atoms/Slider.svelte +5 -4
- package/dist/components/atoms/Switch.svelte +6 -1
- package/dist/components/atoms/Switch.svelte.d.ts +1 -0
- package/dist/components/atoms/Textarea.svelte +26 -1
- package/dist/components/atoms/Textarea.svelte.d.ts +4 -0
- package/dist/components/layouts/AppShell.svelte +15 -8
- package/dist/components/molecules/Accordion.svelte +6 -3
- package/dist/components/molecules/CopyButton.svelte +2 -26
- package/dist/components/molecules/FileButton.svelte +45 -3
- package/dist/components/molecules/IconButton.svelte +23 -3
- package/dist/components/molecules/IconButton.svelte.d.ts +2 -0
- package/dist/components/molecules/Modal.svelte +15 -4
- package/dist/components/molecules/Modal.svelte.d.ts +3 -0
- package/dist/components/molecules/OptionButton.svelte +30 -20
- package/dist/components/molecules/Popover.svelte +46 -25
- package/dist/components/molecules/Popover.svelte.d.ts +7 -2
- package/dist/components/molecules/SelectButton.svelte +20 -16
- package/dist/components/molecules/Tabs.svelte +26 -7
- package/dist/components/molecules/Tabs.svelte.d.ts +2 -0
- package/dist/components/molecules/Toggle.svelte +30 -15
- package/dist/components/molecules/Tooltip.svelte +41 -28
- package/dist/components/molecules/Tooltip.svelte.d.ts +1 -1
- package/dist/components/organisms/DataTable.svelte +85 -4
- package/dist/components/organisms/DataTable.svelte.d.ts +6 -0
- package/dist/floating.d.ts +10 -0
- package/dist/floating.js +56 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/styles/app.css +14 -234
- package/dist/styles/variables.css +1 -1
- package/package.json +4 -3
- package/dist/components/atoms/Chip.svelte +0 -53
- package/dist/components/atoms/Chip.svelte.d.ts +0 -11
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ import { Button, Field, Input, Modal, ThemePicker } from '@dorsk/tsumikit';
|
|
|
69
69
|
## Components
|
|
70
70
|
|
|
71
71
|
**Atoms:** Text, Heading, Button, Input, Textarea, Select, Switch, Checkbox,
|
|
72
|
-
Slider, Progress, Card, Badge,
|
|
72
|
+
Slider, Progress, Card, Badge, Dot, Link, Icon (open registry — pass a
|
|
73
73
|
`children` snippet for any custom SVG).
|
|
74
74
|
|
|
75
75
|
**Molecules:** Field, IconButton, SelectButton, Toggle, OptionButton, Modal,
|
|
@@ -95,6 +95,8 @@ Use the `.cq-*` utilities (`.cq-hide`, `.cq-stack`, `.cq-truncate`,
|
|
|
95
95
|
you drag the sidebar down to that icon rail (width persisted).
|
|
96
96
|
|
|
97
97
|
**Stores:** `theme`, `toasts`, `fontScale` (opt-in). **Actions:** `autoresize`.
|
|
98
|
+
**Helpers:** `copyToClipboard(text)` — async Clipboard API with an
|
|
99
|
+
insecure-context fallback; returns whether it succeeded.
|
|
98
100
|
|
|
99
101
|
## Sizing & zoom
|
|
100
102
|
|
|
@@ -121,7 +123,7 @@ them in JS — less code, better a11y, fewer edge cases:
|
|
|
121
123
|
- **`color-scheme`** per theme so native widgets/scrollbars match; **`@media
|
|
122
124
|
(forced-colors)`** (Windows High Contrast) and **`prefers-contrast`** support;
|
|
123
125
|
**`prefers-reduced-motion`** disables animation globally.
|
|
124
|
-
- **Intrinsic responsive layout**:
|
|
126
|
+
- **Intrinsic responsive layout**: `<AutoGrid>` (auto-fit + `minmax`) and `.cq`
|
|
125
127
|
(container queries) adapt to available space, not just viewport breakpoints.
|
|
126
128
|
|
|
127
129
|
## Syntax highlighting (CodeBlock)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function copyToClipboard(value: string): Promise<boolean>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Copy `value` to the clipboard. Prefers the async Clipboard API and falls back
|
|
2
|
+
// to a hidden-textarea + execCommand for insecure contexts / older browsers.
|
|
3
|
+
// Returns whether the copy succeeded; never throws. UI concerns (toasts, a
|
|
4
|
+
// transient "copied" state) belong to the caller.
|
|
5
|
+
export async function copyToClipboard(value) {
|
|
6
|
+
try {
|
|
7
|
+
if (navigator.clipboard?.writeText) {
|
|
8
|
+
await navigator.clipboard.writeText(value);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
/* fall through to legacy path */
|
|
14
|
+
}
|
|
15
|
+
// Fallback for insecure contexts / older browsers.
|
|
16
|
+
try {
|
|
17
|
+
const ta = document.createElement('textarea');
|
|
18
|
+
ta.value = value;
|
|
19
|
+
ta.style.position = 'fixed';
|
|
20
|
+
ta.style.opacity = '0';
|
|
21
|
+
document.body.appendChild(ta);
|
|
22
|
+
ta.select();
|
|
23
|
+
const ok = document.execCommand('copy');
|
|
24
|
+
document.body.removeChild(ta);
|
|
25
|
+
return ok;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
//
|
|
2
|
+
// Inline label primitive — the project's single pill. Covers three jobs via
|
|
3
|
+
// props rather than separate components:
|
|
4
|
+
// • state → `tone` (neutral/ok/warn/danger/info) semantic palette
|
|
5
|
+
// • info → `mono` for paths/ids/code-ish metadata
|
|
6
|
+
// • tag → `removable` renders a dismiss button + fires `onremove`
|
|
3
7
|
// Polymorphic via `as` so it can be a static <span> or an interactive
|
|
4
|
-
// <button
|
|
5
|
-
// their specifics (per-machine hue, toggle state).
|
|
8
|
+
// <button>. `size="sm"` is the compact form for counts/dense rows.
|
|
6
9
|
import type { Snippet } from 'svelte';
|
|
7
10
|
|
|
8
11
|
type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
|
|
@@ -10,12 +13,20 @@
|
|
|
10
13
|
let {
|
|
11
14
|
tone = 'neutral',
|
|
12
15
|
as = 'span',
|
|
16
|
+
size = 'md',
|
|
17
|
+
mono = false,
|
|
18
|
+
removable = false,
|
|
19
|
+
onremove,
|
|
13
20
|
class: klass = '',
|
|
14
21
|
children,
|
|
15
22
|
...rest
|
|
16
23
|
}: {
|
|
17
24
|
tone?: Tone;
|
|
18
25
|
as?: 'span' | 'button';
|
|
26
|
+
size?: 'sm' | 'md';
|
|
27
|
+
mono?: boolean;
|
|
28
|
+
removable?: boolean;
|
|
29
|
+
onremove?: (e: MouseEvent) => void;
|
|
19
30
|
class?: string;
|
|
20
31
|
children?: Snippet;
|
|
21
32
|
[key: string]: unknown;
|
|
@@ -29,9 +40,22 @@
|
|
|
29
40
|
class:badge-warn={tone === 'warn'}
|
|
30
41
|
class:badge-danger={tone === 'danger'}
|
|
31
42
|
class:badge-info={tone === 'info'}
|
|
43
|
+
class:badge-sm={size === 'sm'}
|
|
44
|
+
class:mono
|
|
45
|
+
class:interactive={as === 'button'}
|
|
32
46
|
{...rest}
|
|
33
47
|
>
|
|
34
48
|
{@render children?.()}
|
|
49
|
+
{#if removable}
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
class="remove"
|
|
53
|
+
aria-label="Remove"
|
|
54
|
+
onclick={(e) => onremove?.(e)}
|
|
55
|
+
>
|
|
56
|
+
×
|
|
57
|
+
</button>
|
|
58
|
+
{/if}
|
|
35
59
|
</svelte:element>
|
|
36
60
|
|
|
37
61
|
<style>
|
|
@@ -48,6 +72,12 @@
|
|
|
48
72
|
color: var(--text-muted);
|
|
49
73
|
border: 1px solid var(--border);
|
|
50
74
|
white-space: nowrap;
|
|
75
|
+
max-width: 100%;
|
|
76
|
+
}
|
|
77
|
+
.badge-sm {
|
|
78
|
+
padding: 0 0.4rem;
|
|
79
|
+
font-size: 0.6875rem;
|
|
80
|
+
gap: 0.15rem;
|
|
51
81
|
}
|
|
52
82
|
.badge-ok {
|
|
53
83
|
color: var(--ok);
|
|
@@ -69,4 +99,36 @@
|
|
|
69
99
|
border-color: color-mix(in srgb, var(--info) 40%, transparent);
|
|
70
100
|
background: color-mix(in srgb, var(--info) 12%, transparent);
|
|
71
101
|
}
|
|
102
|
+
.mono {
|
|
103
|
+
font-family: var(--font-mono);
|
|
104
|
+
font-weight: var(--fw-normal);
|
|
105
|
+
}
|
|
106
|
+
.interactive {
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
transition:
|
|
109
|
+
border-color 0.12s var(--ease),
|
|
110
|
+
color 0.12s var(--ease);
|
|
111
|
+
}
|
|
112
|
+
.interactive:hover {
|
|
113
|
+
border-color: var(--border-strong);
|
|
114
|
+
color: var(--text);
|
|
115
|
+
}
|
|
116
|
+
.remove {
|
|
117
|
+
display: inline-flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
margin: 0 -0.15rem 0 0;
|
|
121
|
+
padding: 0 0.1rem;
|
|
122
|
+
border: 0;
|
|
123
|
+
background: none;
|
|
124
|
+
color: inherit;
|
|
125
|
+
font: inherit;
|
|
126
|
+
line-height: 1;
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
opacity: 0.6;
|
|
129
|
+
transition: opacity 0.12s var(--ease);
|
|
130
|
+
}
|
|
131
|
+
.remove:hover {
|
|
132
|
+
opacity: 1;
|
|
133
|
+
}
|
|
72
134
|
</style>
|
|
@@ -3,6 +3,10 @@ type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
|
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
tone?: Tone;
|
|
5
5
|
as?: 'span' | 'button';
|
|
6
|
+
size?: 'sm' | 'md';
|
|
7
|
+
mono?: boolean;
|
|
8
|
+
removable?: boolean;
|
|
9
|
+
onremove?: (e: MouseEvent) => void;
|
|
6
10
|
class?: string;
|
|
7
11
|
children?: Snippet;
|
|
8
12
|
[key: string]: unknown;
|
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
size?: 'sm' | 'md';
|
|
8
8
|
control?: boolean;
|
|
9
9
|
block?: boolean;
|
|
10
|
+
// Square 2.25rem icon-only tap target (IconButton). `iconInline` is the
|
|
11
|
+
// borderless, compact variant (chip-remove ✕, inline edit ✎); pair with
|
|
12
|
+
// `hoverDanger` to tint it red on hover (delete affordances).
|
|
13
|
+
icon?: boolean;
|
|
14
|
+
iconInline?: boolean;
|
|
15
|
+
hoverDanger?: boolean;
|
|
16
|
+
// Async/busy state: shows a spinner, blocks clicks, sets aria-busy. Stays
|
|
17
|
+
// disabled-equivalent while true (so a double-submit can't fire).
|
|
18
|
+
loading?: boolean;
|
|
10
19
|
class?: string;
|
|
11
20
|
children?: Snippet;
|
|
12
21
|
};
|
|
@@ -16,6 +25,10 @@
|
|
|
16
25
|
size = 'md',
|
|
17
26
|
control = false,
|
|
18
27
|
block = false,
|
|
28
|
+
icon = false,
|
|
29
|
+
iconInline = false,
|
|
30
|
+
hoverDanger = false,
|
|
31
|
+
loading = false,
|
|
19
32
|
type = 'button',
|
|
20
33
|
disabled = false,
|
|
21
34
|
title,
|
|
@@ -29,7 +42,8 @@
|
|
|
29
42
|
<button
|
|
30
43
|
{...rest}
|
|
31
44
|
{type}
|
|
32
|
-
{disabled}
|
|
45
|
+
disabled={disabled || loading}
|
|
46
|
+
aria-busy={loading || undefined}
|
|
33
47
|
{title}
|
|
34
48
|
class="btn {klass}"
|
|
35
49
|
class:btn-primary={variant === 'primary'}
|
|
@@ -38,16 +52,20 @@
|
|
|
38
52
|
class:btn-sm={size === 'sm'}
|
|
39
53
|
class:btn-control={control}
|
|
40
54
|
class:btn-block={block}
|
|
55
|
+
class:btn-icon={icon}
|
|
56
|
+
class:btn-icon-inline={iconInline}
|
|
57
|
+
class:hover-danger={hoverDanger}
|
|
58
|
+
class:loading
|
|
41
59
|
onclick={onclick}
|
|
42
60
|
>
|
|
61
|
+
{#if loading}<span class="btn-spinner" aria-hidden="true"></span>{/if}
|
|
43
62
|
{@render children?.()}
|
|
44
63
|
</button>
|
|
45
64
|
|
|
46
65
|
<style>
|
|
47
|
-
/* The canonical control: owns its variants/sizes from theme tokens.
|
|
48
|
-
scopes via :where() (zero added specificity), so
|
|
49
|
-
|
|
50
|
-
.tapbtn) keep winning by specificity. Icons size themselves (Icon.svelte),
|
|
66
|
+
/* The canonical control: owns its variants/sizes/modifiers from theme tokens.
|
|
67
|
+
Svelte 5 scopes via :where() (zero added specificity), so feature-component
|
|
68
|
+
overrides keep winning by specificity. Icons size themselves (Icon.svelte),
|
|
51
69
|
so no svg sizing rule lives here. */
|
|
52
70
|
.btn {
|
|
53
71
|
display: inline-flex;
|
|
@@ -110,8 +128,9 @@
|
|
|
110
128
|
.btn-block {
|
|
111
129
|
width: 100%;
|
|
112
130
|
}
|
|
113
|
-
|
|
114
|
-
|
|
131
|
+
|
|
132
|
+
/* Uniform-height control: icon buttons, inputs and action buttons that share a
|
|
133
|
+
toolbar/composer row line up exactly. */
|
|
115
134
|
.btn-control {
|
|
116
135
|
display: inline-flex;
|
|
117
136
|
align-items: center;
|
|
@@ -143,7 +162,7 @@
|
|
|
143
162
|
}
|
|
144
163
|
/* .btn-control follows .btn-primary in source order and would otherwise paint
|
|
145
164
|
the primary action with the neutral --surface; restore the accent fill when
|
|
146
|
-
both are present
|
|
165
|
+
both are present. */
|
|
147
166
|
.btn-control.btn-primary {
|
|
148
167
|
background: var(--accent);
|
|
149
168
|
border-color: var(--accent);
|
|
@@ -153,4 +172,57 @@
|
|
|
153
172
|
border-color: var(--accent);
|
|
154
173
|
filter: brightness(1.08);
|
|
155
174
|
}
|
|
175
|
+
|
|
176
|
+
/* Icon-only buttons (IconButton). Square box = consistent 2.25rem tap target. */
|
|
177
|
+
.btn-icon {
|
|
178
|
+
min-height: 2.25rem;
|
|
179
|
+
min-width: 2.25rem;
|
|
180
|
+
padding: var(--sp-2);
|
|
181
|
+
}
|
|
182
|
+
.btn-icon-inline {
|
|
183
|
+
min-height: 0;
|
|
184
|
+
min-width: 0;
|
|
185
|
+
padding: 0 var(--sp-1);
|
|
186
|
+
border: none;
|
|
187
|
+
background: none;
|
|
188
|
+
color: var(--text-muted);
|
|
189
|
+
}
|
|
190
|
+
.btn-icon-inline:hover:not(:disabled) {
|
|
191
|
+
border: none;
|
|
192
|
+
background: none;
|
|
193
|
+
color: var(--text);
|
|
194
|
+
}
|
|
195
|
+
.btn-icon-inline.hover-danger:hover:not(:disabled) {
|
|
196
|
+
color: var(--danger);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Two-state (toggle) buttons — e.g. an IconButton with `pressed`. Reacts to
|
|
200
|
+
the native aria-pressed that flows through, so no extra class. Tint defaults
|
|
201
|
+
to the accent; override per-instance with `style="--btn-on: var(--warn)"`. */
|
|
202
|
+
.btn[aria-pressed='true'] {
|
|
203
|
+
color: var(--btn-on, var(--accent));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Loading: keep full opacity (it's busy, not disabled-looking) + a wait cursor,
|
|
207
|
+
and show a spinner in the current text color. */
|
|
208
|
+
.btn.loading {
|
|
209
|
+
cursor: wait;
|
|
210
|
+
}
|
|
211
|
+
.btn.loading:disabled {
|
|
212
|
+
opacity: 1;
|
|
213
|
+
}
|
|
214
|
+
.btn-spinner {
|
|
215
|
+
width: 0.9em;
|
|
216
|
+
height: 0.9em;
|
|
217
|
+
flex: none;
|
|
218
|
+
border: 2px solid currentColor;
|
|
219
|
+
border-top-color: transparent;
|
|
220
|
+
border-radius: 50%;
|
|
221
|
+
animation: btn-spin 0.6s linear infinite;
|
|
222
|
+
}
|
|
223
|
+
@keyframes btn-spin {
|
|
224
|
+
to {
|
|
225
|
+
transform: rotate(360deg);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
156
228
|
</style>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
let {
|
|
9
9
|
checked = $bindable(false),
|
|
10
10
|
indeterminate = false,
|
|
11
|
+
invalid = false,
|
|
11
12
|
label,
|
|
12
13
|
class: klass = '',
|
|
13
14
|
el = $bindable(null),
|
|
@@ -15,6 +16,8 @@
|
|
|
15
16
|
}: HTMLInputAttributes & {
|
|
16
17
|
checked?: boolean;
|
|
17
18
|
indeterminate?: boolean;
|
|
19
|
+
/** Error state: danger box border + aria-invalid. */
|
|
20
|
+
invalid?: boolean;
|
|
18
21
|
label: string;
|
|
19
22
|
el?: HTMLInputElement | null;
|
|
20
23
|
} = $props();
|
|
@@ -26,7 +29,7 @@
|
|
|
26
29
|
</script>
|
|
27
30
|
|
|
28
31
|
<label class="checkbox {klass}">
|
|
29
|
-
<input bind:this={el} type="checkbox" bind:checked {...rest} />
|
|
32
|
+
<input bind:this={el} type="checkbox" bind:checked {...rest} aria-invalid={invalid || undefined} />
|
|
30
33
|
<span class="box" aria-hidden="true"></span>
|
|
31
34
|
<span class="label-text">{label}</span>
|
|
32
35
|
</label>
|
|
@@ -90,6 +93,9 @@
|
|
|
90
93
|
outline: 2px solid var(--accent);
|
|
91
94
|
outline-offset: 2px;
|
|
92
95
|
}
|
|
96
|
+
input[aria-invalid='true']:not(:checked):not(:indeterminate) + .box {
|
|
97
|
+
border-color: var(--danger);
|
|
98
|
+
}
|
|
93
99
|
input:disabled ~ * {
|
|
94
100
|
opacity: 0.45;
|
|
95
101
|
}
|
|
@@ -2,6 +2,8 @@ import type { HTMLInputAttributes } from 'svelte/elements';
|
|
|
2
2
|
type $$ComponentProps = HTMLInputAttributes & {
|
|
3
3
|
checked?: boolean;
|
|
4
4
|
indeterminate?: boolean;
|
|
5
|
+
/** Error state: danger box border + aria-invalid. */
|
|
6
|
+
invalid?: boolean;
|
|
5
7
|
label: string;
|
|
6
8
|
el?: HTMLInputElement | null;
|
|
7
9
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Status dot — a small coloured disc, optionally followed by a label. Owns its
|
|
3
|
+
// own styling (it does not rely on any global `.dot` rules). Colour comes from
|
|
4
|
+
// either:
|
|
5
|
+
// • status → one of the semantic presets (active/stale/dead/hibernated),
|
|
6
|
+
// each mapped to a token.
|
|
7
|
+
// • color → any CSS colour (or var()) for a one-off; overrides `status`.
|
|
8
|
+
// `glow` adds a soft halo in the dot's own colour. With a `label` the whole
|
|
9
|
+
// thing renders as an inline-flex row (dot + caption); without one it's a bare
|
|
10
|
+
// inline dot, so it can sit inline next to other text.
|
|
11
|
+
import Text from './Text.svelte';
|
|
12
|
+
|
|
13
|
+
type Status = 'active' | 'stale' | 'dead' | 'hibernated';
|
|
14
|
+
|
|
15
|
+
const STATUS_COLOR: Record<Status, string> = {
|
|
16
|
+
active: 'var(--dot-active)',
|
|
17
|
+
stale: 'var(--dot-stale)',
|
|
18
|
+
dead: 'var(--dot-dead)',
|
|
19
|
+
hibernated: 'var(--dot-hibernated)',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
status = 'active',
|
|
24
|
+
color,
|
|
25
|
+
label,
|
|
26
|
+
glow = false,
|
|
27
|
+
class: klass = '',
|
|
28
|
+
...rest
|
|
29
|
+
}: {
|
|
30
|
+
status?: Status;
|
|
31
|
+
color?: string;
|
|
32
|
+
label?: string;
|
|
33
|
+
glow?: boolean;
|
|
34
|
+
class?: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
} = $props();
|
|
37
|
+
|
|
38
|
+
const resolved = $derived(color ?? STATUS_COLOR[status]);
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
{#if label}
|
|
42
|
+
<span class="dot-row {klass}" {...rest}>
|
|
43
|
+
<span class="dot" class:glow style="--dot-color:{resolved}"></span>
|
|
44
|
+
<Text variant="caption">{label}</Text>
|
|
45
|
+
</span>
|
|
46
|
+
{:else}
|
|
47
|
+
<span class="dot {klass}" class:glow style="--dot-color:{resolved}" {...rest}></span>
|
|
48
|
+
{/if}
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
.dot-row {
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: var(--sp-1);
|
|
55
|
+
}
|
|
56
|
+
.dot {
|
|
57
|
+
width: 0.55rem;
|
|
58
|
+
height: 0.55rem;
|
|
59
|
+
border-radius: var(--r-pill);
|
|
60
|
+
flex: none;
|
|
61
|
+
display: inline-block;
|
|
62
|
+
background: var(--dot-color);
|
|
63
|
+
}
|
|
64
|
+
.glow {
|
|
65
|
+
box-shadow: 0 0 6px var(--dot-color);
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type Status = 'active' | 'stale' | 'dead' | 'hibernated';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
status?: Status;
|
|
4
|
+
color?: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
glow?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
declare const Dot: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type Dot = ReturnType<typeof Dot>;
|
|
12
|
+
export default Dot;
|
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
// switches to the monospace family (paths, tokens, env values).
|
|
5
5
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// `size` shadows the native char-width attribute (unused in token-sized
|
|
8
|
+
// layouts) to expose a height preset instead.
|
|
9
|
+
type Props = Omit<HTMLInputAttributes, 'size'> & {
|
|
8
10
|
mono?: boolean;
|
|
11
|
+
size?: 'sm' | 'md';
|
|
12
|
+
/** Error state: danger border + aria-invalid (also styles if a consumer
|
|
13
|
+
* sets aria-invalid directly). */
|
|
14
|
+
invalid?: boolean;
|
|
9
15
|
class?: string;
|
|
10
16
|
value?: HTMLInputAttributes['value'];
|
|
11
17
|
el?: HTMLInputElement | null;
|
|
@@ -13,6 +19,8 @@
|
|
|
13
19
|
|
|
14
20
|
let {
|
|
15
21
|
mono = false,
|
|
22
|
+
size = 'md',
|
|
23
|
+
invalid = false,
|
|
16
24
|
class: klass = '',
|
|
17
25
|
value = $bindable(),
|
|
18
26
|
el = $bindable(null),
|
|
@@ -20,7 +28,15 @@
|
|
|
20
28
|
}: Props = $props();
|
|
21
29
|
</script>
|
|
22
30
|
|
|
23
|
-
<input
|
|
31
|
+
<input
|
|
32
|
+
bind:this={el}
|
|
33
|
+
class="input {klass}"
|
|
34
|
+
class:mono
|
|
35
|
+
class:input-sm={size === 'sm'}
|
|
36
|
+
bind:value
|
|
37
|
+
{...rest}
|
|
38
|
+
aria-invalid={invalid || undefined}
|
|
39
|
+
/>
|
|
24
40
|
|
|
25
41
|
<style>
|
|
26
42
|
.input {
|
|
@@ -36,6 +52,16 @@
|
|
|
36
52
|
outline: none;
|
|
37
53
|
border-color: var(--accent);
|
|
38
54
|
}
|
|
55
|
+
.input-sm {
|
|
56
|
+
padding: var(--sp-2);
|
|
57
|
+
font-size: var(--fs-sm);
|
|
58
|
+
}
|
|
59
|
+
.input[aria-invalid='true'] {
|
|
60
|
+
border-color: var(--danger);
|
|
61
|
+
}
|
|
62
|
+
.input[aria-invalid='true']:focus {
|
|
63
|
+
border-color: var(--danger);
|
|
64
|
+
}
|
|
39
65
|
.mono {
|
|
40
66
|
font-family: var(--font-mono);
|
|
41
67
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
-
type Props = HTMLInputAttributes & {
|
|
2
|
+
type Props = Omit<HTMLInputAttributes, 'size'> & {
|
|
3
3
|
mono?: boolean;
|
|
4
|
+
size?: 'sm' | 'md';
|
|
5
|
+
/** Error state: danger border + aria-invalid (also styles if a consumer
|
|
6
|
+
* sets aria-invalid directly). */
|
|
7
|
+
invalid?: boolean;
|
|
4
8
|
class?: string;
|
|
5
9
|
value?: HTMLInputAttributes['value'];
|
|
6
10
|
el?: HTMLInputElement | null;
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
type Props = HTMLSelectAttributes & {
|
|
19
19
|
variant?: 'default' | 'ghost';
|
|
20
|
+
/** Error state: danger border + aria-invalid. */
|
|
21
|
+
invalid?: boolean;
|
|
20
22
|
class?: string;
|
|
21
23
|
value?: HTMLSelectAttributes['value'];
|
|
22
24
|
children?: Snippet;
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
|
|
25
27
|
let {
|
|
26
28
|
variant = 'default',
|
|
29
|
+
invalid = false,
|
|
27
30
|
class: klass = '',
|
|
28
31
|
value = $bindable(),
|
|
29
32
|
children,
|
|
@@ -32,12 +35,12 @@
|
|
|
32
35
|
</script>
|
|
33
36
|
|
|
34
37
|
{#if variant === 'ghost'}
|
|
35
|
-
<select class="select ghost {klass}" bind:value {...rest}>
|
|
38
|
+
<select class="select ghost {klass}" bind:value {...rest} aria-invalid={invalid || undefined}>
|
|
36
39
|
{@render children?.()}
|
|
37
40
|
</select>
|
|
38
41
|
{:else}
|
|
39
42
|
<div class="select-wrap {klass}">
|
|
40
|
-
<select class="select" bind:value {...rest}>
|
|
43
|
+
<select class="select" bind:value {...rest} aria-invalid={invalid || undefined}>
|
|
41
44
|
{@render children?.()}
|
|
42
45
|
</select>
|
|
43
46
|
<span class="select-chevron" aria-hidden="true">
|
|
@@ -71,6 +74,10 @@
|
|
|
71
74
|
outline: none;
|
|
72
75
|
border-color: var(--accent);
|
|
73
76
|
}
|
|
77
|
+
.select[aria-invalid='true'],
|
|
78
|
+
.select[aria-invalid='true']:focus {
|
|
79
|
+
border-color: var(--danger);
|
|
80
|
+
}
|
|
74
81
|
.select-chevron {
|
|
75
82
|
position: absolute;
|
|
76
83
|
top: 50%;
|
|
@@ -2,6 +2,8 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
import type { HTMLSelectAttributes } from 'svelte/elements';
|
|
3
3
|
type Props = HTMLSelectAttributes & {
|
|
4
4
|
variant?: 'default' | 'ghost';
|
|
5
|
+
/** Error state: danger border + aria-invalid. */
|
|
6
|
+
invalid?: boolean;
|
|
5
7
|
class?: string;
|
|
6
8
|
value?: HTMLSelectAttributes['value'];
|
|
7
9
|
children?: Snippet;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// engines from tokens. The track shows a filled portion up to the current
|
|
5
5
|
// value, and an optional `output` displays the value with a correct
|
|
6
6
|
// for-association. `bind:value` and all native attrs/events pass through.
|
|
7
|
+
// Recolor per-instance via `--slider-accent` (defaults to the theme accent).
|
|
7
8
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
8
9
|
|
|
9
10
|
let {
|
|
@@ -73,7 +74,7 @@
|
|
|
73
74
|
border-radius: var(--r-pill);
|
|
74
75
|
background: linear-gradient(
|
|
75
76
|
to right,
|
|
76
|
-
var(--accent) var(--pct),
|
|
77
|
+
var(--slider-accent, var(--accent)) var(--pct),
|
|
77
78
|
var(--bg-elevated-2) var(--pct)
|
|
78
79
|
);
|
|
79
80
|
}
|
|
@@ -85,7 +86,7 @@
|
|
|
85
86
|
input[type='range']::-moz-range-progress {
|
|
86
87
|
height: 0.35rem;
|
|
87
88
|
border-radius: var(--r-pill);
|
|
88
|
-
background: var(--accent);
|
|
89
|
+
background: var(--slider-accent, var(--accent));
|
|
89
90
|
}
|
|
90
91
|
/* Thumb */
|
|
91
92
|
input[type='range']::-webkit-slider-thumb {
|
|
@@ -95,7 +96,7 @@
|
|
|
95
96
|
width: 1.2rem;
|
|
96
97
|
height: 1.2rem;
|
|
97
98
|
border-radius: 50%;
|
|
98
|
-
background: var(--accent);
|
|
99
|
+
background: var(--slider-accent, var(--accent));
|
|
99
100
|
border: 2px solid var(--bg);
|
|
100
101
|
box-shadow: var(--shadow-sm);
|
|
101
102
|
transition: transform 0.1s var(--ease);
|
|
@@ -104,7 +105,7 @@
|
|
|
104
105
|
width: 1.2rem;
|
|
105
106
|
height: 1.2rem;
|
|
106
107
|
border-radius: 50%;
|
|
107
|
-
background: var(--accent);
|
|
108
|
+
background: var(--slider-accent, var(--accent));
|
|
108
109
|
border: 2px solid var(--bg);
|
|
109
110
|
box-shadow: var(--shadow-sm);
|
|
110
111
|
}
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
let {
|
|
8
8
|
checked = false,
|
|
9
|
+
invalid = false,
|
|
9
10
|
label,
|
|
10
11
|
class: klass = '',
|
|
11
12
|
...rest
|
|
12
|
-
}: HTMLButtonAttributes & { checked?: boolean; label: string } = $props();
|
|
13
|
+
}: HTMLButtonAttributes & { checked?: boolean; invalid?: boolean; label: string } = $props();
|
|
13
14
|
</script>
|
|
14
15
|
|
|
15
16
|
<button
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
class:on={checked}
|
|
20
21
|
role="switch"
|
|
21
22
|
aria-checked={checked}
|
|
23
|
+
aria-invalid={invalid || undefined}
|
|
22
24
|
aria-label={label}
|
|
23
25
|
>
|
|
24
26
|
<span class="knob"></span>
|
|
@@ -61,4 +63,7 @@
|
|
|
61
63
|
opacity: 0.45;
|
|
62
64
|
cursor: not-allowed;
|
|
63
65
|
}
|
|
66
|
+
.switch[aria-invalid='true']:not(.on) {
|
|
67
|
+
border-color: var(--danger);
|
|
68
|
+
}
|
|
64
69
|
</style>
|