@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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// List + detail columns. Above `breakpoint` (measured on this component's own
|
|
3
|
+
// box, not the viewport) both columns render and scroll independently. Below
|
|
4
|
+
// it exactly one pane renders: the list while nothing is `selected`, the
|
|
5
|
+
// detail (with a sticky back header) once it is. The consumer drives
|
|
6
|
+
// `selected` — typically from the URL — so each pane can be its own route.
|
|
7
|
+
import type { Snippet } from 'svelte';
|
|
8
|
+
import { browser } from '../../env';
|
|
9
|
+
import Icon from '../atoms/Icon.svelte';
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
list,
|
|
13
|
+
detail,
|
|
14
|
+
detailHeader,
|
|
15
|
+
empty,
|
|
16
|
+
listWidth = '17rem',
|
|
17
|
+
breakpoint = '48rem',
|
|
18
|
+
selected = false,
|
|
19
|
+
onback,
|
|
20
|
+
backLabel = 'Back',
|
|
21
|
+
listLabel = 'List',
|
|
22
|
+
class: klass = '',
|
|
23
|
+
style,
|
|
24
|
+
...rest
|
|
25
|
+
}: {
|
|
26
|
+
list: Snippet;
|
|
27
|
+
detail: Snippet;
|
|
28
|
+
/** Rendered above the detail; on mobile it shares the sticky header with the back button. */
|
|
29
|
+
detailHeader?: Snippet;
|
|
30
|
+
/** Desktop placeholder in the detail column while nothing is selected. */
|
|
31
|
+
empty?: Snippet;
|
|
32
|
+
listWidth?: string;
|
|
33
|
+
/** Own-width threshold (px/em/rem) below which the panes become two pages. */
|
|
34
|
+
breakpoint?: string;
|
|
35
|
+
selected?: boolean;
|
|
36
|
+
onback?: () => void;
|
|
37
|
+
backLabel?: string;
|
|
38
|
+
listLabel?: string;
|
|
39
|
+
class?: string;
|
|
40
|
+
style?: string;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
} = $props();
|
|
43
|
+
|
|
44
|
+
let root = $state<HTMLElement | null>(null);
|
|
45
|
+
let mobile = $state(false);
|
|
46
|
+
|
|
47
|
+
function toPx(length: string, el: HTMLElement): number {
|
|
48
|
+
const n = Number.parseFloat(length);
|
|
49
|
+
const fontSize = (node: Element) => Number.parseFloat(getComputedStyle(node).fontSize);
|
|
50
|
+
if (length.endsWith('rem')) return n * fontSize(document.documentElement);
|
|
51
|
+
if (length.endsWith('em')) return n * fontSize(el);
|
|
52
|
+
return n;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
$effect(() => {
|
|
56
|
+
if (!browser || !root) return;
|
|
57
|
+
const el = root;
|
|
58
|
+
const limit = toPx(breakpoint, el);
|
|
59
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
60
|
+
mobile = entry.contentRect.width < limit;
|
|
61
|
+
});
|
|
62
|
+
observer.observe(el);
|
|
63
|
+
return () => observer.disconnect();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const showList = $derived(!mobile || !selected);
|
|
67
|
+
const showDetail = $derived(!mobile || selected);
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<div
|
|
71
|
+
bind:this={root}
|
|
72
|
+
class="md {klass}"
|
|
73
|
+
class:mobile
|
|
74
|
+
class:selected
|
|
75
|
+
style="--md-list-w: {listWidth};{style ? ` ${style}` : ''}"
|
|
76
|
+
data-tsu="MasterDetail"
|
|
77
|
+
{...rest}
|
|
78
|
+
>
|
|
79
|
+
{#if showList}
|
|
80
|
+
<aside class="md-list" aria-label={listLabel}>
|
|
81
|
+
{@render list()}
|
|
82
|
+
</aside>
|
|
83
|
+
{/if}
|
|
84
|
+
{#if showDetail}
|
|
85
|
+
<section class="md-detail">
|
|
86
|
+
{#if mobile}
|
|
87
|
+
<header class="md-detail-header">
|
|
88
|
+
<button type="button" class="md-back" onclick={() => onback?.()}>
|
|
89
|
+
<Icon name="chevron-left" />
|
|
90
|
+
<span>{backLabel}</span>
|
|
91
|
+
</button>
|
|
92
|
+
{#if detailHeader}
|
|
93
|
+
<div class="md-detail-header-content">{@render detailHeader()}</div>
|
|
94
|
+
{/if}
|
|
95
|
+
</header>
|
|
96
|
+
{:else if detailHeader}
|
|
97
|
+
<header class="md-detail-header">
|
|
98
|
+
<div class="md-detail-header-content">{@render detailHeader()}</div>
|
|
99
|
+
</header>
|
|
100
|
+
{/if}
|
|
101
|
+
<div class="md-detail-body">
|
|
102
|
+
{#if selected}
|
|
103
|
+
{@render detail()}
|
|
104
|
+
{:else}
|
|
105
|
+
{@render empty?.()}
|
|
106
|
+
{/if}
|
|
107
|
+
</div>
|
|
108
|
+
</section>
|
|
109
|
+
{/if}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<style>
|
|
113
|
+
.md {
|
|
114
|
+
display: grid;
|
|
115
|
+
grid-template-columns: var(--md-list-w) minmax(0, 1fr);
|
|
116
|
+
min-width: 0;
|
|
117
|
+
min-height: 0;
|
|
118
|
+
height: 100%;
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
}
|
|
121
|
+
.md-list,
|
|
122
|
+
.md-detail {
|
|
123
|
+
min-width: 0;
|
|
124
|
+
min-height: 0;
|
|
125
|
+
overflow-y: auto;
|
|
126
|
+
overflow-x: hidden;
|
|
127
|
+
-webkit-overflow-scrolling: touch;
|
|
128
|
+
}
|
|
129
|
+
.md-list {
|
|
130
|
+
border-right: 1px solid var(--border);
|
|
131
|
+
}
|
|
132
|
+
.md-detail {
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
}
|
|
136
|
+
.md-detail-header {
|
|
137
|
+
position: sticky;
|
|
138
|
+
top: 0;
|
|
139
|
+
z-index: var(--z-header);
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: var(--sp-2);
|
|
143
|
+
min-width: 0;
|
|
144
|
+
min-height: var(--touch-target);
|
|
145
|
+
padding-inline: var(--sp-3);
|
|
146
|
+
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
147
|
+
backdrop-filter: blur(8px);
|
|
148
|
+
border-bottom: 1px solid var(--border);
|
|
149
|
+
}
|
|
150
|
+
.md-detail-header-content {
|
|
151
|
+
flex: 1;
|
|
152
|
+
min-width: 0;
|
|
153
|
+
}
|
|
154
|
+
.md-detail-body {
|
|
155
|
+
flex: 1;
|
|
156
|
+
min-height: 0;
|
|
157
|
+
}
|
|
158
|
+
.md-back {
|
|
159
|
+
display: inline-flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
gap: var(--sp-1);
|
|
162
|
+
min-height: var(--touch-target);
|
|
163
|
+
min-width: var(--touch-target);
|
|
164
|
+
padding-inline: var(--sp-2);
|
|
165
|
+
margin-inline-start: calc(-1 * var(--sp-2));
|
|
166
|
+
border: 0;
|
|
167
|
+
border-radius: var(--r-sm);
|
|
168
|
+
background: transparent;
|
|
169
|
+
color: var(--accent);
|
|
170
|
+
font: inherit;
|
|
171
|
+
font-size: var(--fs-sm);
|
|
172
|
+
font-weight: var(--fw-medium);
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
flex-shrink: 0;
|
|
175
|
+
}
|
|
176
|
+
.md-back:hover {
|
|
177
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
178
|
+
}
|
|
179
|
+
.md-back:focus-visible {
|
|
180
|
+
outline: 2px solid var(--accent);
|
|
181
|
+
outline-offset: 2px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.md.mobile {
|
|
185
|
+
grid-template-columns: minmax(0, 1fr);
|
|
186
|
+
}
|
|
187
|
+
.md.mobile .md-list {
|
|
188
|
+
border-right: 0;
|
|
189
|
+
width: 100%;
|
|
190
|
+
padding-inline: var(--safe-left) var(--safe-right);
|
|
191
|
+
padding-bottom: var(--safe-bottom);
|
|
192
|
+
}
|
|
193
|
+
.md.mobile .md-detail {
|
|
194
|
+
width: 100%;
|
|
195
|
+
animation: md-slide-in 0.2s var(--ease);
|
|
196
|
+
}
|
|
197
|
+
.md.mobile .md-detail-header {
|
|
198
|
+
padding-inline: max(var(--sp-3), var(--safe-left)) max(var(--sp-3), var(--safe-right));
|
|
199
|
+
}
|
|
200
|
+
.md.mobile .md-detail-body {
|
|
201
|
+
padding-inline: var(--safe-left) var(--safe-right);
|
|
202
|
+
padding-bottom: var(--safe-bottom);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@keyframes md-slide-in {
|
|
206
|
+
from {
|
|
207
|
+
transform: translateX(8%);
|
|
208
|
+
opacity: 0;
|
|
209
|
+
}
|
|
210
|
+
to {
|
|
211
|
+
transform: none;
|
|
212
|
+
opacity: 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
@media (prefers-reduced-motion: reduce) {
|
|
216
|
+
.md.mobile .md-detail {
|
|
217
|
+
animation: none;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
list: Snippet;
|
|
4
|
+
detail: Snippet;
|
|
5
|
+
/** Rendered above the detail; on mobile it shares the sticky header with the back button. */
|
|
6
|
+
detailHeader?: Snippet;
|
|
7
|
+
/** Desktop placeholder in the detail column while nothing is selected. */
|
|
8
|
+
empty?: Snippet;
|
|
9
|
+
listWidth?: string;
|
|
10
|
+
/** Own-width threshold (px/em/rem) below which the panes become two pages. */
|
|
11
|
+
breakpoint?: string;
|
|
12
|
+
selected?: boolean;
|
|
13
|
+
onback?: () => void;
|
|
14
|
+
backLabel?: string;
|
|
15
|
+
listLabel?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
style?: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
declare const MasterDetail: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
21
|
+
type MasterDetail = ReturnType<typeof MasterDetail>;
|
|
22
|
+
export default MasterDetail;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { capFromPointer, capKeyStep, capTone, clampCap, snapCap } from '../../cap-bar';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
value = 0,
|
|
7
|
+
cap = $bindable(100),
|
|
8
|
+
min = 0,
|
|
9
|
+
max = 100,
|
|
10
|
+
step = 5,
|
|
11
|
+
warnAt = 75,
|
|
12
|
+
size = 'md',
|
|
13
|
+
label,
|
|
14
|
+
labelWidth = '96px',
|
|
15
|
+
readout,
|
|
16
|
+
readoutWidth = '76px',
|
|
17
|
+
hint,
|
|
18
|
+
caption,
|
|
19
|
+
tooltip,
|
|
20
|
+
readonly = false,
|
|
21
|
+
oninput,
|
|
22
|
+
onchange,
|
|
23
|
+
class: klass = '',
|
|
24
|
+
style: styleProp = ''
|
|
25
|
+
}: {
|
|
26
|
+
/** Consumption, in percent of `max`. */
|
|
27
|
+
value?: number;
|
|
28
|
+
/** Cap position; bindable, moved by drag/keyboard. */
|
|
29
|
+
cap?: number;
|
|
30
|
+
min?: number;
|
|
31
|
+
max?: number;
|
|
32
|
+
/** Snap increment for drag and keyboard. */
|
|
33
|
+
step?: number;
|
|
34
|
+
/** Consumption at which the fill turns `--warn`. */
|
|
35
|
+
warnAt?: number;
|
|
36
|
+
size?: 'md' | 'lg';
|
|
37
|
+
label?: string | Snippet;
|
|
38
|
+
labelWidth?: string;
|
|
39
|
+
/** Overrides the default `{value}% · {hint}` readout. */
|
|
40
|
+
readout?: string | Snippet;
|
|
41
|
+
readoutWidth?: string;
|
|
42
|
+
hint?: string;
|
|
43
|
+
caption?: Snippet;
|
|
44
|
+
/** Track tooltip; defaults to "cap N% — drag the bar". */
|
|
45
|
+
tooltip?: string;
|
|
46
|
+
readonly?: boolean;
|
|
47
|
+
/** Fires live while dragging or stepping. */
|
|
48
|
+
oninput?: (cap: number) => void;
|
|
49
|
+
/** Fires once the cap is committed (pointer release / key-up). */
|
|
50
|
+
onchange?: (cap: number) => void;
|
|
51
|
+
class?: string;
|
|
52
|
+
style?: string;
|
|
53
|
+
} = $props();
|
|
54
|
+
|
|
55
|
+
const range = $derived(max - min || 1);
|
|
56
|
+
const pct = $derived(clampCap(((value - min) / range) * 100, 0, 100));
|
|
57
|
+
const capPct = $derived(clampCap(((cap - min) / range) * 100, 0, 100));
|
|
58
|
+
const tone = $derived(capTone(value, cap, warnAt));
|
|
59
|
+
const readoutText = $derived(hint ? `${value}% · ${hint}` : `${value}%`);
|
|
60
|
+
const tip = $derived(tooltip ?? `cap ${cap}% — drag the bar`);
|
|
61
|
+
const ariaLabel = $derived(typeof label === 'string' ? `${label} cap` : 'cap');
|
|
62
|
+
|
|
63
|
+
let trackEl = $state<HTMLDivElement | null>(null);
|
|
64
|
+
let dragging = $state(false);
|
|
65
|
+
let committed = cap;
|
|
66
|
+
|
|
67
|
+
function setCap(next: number) {
|
|
68
|
+
if (next === cap) return;
|
|
69
|
+
cap = next;
|
|
70
|
+
oninput?.(cap);
|
|
71
|
+
}
|
|
72
|
+
function commit() {
|
|
73
|
+
if (cap === committed) return;
|
|
74
|
+
committed = cap;
|
|
75
|
+
onchange?.(cap);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pointerDown(e: PointerEvent) {
|
|
79
|
+
if (readonly || !trackEl || e.button !== 0) return;
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
dragging = true;
|
|
82
|
+
trackEl.setPointerCapture(e.pointerId);
|
|
83
|
+
setCap(capFromPointer(e.clientX, trackEl.getBoundingClientRect(), step, min, max));
|
|
84
|
+
trackEl.querySelector<HTMLElement>('.handle')?.focus();
|
|
85
|
+
}
|
|
86
|
+
function pointerMove(e: PointerEvent) {
|
|
87
|
+
if (!dragging || !trackEl) return;
|
|
88
|
+
setCap(capFromPointer(e.clientX, trackEl.getBoundingClientRect(), step, min, max));
|
|
89
|
+
}
|
|
90
|
+
function pointerUp() {
|
|
91
|
+
if (!dragging) return;
|
|
92
|
+
dragging = false;
|
|
93
|
+
commit();
|
|
94
|
+
}
|
|
95
|
+
function keyDown(e: KeyboardEvent) {
|
|
96
|
+
if (readonly) return;
|
|
97
|
+
const next = capKeyStep(e.key, e.shiftKey, snapCap(cap, step, min, max), step, min, max);
|
|
98
|
+
if (next === null) return;
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
setCap(next);
|
|
101
|
+
}
|
|
102
|
+
function keyUp(e: KeyboardEvent) {
|
|
103
|
+
if (readonly || capKeyStep(e.key, e.shiftKey, cap, step, min, max) === null) return;
|
|
104
|
+
commit();
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<div
|
|
109
|
+
data-tsu="CapBar"
|
|
110
|
+
class="cap-bar size-{size} tone-{tone} {klass}"
|
|
111
|
+
class:readonly
|
|
112
|
+
class:dragging
|
|
113
|
+
style="--label-w: {labelWidth}; --readout-w: {readoutWidth}; --pct: {pct}%; --cap: {capPct}%; {styleProp}"
|
|
114
|
+
>
|
|
115
|
+
{#if label}
|
|
116
|
+
<div class="label">
|
|
117
|
+
{#if typeof label === 'string'}{label}{:else}{@render label()}{/if}
|
|
118
|
+
</div>
|
|
119
|
+
{/if}
|
|
120
|
+
<div
|
|
121
|
+
class="track"
|
|
122
|
+
role="presentation"
|
|
123
|
+
bind:this={trackEl}
|
|
124
|
+
title={tip}
|
|
125
|
+
onpointerdown={pointerDown}
|
|
126
|
+
onpointermove={pointerMove}
|
|
127
|
+
onpointerup={pointerUp}
|
|
128
|
+
onpointercancel={pointerUp}
|
|
129
|
+
>
|
|
130
|
+
<div class="fill"></div>
|
|
131
|
+
<div
|
|
132
|
+
class="handle"
|
|
133
|
+
role="slider"
|
|
134
|
+
tabindex={readonly ? -1 : 0}
|
|
135
|
+
aria-label={ariaLabel}
|
|
136
|
+
aria-valuemin={min}
|
|
137
|
+
aria-valuemax={max}
|
|
138
|
+
aria-valuenow={cap}
|
|
139
|
+
aria-valuetext="cap {cap}%"
|
|
140
|
+
aria-disabled={readonly ? 'true' : undefined}
|
|
141
|
+
onkeydown={keyDown}
|
|
142
|
+
onkeyup={keyUp}
|
|
143
|
+
></div>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="readout">
|
|
146
|
+
{#if readout === undefined}{readoutText}{:else if typeof readout === 'string'}{readout}{:else}{@render readout()}{/if}
|
|
147
|
+
</div>
|
|
148
|
+
{#if caption}
|
|
149
|
+
<div class="caption">{@render caption()}</div>
|
|
150
|
+
{/if}
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<style>
|
|
154
|
+
.cap-bar {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-columns: var(--label-w) 1fr var(--readout-w);
|
|
157
|
+
align-items: center;
|
|
158
|
+
column-gap: var(--sp-3);
|
|
159
|
+
row-gap: var(--sp-1);
|
|
160
|
+
width: 100%;
|
|
161
|
+
font-size: var(--fs-sm);
|
|
162
|
+
--track-h: 6px;
|
|
163
|
+
--fill: var(--ok);
|
|
164
|
+
}
|
|
165
|
+
.cap-bar.size-lg {
|
|
166
|
+
--track-h: 10px;
|
|
167
|
+
}
|
|
168
|
+
.tone-warn {
|
|
169
|
+
--fill: var(--warn);
|
|
170
|
+
}
|
|
171
|
+
.tone-danger {
|
|
172
|
+
--fill: var(--danger);
|
|
173
|
+
}
|
|
174
|
+
.label {
|
|
175
|
+
color: var(--text-muted);
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
text-overflow: ellipsis;
|
|
178
|
+
white-space: nowrap;
|
|
179
|
+
}
|
|
180
|
+
.cap-bar:not(:has(.label)) .track {
|
|
181
|
+
grid-column: 1 / 3;
|
|
182
|
+
}
|
|
183
|
+
.track {
|
|
184
|
+
position: relative;
|
|
185
|
+
height: var(--track-h);
|
|
186
|
+
border-radius: var(--r-pill);
|
|
187
|
+
background: color-mix(in oklab, var(--accent) 26%, var(--bg));
|
|
188
|
+
cursor: ew-resize;
|
|
189
|
+
touch-action: none;
|
|
190
|
+
user-select: none;
|
|
191
|
+
}
|
|
192
|
+
.track::after {
|
|
193
|
+
content: '';
|
|
194
|
+
position: absolute;
|
|
195
|
+
inset: 0 0 0 var(--cap);
|
|
196
|
+
border-radius: 0 var(--r-pill) var(--r-pill) 0;
|
|
197
|
+
background: var(--bg);
|
|
198
|
+
box-shadow: inset 0 0 0 1px var(--border);
|
|
199
|
+
pointer-events: none;
|
|
200
|
+
}
|
|
201
|
+
.fill {
|
|
202
|
+
position: absolute;
|
|
203
|
+
inset: 0 auto 0 0;
|
|
204
|
+
width: var(--pct);
|
|
205
|
+
border-radius: var(--r-pill);
|
|
206
|
+
background: var(--fill);
|
|
207
|
+
transition: width 0.2s var(--ease);
|
|
208
|
+
z-index: 1;
|
|
209
|
+
}
|
|
210
|
+
.handle {
|
|
211
|
+
position: absolute;
|
|
212
|
+
top: 50%;
|
|
213
|
+
left: var(--cap);
|
|
214
|
+
width: 3px;
|
|
215
|
+
height: 16px;
|
|
216
|
+
transform: translate(-50%, -50%);
|
|
217
|
+
border-radius: 2px;
|
|
218
|
+
background: var(--text);
|
|
219
|
+
box-shadow: 0 0 0 2px var(--surface);
|
|
220
|
+
cursor: ew-resize;
|
|
221
|
+
z-index: 2;
|
|
222
|
+
}
|
|
223
|
+
.handle:focus-visible {
|
|
224
|
+
outline: 2px solid var(--accent);
|
|
225
|
+
outline-offset: 2px;
|
|
226
|
+
}
|
|
227
|
+
.readonly .track,
|
|
228
|
+
.readonly .handle {
|
|
229
|
+
cursor: default;
|
|
230
|
+
pointer-events: none;
|
|
231
|
+
}
|
|
232
|
+
.readonly .handle {
|
|
233
|
+
opacity: 0.6;
|
|
234
|
+
}
|
|
235
|
+
.readout {
|
|
236
|
+
text-align: right;
|
|
237
|
+
font-variant-numeric: tabular-nums;
|
|
238
|
+
color: var(--text-muted);
|
|
239
|
+
white-space: nowrap;
|
|
240
|
+
}
|
|
241
|
+
.caption {
|
|
242
|
+
grid-column: 2 / 3;
|
|
243
|
+
font-size: var(--fs-xs);
|
|
244
|
+
color: var(--text-faint);
|
|
245
|
+
}
|
|
246
|
+
.cap-bar:not(:has(.label)) .caption {
|
|
247
|
+
grid-column: 1 / 3;
|
|
248
|
+
}
|
|
249
|
+
@media (prefers-reduced-motion: reduce) {
|
|
250
|
+
.fill {
|
|
251
|
+
transition: none;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** Consumption, in percent of `max`. */
|
|
4
|
+
value?: number;
|
|
5
|
+
/** Cap position; bindable, moved by drag/keyboard. */
|
|
6
|
+
cap?: number;
|
|
7
|
+
min?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
/** Snap increment for drag and keyboard. */
|
|
10
|
+
step?: number;
|
|
11
|
+
/** Consumption at which the fill turns `--warn`. */
|
|
12
|
+
warnAt?: number;
|
|
13
|
+
size?: 'md' | 'lg';
|
|
14
|
+
label?: string | Snippet;
|
|
15
|
+
labelWidth?: string;
|
|
16
|
+
/** Overrides the default `{value}% · {hint}` readout. */
|
|
17
|
+
readout?: string | Snippet;
|
|
18
|
+
readoutWidth?: string;
|
|
19
|
+
hint?: string;
|
|
20
|
+
caption?: Snippet;
|
|
21
|
+
/** Track tooltip; defaults to "cap N% — drag the bar". */
|
|
22
|
+
tooltip?: string;
|
|
23
|
+
readonly?: boolean;
|
|
24
|
+
/** Fires live while dragging or stepping. */
|
|
25
|
+
oninput?: (cap: number) => void;
|
|
26
|
+
/** Fires once the cap is committed (pointer release / key-up). */
|
|
27
|
+
onchange?: (cap: number) => void;
|
|
28
|
+
class?: string;
|
|
29
|
+
style?: string;
|
|
30
|
+
};
|
|
31
|
+
declare const CapBar: import("svelte").Component<$$ComponentProps, {}, "cap">;
|
|
32
|
+
type CapBar = ReturnType<typeof CapBar>;
|
|
33
|
+
export default CapBar;
|