@dorsk/tsumikit 0.35.0 → 0.36.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 +14 -3
- 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/layouts/MasterDetail.svelte +220 -0
- package/dist/components/layouts/MasterDetail.svelte.d.ts +22 -0
- package/dist/components/layouts/ResizablePanel.svelte +3 -7
- package/dist/components/molecules/CapBar.svelte +254 -0
- package/dist/components/molecules/CapBar.svelte.d.ts +33 -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/WorkingDir.svelte +134 -0
- package/dist/components/molecules/WorkingDir.svelte.d.ts +21 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/working-dir.d.ts +12 -0
- package/dist/working-dir.js +38 -0
- package/package.json +10 -2
|
@@ -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;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Bordered zone whose legend rides the top border. Real <fieldset>/<legend>
|
|
3
|
+
// for semantics. `droppable` turns it into an HTML5 drop target: a hovering
|
|
4
|
+
// drag that passes `accepts` highlights the zone and shows `dropHint`;
|
|
5
|
+
// `ondrop` receives the payload read from `dataTransfer` under `mime`.
|
|
6
|
+
// Keyboard users cannot drag — the consumer must offer a non-drag path
|
|
7
|
+
// (a move menu, a select) to the same action. Pointer-based DnD can drive
|
|
8
|
+
// the highlight through the bindable `over` prop.
|
|
9
|
+
import type { Snippet } from 'svelte';
|
|
10
|
+
|
|
11
|
+
type Tone = 'accent' | 'neutral' | 'strong';
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
legend,
|
|
15
|
+
tone = 'accent',
|
|
16
|
+
dashed = true,
|
|
17
|
+
padding = 'md',
|
|
18
|
+
droppable = false,
|
|
19
|
+
mime = 'text/plain',
|
|
20
|
+
accepts,
|
|
21
|
+
ondrop,
|
|
22
|
+
dropHint,
|
|
23
|
+
over = $bindable(false),
|
|
24
|
+
disabled = false,
|
|
25
|
+
class: klass = '',
|
|
26
|
+
style = '',
|
|
27
|
+
children,
|
|
28
|
+
...rest
|
|
29
|
+
}: {
|
|
30
|
+
legend?: string | Snippet;
|
|
31
|
+
tone?: Tone;
|
|
32
|
+
dashed?: boolean;
|
|
33
|
+
padding?: 'sm' | 'md' | 'lg';
|
|
34
|
+
droppable?: boolean;
|
|
35
|
+
mime?: string;
|
|
36
|
+
accepts?: (data: string, e: DragEvent) => boolean;
|
|
37
|
+
ondrop?: (data: string, e: DragEvent) => void;
|
|
38
|
+
dropHint?: string | Snippet;
|
|
39
|
+
over?: boolean;
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
class?: string;
|
|
42
|
+
style?: string;
|
|
43
|
+
children?: Snippet;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
} = $props();
|
|
46
|
+
|
|
47
|
+
let depth = 0;
|
|
48
|
+
|
|
49
|
+
function payload(e: DragEvent): string {
|
|
50
|
+
return e.dataTransfer?.getData(mime) ?? '';
|
|
51
|
+
}
|
|
52
|
+
function valid(e: DragEvent): boolean {
|
|
53
|
+
if (!droppable || disabled) return false;
|
|
54
|
+
if (!Array.from(e.dataTransfer?.types ?? []).includes(mime)) return false;
|
|
55
|
+
return accepts ? accepts(payload(e), e) : true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onDragEnter(e: DragEvent) {
|
|
59
|
+
if (!valid(e)) return;
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
depth++;
|
|
62
|
+
if (depth === 1) over = true;
|
|
63
|
+
}
|
|
64
|
+
function onDragLeave(e: DragEvent) {
|
|
65
|
+
if (!droppable || disabled) return;
|
|
66
|
+
depth = Math.max(0, depth - 1);
|
|
67
|
+
if (depth === 0) over = false;
|
|
68
|
+
}
|
|
69
|
+
function onDragOver(e: DragEvent) {
|
|
70
|
+
if (!valid(e)) return;
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
73
|
+
}
|
|
74
|
+
function onDrop(e: DragEvent) {
|
|
75
|
+
depth = 0;
|
|
76
|
+
over = false;
|
|
77
|
+
if (!valid(e)) return;
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
ondrop?.(payload(e), e);
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<fieldset
|
|
84
|
+
class="fieldset fieldset-{tone} pad-{padding} {klass}"
|
|
85
|
+
class:dashed
|
|
86
|
+
class:droppable
|
|
87
|
+
class:over
|
|
88
|
+
class:disabled
|
|
89
|
+
data-tsu="Fieldset"
|
|
90
|
+
{style}
|
|
91
|
+
ondragenter={onDragEnter}
|
|
92
|
+
ondragleave={onDragLeave}
|
|
93
|
+
ondragover={onDragOver}
|
|
94
|
+
ondrop={onDrop}
|
|
95
|
+
{...rest}
|
|
96
|
+
>
|
|
97
|
+
{#if legend}
|
|
98
|
+
<legend class="fieldset-legend">
|
|
99
|
+
{#if typeof legend === 'string'}{legend}{:else}{@render legend()}{/if}
|
|
100
|
+
</legend>
|
|
101
|
+
{/if}
|
|
102
|
+
{@render children?.()}
|
|
103
|
+
{#if droppable && over && dropHint}
|
|
104
|
+
<div class="fieldset-hint" aria-live="polite">
|
|
105
|
+
{#if typeof dropHint === 'string'}{dropHint}{:else}{@render dropHint()}{/if}
|
|
106
|
+
</div>
|
|
107
|
+
{/if}
|
|
108
|
+
</fieldset>
|
|
109
|
+
|
|
110
|
+
<style>
|
|
111
|
+
.fieldset {
|
|
112
|
+
--fieldset-border: var(--accent-dim);
|
|
113
|
+
--fieldset-pad: var(--sp-4);
|
|
114
|
+
position: relative;
|
|
115
|
+
min-width: 0;
|
|
116
|
+
margin: 0;
|
|
117
|
+
padding: calc(var(--fieldset-pad) + var(--sp-1)) var(--fieldset-pad) var(--fieldset-pad);
|
|
118
|
+
border: 2px solid var(--fieldset-border);
|
|
119
|
+
border-radius: var(--r-lg);
|
|
120
|
+
background: var(--bg);
|
|
121
|
+
transition:
|
|
122
|
+
border-color 0.12s var(--ease),
|
|
123
|
+
background 0.12s var(--ease);
|
|
124
|
+
}
|
|
125
|
+
.fieldset.dashed {
|
|
126
|
+
border-style: dashed;
|
|
127
|
+
}
|
|
128
|
+
.fieldset-neutral {
|
|
129
|
+
--fieldset-border: var(--border);
|
|
130
|
+
}
|
|
131
|
+
.fieldset-strong {
|
|
132
|
+
--fieldset-border: var(--border-strong);
|
|
133
|
+
}
|
|
134
|
+
.pad-sm {
|
|
135
|
+
--fieldset-pad: var(--sp-3);
|
|
136
|
+
}
|
|
137
|
+
.pad-lg {
|
|
138
|
+
--fieldset-pad: var(--sp-6);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.fieldset-legend {
|
|
142
|
+
position: absolute;
|
|
143
|
+
top: -13px;
|
|
144
|
+
left: 16px;
|
|
145
|
+
display: inline-flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: var(--sp-2);
|
|
148
|
+
max-width: calc(100% - 32px);
|
|
149
|
+
padding: 0 8px;
|
|
150
|
+
background: var(--bg);
|
|
151
|
+
font-size: var(--fs-sm);
|
|
152
|
+
font-weight: var(--fw-semibold);
|
|
153
|
+
line-height: 1.5;
|
|
154
|
+
color: var(--text);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.fieldset.over {
|
|
158
|
+
--fieldset-border: var(--accent);
|
|
159
|
+
background: color-mix(in srgb, var(--accent) 6%, var(--bg));
|
|
160
|
+
}
|
|
161
|
+
.fieldset.over .fieldset-legend {
|
|
162
|
+
background: color-mix(in srgb, var(--accent) 6%, var(--bg));
|
|
163
|
+
}
|
|
164
|
+
.fieldset-hint {
|
|
165
|
+
margin-top: var(--sp-3);
|
|
166
|
+
padding: var(--sp-2) var(--sp-3);
|
|
167
|
+
border: 1px dashed var(--accent);
|
|
168
|
+
border-radius: var(--r-md);
|
|
169
|
+
font-size: var(--fs-sm);
|
|
170
|
+
color: var(--accent);
|
|
171
|
+
text-align: center;
|
|
172
|
+
pointer-events: none;
|
|
173
|
+
}
|
|
174
|
+
.fieldset.disabled {
|
|
175
|
+
opacity: 0.6;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@media (prefers-reduced-motion: reduce) {
|
|
179
|
+
.fieldset {
|
|
180
|
+
transition: none;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Tone = 'accent' | 'neutral' | 'strong';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
legend?: string | Snippet;
|
|
5
|
+
tone?: Tone;
|
|
6
|
+
dashed?: boolean;
|
|
7
|
+
padding?: 'sm' | 'md' | 'lg';
|
|
8
|
+
droppable?: boolean;
|
|
9
|
+
mime?: string;
|
|
10
|
+
accepts?: (data: string, e: DragEvent) => boolean;
|
|
11
|
+
ondrop?: (data: string, e: DragEvent) => void;
|
|
12
|
+
dropHint?: string | Snippet;
|
|
13
|
+
over?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
class?: string;
|
|
16
|
+
style?: string;
|
|
17
|
+
children?: Snippet;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
declare const Fieldset: import("svelte").Component<$$ComponentProps, {}, "over">;
|
|
21
|
+
type Fieldset = ReturnType<typeof Fieldset>;
|
|
22
|
+
export default Fieldset;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Tone } from '../../tone';
|
|
3
|
+
|
|
4
|
+
export type PrState = 'open' | 'merged' | 'closed' | 'draft';
|
|
5
|
+
|
|
6
|
+
export type PullRequest = {
|
|
7
|
+
url: string;
|
|
8
|
+
owner?: string;
|
|
9
|
+
repo?: string;
|
|
10
|
+
number: number;
|
|
11
|
+
state?: PrState;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type DiffStats = { additions: number; deletions: number };
|
|
15
|
+
|
|
16
|
+
export const PR_TONE: Record<PrState, Tone> = {
|
|
17
|
+
open: 'ok',
|
|
18
|
+
merged: 'accent',
|
|
19
|
+
closed: 'danger',
|
|
20
|
+
draft: 'neutral',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const TONE_COLOR: Record<Tone, string> = {
|
|
24
|
+
neutral: 'var(--text-muted)',
|
|
25
|
+
ok: 'var(--ok)',
|
|
26
|
+
success: 'var(--ok)',
|
|
27
|
+
warn: 'var(--warn)',
|
|
28
|
+
danger: 'var(--danger)',
|
|
29
|
+
info: 'var(--info)',
|
|
30
|
+
accent: 'var(--accent)',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function prLabel(pr: PullRequest): string {
|
|
34
|
+
return pr.owner && pr.repo ? `${pr.owner}/${pr.repo}#${pr.number}` : `#${pr.number}`;
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<script lang="ts">
|
|
39
|
+
import Icon from '../atoms/Icon.svelte';
|
|
40
|
+
|
|
41
|
+
let {
|
|
42
|
+
branch,
|
|
43
|
+
pr,
|
|
44
|
+
diff,
|
|
45
|
+
maxWidth = '14rem',
|
|
46
|
+
collapse = 'auto',
|
|
47
|
+
class: klass = '',
|
|
48
|
+
...rest
|
|
49
|
+
}: {
|
|
50
|
+
branch?: string;
|
|
51
|
+
pr?: PullRequest;
|
|
52
|
+
diff?: DiffStats;
|
|
53
|
+
/** Cap on the branch chip; the name truncates with an ellipsis past it. */
|
|
54
|
+
maxWidth?: string;
|
|
55
|
+
/** `auto` drops chip text below 18rem of the nearest `.cq` container,
|
|
56
|
+
* `glyph` always shows icon-only; the full text stays in the tooltip. */
|
|
57
|
+
collapse?: 'auto' | 'never' | 'glyph';
|
|
58
|
+
class?: string;
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
} = $props();
|
|
61
|
+
|
|
62
|
+
const prTone = $derived(pr ? TONE_COLOR[PR_TONE[pr.state ?? 'open']] : undefined);
|
|
63
|
+
const label = $derived(pr ? prLabel(pr) : '');
|
|
64
|
+
const prTitle = $derived(pr ? (pr.state ? `${label} · ${pr.state}` : label) : '');
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
{#snippet pullRequestGlyph()}
|
|
68
|
+
<circle cx="5" cy="6" r="3" />
|
|
69
|
+
<path d="M5 9v12" />
|
|
70
|
+
<circle cx="19" cy="18" r="3" />
|
|
71
|
+
<path d="m15 9-3-3 3-3" />
|
|
72
|
+
<path d="M12 6h5a2 2 0 0 1 2 2v7" />
|
|
73
|
+
{/snippet}
|
|
74
|
+
|
|
75
|
+
{#if branch || pr || diff}
|
|
76
|
+
<span
|
|
77
|
+
data-tsu="GitRef"
|
|
78
|
+
class="git-ref collapse-{collapse} {klass}"
|
|
79
|
+
style:--git-ref-max-width={maxWidth}
|
|
80
|
+
{...rest}
|
|
81
|
+
>
|
|
82
|
+
{#if branch}
|
|
83
|
+
<span class="chip branch" title={branch}>
|
|
84
|
+
<Icon name="fork" label="Branch" />
|
|
85
|
+
<span class="text">{branch}</span>
|
|
86
|
+
</span>
|
|
87
|
+
{/if}
|
|
88
|
+
{#if pr}
|
|
89
|
+
<a
|
|
90
|
+
class="chip pr"
|
|
91
|
+
href={pr.url}
|
|
92
|
+
target="_blank"
|
|
93
|
+
rel="noopener noreferrer"
|
|
94
|
+
title={prTitle}
|
|
95
|
+
style:--git-ref-tone={prTone}
|
|
96
|
+
>
|
|
97
|
+
<Icon label="Pull request">{@render pullRequestGlyph()}</Icon>
|
|
98
|
+
<span class="text">{label}</span>
|
|
99
|
+
</a>
|
|
100
|
+
{/if}
|
|
101
|
+
{#if diff}
|
|
102
|
+
<span class="diff" title="+{diff.additions} −{diff.deletions}">
|
|
103
|
+
<span class="add">+{diff.additions}</span>
|
|
104
|
+
<span class="del">−{diff.deletions}</span>
|
|
105
|
+
</span>
|
|
106
|
+
{/if}
|
|
107
|
+
</span>
|
|
108
|
+
{/if}
|
|
109
|
+
|
|
110
|
+
<style>
|
|
111
|
+
.git-ref {
|
|
112
|
+
display: inline-flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: var(--sp-1);
|
|
115
|
+
min-width: 0;
|
|
116
|
+
font-family: var(--font-mono);
|
|
117
|
+
font-size: var(--fs-xs);
|
|
118
|
+
line-height: 1.4;
|
|
119
|
+
white-space: nowrap;
|
|
120
|
+
}
|
|
121
|
+
.chip {
|
|
122
|
+
display: inline-flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 0.25em;
|
|
125
|
+
min-width: 0;
|
|
126
|
+
padding: 0.15rem var(--sp-2);
|
|
127
|
+
border-radius: var(--r-pill);
|
|
128
|
+
background: var(--bg-elevated-2);
|
|
129
|
+
color: var(--text-muted);
|
|
130
|
+
border: 1px solid var(--border);
|
|
131
|
+
text-decoration: none;
|
|
132
|
+
}
|
|
133
|
+
.branch {
|
|
134
|
+
max-width: var(--git-ref-max-width, 14rem);
|
|
135
|
+
}
|
|
136
|
+
.pr {
|
|
137
|
+
color: var(--git-ref-tone);
|
|
138
|
+
border-color: color-mix(in srgb, var(--git-ref-tone) 40%, transparent);
|
|
139
|
+
background: color-mix(in srgb, var(--git-ref-tone) 12%, transparent);
|
|
140
|
+
transition: border-color 0.12s var(--ease);
|
|
141
|
+
}
|
|
142
|
+
.pr:hover {
|
|
143
|
+
border-color: var(--git-ref-tone);
|
|
144
|
+
}
|
|
145
|
+
.pr:focus-visible {
|
|
146
|
+
outline: 2px solid var(--git-ref-tone);
|
|
147
|
+
outline-offset: 2px;
|
|
148
|
+
}
|
|
149
|
+
.text {
|
|
150
|
+
min-width: 0;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
text-overflow: ellipsis;
|
|
153
|
+
white-space: nowrap;
|
|
154
|
+
}
|
|
155
|
+
.diff {
|
|
156
|
+
display: inline-flex;
|
|
157
|
+
gap: 0.35em;
|
|
158
|
+
font-variant-numeric: tabular-nums;
|
|
159
|
+
}
|
|
160
|
+
.add {
|
|
161
|
+
color: var(--ok);
|
|
162
|
+
}
|
|
163
|
+
.del {
|
|
164
|
+
color: var(--danger);
|
|
165
|
+
}
|
|
166
|
+
.collapse-glyph .text {
|
|
167
|
+
display: none;
|
|
168
|
+
}
|
|
169
|
+
@container (max-width: 18rem) {
|
|
170
|
+
.collapse-auto .text {
|
|
171
|
+
display: none;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Tone } from '../../tone';
|
|
2
|
+
export type PrState = 'open' | 'merged' | 'closed' | 'draft';
|
|
3
|
+
export type PullRequest = {
|
|
4
|
+
url: string;
|
|
5
|
+
owner?: string;
|
|
6
|
+
repo?: string;
|
|
7
|
+
number: number;
|
|
8
|
+
state?: PrState;
|
|
9
|
+
};
|
|
10
|
+
export type DiffStats = {
|
|
11
|
+
additions: number;
|
|
12
|
+
deletions: number;
|
|
13
|
+
};
|
|
14
|
+
export declare const PR_TONE: Record<PrState, Tone>;
|
|
15
|
+
export declare function prLabel(pr: PullRequest): string;
|
|
16
|
+
type $$ComponentProps = {
|
|
17
|
+
branch?: string;
|
|
18
|
+
pr?: PullRequest;
|
|
19
|
+
diff?: DiffStats;
|
|
20
|
+
/** Cap on the branch chip; the name truncates with an ellipsis past it. */
|
|
21
|
+
maxWidth?: string;
|
|
22
|
+
/** `auto` drops chip text below 18rem of the nearest `.cq` container,
|
|
23
|
+
* `glyph` always shows icon-only; the full text stays in the tooltip. */
|
|
24
|
+
collapse?: 'auto' | 'never' | 'glyph';
|
|
25
|
+
class?: string;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
declare const GitRef: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
29
|
+
type GitRef = ReturnType<typeof GitRef>;
|
|
30
|
+
export default GitRef;
|