@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,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;
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
6
6
|
danger?: boolean;
|
|
7
7
|
disabled?: boolean;
|
|
8
|
+
/** Free-form trailing pill ("admin", "pro", "beta"…), rendered as a Badge after the label. */
|
|
9
|
+
tag?: string;
|
|
10
|
+
tagTone?: import('svelte').ComponentProps<typeof import('../atoms/Badge.svelte').default>['tone'];
|
|
8
11
|
}
|
|
9
12
|
</script>
|
|
10
13
|
|
|
@@ -13,10 +16,12 @@
|
|
|
13
16
|
// `menuitem`s navigable with ↑/↓/Home/End, activated with Enter/Space (and
|
|
14
17
|
// click). Selecting an item runs its action and closes the menu. Focus moves
|
|
15
18
|
// to the first item when the menu opens. Escape / click-outside close it
|
|
16
|
-
// (inherited from the native popover).
|
|
19
|
+
// (inherited from the native popover). An item's `tag` renders as a soft
|
|
20
|
+
// Badge pill after the label; the `tag` snippet replaces that pill.
|
|
17
21
|
import type { ComponentProps, Snippet } from 'svelte';
|
|
18
22
|
import Popover from './Popover.svelte';
|
|
19
23
|
import Icon from '../atoms/Icon.svelte';
|
|
24
|
+
import Badge from '../atoms/Badge.svelte';
|
|
20
25
|
|
|
21
26
|
type PopoverProps = ComponentProps<typeof Popover>;
|
|
22
27
|
type TriggerChrome = Pick<
|
|
@@ -40,6 +45,7 @@
|
|
|
40
45
|
label,
|
|
41
46
|
items,
|
|
42
47
|
trigger,
|
|
48
|
+
tag,
|
|
43
49
|
placement = 'bottom-start',
|
|
44
50
|
variant,
|
|
45
51
|
tone,
|
|
@@ -58,6 +64,8 @@
|
|
|
58
64
|
label: string;
|
|
59
65
|
items: MenuItem[];
|
|
60
66
|
trigger: Snippet;
|
|
67
|
+
/** Custom trailing content for items that carry a `tag`. */
|
|
68
|
+
tag?: Snippet<[MenuItem]>;
|
|
61
69
|
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
62
70
|
} & TriggerChrome = $props();
|
|
63
71
|
|
|
@@ -133,6 +141,15 @@
|
|
|
133
141
|
>
|
|
134
142
|
{#if item.icon}<Icon name={item.icon} />{/if}
|
|
135
143
|
<span>{item.label}</span>
|
|
144
|
+
{#if item.tag !== undefined}
|
|
145
|
+
{#if tag}
|
|
146
|
+
{@render tag(item)}
|
|
147
|
+
{:else}
|
|
148
|
+
<span class="menu-tag">
|
|
149
|
+
<Badge size="xs" tone={item.tagTone ?? 'neutral'} border={false}>{item.tag}</Badge>
|
|
150
|
+
</span>
|
|
151
|
+
{/if}
|
|
152
|
+
{/if}
|
|
136
153
|
</button>
|
|
137
154
|
{/each}
|
|
138
155
|
</div>
|
|
@@ -166,6 +183,12 @@
|
|
|
166
183
|
.menu-item.danger {
|
|
167
184
|
color: var(--danger);
|
|
168
185
|
}
|
|
186
|
+
.menu-tag {
|
|
187
|
+
display: inline-flex;
|
|
188
|
+
flex: none;
|
|
189
|
+
margin-inline-start: auto;
|
|
190
|
+
padding-inline-start: var(--sp-3);
|
|
191
|
+
}
|
|
169
192
|
.menu-item:disabled {
|
|
170
193
|
opacity: 0.45;
|
|
171
194
|
cursor: not-allowed;
|
|
@@ -4,6 +4,9 @@ export interface MenuItem {
|
|
|
4
4
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
5
5
|
danger?: boolean;
|
|
6
6
|
disabled?: boolean;
|
|
7
|
+
/** Free-form trailing pill ("admin", "pro", "beta"…), rendered as a Badge after the label. */
|
|
8
|
+
tag?: string;
|
|
9
|
+
tagTone?: import('svelte').ComponentProps<typeof import('../atoms/Badge.svelte').default>['tone'];
|
|
7
10
|
}
|
|
8
11
|
import type { ComponentProps, Snippet } from 'svelte';
|
|
9
12
|
import Popover from './Popover.svelte';
|
|
@@ -13,6 +16,8 @@ type $$ComponentProps = {
|
|
|
13
16
|
label: string;
|
|
14
17
|
items: MenuItem[];
|
|
15
18
|
trigger: Snippet;
|
|
19
|
+
/** Custom trailing content for items that carry a `tag`. */
|
|
20
|
+
tag?: Snippet<[MenuItem]>;
|
|
16
21
|
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
17
22
|
} & TriggerChrome;
|
|
18
23
|
declare const Menu: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -3,40 +3,78 @@
|
|
|
3
3
|
value: string;
|
|
4
4
|
label: string;
|
|
5
5
|
hint?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
note?: string;
|
|
6
8
|
disabled?: boolean;
|
|
7
9
|
}
|
|
8
10
|
</script>
|
|
9
11
|
|
|
10
12
|
<script lang="ts">
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
import type { Snippet } from 'svelte';
|
|
14
|
+
|
|
15
|
+
// Native <input type="radio"> sharing one `name`: the browser handles
|
|
16
|
+
// single-selection, arrow keys and form participation. `variant="rows"`
|
|
17
|
+
// turns each option into a bordered row with optional trailing `action`
|
|
18
|
+
// (outside the label, so clicking it never toggles) and a `below` panel.
|
|
15
19
|
let {
|
|
16
20
|
options,
|
|
17
21
|
value = $bindable(),
|
|
18
22
|
name = `radio-${Math.random().toString(36).slice(2, 8)}`,
|
|
19
23
|
label,
|
|
24
|
+
variant = 'list',
|
|
25
|
+
action,
|
|
26
|
+
below,
|
|
20
27
|
class: klass = ''
|
|
21
28
|
}: {
|
|
22
29
|
options: RadioOption[];
|
|
23
30
|
value?: string;
|
|
24
31
|
name?: string;
|
|
25
32
|
label: string;
|
|
33
|
+
variant?: 'list' | 'rows';
|
|
34
|
+
action?: Snippet<[RadioOption]>;
|
|
35
|
+
below?: Snippet<[RadioOption]>;
|
|
26
36
|
class?: string;
|
|
27
37
|
} = $props();
|
|
28
38
|
</script>
|
|
29
39
|
|
|
30
|
-
<div
|
|
40
|
+
<div
|
|
41
|
+
role="radiogroup"
|
|
42
|
+
aria-label={label}
|
|
43
|
+
class="radio-group {klass}"
|
|
44
|
+
class:rows={variant === 'rows'}
|
|
45
|
+
data-tsu="RadioGroup"
|
|
46
|
+
data-variant={variant}
|
|
47
|
+
>
|
|
31
48
|
{#each options as o (o.value)}
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
{#if variant === 'rows'}
|
|
50
|
+
<div class="row-wrap">
|
|
51
|
+
<div class="row" class:selected={o.value === value} class:disabled={o.disabled}>
|
|
52
|
+
<label class="radio">
|
|
53
|
+
<input type="radio" {name} value={o.value} bind:group={value} disabled={o.disabled} />
|
|
54
|
+
<span class="dot-ctl" aria-hidden="true"></span>
|
|
55
|
+
<span class="texts">
|
|
56
|
+
<span class="label-line">
|
|
57
|
+
<span class="label-text">{o.label}</span>
|
|
58
|
+
{#if o.note}<span class="note">{o.note}</span>{/if}
|
|
59
|
+
</span>
|
|
60
|
+
{#if o.description}<span class="description">{o.description}</span>{/if}
|
|
61
|
+
{#if o.hint}<span class="hint">{o.hint}</span>{/if}
|
|
62
|
+
</span>
|
|
63
|
+
</label>
|
|
64
|
+
{#if action}<span class="action">{@render action(o)}</span>{/if}
|
|
65
|
+
</div>
|
|
66
|
+
{#if below}<div class="below">{@render below(o)}</div>{/if}
|
|
67
|
+
</div>
|
|
68
|
+
{:else}
|
|
69
|
+
<label class="radio" class:disabled={o.disabled}>
|
|
70
|
+
<input type="radio" {name} value={o.value} bind:group={value} disabled={o.disabled} />
|
|
71
|
+
<span class="dot-ctl" aria-hidden="true"></span>
|
|
72
|
+
<span class="texts">
|
|
73
|
+
<span class="label-text">{o.label}</span>
|
|
74
|
+
{#if o.hint}<span class="hint">{o.hint}</span>{/if}
|
|
75
|
+
</span>
|
|
76
|
+
</label>
|
|
77
|
+
{/if}
|
|
40
78
|
{/each}
|
|
41
79
|
</div>
|
|
42
80
|
|
|
@@ -54,8 +92,12 @@
|
|
|
54
92
|
font-size: var(--fs-sm);
|
|
55
93
|
color: var(--text);
|
|
56
94
|
}
|
|
57
|
-
.radio.disabled
|
|
95
|
+
.radio.disabled,
|
|
96
|
+
.row.disabled .radio {
|
|
58
97
|
cursor: not-allowed;
|
|
98
|
+
}
|
|
99
|
+
.radio.disabled,
|
|
100
|
+
.row.disabled {
|
|
59
101
|
opacity: 0.45;
|
|
60
102
|
}
|
|
61
103
|
input {
|
|
@@ -102,9 +144,73 @@
|
|
|
102
144
|
display: flex;
|
|
103
145
|
flex-direction: column;
|
|
104
146
|
gap: 1px;
|
|
147
|
+
min-width: 0;
|
|
105
148
|
}
|
|
106
149
|
.hint {
|
|
107
150
|
font-size: var(--fs-xs);
|
|
108
151
|
color: var(--text-faint);
|
|
109
152
|
}
|
|
153
|
+
|
|
154
|
+
.rows {
|
|
155
|
+
gap: var(--sp-1);
|
|
156
|
+
}
|
|
157
|
+
.row-wrap {
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
}
|
|
161
|
+
.row {
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: flex-start;
|
|
164
|
+
gap: var(--sp-2);
|
|
165
|
+
padding: var(--pad, var(--sp-2) var(--sp-3));
|
|
166
|
+
border: 1px solid var(--border);
|
|
167
|
+
border-radius: var(--r-md);
|
|
168
|
+
transition:
|
|
169
|
+
border-color 0.12s var(--ease),
|
|
170
|
+
background-color 0.12s var(--ease);
|
|
171
|
+
}
|
|
172
|
+
.row.selected {
|
|
173
|
+
border-color: var(--accent-dim);
|
|
174
|
+
background: var(--bg-elevated);
|
|
175
|
+
}
|
|
176
|
+
.row .radio {
|
|
177
|
+
flex: 1 1 auto;
|
|
178
|
+
min-width: 0;
|
|
179
|
+
}
|
|
180
|
+
.row .texts {
|
|
181
|
+
flex: 1 1 auto;
|
|
182
|
+
}
|
|
183
|
+
.label-line {
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: baseline;
|
|
186
|
+
gap: var(--sp-2);
|
|
187
|
+
min-width: 0;
|
|
188
|
+
}
|
|
189
|
+
.row .label-text {
|
|
190
|
+
font-weight: 600;
|
|
191
|
+
}
|
|
192
|
+
.note {
|
|
193
|
+
font-size: var(--fs-xs);
|
|
194
|
+
color: var(--text-faint);
|
|
195
|
+
white-space: nowrap;
|
|
196
|
+
}
|
|
197
|
+
.description {
|
|
198
|
+
font-size: var(--fs-xs);
|
|
199
|
+
color: var(--text-muted);
|
|
200
|
+
overflow: hidden;
|
|
201
|
+
text-overflow: ellipsis;
|
|
202
|
+
white-space: nowrap;
|
|
203
|
+
}
|
|
204
|
+
.action {
|
|
205
|
+
flex: none;
|
|
206
|
+
margin-left: auto;
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
}
|
|
210
|
+
.below {
|
|
211
|
+
padding: var(--sp-2) 0 0 var(--sp-3);
|
|
212
|
+
}
|
|
213
|
+
.below:empty {
|
|
214
|
+
display: none;
|
|
215
|
+
}
|
|
110
216
|
</style>
|
|
@@ -2,13 +2,19 @@ export interface RadioOption {
|
|
|
2
2
|
value: string;
|
|
3
3
|
label: string;
|
|
4
4
|
hint?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
note?: string;
|
|
5
7
|
disabled?: boolean;
|
|
6
8
|
}
|
|
9
|
+
import type { Snippet } from 'svelte';
|
|
7
10
|
type $$ComponentProps = {
|
|
8
11
|
options: RadioOption[];
|
|
9
12
|
value?: string;
|
|
10
13
|
name?: string;
|
|
11
14
|
label: string;
|
|
15
|
+
variant?: 'list' | 'rows';
|
|
16
|
+
action?: Snippet<[RadioOption]>;
|
|
17
|
+
below?: Snippet<[RadioOption]>;
|
|
12
18
|
class?: string;
|
|
13
19
|
};
|
|
14
20
|
declare const RadioGroup: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// it, `sticky` pins it under the app header and publishes its height as
|
|
5
5
|
// `--section-header-h` on the parent so sibling sticky content can offset
|
|
6
6
|
// itself. `collapsible` turns the title into a disclosure button that
|
|
7
|
-
// toggles the `children` block rendered beneath.
|
|
7
|
+
// toggles the `children` block rendered beneath. `variant="group"` is the
|
|
8
|
+
// non-wrapping list-group row: `lead` · title · count · flexible rule · actions.
|
|
8
9
|
import type { Snippet } from 'svelte';
|
|
9
10
|
import Heading from '../atoms/Heading.svelte';
|
|
10
11
|
import Icon, { type IconName } from '../atoms/Icon.svelte';
|
|
@@ -16,6 +17,8 @@
|
|
|
16
17
|
let {
|
|
17
18
|
title,
|
|
18
19
|
label,
|
|
20
|
+
variant = 'default',
|
|
21
|
+
lead,
|
|
19
22
|
level = 2,
|
|
20
23
|
size,
|
|
21
24
|
subtitle,
|
|
@@ -36,6 +39,9 @@
|
|
|
36
39
|
title?: string;
|
|
37
40
|
/** Alias for `title`. */
|
|
38
41
|
label?: string;
|
|
42
|
+
variant?: 'default' | 'group';
|
|
43
|
+
/** Leading content before the title (status dot, badge); group rows. */
|
|
44
|
+
lead?: Snippet;
|
|
39
45
|
level?: 1 | 2 | 3 | 4;
|
|
40
46
|
size?: Size;
|
|
41
47
|
subtitle?: string;
|
|
@@ -76,6 +82,7 @@
|
|
|
76
82
|
data-tsu="SectionHeader"
|
|
77
83
|
class="section-header sh-{tone} {klass}"
|
|
78
84
|
class:sh-divider={divider}
|
|
85
|
+
class:sh-group={variant === 'group'}
|
|
79
86
|
class:sh-sticky={sticky}
|
|
80
87
|
class:sh-uppercase={uppercase}
|
|
81
88
|
class:sh-open={open}
|
|
@@ -99,6 +106,9 @@
|
|
|
99
106
|
{#if subtitle}
|
|
100
107
|
<Text variant="caption" class="sh-subtitle">{subtitle}</Text>
|
|
101
108
|
{/if}
|
|
109
|
+
{#if variant === 'group'}
|
|
110
|
+
<span class="sh-rule" aria-hidden="true"></span>
|
|
111
|
+
{/if}
|
|
102
112
|
{#if actions}
|
|
103
113
|
<div class="sh-actions">{@render actions()}</div>
|
|
104
114
|
{/if}
|
|
@@ -109,6 +119,9 @@
|
|
|
109
119
|
</div>
|
|
110
120
|
|
|
111
121
|
{#snippet head()}
|
|
122
|
+
{#if lead}
|
|
123
|
+
<span class="sh-lead">{@render lead()}</span>
|
|
124
|
+
{/if}
|
|
112
125
|
{#if hue !== undefined}
|
|
113
126
|
<span class="sh-swatch" style:--sh-hue={hue} aria-hidden="true"></span>
|
|
114
127
|
{/if}
|
|
@@ -148,6 +161,27 @@
|
|
|
148
161
|
padding-bottom: var(--sp-2);
|
|
149
162
|
border-bottom: 1px solid var(--border);
|
|
150
163
|
}
|
|
164
|
+
.sh-group > .sh-row {
|
|
165
|
+
flex-wrap: nowrap;
|
|
166
|
+
gap: var(--sp-2);
|
|
167
|
+
}
|
|
168
|
+
.sh-lead {
|
|
169
|
+
display: inline-flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
gap: var(--sp-2);
|
|
172
|
+
flex: none;
|
|
173
|
+
}
|
|
174
|
+
.sh-rule {
|
|
175
|
+
flex: 1 1 auto;
|
|
176
|
+
min-width: var(--sp-4);
|
|
177
|
+
height: 1px;
|
|
178
|
+
background: var(--border);
|
|
179
|
+
}
|
|
180
|
+
.sh-group .sh-actions {
|
|
181
|
+
flex: none;
|
|
182
|
+
flex-wrap: nowrap;
|
|
183
|
+
margin-inline-start: 0;
|
|
184
|
+
}
|
|
151
185
|
.sh-sticky {
|
|
152
186
|
position: sticky;
|
|
153
187
|
top: var(--sticky-offset, var(--header-h, 0));
|
|
@@ -6,6 +6,9 @@ type $$ComponentProps = {
|
|
|
6
6
|
title?: string;
|
|
7
7
|
/** Alias for `title`. */
|
|
8
8
|
label?: string;
|
|
9
|
+
variant?: 'default' | 'group';
|
|
10
|
+
/** Leading content before the title (status dot, badge); group rows. */
|
|
11
|
+
lead?: Snippet;
|
|
9
12
|
level?: 1 | 2 | 3 | 4;
|
|
10
13
|
size?: Size;
|
|
11
14
|
subtitle?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
// A timestamp you can read
|
|
3
|
-
// renders in the chosen `mode` (date / time / datetime / relative / iso
|
|
2
|
+
// A timestamp you can read six ways and inspect in one place. Inline it
|
|
3
|
+
// renders in the chosen `mode` (date / time / datetime / relative / iso /
|
|
4
|
+
// short-iso),
|
|
4
5
|
// in the viewer's zone or UTC via the `utc` flag; by default clicking it
|
|
5
6
|
// opens a popover that lays out the same instant as ISO-UTC, local, relative,
|
|
6
7
|
// the IANA zone and the unix epoch — so the one value answers "when,
|
|
@@ -45,8 +46,8 @@
|
|
|
45
46
|
value: TimeInput | null | undefined;
|
|
46
47
|
/** Inline display mode. Default 'datetime'. */
|
|
47
48
|
mode?: TimestampMode;
|
|
48
|
-
/** Render the date/time/datetime modes in UTC rather than the
|
|
49
|
-
* zone. Default false. Ignored by 'iso' (always UTC) and 'relative'
|
|
49
|
+
/** Render the date/time/datetime/short-iso modes in UTC rather than the
|
|
50
|
+
* viewer's zone. Default false. Ignored by 'iso' (always UTC) and 'relative'
|
|
50
51
|
* (zoneless). Handy for calendar-date fields, where a local midnight-UTC
|
|
51
52
|
* value would otherwise show the wrong day. */
|
|
52
53
|
utc?: boolean;
|
|
@@ -101,7 +102,8 @@
|
|
|
101
102
|
{ id: 'time', name: 'Time' },
|
|
102
103
|
{ id: 'datetime', name: 'Date+time' },
|
|
103
104
|
{ id: 'relative', name: 'Relative' },
|
|
104
|
-
{ id: 'iso', name: 'ISO' }
|
|
105
|
+
{ id: 'iso', name: 'ISO' },
|
|
106
|
+
{ id: 'short-iso', name: 'YYYY-MM-DD' }
|
|
105
107
|
];
|
|
106
108
|
|
|
107
109
|
const rows = $derived(
|
|
@@ -7,8 +7,8 @@ type $$ComponentProps = {
|
|
|
7
7
|
value: TimeInput | null | undefined;
|
|
8
8
|
/** Inline display mode. Default 'datetime'. */
|
|
9
9
|
mode?: TimestampMode;
|
|
10
|
-
/** Render the date/time/datetime modes in UTC rather than the
|
|
11
|
-
* zone. Default false. Ignored by 'iso' (always UTC) and 'relative'
|
|
10
|
+
/** Render the date/time/datetime/short-iso modes in UTC rather than the
|
|
11
|
+
* viewer's zone. Default false. Ignored by 'iso' (always UTC) and 'relative'
|
|
12
12
|
* (zoneless). Handy for calendar-date fields, where a local midnight-UTC
|
|
13
13
|
* value would otherwise show the wrong day. */
|
|
14
14
|
utc?: boolean;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Working-directory chip that abbreviates fish-style as its slot narrows:
|
|
3
|
+
// ancestors shrink to their first letter left → right, then the leaf alone,
|
|
4
|
+
// then the leaf ellipsised down to `minLeaf` chars. Candidate generation
|
|
5
|
+
// lives in `$lib/working-dir`; this component only measures and picks.
|
|
6
|
+
import Badge from '../atoms/Badge.svelte';
|
|
7
|
+
import Icon, { type IconName } from '../atoms/Icon.svelte';
|
|
8
|
+
import { copyToClipboard } from '../../clipboard';
|
|
9
|
+
import { normalizeWorkingDir, workingDirCandidates } from '../../working-dir';
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
path,
|
|
13
|
+
minLeaf = 18,
|
|
14
|
+
mono = true,
|
|
15
|
+
full = false,
|
|
16
|
+
icon = 'folder',
|
|
17
|
+
title,
|
|
18
|
+
copy = false,
|
|
19
|
+
copiedLabel = 'Copied',
|
|
20
|
+
oncopy,
|
|
21
|
+
class: klass = '',
|
|
22
|
+
style = ''
|
|
23
|
+
}: {
|
|
24
|
+
path: string;
|
|
25
|
+
/** Char floor the chip reserves so the (truncated) leaf is always legible. */
|
|
26
|
+
minLeaf?: number;
|
|
27
|
+
mono?: boolean;
|
|
28
|
+
/** Skip the fit algorithm: render the whole path and size to content. */
|
|
29
|
+
full?: boolean;
|
|
30
|
+
icon?: IconName;
|
|
31
|
+
/** Defaults to the full path. */
|
|
32
|
+
title?: string;
|
|
33
|
+
/** Render as a button that copies the full path on click. */
|
|
34
|
+
copy?: boolean;
|
|
35
|
+
copiedLabel?: string;
|
|
36
|
+
oncopy?: (ok: boolean) => void;
|
|
37
|
+
class?: string;
|
|
38
|
+
style?: string;
|
|
39
|
+
} = $props();
|
|
40
|
+
|
|
41
|
+
const normalized = $derived(normalizeWorkingDir(path));
|
|
42
|
+
const candidates = $derived(workingDirCandidates(path, { minLeaf }));
|
|
43
|
+
|
|
44
|
+
let avail = $state(Infinity);
|
|
45
|
+
let chPx = $state(8);
|
|
46
|
+
let chrome = $state(0);
|
|
47
|
+
let rail: HTMLSpanElement | undefined = $state();
|
|
48
|
+
let txt: HTMLSpanElement | undefined = $state();
|
|
49
|
+
let probe: HTMLSpanElement | undefined = $state();
|
|
50
|
+
let status = $state('');
|
|
51
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
52
|
+
|
|
53
|
+
$effect(() => {
|
|
54
|
+
if (!rail || full) return;
|
|
55
|
+
const measure = () => {
|
|
56
|
+
avail = rail!.clientWidth;
|
|
57
|
+
if (probe) chPx = probe.getBoundingClientRect().width / 10 || chPx;
|
|
58
|
+
const badgeEl = txt?.parentElement;
|
|
59
|
+
if (badgeEl && txt) chrome = Math.max(0, badgeEl.offsetWidth - txt.offsetWidth);
|
|
60
|
+
};
|
|
61
|
+
measure();
|
|
62
|
+
const ro = new ResizeObserver(measure);
|
|
63
|
+
ro.observe(rail);
|
|
64
|
+
return () => ro.disconnect();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const shown = $derived.by(() => {
|
|
68
|
+
if (full) return normalized;
|
|
69
|
+
const budget = Math.max(0, avail - chrome - 1);
|
|
70
|
+
return candidates.find((c) => c.length * chPx <= budget) ?? candidates[candidates.length - 1];
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
async function onclick(e: MouseEvent) {
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
const ok = await copyToClipboard(normalized);
|
|
76
|
+
oncopy?.(ok);
|
|
77
|
+
status = ok ? copiedLabel : 'Copy failed';
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
timer = setTimeout(() => {
|
|
80
|
+
status = '';
|
|
81
|
+
}, 1500);
|
|
82
|
+
}
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<span
|
|
86
|
+
data-tsu="WorkingDir"
|
|
87
|
+
class="rail {klass}"
|
|
88
|
+
class:full
|
|
89
|
+
bind:this={rail}
|
|
90
|
+
{style}
|
|
91
|
+
>
|
|
92
|
+
<Badge
|
|
93
|
+
as={copy ? 'button' : 'span'}
|
|
94
|
+
{mono}
|
|
95
|
+
style="min-width:0;max-width:100%;overflow:hidden;text-align:left"
|
|
96
|
+
title={title ?? normalized}
|
|
97
|
+
onpointerdown={copy ? (e: PointerEvent) => e.stopPropagation() : undefined}
|
|
98
|
+
onclick={copy ? onclick : undefined}
|
|
99
|
+
>
|
|
100
|
+
<Icon name={icon} />
|
|
101
|
+
<span class="text" bind:this={txt}>{shown}</span>
|
|
102
|
+
{#if !full}
|
|
103
|
+
<span class="probe" bind:this={probe} aria-hidden="true">{'0'.repeat(10)}</span>
|
|
104
|
+
{/if}
|
|
105
|
+
</Badge>
|
|
106
|
+
{#if copy}
|
|
107
|
+
<span class="sr-only" role="status" aria-live="polite">{status}</span>
|
|
108
|
+
{/if}
|
|
109
|
+
</span>
|
|
110
|
+
|
|
111
|
+
<style>
|
|
112
|
+
.rail {
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
flex: 1 1 0;
|
|
116
|
+
min-width: 0;
|
|
117
|
+
overflow: hidden;
|
|
118
|
+
}
|
|
119
|
+
.rail.full {
|
|
120
|
+
flex: none;
|
|
121
|
+
max-width: 100%;
|
|
122
|
+
}
|
|
123
|
+
.text {
|
|
124
|
+
overflow: hidden;
|
|
125
|
+
white-space: nowrap;
|
|
126
|
+
text-overflow: ellipsis;
|
|
127
|
+
}
|
|
128
|
+
.probe {
|
|
129
|
+
position: absolute;
|
|
130
|
+
visibility: hidden;
|
|
131
|
+
white-space: pre;
|
|
132
|
+
pointer-events: none;
|
|
133
|
+
}
|
|
134
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type IconName } from '../atoms/Icon.svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
path: string;
|
|
4
|
+
/** Char floor the chip reserves so the (truncated) leaf is always legible. */
|
|
5
|
+
minLeaf?: number;
|
|
6
|
+
mono?: boolean;
|
|
7
|
+
/** Skip the fit algorithm: render the whole path and size to content. */
|
|
8
|
+
full?: boolean;
|
|
9
|
+
icon?: IconName;
|
|
10
|
+
/** Defaults to the full path. */
|
|
11
|
+
title?: string;
|
|
12
|
+
/** Render as a button that copies the full path on click. */
|
|
13
|
+
copy?: boolean;
|
|
14
|
+
copiedLabel?: string;
|
|
15
|
+
oncopy?: (ok: boolean) => void;
|
|
16
|
+
class?: string;
|
|
17
|
+
style?: string;
|
|
18
|
+
};
|
|
19
|
+
declare const WorkingDir: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
20
|
+
type WorkingDir = ReturnType<typeof WorkingDir>;
|
|
21
|
+
export default WorkingDir;
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { default as Button } from './components/atoms/Button.svelte';
|
|
|
7
7
|
export { default as Card } from './components/atoms/Card.svelte';
|
|
8
8
|
export { default as Checkbox } from './components/atoms/Checkbox.svelte';
|
|
9
9
|
export { default as Dot } from './components/atoms/Dot.svelte';
|
|
10
|
+
export { default as Gauge, type GaugeTone } from './components/atoms/Gauge.svelte';
|
|
10
11
|
export { default as Heading } from './components/atoms/Heading.svelte';
|
|
11
12
|
export type { IconName } from './components/atoms/Icon.svelte';
|
|
12
13
|
export { default as Icon } from './components/atoms/Icon.svelte';
|
|
@@ -16,6 +17,7 @@ export { default as Link } from './components/atoms/Link.svelte';
|
|
|
16
17
|
export { default as Progress } from './components/atoms/Progress.svelte';
|
|
17
18
|
export { default as Scrim } from './components/atoms/Scrim.svelte';
|
|
18
19
|
export { default as SegmentedProgress, type ProgressSegment, } from './components/atoms/SegmentedProgress.svelte';
|
|
20
|
+
export type { SelectOption } from './components/atoms/Select.svelte';
|
|
19
21
|
export { default as Select } from './components/atoms/Select.svelte';
|
|
20
22
|
export { default as Skeleton } from './components/atoms/Skeleton.svelte';
|
|
21
23
|
export { default as Slider } from './components/atoms/Slider.svelte';
|
|
@@ -27,6 +29,7 @@ export { default as AppShell } from './components/layouts/AppShell.svelte';
|
|
|
27
29
|
export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
|
|
28
30
|
export { default as Cluster } from './components/layouts/Cluster.svelte';
|
|
29
31
|
export { default as Container } from './components/layouts/Container.svelte';
|
|
32
|
+
export { default as MasterDetail } from './components/layouts/MasterDetail.svelte';
|
|
30
33
|
export { default as NavItem } from './components/layouts/NavItem.svelte';
|
|
31
34
|
export { default as NavSection } from './components/layouts/NavSection.svelte';
|
|
32
35
|
export { default as ResizablePanel } from './components/layouts/ResizablePanel.svelte';
|
|
@@ -35,15 +38,19 @@ export { default as Stack } from './components/layouts/Stack.svelte';
|
|
|
35
38
|
export { type AccordionItem, default as Accordion, } from './components/molecules/Accordion.svelte';
|
|
36
39
|
export { type BreadcrumbItem, default as Breadcrumb, } from './components/molecules/Breadcrumb.svelte';
|
|
37
40
|
export { default as Callout } from './components/molecules/Callout.svelte';
|
|
41
|
+
export { default as CapBar } from './components/molecules/CapBar.svelte';
|
|
38
42
|
export { default as CodeBlock } from './components/molecules/CodeBlock.svelte';
|
|
39
43
|
export { default as ConfirmModal } from './components/molecules/ConfirmModal.svelte';
|
|
40
44
|
export { default as CopyButton } from './components/molecules/CopyButton.svelte';
|
|
45
|
+
export { default as Drawer } from './components/molecules/Drawer.svelte';
|
|
41
46
|
export { default as Dropzone } from './components/molecules/Dropzone.svelte';
|
|
42
47
|
export { default as EmptyState } from './components/molecules/EmptyState.svelte';
|
|
43
48
|
export { default as Field } from './components/molecules/Field.svelte';
|
|
49
|
+
export { default as Fieldset } from './components/molecules/Fieldset.svelte';
|
|
44
50
|
export { default as FileButton } from './components/molecules/FileButton.svelte';
|
|
45
51
|
export { default as FilterInput, type FilterInputContext, } from './components/molecules/FilterInput.svelte';
|
|
46
52
|
export { default as FontScalePicker } from './components/molecules/FontScalePicker.svelte';
|
|
53
|
+
export { type DiffStats, default as GitRef, type PrState, type PullRequest, } from './components/molecules/GitRef.svelte';
|
|
47
54
|
export { default as IconButton } from './components/molecules/IconButton.svelte';
|
|
48
55
|
export { default as KeyValue, type KeyValueRow, type KeyValueTone, } from './components/molecules/KeyValue.svelte';
|
|
49
56
|
export { default as LoadMore } from './components/molecules/LoadMore.svelte';
|
|
@@ -64,6 +71,7 @@ export { default as Toaster } from './components/molecules/Toaster.svelte';
|
|
|
64
71
|
export { default as Toggle } from './components/molecules/Toggle.svelte';
|
|
65
72
|
export { default as Tooltip } from './components/molecules/Tooltip.svelte';
|
|
66
73
|
export { default as Truncate } from './components/molecules/Truncate.svelte';
|
|
74
|
+
export { default as WorkingDir } from './components/molecules/WorkingDir.svelte';
|
|
67
75
|
export { type Column, default as DataTable, type RowTone, } from './components/organisms/DataTable.svelte';
|
|
68
76
|
export { default as FilterSearchBar } from './components/organisms/FilterSearchBar.svelte';
|
|
69
77
|
export { FIELD_KEY, type FieldContext, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
|
|
@@ -74,3 +82,4 @@ export { type Toast, type ToastAction, type ToastOptions, type ToastTone, type T
|
|
|
74
82
|
export { formatTimestamp, localTimeZone, relativeTime, type TimeInput, type TimestampMode, } from './timestamp';
|
|
75
83
|
export { canonicalTone, type Tone } from './tone';
|
|
76
84
|
export { type TruncateMode, type TruncateOptions, truncate } from './truncate';
|
|
85
|
+
export { normalizeWorkingDir, type WorkingDirOptions, workingDirCandidates } from './working-dir';
|