@dorsk/tsumikit 0.35.1 → 0.37.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 +26 -5
- package/dist/cap-bar.d.ts +8 -0
- package/dist/cap-bar.js +43 -0
- package/dist/components/atoms/Gauge.svelte +165 -0
- package/dist/components/atoms/Gauge.svelte.d.ts +24 -0
- package/dist/components/atoms/Select.svelte +112 -4
- package/dist/components/atoms/Select.svelte.d.ts +12 -0
- package/dist/components/layouts/MasterDetail.svelte +220 -0
- package/dist/components/layouts/MasterDetail.svelte.d.ts +22 -0
- package/dist/components/molecules/CapBar.svelte +254 -0
- package/dist/components/molecules/CapBar.svelte.d.ts +33 -0
- package/dist/components/molecules/Drawer.svelte +299 -0
- package/dist/components/molecules/Drawer.svelte.d.ts +35 -0
- package/dist/components/molecules/Fieldset.svelte +183 -0
- package/dist/components/molecules/Fieldset.svelte.d.ts +22 -0
- package/dist/components/molecules/GitRef.svelte +174 -0
- package/dist/components/molecules/GitRef.svelte.d.ts +30 -0
- package/dist/components/molecules/Menu.svelte +24 -1
- package/dist/components/molecules/Menu.svelte.d.ts +5 -0
- package/dist/components/molecules/RadioGroup.svelte +120 -14
- package/dist/components/molecules/RadioGroup.svelte.d.ts +6 -0
- package/dist/components/molecules/SectionHeader.svelte +35 -1
- package/dist/components/molecules/SectionHeader.svelte.d.ts +3 -0
- package/dist/components/molecules/Timestamp.svelte +7 -5
- package/dist/components/molecules/Timestamp.svelte.d.ts +2 -2
- package/dist/components/molecules/WorkingDir.svelte +134 -0
- package/dist/components/molecules/WorkingDir.svelte.d.ts +21 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/timestamp.d.ts +11 -5
- package/dist/timestamp.js +16 -2
- package/dist/working-dir.d.ts +12 -0
- package/dist/working-dir.js +38 -0
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -147,8 +147,10 @@ for a light theme (`:root` defaults to dark, so dark themes may omit it).
|
|
|
147
147
|
**Atoms:** Text, Heading, Button, Input (`icon` inset leading glyph,
|
|
148
148
|
`clearable` + `onclear`, `shape="pill"`, `width` fixed + `flex: none`,
|
|
149
149
|
`onenter(value)`; the bare `<input>` DOM is unchanged unless `icon`/`clearable`
|
|
150
|
-
is used), Textarea, Select
|
|
151
|
-
|
|
150
|
+
is used), Textarea, Select (`options` array with per-option `icon`/`emoji`/`hint`/`disabled`;
|
|
151
|
+
the trigger overlays the selected option's glyph + muted hint on the native control), Switch, Checkbox,
|
|
152
|
+
Slider, Progress, Gauge (vertical consumption cell, `variant` continuous/segments,
|
|
153
|
+
threshold tones via `warnAt`/`dangerAt`, `corner` snippet), Artwork (lazy cover image with seeded gradient + initials
|
|
152
154
|
fallback, `aspect`, `status` overlay), Card (`tone` tints the surface for inline banners), Badge
|
|
153
155
|
(`tone` semantic palette or `color` for any CSS colour, `size` xs/sm/md, `dot`,
|
|
154
156
|
`icon`, `numeric`, `truncate`, `variant="text"`; all tints derive from
|
|
@@ -157,9 +159,24 @@ always/hover/none, `align`), Icon (open registry — pass a `children` snippet f
|
|
|
157
159
|
any custom SVG).
|
|
158
160
|
|
|
159
161
|
**Molecules:** Field (`grow`), IconButton, SelectButton, Toggle, OptionButton, Modal,
|
|
160
|
-
Popover, Menu
|
|
162
|
+
Popover, Menu (items take a free-form trailing `tag` + `tagTone`, or a `tag` snippet),
|
|
163
|
+
Tabs, RadioGroup (`variant="rows"`: bordered rows, per-option `note`/`description`,
|
|
164
|
+
`action(option)` trailing control that never toggles, `below(option)` inline panel),
|
|
165
|
+
Tooltip, Accordion, CopyButton, FileButton,
|
|
161
166
|
Dropzone, CodeBlock, Callout, EmptyState, ConfirmModal, Pagination, Toaster,
|
|
162
|
-
ThemePicker, FontScalePicker, SectionHeader, KeyValue, LoadMore
|
|
167
|
+
ThemePicker, FontScalePicker, SectionHeader, KeyValue, LoadMore,
|
|
168
|
+
GitRef (branch chip + PR link tinted by state + `+N −N` diff; `collapse`
|
|
169
|
+
auto/never/glyph degrades to icons inside a narrow `.cq` container),
|
|
170
|
+
CapBar (consumption track with a draggable, keyboard-steppable cap handle; `oninput` live, `onchange` on commit),
|
|
171
|
+
Drawer (side-panel `<dialog>`: `side`, `width` clamped to the viewport, full-screen
|
|
172
|
+
under 48rem; `nav` page column that turns into a horizontal strip on narrow
|
|
173
|
+
screens, or `navMobile`; sticky `footer`; Escape / scrim / close button all close),
|
|
174
|
+
Timestamp (`mode` date/time/datetime/relative/iso/`short-iso` for a locale-independent `YYYY-MM-DD`; `utc`; click for a details popover), WorkingDir
|
|
175
|
+
(fish-style path chip: ancestors abbreviate left to right as the slot narrows,
|
|
176
|
+
then the leaf ellipsises down to `minLeaf`; `full` skips the fit, `copy` makes it
|
|
177
|
+
a copy-on-click button; candidates via the `workingDirCandidates` helper).
|
|
178
|
+
Fieldset (bordered zone, legend rides the border; `droppable` makes it an HTML5
|
|
179
|
+
drop target with `accepts`/`ondrop`/`dropHint` — keyboard alternative is the consumer's job).
|
|
163
180
|
|
|
164
181
|
**Organisms:** DataTable (generic `<T>`, typed columns + cell snippets;
|
|
165
182
|
`layout="fixed"` makes column widths authoritative, `Column.truncate` /
|
|
@@ -186,7 +203,9 @@ children are `min-width: 0` so a wide title/actions row can't widen the grid on
|
|
|
186
203
|
mobile), NavItem
|
|
187
204
|
(collapses to an icon rail when the sidebar is narrow; `icon` from the
|
|
188
205
|
registry, `iconPath` / `iconChildren` for custom glyphs, `activeStyle="bar"`
|
|
189
|
-
for the inset-bar active look),
|
|
206
|
+
for the inset-bar active look), MasterDetail (list + detail columns that
|
|
207
|
+
become two pages — one pane at a time with a sticky 44px back header — below
|
|
208
|
+
their own `breakpoint`, so `selected` can be driven from the URL), Container, Stack
|
|
190
209
|
(vertical), Cluster (wrapping row; `stackAt="xs|sm|md|lg"` makes it its own
|
|
191
210
|
query container and stacks children full-width below 18/30/40/48rem — phone
|
|
192
211
|
action rows without a viewport query), AutoGrid (intrinsically responsive
|
|
@@ -309,6 +328,8 @@ chip), `uppercase` (eyebrow group label), `divider`, `sticky` (pins at
|
|
|
309
328
|
`--sticky-offset` / `--header-h` and publishes `--section-header-h` on its
|
|
310
329
|
parent), `collapsible` + `bind:open` (title becomes a disclosure button with
|
|
311
330
|
`aria-expanded`; `children` render beneath while open), `actions` snippet.
|
|
331
|
+
`variant="group"` is the non-wrapping list-group row: `lead` snippet (dot /
|
|
332
|
+
badge) · title · `count` · flexible 1px rule · `actions` (sort menu, eye toggle).
|
|
312
333
|
|
|
313
334
|
`Card` gains `header` / `footer` snippets rendered outside the padded body
|
|
314
335
|
behind a divider, `title` / `subtitle` / `actions` sugar that renders a
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function clampCap(value: number, min?: number, max?: number): number;
|
|
2
|
+
export declare function snapCap(value: number, step?: number, min?: number, max?: number): number;
|
|
3
|
+
export declare function capFromPointer(clientX: number, rect: {
|
|
4
|
+
left: number;
|
|
5
|
+
width: number;
|
|
6
|
+
}, step?: number, min?: number, max?: number): number;
|
|
7
|
+
export declare function capKeyStep(key: string, shift: boolean, current: number, step?: number, min?: number, max?: number): number | null;
|
|
8
|
+
export declare function capTone(value: number, cap: number, warnAt?: number): 'ok' | 'warn' | 'danger';
|
package/dist/cap-bar.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function clampCap(value, min = 0, max = 100) {
|
|
2
|
+
if (Number.isNaN(value))
|
|
3
|
+
return min;
|
|
4
|
+
return Math.min(max, Math.max(min, value));
|
|
5
|
+
}
|
|
6
|
+
export function snapCap(value, step = 5, min = 0, max = 100) {
|
|
7
|
+
const s = step > 0 ? step : 1;
|
|
8
|
+
return clampCap(min + Math.round((value - min) / s) * s, min, max);
|
|
9
|
+
}
|
|
10
|
+
export function capFromPointer(clientX, rect, step = 5, min = 0, max = 100) {
|
|
11
|
+
if (rect.width <= 0)
|
|
12
|
+
return min;
|
|
13
|
+
const ratio = (clientX - rect.left) / rect.width;
|
|
14
|
+
return snapCap(min + ratio * (max - min), step, min, max);
|
|
15
|
+
}
|
|
16
|
+
export function capKeyStep(key, shift, current, step = 5, min = 0, max = 100) {
|
|
17
|
+
const delta = shift ? step * 5 : step;
|
|
18
|
+
switch (key) {
|
|
19
|
+
case 'ArrowRight':
|
|
20
|
+
case 'ArrowUp':
|
|
21
|
+
return snapCap(current + delta, step, min, max);
|
|
22
|
+
case 'ArrowLeft':
|
|
23
|
+
case 'ArrowDown':
|
|
24
|
+
return snapCap(current - delta, step, min, max);
|
|
25
|
+
case 'Home':
|
|
26
|
+
return min;
|
|
27
|
+
case 'End':
|
|
28
|
+
return max;
|
|
29
|
+
case 'PageUp':
|
|
30
|
+
return snapCap(current + step * 5, step, min, max);
|
|
31
|
+
case 'PageDown':
|
|
32
|
+
return snapCap(current - step * 5, step, min, max);
|
|
33
|
+
default:
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function capTone(value, cap, warnAt = 75) {
|
|
38
|
+
if (value >= cap)
|
|
39
|
+
return 'danger';
|
|
40
|
+
if (value >= warnAt)
|
|
41
|
+
return 'warn';
|
|
42
|
+
return 'ok';
|
|
43
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type GaugeTone = 'ok' | 'warn' | 'danger';
|
|
3
|
+
|
|
4
|
+
export function clampPct(value: number): number {
|
|
5
|
+
return Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function gaugeTone(value: number, warnAt = 70, dangerAt = 90): GaugeTone {
|
|
9
|
+
const v = clampPct(value);
|
|
10
|
+
if (v >= dangerAt) return 'danger';
|
|
11
|
+
if (v >= warnAt) return 'warn';
|
|
12
|
+
return 'ok';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function litSegments(value: number, segments = 3): number {
|
|
16
|
+
const v = clampPct(value);
|
|
17
|
+
if (segments <= 0) return 0;
|
|
18
|
+
return Math.min(segments, Math.floor((v / 100) * segments));
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<script lang="ts">
|
|
23
|
+
import type { Snippet } from 'svelte';
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
value,
|
|
27
|
+
variant = 'continuous',
|
|
28
|
+
segments = 3,
|
|
29
|
+
tone,
|
|
30
|
+
warnAt = 70,
|
|
31
|
+
dangerAt = 90,
|
|
32
|
+
label,
|
|
33
|
+
as = 'div',
|
|
34
|
+
width,
|
|
35
|
+
height,
|
|
36
|
+
corner,
|
|
37
|
+
class: klass = '',
|
|
38
|
+
style = '',
|
|
39
|
+
...rest
|
|
40
|
+
}: {
|
|
41
|
+
value: number;
|
|
42
|
+
variant?: 'continuous' | 'segments';
|
|
43
|
+
// `segments` variant only: number of stacked bars, lit bottom-up.
|
|
44
|
+
segments?: number;
|
|
45
|
+
// Forces the fill colour; otherwise derived from `warnAt` / `dangerAt`.
|
|
46
|
+
tone?: GaugeTone;
|
|
47
|
+
warnAt?: number;
|
|
48
|
+
dangerAt?: number;
|
|
49
|
+
label?: string;
|
|
50
|
+
as?: 'div' | 'a' | 'button';
|
|
51
|
+
width?: string;
|
|
52
|
+
height?: string;
|
|
53
|
+
corner?: Snippet;
|
|
54
|
+
class?: string;
|
|
55
|
+
style?: string;
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
} = $props();
|
|
58
|
+
|
|
59
|
+
const pct = $derived(clampPct(value));
|
|
60
|
+
const resolvedTone = $derived(tone ?? gaugeTone(pct, warnAt, dangerAt));
|
|
61
|
+
const lit = $derived(litSegments(pct, segments));
|
|
62
|
+
const segmentCount = $derived(Math.max(0, Math.floor(segments)));
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<svelte:element
|
|
66
|
+
this={as}
|
|
67
|
+
data-tsu="Gauge"
|
|
68
|
+
class="gauge tone-{resolvedTone} {klass}"
|
|
69
|
+
class:interactive={as !== 'div'}
|
|
70
|
+
type={as === 'button' ? 'button' : undefined}
|
|
71
|
+
role="meter"
|
|
72
|
+
aria-label={label}
|
|
73
|
+
aria-valuemin={0}
|
|
74
|
+
aria-valuemax={100}
|
|
75
|
+
aria-valuenow={pct}
|
|
76
|
+
style:--gauge-w={width}
|
|
77
|
+
style:--gauge-h={height}
|
|
78
|
+
{style}
|
|
79
|
+
{...rest}
|
|
80
|
+
>
|
|
81
|
+
{#if variant === 'segments'}
|
|
82
|
+
<span class="segments" style:--gauge-segments={segmentCount}>
|
|
83
|
+
{#each { length: segmentCount } as _, i (i)}
|
|
84
|
+
<span class="segment" class:lit={i < lit}></span>
|
|
85
|
+
{/each}
|
|
86
|
+
</span>
|
|
87
|
+
{:else}
|
|
88
|
+
<span class="fill" style:height="{pct}%"></span>
|
|
89
|
+
{/if}
|
|
90
|
+
{#if corner}
|
|
91
|
+
<span class="corner">{@render corner()}</span>
|
|
92
|
+
{/if}
|
|
93
|
+
</svelte:element>
|
|
94
|
+
|
|
95
|
+
<style>
|
|
96
|
+
.gauge {
|
|
97
|
+
position: relative;
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
flex: none;
|
|
100
|
+
box-sizing: border-box;
|
|
101
|
+
width: var(--gauge-w, 0.875rem);
|
|
102
|
+
height: var(--gauge-h, 1.25rem);
|
|
103
|
+
margin: 0;
|
|
104
|
+
padding: 0;
|
|
105
|
+
border: 1px solid var(--border);
|
|
106
|
+
border-radius: var(--r-sm);
|
|
107
|
+
background: var(--bg-elevated-2);
|
|
108
|
+
overflow: visible;
|
|
109
|
+
color: inherit;
|
|
110
|
+
font: inherit;
|
|
111
|
+
text-decoration: none;
|
|
112
|
+
vertical-align: middle;
|
|
113
|
+
}
|
|
114
|
+
.interactive {
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
}
|
|
117
|
+
.interactive:hover {
|
|
118
|
+
border-color: var(--gauge-fill);
|
|
119
|
+
}
|
|
120
|
+
.interactive:focus-visible {
|
|
121
|
+
outline: 2px solid var(--accent);
|
|
122
|
+
outline-offset: 2px;
|
|
123
|
+
}
|
|
124
|
+
.tone-ok {
|
|
125
|
+
--gauge-fill: var(--ok);
|
|
126
|
+
}
|
|
127
|
+
.tone-warn {
|
|
128
|
+
--gauge-fill: var(--warn);
|
|
129
|
+
}
|
|
130
|
+
.tone-danger {
|
|
131
|
+
--gauge-fill: var(--danger);
|
|
132
|
+
}
|
|
133
|
+
.fill {
|
|
134
|
+
position: absolute;
|
|
135
|
+
inset: auto 0 0 0;
|
|
136
|
+
border-radius: calc(var(--r-sm) - 1px);
|
|
137
|
+
background: var(--gauge-fill);
|
|
138
|
+
transition: height 0.2s var(--ease), background 0.2s var(--ease);
|
|
139
|
+
}
|
|
140
|
+
.segments {
|
|
141
|
+
position: absolute;
|
|
142
|
+
inset: 2px;
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-direction: column-reverse;
|
|
145
|
+
gap: 2px;
|
|
146
|
+
}
|
|
147
|
+
.segment {
|
|
148
|
+
flex: 1 1 0%;
|
|
149
|
+
border-radius: 2px;
|
|
150
|
+
background: var(--bg-elevated);
|
|
151
|
+
transition: background 0.2s var(--ease);
|
|
152
|
+
}
|
|
153
|
+
.segment.lit {
|
|
154
|
+
background: var(--gauge-fill);
|
|
155
|
+
}
|
|
156
|
+
.corner {
|
|
157
|
+
position: absolute;
|
|
158
|
+
right: -0.35em;
|
|
159
|
+
bottom: -0.35em;
|
|
160
|
+
display: inline-flex;
|
|
161
|
+
font-size: 0.625rem;
|
|
162
|
+
line-height: 1;
|
|
163
|
+
pointer-events: none;
|
|
164
|
+
}
|
|
165
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type GaugeTone = 'ok' | 'warn' | 'danger';
|
|
2
|
+
export declare function clampPct(value: number): number;
|
|
3
|
+
export declare function gaugeTone(value: number, warnAt?: number, dangerAt?: number): GaugeTone;
|
|
4
|
+
export declare function litSegments(value: number, segments?: number): number;
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
type $$ComponentProps = {
|
|
7
|
+
value: number;
|
|
8
|
+
variant?: 'continuous' | 'segments';
|
|
9
|
+
segments?: number;
|
|
10
|
+
tone?: GaugeTone;
|
|
11
|
+
warnAt?: number;
|
|
12
|
+
dangerAt?: number;
|
|
13
|
+
label?: string;
|
|
14
|
+
as?: 'div' | 'a' | 'button';
|
|
15
|
+
width?: string;
|
|
16
|
+
height?: string;
|
|
17
|
+
corner?: Snippet;
|
|
18
|
+
class?: string;
|
|
19
|
+
style?: string;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
};
|
|
22
|
+
declare const Gauge: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
23
|
+
type Gauge = ReturnType<typeof Gauge>;
|
|
24
|
+
export default Gauge;
|
|
@@ -1,7 +1,30 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { IconName } from './Icon.svelte';
|
|
3
|
+
|
|
4
|
+
export type SelectOption = {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: IconName;
|
|
8
|
+
emoji?: string;
|
|
9
|
+
/** Muted, right-aligned secondary text (e.g. "62%"). */
|
|
10
|
+
hint?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
};
|
|
13
|
+
</script>
|
|
14
|
+
|
|
1
15
|
<script lang="ts">
|
|
2
16
|
// Native <select> primitive. Owns its styling from theme tokens; supports
|
|
3
|
-
// `bind:value` and passes through every native attribute. Options are
|
|
4
|
-
// children
|
|
17
|
+
// `bind:value` and passes through every native attribute. Options are either
|
|
18
|
+
// slotted children (full control over <option> rendering) or an `options`
|
|
19
|
+
// array, which adds per-option icon/emoji/hint.
|
|
20
|
+
//
|
|
21
|
+
// A native <select> cannot render icons in its popup or trigger. With
|
|
22
|
+
// `options`, the trigger text is drawn by a pointer-transparent face layer
|
|
23
|
+
// (leading icon/emoji, label, muted right-aligned hint) laid over the native
|
|
24
|
+
// control whose own text is made transparent, so keyboard, focus, form
|
|
25
|
+
// submission and the platform popup stay native. The popup degrades
|
|
26
|
+
// gracefully: emoji and hint are folded into the option text
|
|
27
|
+
// (`🐼 personal · 62%`); an `icon` only shows in the trigger.
|
|
5
28
|
//
|
|
6
29
|
// The default variant hides the OS chevron (appearance: none) and draws its
|
|
7
30
|
// own, so the control looks identical across platforms and the chevron sits
|
|
@@ -42,6 +65,8 @@
|
|
|
42
65
|
'aria-invalid'?: HTMLSelectAttributes['aria-invalid'];
|
|
43
66
|
class?: string;
|
|
44
67
|
value?: HTMLSelectAttributes['value'];
|
|
68
|
+
/** Rich option list; rendered instead of `children` when given. */
|
|
69
|
+
options?: SelectOption[];
|
|
45
70
|
children?: Snippet;
|
|
46
71
|
};
|
|
47
72
|
|
|
@@ -58,6 +83,7 @@
|
|
|
58
83
|
'aria-invalid': ariaInvalid,
|
|
59
84
|
class: klass = '',
|
|
60
85
|
value = $bindable(),
|
|
86
|
+
options,
|
|
61
87
|
children,
|
|
62
88
|
...rest
|
|
63
89
|
}: Props = $props();
|
|
@@ -70,8 +96,23 @@
|
|
|
70
96
|
|
|
71
97
|
const small = $derived(compact || size === 'sm');
|
|
72
98
|
const showChevron = $derived(chevron ?? variant !== 'embedded');
|
|
99
|
+
const selected = $derived(options?.find((o) => o.value === value));
|
|
100
|
+
const hasFace = $derived(!!options && variant !== 'ghost');
|
|
101
|
+
|
|
102
|
+
const optionText = (o: SelectOption) =>
|
|
103
|
+
[o.emoji, o.label, o.hint && `· ${o.hint}`].filter(Boolean).join(' ');
|
|
73
104
|
</script>
|
|
74
105
|
|
|
106
|
+
{#snippet optionList()}
|
|
107
|
+
{#if options}
|
|
108
|
+
{#each options as o (o.value)}
|
|
109
|
+
<option value={o.value} disabled={o.disabled}>{optionText(o)}</option>
|
|
110
|
+
{/each}
|
|
111
|
+
{:else}
|
|
112
|
+
{@render children?.()}
|
|
113
|
+
{/if}
|
|
114
|
+
{/snippet}
|
|
115
|
+
|
|
75
116
|
{#if variant === 'ghost'}
|
|
76
117
|
<select
|
|
77
118
|
bind:this={el}
|
|
@@ -83,7 +124,7 @@
|
|
|
83
124
|
aria-describedby={ariaDescribedby ?? field?.describedBy}
|
|
84
125
|
aria-invalid={ariaInvalid ?? (isInvalid ? 'true' : undefined)}
|
|
85
126
|
>
|
|
86
|
-
{@render
|
|
127
|
+
{@render optionList()}
|
|
87
128
|
</select>
|
|
88
129
|
{:else}
|
|
89
130
|
<div
|
|
@@ -100,14 +141,28 @@
|
|
|
100
141
|
class:select-sm={size === 'sm'}
|
|
101
142
|
class:w-auto={width === 'auto'}
|
|
102
143
|
class:embedded={variant === 'embedded'}
|
|
144
|
+
class:has-face={hasFace}
|
|
103
145
|
bind:value
|
|
104
146
|
{...rest}
|
|
105
147
|
id={id ?? field?.id}
|
|
106
148
|
aria-describedby={ariaDescribedby ?? field?.describedBy}
|
|
107
149
|
aria-invalid={ariaInvalid ?? (isInvalid ? 'true' : undefined)}
|
|
108
150
|
>
|
|
109
|
-
{@render
|
|
151
|
+
{@render optionList()}
|
|
110
152
|
</select>
|
|
153
|
+
{#if hasFace}
|
|
154
|
+
<span class="select-face" class:compact={small} aria-hidden="true">
|
|
155
|
+
{#if selected?.icon}
|
|
156
|
+
<Icon name={selected.icon} size={small ? 14 : 16} />
|
|
157
|
+
{:else if selected?.emoji}
|
|
158
|
+
<span class="select-emoji">{selected.emoji}</span>
|
|
159
|
+
{/if}
|
|
160
|
+
<span class="select-label">{selected?.label ?? ''}</span>
|
|
161
|
+
{#if selected?.hint}
|
|
162
|
+
<span class="select-hint">{selected.hint}</span>
|
|
163
|
+
{/if}
|
|
164
|
+
</span>
|
|
165
|
+
{/if}
|
|
111
166
|
{#if showChevron}
|
|
112
167
|
<span class="select-chevron" aria-hidden="true">
|
|
113
168
|
<Icon name="chevron-down" size={16} />
|
|
@@ -178,6 +233,59 @@
|
|
|
178
233
|
.select[aria-invalid='true']:focus {
|
|
179
234
|
border-color: var(--danger);
|
|
180
235
|
}
|
|
236
|
+
/* The face draws the trigger text; the native text underneath stays laid out
|
|
237
|
+
(so `width="auto"` still sizes to it) but invisible. Popup options keep
|
|
238
|
+
their own colour so the transparency does not inherit into the list. */
|
|
239
|
+
.select.has-face {
|
|
240
|
+
color: transparent;
|
|
241
|
+
}
|
|
242
|
+
.select.has-face option {
|
|
243
|
+
color: var(--text);
|
|
244
|
+
background: var(--bg);
|
|
245
|
+
}
|
|
246
|
+
.select-face {
|
|
247
|
+
position: absolute;
|
|
248
|
+
inset: 0;
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
gap: var(--sp-2);
|
|
252
|
+
padding: 0 var(--sp-3);
|
|
253
|
+
padding-right: calc(var(--sp-3) + 1.25rem);
|
|
254
|
+
color: var(--text);
|
|
255
|
+
pointer-events: none;
|
|
256
|
+
overflow: hidden;
|
|
257
|
+
}
|
|
258
|
+
.select-wrap.no-chevron .select-face {
|
|
259
|
+
padding-right: var(--sp-3);
|
|
260
|
+
}
|
|
261
|
+
.select-face.compact {
|
|
262
|
+
padding: 0 var(--sp-2);
|
|
263
|
+
padding-right: calc(var(--sp-2) + 1rem);
|
|
264
|
+
font-size: var(--fs-xs);
|
|
265
|
+
}
|
|
266
|
+
.select-wrap.no-chevron .select-face.compact {
|
|
267
|
+
padding-right: var(--sp-2);
|
|
268
|
+
}
|
|
269
|
+
.select:disabled ~ .select-face {
|
|
270
|
+
color: var(--text-muted);
|
|
271
|
+
}
|
|
272
|
+
.select-emoji {
|
|
273
|
+
line-height: 1;
|
|
274
|
+
}
|
|
275
|
+
.select-label {
|
|
276
|
+
flex: 1 1 auto;
|
|
277
|
+
min-width: 0;
|
|
278
|
+
overflow: hidden;
|
|
279
|
+
text-overflow: ellipsis;
|
|
280
|
+
white-space: nowrap;
|
|
281
|
+
}
|
|
282
|
+
.select-hint {
|
|
283
|
+
flex: none;
|
|
284
|
+
margin-left: auto;
|
|
285
|
+
color: var(--text-muted);
|
|
286
|
+
font-size: var(--fs-xs);
|
|
287
|
+
font-variant-numeric: tabular-nums;
|
|
288
|
+
}
|
|
181
289
|
.select-chevron {
|
|
182
290
|
position: absolute;
|
|
183
291
|
top: 50%;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import type { IconName } from './Icon.svelte';
|
|
2
|
+
export type SelectOption = {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: IconName;
|
|
6
|
+
emoji?: string;
|
|
7
|
+
/** Muted, right-aligned secondary text (e.g. "62%"). */
|
|
8
|
+
hint?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
};
|
|
1
11
|
import type { Snippet } from 'svelte';
|
|
2
12
|
import type { HTMLSelectAttributes } from 'svelte/elements';
|
|
3
13
|
type Props = Omit<HTMLSelectAttributes, 'size'> & {
|
|
@@ -21,6 +31,8 @@ type Props = Omit<HTMLSelectAttributes, 'size'> & {
|
|
|
21
31
|
'aria-invalid'?: HTMLSelectAttributes['aria-invalid'];
|
|
22
32
|
class?: string;
|
|
23
33
|
value?: HTMLSelectAttributes['value'];
|
|
34
|
+
/** Rich option list; rendered instead of `children` when given. */
|
|
35
|
+
options?: SelectOption[];
|
|
24
36
|
children?: Snippet;
|
|
25
37
|
};
|
|
26
38
|
declare const Select: import("svelte").Component<Props, {}, "value">;
|