@dorsk/tsumikit 0.37.5 → 0.38.1
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
CHANGED
|
@@ -149,7 +149,7 @@ for a light theme (`:root` defaults to dark, so dark themes may omit it).
|
|
|
149
149
|
`onenter(value)`; the bare `<input>` DOM is unchanged unless `icon`/`clearable`
|
|
150
150
|
is used), Textarea, Select (`options` array with per-option `icon`/`emoji`/`hint`/`disabled`;
|
|
151
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,
|
|
152
|
+
Slider (`ticks` draws a dot per step for a segmented slider), Progress, Gauge (vertical consumption cell, `variant` continuous/segments,
|
|
153
153
|
threshold tones via `warnAt`/`dangerAt`, `corner` snippet), Artwork (lazy cover image with seeded gradient + initials
|
|
154
154
|
fallback, `aspect`, `status` overlay), Card (`tone` tints the surface for inline banners), Badge
|
|
155
155
|
(`tone` semantic palette or `color` for any CSS colour, `size` xs/sm/md, `dot`,
|
|
@@ -164,7 +164,7 @@ Tabs, RadioGroup (`variant="rows"`: bordered rows, per-option `note`/`descriptio
|
|
|
164
164
|
`action(option)` trailing control that never toggles, `below(option)` inline panel),
|
|
165
165
|
Tooltip, Accordion, CopyButton, FileButton,
|
|
166
166
|
Dropzone, CodeBlock, Callout, EmptyState, ConfirmModal, Pagination, Toaster,
|
|
167
|
-
ThemePicker, FontScalePicker, SectionHeader, KeyValue, LoadMore,
|
|
167
|
+
ThemePicker (popover grid of 2×2 palette swatches: bg · surface · text · accent per theme), FontScalePicker (popover with a stepped slider across the five text sizes), SectionHeader, KeyValue, LoadMore,
|
|
168
168
|
GitRef (branch chip + PR link tinted by state + `+N −N` diff; `collapse`
|
|
169
169
|
auto/never/glyph degrades to icons inside a narrow `.cq` container),
|
|
170
170
|
CapBar (consumption track with a draggable, keyboard-steppable cap handle; `oninput` live, `onchange` on commit),
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
'aria-describedby': ariaDescribedby,
|
|
21
21
|
'aria-invalid': ariaInvalid,
|
|
22
22
|
invalid = false,
|
|
23
|
+
ticks = false,
|
|
23
24
|
class: klass = '',
|
|
24
25
|
el = $bindable(null),
|
|
25
26
|
...rest
|
|
@@ -33,6 +34,9 @@
|
|
|
33
34
|
format?: (v: number) => string;
|
|
34
35
|
/** Error state: aria-invalid. */
|
|
35
36
|
invalid?: boolean;
|
|
37
|
+
/** Visible dots at each step (segmented slider). `true` = one per `step`;
|
|
38
|
+
* a number array places dots at those values. */
|
|
39
|
+
ticks?: boolean | number[];
|
|
36
40
|
id?: string;
|
|
37
41
|
'aria-describedby'?: string | null;
|
|
38
42
|
'aria-invalid'?: HTMLInputAttributes['aria-invalid'];
|
|
@@ -46,13 +50,19 @@
|
|
|
46
50
|
|
|
47
51
|
$effect(() => warnUnlabelled(el, 'Slider'));
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
const pct = $derived(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
const frac = (v: number) => Math.max(0, Math.min(1, (v - +min) / (+max - +min || 1)));
|
|
54
|
+
const pct = $derived(frac(Number(value)) * 100);
|
|
55
|
+
const tickValues = $derived.by(() => {
|
|
56
|
+
if (!ticks) return [];
|
|
57
|
+
if (Array.isArray(ticks)) return ticks;
|
|
58
|
+
const n = Math.floor((+max - +min) / (+step || 1));
|
|
59
|
+
if (n > 200) return [];
|
|
60
|
+
return Array.from({ length: n + 1 }, (_, i) => +min + i * +step);
|
|
61
|
+
});
|
|
53
62
|
</script>
|
|
54
63
|
|
|
55
|
-
<div class="slider {klass}" style="--pct: {pct}%" data-tsu="Slider">
|
|
64
|
+
<div class="slider {klass}" class:has-ticks={tickValues.length > 0} style="--pct: {pct}%" data-tsu="Slider">
|
|
65
|
+
<span class="range">
|
|
56
66
|
<input
|
|
57
67
|
bind:this={el}
|
|
58
68
|
{id}
|
|
@@ -66,6 +76,14 @@
|
|
|
66
76
|
aria-describedby={ariaDescribedby ?? field?.describedBy}
|
|
67
77
|
aria-invalid={ariaInvalid ?? (isInvalid ? 'true' : undefined)}
|
|
68
78
|
/>
|
|
79
|
+
{#if tickValues.length > 0}
|
|
80
|
+
<span class="ticks" aria-hidden="true">
|
|
81
|
+
{#each tickValues as t (t)}
|
|
82
|
+
<i class="tick" class:reached={t <= Number(value)} style="--f: {frac(t)}"></i>
|
|
83
|
+
{/each}
|
|
84
|
+
</span>
|
|
85
|
+
{/if}
|
|
86
|
+
</span>
|
|
69
87
|
{#if showValue}
|
|
70
88
|
<output for={id} class="slider-out">{format(Number(value))}</output>
|
|
71
89
|
{/if}
|
|
@@ -78,10 +96,18 @@
|
|
|
78
96
|
gap: var(--sp-3);
|
|
79
97
|
width: 100%;
|
|
80
98
|
}
|
|
99
|
+
.range {
|
|
100
|
+
position: relative;
|
|
101
|
+
display: flex;
|
|
102
|
+
flex: 1;
|
|
103
|
+
min-width: 0;
|
|
104
|
+
}
|
|
81
105
|
input[type='range'] {
|
|
82
106
|
appearance: none;
|
|
83
107
|
-webkit-appearance: none;
|
|
84
108
|
flex: 1;
|
|
109
|
+
position: relative;
|
|
110
|
+
z-index: 1;
|
|
85
111
|
height: 1.25rem;
|
|
86
112
|
background: none;
|
|
87
113
|
cursor: pointer;
|
|
@@ -145,6 +171,26 @@
|
|
|
145
171
|
opacity: 0.45;
|
|
146
172
|
cursor: not-allowed;
|
|
147
173
|
}
|
|
174
|
+
.ticks {
|
|
175
|
+
position: absolute;
|
|
176
|
+
inset: 0;
|
|
177
|
+
z-index: 2;
|
|
178
|
+
pointer-events: none;
|
|
179
|
+
}
|
|
180
|
+
.tick {
|
|
181
|
+
position: absolute;
|
|
182
|
+
top: 50%;
|
|
183
|
+
left: calc(0.6rem + (100% - 1.2rem) * var(--f));
|
|
184
|
+
width: 0.35rem;
|
|
185
|
+
height: 0.35rem;
|
|
186
|
+
margin: -0.175rem 0 0 -0.175rem;
|
|
187
|
+
border-radius: 50%;
|
|
188
|
+
background: var(--border-strong);
|
|
189
|
+
}
|
|
190
|
+
.tick.reached {
|
|
191
|
+
background: var(--bg);
|
|
192
|
+
opacity: 0.6;
|
|
193
|
+
}
|
|
148
194
|
.slider-out {
|
|
149
195
|
min-width: 2.5rem;
|
|
150
196
|
text-align: right;
|
|
@@ -9,6 +9,9 @@ type $$ComponentProps = HTMLInputAttributes & {
|
|
|
9
9
|
format?: (v: number) => string;
|
|
10
10
|
/** Error state: aria-invalid. */
|
|
11
11
|
invalid?: boolean;
|
|
12
|
+
/** Visible dots at each step (segmented slider). `true` = one per `step`;
|
|
13
|
+
* a number array places dots at those values. */
|
|
14
|
+
ticks?: boolean | number[];
|
|
12
15
|
id?: string;
|
|
13
16
|
'aria-describedby'?: string | null;
|
|
14
17
|
'aria-invalid'?: HTMLInputAttributes['aria-invalid'];
|
|
@@ -1,22 +1,91 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
2
|
+
// Text-size control: the "A" trigger opens a popover with a stepped slider
|
|
3
|
+
// across SCALE_LEVELS. Drives --fs-scale (text only), chrome stays fixed.
|
|
4
|
+
// Everything inside the popover is sized in plain rem, never --fs-*: if the
|
|
5
|
+
// panel rescaled with the page the slider would move under the pointer
|
|
6
|
+
// mid-drag and oscillate between steps.
|
|
7
|
+
import Popover from './Popover.svelte';
|
|
8
|
+
import Slider from '../atoms/Slider.svelte';
|
|
6
9
|
import { fontScale, SCALE_LEVELS } from '../../stores/fontscale.svelte';
|
|
7
10
|
|
|
8
11
|
let { class: klass = '' }: { class?: string } = $props();
|
|
9
12
|
|
|
10
|
-
const
|
|
13
|
+
const index = $derived(Math.max(0, SCALE_LEVELS.findIndex((l) => l.id === fontScale.levelId)));
|
|
14
|
+
const level = $derived(SCALE_LEVELS[index]);
|
|
15
|
+
const set = (i: number) => fontScale.set(SCALE_LEVELS[Math.max(0, Math.min(SCALE_LEVELS.length - 1, i))].id);
|
|
11
16
|
</script>
|
|
12
17
|
|
|
13
|
-
<
|
|
14
|
-
data-tsu="FontScalePicker"
|
|
15
|
-
class=
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
<Popover label="Text size" placement="bottom-end" triggerClass={klass} box="md">
|
|
19
|
+
{#snippet trigger()}<span class="glyph" data-tsu="FontScalePicker" title="Text size: {level.label}">A</span>{/snippet}
|
|
20
|
+
<div class="panel">
|
|
21
|
+
<div class="row">
|
|
22
|
+
<button type="button" class="end small" aria-label="Smaller text" onclick={() => set(index - 1)}>a</button>
|
|
23
|
+
<Slider
|
|
24
|
+
value={index}
|
|
25
|
+
min={0}
|
|
26
|
+
max={SCALE_LEVELS.length - 1}
|
|
27
|
+
step={1}
|
|
28
|
+
ticks
|
|
29
|
+
label="Text size"
|
|
30
|
+
aria-valuetext={level.label}
|
|
31
|
+
oninput={(e) => set(Number((e.currentTarget as HTMLInputElement).value))}
|
|
32
|
+
/>
|
|
33
|
+
<button type="button" class="end large" aria-label="Larger text" onclick={() => set(index + 1)}>A</button>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="caption" aria-live="polite">{level.label} · {Math.round(level.value * 100)}%</div>
|
|
36
|
+
</div>
|
|
37
|
+
</Popover>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
.glyph {
|
|
41
|
+
font-weight: var(--fw-bold);
|
|
42
|
+
font-size: 1rem;
|
|
43
|
+
line-height: 1;
|
|
44
|
+
}
|
|
45
|
+
.panel {
|
|
46
|
+
width: 15rem;
|
|
47
|
+
font-size: 0.875rem;
|
|
48
|
+
line-height: 1.4;
|
|
49
|
+
padding: var(--sp-2) var(--sp-3);
|
|
50
|
+
}
|
|
51
|
+
.row {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: var(--sp-2);
|
|
55
|
+
}
|
|
56
|
+
.end {
|
|
57
|
+
flex: none;
|
|
58
|
+
width: 1.75rem;
|
|
59
|
+
height: 1.75rem;
|
|
60
|
+
padding: 0;
|
|
61
|
+
border: 0;
|
|
62
|
+
border-radius: var(--r-sm);
|
|
63
|
+
background: none;
|
|
64
|
+
color: var(--text-muted);
|
|
65
|
+
font-family: inherit;
|
|
66
|
+
font-weight: var(--fw-bold);
|
|
67
|
+
line-height: 1;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
}
|
|
70
|
+
.end:hover {
|
|
71
|
+
background: var(--bg-elevated-2);
|
|
72
|
+
color: var(--text);
|
|
73
|
+
}
|
|
74
|
+
.end:focus-visible {
|
|
75
|
+
outline: 2px solid var(--accent);
|
|
76
|
+
outline-offset: 1px;
|
|
77
|
+
}
|
|
78
|
+
.small {
|
|
79
|
+
font-size: 0.75rem;
|
|
80
|
+
}
|
|
81
|
+
.large {
|
|
82
|
+
font-size: 1.125rem;
|
|
83
|
+
}
|
|
84
|
+
.caption {
|
|
85
|
+
margin-top: var(--sp-1);
|
|
86
|
+
text-align: center;
|
|
87
|
+
font-size: 0.75rem;
|
|
88
|
+
color: var(--text-muted);
|
|
89
|
+
font-variant-numeric: tabular-nums;
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
@@ -1,30 +1,136 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
// Theme switcher:
|
|
3
|
-
//
|
|
4
|
-
// theme
|
|
5
|
-
//
|
|
6
|
-
|
|
2
|
+
// Theme switcher: the trigger shows the active theme's palette swatch and
|
|
3
|
+
// opens a popover grid of swatches, light then dark. Each swatch is scoped
|
|
4
|
+
// with data-theme so its four quadrants resolve that theme's raw palette
|
|
5
|
+
// (--c-bg, --c-surface, --c-text, --c-accent; the derived --bg etc. are
|
|
6
|
+
// resolved once at :root and would not re-scope). The root default theme
|
|
7
|
+
// has no [data-theme] block, so its swatch carries the :root values.
|
|
8
|
+
import Popover from './Popover.svelte';
|
|
7
9
|
import { type ThemeDef, theme } from '../../stores/theme.svelte';
|
|
8
10
|
|
|
9
11
|
let { class: klass = '' }: { class?: string } = $props();
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
let hovered = $state<ThemeDef | null>(null);
|
|
14
|
+
const shown = $derived(hovered ?? theme.option);
|
|
15
|
+
const groups = $derived(
|
|
16
|
+
(['light', 'dark'] as const).map((mode) => ({
|
|
17
|
+
mode,
|
|
18
|
+
themes: theme.all.filter((t) => t.mode === mode)
|
|
19
|
+
}))
|
|
20
|
+
);
|
|
19
21
|
</script>
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
data-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
{#snippet swatch(id: string)}
|
|
24
|
+
<span class="swatch" data-theme={id} aria-hidden="true">
|
|
25
|
+
<i class="q bg"></i><i class="q surface"></i><i class="q text"></i><i class="q accent"></i>
|
|
26
|
+
</span>
|
|
27
|
+
{/snippet}
|
|
28
|
+
|
|
29
|
+
<Popover label="Theme: {theme.label}" placement="bottom-end" triggerClass={klass} box="md">
|
|
30
|
+
{#snippet trigger()}<span class="trigger" data-tsu="ThemePicker" title="Theme: {theme.label}">{@render swatch(theme.current)}</span>{/snippet}
|
|
31
|
+
<div class="panel">
|
|
32
|
+
{#each groups as g (g.mode)}
|
|
33
|
+
<div class="group-label">{g.mode}</div>
|
|
34
|
+
<div class="grid" role="group" aria-label="{g.mode} themes">
|
|
35
|
+
{#each g.themes as t (t.id)}
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
class="cell"
|
|
39
|
+
class:current={t.id === theme.current}
|
|
40
|
+
aria-pressed={t.id === theme.current}
|
|
41
|
+
aria-label={t.label}
|
|
42
|
+
title="{t.icon ?? theme.fallbackIcon} {t.label}"
|
|
43
|
+
onclick={() => theme.set(t.id)}
|
|
44
|
+
onpointerenter={() => (hovered = t)}
|
|
45
|
+
onpointerleave={() => (hovered = null)}
|
|
46
|
+
onfocus={() => (hovered = t)}
|
|
47
|
+
onblur={() => (hovered = null)}
|
|
48
|
+
>
|
|
49
|
+
{@render swatch(t.id)}
|
|
50
|
+
</button>
|
|
51
|
+
{/each}
|
|
52
|
+
</div>
|
|
53
|
+
{/each}
|
|
54
|
+
<div class="caption" aria-live="polite">{shown.icon ?? theme.fallbackIcon} {shown.label}</div>
|
|
55
|
+
</div>
|
|
56
|
+
</Popover>
|
|
57
|
+
|
|
58
|
+
<style>
|
|
59
|
+
.trigger {
|
|
60
|
+
display: inline-flex;
|
|
61
|
+
}
|
|
62
|
+
.swatch {
|
|
63
|
+
display: grid;
|
|
64
|
+
grid-template-columns: 1fr 1fr;
|
|
65
|
+
width: 1.25rem;
|
|
66
|
+
height: 1.25rem;
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
border-radius: var(--r-sm);
|
|
69
|
+
box-shadow: inset 0 0 0 1px var(--border);
|
|
70
|
+
}
|
|
71
|
+
.swatch[data-theme='dark'] {
|
|
72
|
+
--c-bg: #0f1115;
|
|
73
|
+
--c-surface: #21262f;
|
|
74
|
+
--c-text: #e6e9ef;
|
|
75
|
+
--c-accent: #5ad6a0;
|
|
76
|
+
}
|
|
77
|
+
.q {
|
|
78
|
+
display: block;
|
|
79
|
+
}
|
|
80
|
+
.q.bg {
|
|
81
|
+
background: var(--c-bg);
|
|
82
|
+
}
|
|
83
|
+
.q.surface {
|
|
84
|
+
background: var(--c-surface);
|
|
85
|
+
}
|
|
86
|
+
.q.text {
|
|
87
|
+
background: var(--c-text);
|
|
88
|
+
}
|
|
89
|
+
.q.accent {
|
|
90
|
+
background: var(--c-accent);
|
|
91
|
+
}
|
|
92
|
+
.panel {
|
|
93
|
+
padding: var(--sp-2);
|
|
94
|
+
width: max-content;
|
|
95
|
+
}
|
|
96
|
+
.group-label {
|
|
97
|
+
margin: var(--sp-1) var(--sp-1) var(--sp-1);
|
|
98
|
+
font-size: var(--fs-xs);
|
|
99
|
+
text-transform: uppercase;
|
|
100
|
+
letter-spacing: 0.06em;
|
|
101
|
+
color: var(--text-faint);
|
|
102
|
+
}
|
|
103
|
+
.grid {
|
|
104
|
+
display: grid;
|
|
105
|
+
grid-template-columns: repeat(6, auto);
|
|
106
|
+
gap: var(--sp-1);
|
|
107
|
+
}
|
|
108
|
+
.cell {
|
|
109
|
+
display: inline-flex;
|
|
110
|
+
padding: 3px;
|
|
111
|
+
border: 2px solid transparent;
|
|
112
|
+
border-radius: var(--r-md);
|
|
113
|
+
background: none;
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
}
|
|
116
|
+
.cell .swatch {
|
|
117
|
+
width: 1.6rem;
|
|
118
|
+
height: 1.6rem;
|
|
119
|
+
}
|
|
120
|
+
.cell:hover {
|
|
121
|
+
background: var(--bg-elevated-2);
|
|
122
|
+
}
|
|
123
|
+
.cell.current {
|
|
124
|
+
border-color: var(--accent);
|
|
125
|
+
}
|
|
126
|
+
.cell:focus-visible {
|
|
127
|
+
outline: 2px solid var(--accent);
|
|
128
|
+
outline-offset: 1px;
|
|
129
|
+
}
|
|
130
|
+
.caption {
|
|
131
|
+
margin-top: var(--sp-2);
|
|
132
|
+
text-align: center;
|
|
133
|
+
font-size: var(--fs-sm);
|
|
134
|
+
color: var(--text-muted);
|
|
135
|
+
}
|
|
136
|
+
</style>
|
package/package.json
CHANGED