@dorsk/tsumikit 0.16.0 → 0.17.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/dist/components/atoms/Button.svelte +41 -4
- package/dist/components/atoms/Button.svelte.d.ts +5 -0
- package/dist/components/atoms/Input.svelte +10 -0
- package/dist/components/atoms/Input.svelte.d.ts +3 -0
- package/dist/components/atoms/Select.svelte +18 -3
- package/dist/components/atoms/Select.svelte.d.ts +6 -2
- package/dist/components/layouts/Cluster.svelte +21 -0
- package/dist/components/layouts/Cluster.svelte.d.ts +2 -0
- package/dist/components/layouts/ResizablePanel.svelte +89 -14
- package/dist/components/layouts/ResizablePanel.svelte.d.ts +5 -0
- package/dist/components/molecules/IconButton.svelte +4 -0
- package/dist/components/molecules/IconButton.svelte.d.ts +2 -0
- package/dist/components/molecules/OptionButton.svelte +19 -0
- package/dist/components/molecules/OptionButton.svelte.d.ts +5 -0
- package/dist/components/molecules/SegmentedControl.svelte +13 -0
- package/dist/components/molecules/SegmentedControl.svelte.d.ts +3 -0
- package/package.json +1 -1
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
size?: 'sm' | 'md' | 'lg';
|
|
12
12
|
control?: boolean;
|
|
13
13
|
block?: boolean;
|
|
14
|
+
// Fill the available width of a flex/Cluster row (flex: 1). `block` stretches
|
|
15
|
+
// to 100% (grid/stack); `grow` shares a flex row with siblings.
|
|
16
|
+
grow?: boolean;
|
|
17
|
+
// Render as a link (`<a href>`) while keeping button chrome — for navigations
|
|
18
|
+
// and open-in-new-tab actions that shouldn't be a bare <a>. Setting `href`
|
|
19
|
+
// implies `as="a"`.
|
|
20
|
+
as?: 'button' | 'a';
|
|
21
|
+
href?: string;
|
|
22
|
+
target?: string;
|
|
23
|
+
rel?: string;
|
|
14
24
|
// Square 2.25rem icon-only tap target (IconButton). `iconInline` is the
|
|
15
25
|
// borderless, compact variant (chip-remove ✕, inline edit ✎); pair with
|
|
16
26
|
// `hoverDanger` to tint it red on hover (delete affordances).
|
|
@@ -33,6 +43,9 @@
|
|
|
33
43
|
size = 'md',
|
|
34
44
|
control = false,
|
|
35
45
|
block = false,
|
|
46
|
+
grow = false,
|
|
47
|
+
as = 'button',
|
|
48
|
+
href,
|
|
36
49
|
icon = false,
|
|
37
50
|
chip = false,
|
|
38
51
|
iconInline = false,
|
|
@@ -46,16 +59,27 @@
|
|
|
46
59
|
children,
|
|
47
60
|
...rest
|
|
48
61
|
}: ButtonProps = $props();
|
|
62
|
+
|
|
63
|
+
// A link-flavoured Button is a real <a>; native `disabled`/`type` don't apply,
|
|
64
|
+
// so a disabled link is expressed via aria-disabled + inert pointer handling.
|
|
65
|
+
const tag = $derived(href !== undefined || as === 'a' ? 'a' : 'button');
|
|
66
|
+
const inactive = $derived(disabled || loading);
|
|
67
|
+
const elementAttrs = $derived(
|
|
68
|
+
tag === 'a'
|
|
69
|
+
? { href, 'aria-disabled': inactive || undefined }
|
|
70
|
+
: { type, disabled: inactive }
|
|
71
|
+
);
|
|
49
72
|
</script>
|
|
50
73
|
|
|
51
|
-
<
|
|
74
|
+
<svelte:element
|
|
75
|
+
this={tag}
|
|
52
76
|
data-tsu="Button"
|
|
53
77
|
{...rest}
|
|
54
|
-
{
|
|
55
|
-
disabled={disabled || loading}
|
|
78
|
+
{...elementAttrs}
|
|
56
79
|
aria-busy={loading || undefined}
|
|
57
80
|
{title}
|
|
58
81
|
class="btn {klass}"
|
|
82
|
+
class:btn-grow={grow}
|
|
59
83
|
class:btn-primary={variant === 'primary'}
|
|
60
84
|
class:btn-ghost={variant === 'ghost'}
|
|
61
85
|
class:btn-danger={variant === 'danger'}
|
|
@@ -77,7 +101,7 @@
|
|
|
77
101
|
>
|
|
78
102
|
{#if loading}<span class="btn-spinner" aria-hidden="true"></span>{/if}
|
|
79
103
|
{@render children?.()}
|
|
80
|
-
</
|
|
104
|
+
</svelte:element>
|
|
81
105
|
|
|
82
106
|
<style>
|
|
83
107
|
/* The canonical control: owns its variants/sizes/modifiers from theme tokens.
|
|
@@ -151,6 +175,19 @@
|
|
|
151
175
|
.btn-block {
|
|
152
176
|
width: 100%;
|
|
153
177
|
}
|
|
178
|
+
.btn-grow {
|
|
179
|
+
flex: 1 1 0;
|
|
180
|
+
}
|
|
181
|
+
/* Link-flavoured Button: keep button chrome, drop the anchor's default link
|
|
182
|
+
look, and make a disabled link truly inert. */
|
|
183
|
+
a.btn {
|
|
184
|
+
text-decoration: none;
|
|
185
|
+
}
|
|
186
|
+
a.btn[aria-disabled='true'] {
|
|
187
|
+
opacity: 0.45;
|
|
188
|
+
pointer-events: none;
|
|
189
|
+
cursor: not-allowed;
|
|
190
|
+
}
|
|
154
191
|
|
|
155
192
|
/* Semantic state tones — tint text + border, subtle fill on hover. Layered on
|
|
156
193
|
the neutral/ghost variant; each defines --btn-tone so one ruleset paints. */
|
|
@@ -6,6 +6,11 @@ type ButtonProps = HTMLButtonAttributes & {
|
|
|
6
6
|
size?: 'sm' | 'md' | 'lg';
|
|
7
7
|
control?: boolean;
|
|
8
8
|
block?: boolean;
|
|
9
|
+
grow?: boolean;
|
|
10
|
+
as?: 'button' | 'a';
|
|
11
|
+
href?: string;
|
|
12
|
+
target?: string;
|
|
13
|
+
rel?: string;
|
|
9
14
|
icon?: boolean;
|
|
10
15
|
chip?: boolean;
|
|
11
16
|
iconInline?: boolean;
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
type Props = Omit<HTMLInputAttributes, 'size'> & {
|
|
10
10
|
mono?: boolean;
|
|
11
11
|
size?: 'sm' | 'md';
|
|
12
|
+
/** Fill the available width of a flex/Cluster row (flex: 1) instead of
|
|
13
|
+
* needing a raw `style="flex:1"` at the call-site. */
|
|
14
|
+
grow?: boolean;
|
|
12
15
|
/** Error state: danger border + aria-invalid (also styles if a consumer
|
|
13
16
|
* sets aria-invalid directly). */
|
|
14
17
|
invalid?: boolean;
|
|
@@ -20,6 +23,7 @@
|
|
|
20
23
|
let {
|
|
21
24
|
mono = false,
|
|
22
25
|
size = 'md',
|
|
26
|
+
grow = false,
|
|
23
27
|
invalid = false,
|
|
24
28
|
class: klass = '',
|
|
25
29
|
value = $bindable(),
|
|
@@ -34,6 +38,7 @@
|
|
|
34
38
|
class="input {klass}"
|
|
35
39
|
class:mono
|
|
36
40
|
class:input-sm={size === 'sm'}
|
|
41
|
+
class:input-grow={grow}
|
|
37
42
|
bind:value
|
|
38
43
|
{...rest}
|
|
39
44
|
aria-invalid={invalid || undefined}
|
|
@@ -57,6 +62,11 @@
|
|
|
57
62
|
padding: var(--sp-2);
|
|
58
63
|
font-size: var(--fs-sm);
|
|
59
64
|
}
|
|
65
|
+
.input-grow {
|
|
66
|
+
flex: 1 1 0;
|
|
67
|
+
width: auto;
|
|
68
|
+
min-width: 0;
|
|
69
|
+
}
|
|
60
70
|
.input[aria-invalid='true'] {
|
|
61
71
|
border-color: var(--danger);
|
|
62
72
|
}
|
|
@@ -2,6 +2,9 @@ import type { HTMLInputAttributes } from 'svelte/elements';
|
|
|
2
2
|
type Props = Omit<HTMLInputAttributes, 'size'> & {
|
|
3
3
|
mono?: boolean;
|
|
4
4
|
size?: 'sm' | 'md';
|
|
5
|
+
/** Fill the available width of a flex/Cluster row (flex: 1) instead of
|
|
6
|
+
* needing a raw `style="flex:1"` at the call-site. */
|
|
7
|
+
grow?: boolean;
|
|
5
8
|
/** Error state: danger border + aria-invalid (also styles if a consumer
|
|
6
9
|
* sets aria-invalid directly). */
|
|
7
10
|
invalid?: boolean;
|
|
@@ -15,12 +15,18 @@
|
|
|
15
15
|
import type { HTMLSelectAttributes } from 'svelte/elements';
|
|
16
16
|
import Icon from './Icon.svelte';
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// `size` shadows the native option-count attribute (unused in token layouts) to
|
|
19
|
+
// expose the sm|md height scale instead.
|
|
20
|
+
type Props = Omit<HTMLSelectAttributes, 'size'> & {
|
|
19
21
|
variant?: 'default' | 'ghost';
|
|
20
22
|
/** Error state: danger border + aria-invalid. */
|
|
21
23
|
invalid?: boolean;
|
|
22
|
-
/** Compact inline form: smaller padding + font for dense toolbars/headers.
|
|
24
|
+
/** Compact inline form: smaller padding + font for dense toolbars/headers.
|
|
25
|
+
* Deprecated alias for `size="sm"` (kept for backward compatibility). */
|
|
23
26
|
compact?: boolean;
|
|
27
|
+
/** Size scale matching Button/SegmentedControl: `sm` also adopts the shared
|
|
28
|
+
* `--control-height-compact` toolbar height so it lines up with siblings. */
|
|
29
|
+
size?: 'sm' | 'md';
|
|
24
30
|
/** Draw the custom chevron (default). Set false for a bare inline select. */
|
|
25
31
|
chevron?: boolean;
|
|
26
32
|
class?: string;
|
|
@@ -32,12 +38,15 @@
|
|
|
32
38
|
variant = 'default',
|
|
33
39
|
invalid = false,
|
|
34
40
|
compact = false,
|
|
41
|
+
size = 'md',
|
|
35
42
|
chevron = true,
|
|
36
43
|
class: klass = '',
|
|
37
44
|
value = $bindable(),
|
|
38
45
|
children,
|
|
39
46
|
...rest
|
|
40
47
|
}: Props = $props();
|
|
48
|
+
|
|
49
|
+
const small = $derived(compact || size === 'sm');
|
|
41
50
|
</script>
|
|
42
51
|
|
|
43
52
|
{#if variant === 'ghost'}
|
|
@@ -48,7 +57,8 @@
|
|
|
48
57
|
<div class="select-wrap {klass}" class:no-chevron={!chevron} data-tsu="Select">
|
|
49
58
|
<select
|
|
50
59
|
class="select"
|
|
51
|
-
class:compact
|
|
60
|
+
class:compact={small}
|
|
61
|
+
class:select-sm={size === 'sm'}
|
|
52
62
|
bind:value
|
|
53
63
|
{...rest}
|
|
54
64
|
aria-invalid={invalid || undefined}
|
|
@@ -96,6 +106,11 @@
|
|
|
96
106
|
.select-wrap:not(.no-chevron) .select.compact {
|
|
97
107
|
padding-right: calc(var(--sp-2) + 1rem);
|
|
98
108
|
}
|
|
109
|
+
/* Toolbar contract: size="sm" shares the compact control height so it lines up
|
|
110
|
+
with Button size="sm", Popover size="sm" and SegmentedControl size="sm". */
|
|
111
|
+
.select.select-sm {
|
|
112
|
+
height: var(--control-height-compact);
|
|
113
|
+
}
|
|
99
114
|
.select:focus {
|
|
100
115
|
outline: none;
|
|
101
116
|
border-color: var(--accent);
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLSelectAttributes } from 'svelte/elements';
|
|
3
|
-
type Props = HTMLSelectAttributes & {
|
|
3
|
+
type Props = Omit<HTMLSelectAttributes, 'size'> & {
|
|
4
4
|
variant?: 'default' | 'ghost';
|
|
5
5
|
/** Error state: danger border + aria-invalid. */
|
|
6
6
|
invalid?: boolean;
|
|
7
|
-
/** Compact inline form: smaller padding + font for dense toolbars/headers.
|
|
7
|
+
/** Compact inline form: smaller padding + font for dense toolbars/headers.
|
|
8
|
+
* Deprecated alias for `size="sm"` (kept for backward compatibility). */
|
|
8
9
|
compact?: boolean;
|
|
10
|
+
/** Size scale matching Button/SegmentedControl: `sm` also adopts the shared
|
|
11
|
+
* `--control-height-compact` toolbar height so it lines up with siblings. */
|
|
12
|
+
size?: 'sm' | 'md';
|
|
9
13
|
/** Draw the custom chevron (default). Set false for a bare inline select. */
|
|
10
14
|
chevron?: boolean;
|
|
11
15
|
class?: string;
|
|
@@ -5,12 +5,25 @@
|
|
|
5
5
|
// `wrap={false}` to keep one line. Polymorphic via `as`.
|
|
6
6
|
import type { Snippet } from 'svelte';
|
|
7
7
|
|
|
8
|
+
// `size` cascades a uniform control tier to descendant controls that honour the
|
|
9
|
+
// shared `--control-height` contract (Button/IconButton/Popover `control`,
|
|
10
|
+
// SegmentedControl/Select `control`), so a whole toolbar lines up from one prop
|
|
11
|
+
// instead of per-child sizing. `grow` makes direct children share the row width
|
|
12
|
+
// equally (flex: 1) — the toolbar equivalent of `style="flex:1"` on each child.
|
|
13
|
+
const CONTROL_TIER = {
|
|
14
|
+
sm: 'var(--control-height-compact)',
|
|
15
|
+
md: 'var(--control-height-default)',
|
|
16
|
+
lg: 'var(--control-height-large)'
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
8
19
|
let {
|
|
9
20
|
as = 'div',
|
|
10
21
|
gap = 'var(--sp-2)',
|
|
11
22
|
align = 'center',
|
|
12
23
|
justify,
|
|
13
24
|
wrap = true,
|
|
25
|
+
size,
|
|
26
|
+
grow = false,
|
|
14
27
|
class: klass = '',
|
|
15
28
|
children,
|
|
16
29
|
...rest
|
|
@@ -20,6 +33,8 @@
|
|
|
20
33
|
align?: string;
|
|
21
34
|
justify?: string;
|
|
22
35
|
wrap?: boolean;
|
|
36
|
+
size?: 'sm' | 'md' | 'lg';
|
|
37
|
+
grow?: boolean;
|
|
23
38
|
class?: string;
|
|
24
39
|
children?: Snippet;
|
|
25
40
|
[key: string]: unknown;
|
|
@@ -30,10 +45,12 @@
|
|
|
30
45
|
this={as}
|
|
31
46
|
data-tsu="Cluster"
|
|
32
47
|
class="cluster-c {klass}"
|
|
48
|
+
class:cluster-grow={grow}
|
|
33
49
|
style:gap
|
|
34
50
|
style:align-items={align}
|
|
35
51
|
style:justify-content={justify}
|
|
36
52
|
style:flex-wrap={wrap ? 'wrap' : 'nowrap'}
|
|
53
|
+
style:--control-height={size ? CONTROL_TIER[size] : undefined}
|
|
37
54
|
{...rest}
|
|
38
55
|
>
|
|
39
56
|
{@render children?.()}
|
|
@@ -43,4 +60,8 @@
|
|
|
43
60
|
.cluster-c {
|
|
44
61
|
display: flex;
|
|
45
62
|
}
|
|
63
|
+
.cluster-grow > :global(*) {
|
|
64
|
+
flex: 1 1 0;
|
|
65
|
+
min-width: 0;
|
|
66
|
+
}
|
|
46
67
|
</style>
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
widthKey,
|
|
17
17
|
collapsed = $bindable(false),
|
|
18
18
|
persistCollapsed = true,
|
|
19
|
-
resizeStep = 16
|
|
19
|
+
resizeStep = 16,
|
|
20
|
+
handlePlacement = 'bottom',
|
|
21
|
+
stickyHandle = true
|
|
20
22
|
}: {
|
|
21
23
|
/** Content shown while the panel is expanded. */
|
|
22
24
|
panel: Snippet;
|
|
@@ -38,12 +40,19 @@
|
|
|
38
40
|
persistCollapsed?: boolean;
|
|
39
41
|
/** Pixels added or removed by each resize-separator arrow key press. */
|
|
40
42
|
resizeStep?: number;
|
|
43
|
+
/** Anchor the collapse handle at the top or bottom of the panel edge. */
|
|
44
|
+
handlePlacement?: 'top' | 'bottom';
|
|
45
|
+
/** Keep the collapse handle in view when the panel scrolls past the
|
|
46
|
+
* viewport, repositioning on scroll/resize via requestAnimationFrame. */
|
|
47
|
+
stickyHandle?: boolean;
|
|
41
48
|
} = $props();
|
|
42
49
|
|
|
43
50
|
let root: HTMLDivElement;
|
|
51
|
+
let handleEl = $state<HTMLButtonElement | null>(null);
|
|
44
52
|
let panelWidth = $state<number>();
|
|
45
53
|
let resizing = $state(false);
|
|
46
54
|
let restored = false;
|
|
55
|
+
let stickyShift = $state(0);
|
|
47
56
|
|
|
48
57
|
const boundedMin = $derived(Math.max(1, Math.min(minWidth, maxWidth)));
|
|
49
58
|
const boundedMax = $derived(Math.max(boundedMin, maxWidth));
|
|
@@ -96,6 +105,55 @@
|
|
|
96
105
|
(nextWidth) => setWidth(nextWidth, false)
|
|
97
106
|
);
|
|
98
107
|
|
|
108
|
+
// Keep the collapse handle within the viewport (and its panel) while a long
|
|
109
|
+
// panel scrolls past the fold. Scroll fires often, so coalesce recomputes
|
|
110
|
+
// into one per animation frame.
|
|
111
|
+
let stickyFrame: number | undefined;
|
|
112
|
+
|
|
113
|
+
function computeSticky() {
|
|
114
|
+
if (!stickyHandle || !root || !handleEl) {
|
|
115
|
+
stickyShift = 0;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const bounds = root.getBoundingClientRect();
|
|
119
|
+
const viewport = window.innerHeight || document.documentElement.clientHeight;
|
|
120
|
+
const handleHeight = handleEl.offsetHeight;
|
|
121
|
+
const margin = 8;
|
|
122
|
+
const naturalTop =
|
|
123
|
+
handlePlacement === 'top'
|
|
124
|
+
? bounds.top + margin
|
|
125
|
+
: bounds.bottom - margin - handleHeight;
|
|
126
|
+
const panelLo = bounds.top + margin;
|
|
127
|
+
const panelHi = bounds.bottom - margin - handleHeight;
|
|
128
|
+
const inViewport = Math.min(Math.max(naturalTop, margin), viewport - margin - handleHeight);
|
|
129
|
+
const clamped = Math.min(Math.max(inViewport, panelLo), Math.max(panelLo, panelHi));
|
|
130
|
+
stickyShift = Math.round(clamped - naturalTop);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function scheduleSticky() {
|
|
134
|
+
if (!browser || stickyFrame !== undefined) return;
|
|
135
|
+
stickyFrame = requestAnimationFrame(() => {
|
|
136
|
+
stickyFrame = undefined;
|
|
137
|
+
computeSticky();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
$effect(() => {
|
|
142
|
+
if (!browser || !stickyHandle) {
|
|
143
|
+
stickyShift = 0;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
computeSticky();
|
|
147
|
+
addEventListener('scroll', scheduleSticky, true);
|
|
148
|
+
addEventListener('resize', scheduleSticky);
|
|
149
|
+
return () => {
|
|
150
|
+
removeEventListener('scroll', scheduleSticky, true);
|
|
151
|
+
removeEventListener('resize', scheduleSticky);
|
|
152
|
+
if (stickyFrame !== undefined) cancelAnimationFrame(stickyFrame);
|
|
153
|
+
stickyFrame = undefined;
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
|
|
99
157
|
onDestroy(() => pointerWidths.discard());
|
|
100
158
|
|
|
101
159
|
function toggle() {
|
|
@@ -196,14 +254,18 @@
|
|
|
196
254
|
{/if}
|
|
197
255
|
|
|
198
256
|
<button
|
|
257
|
+
bind:this={handleEl}
|
|
199
258
|
type="button"
|
|
200
259
|
class="collapse-control"
|
|
260
|
+
class:top={handlePlacement === 'top'}
|
|
261
|
+
class:bottom={handlePlacement === 'bottom'}
|
|
201
262
|
aria-label={toggleLabel}
|
|
202
263
|
aria-expanded={!collapsed}
|
|
203
264
|
title={toggleLabel}
|
|
265
|
+
style="transform: translateY({stickyShift}px)"
|
|
204
266
|
onclick={toggle}
|
|
205
267
|
>
|
|
206
|
-
<Icon name={toggleIcon} size={
|
|
268
|
+
<Icon name={toggleIcon} size={14} />
|
|
207
269
|
</button>
|
|
208
270
|
</aside>
|
|
209
271
|
|
|
@@ -260,32 +322,45 @@
|
|
|
260
322
|
min-height: 0;
|
|
261
323
|
overflow: auto;
|
|
262
324
|
}
|
|
325
|
+
/* Subtle chevron handle anchored to the panel's inner edge — never a filled
|
|
326
|
+
button, never overlapping the neighbouring main content. It stays visible
|
|
327
|
+
when collapsed (the panel is 0px wide) because it lives on the layout edge,
|
|
328
|
+
and its `transform` is nudged by the sticky logic to track the viewport. */
|
|
263
329
|
.collapse-control {
|
|
264
330
|
position: absolute;
|
|
265
|
-
|
|
266
|
-
bottom: var(--sp-3);
|
|
331
|
+
left: var(--sp-1);
|
|
267
332
|
z-index: 3;
|
|
268
|
-
display: flex;
|
|
333
|
+
display: inline-flex;
|
|
269
334
|
align-items: center;
|
|
270
335
|
justify-content: center;
|
|
271
|
-
width:
|
|
272
|
-
height:
|
|
336
|
+
width: 18px;
|
|
337
|
+
height: 30px;
|
|
273
338
|
padding: 0;
|
|
274
|
-
color: var(--text-
|
|
339
|
+
color: var(--text-faint);
|
|
275
340
|
font: inherit;
|
|
276
|
-
background:
|
|
277
|
-
border: 1px solid
|
|
341
|
+
background: transparent;
|
|
342
|
+
border: 1px solid transparent;
|
|
278
343
|
border-radius: var(--r-sm);
|
|
279
|
-
box-shadow: var(--shadow-sm);
|
|
280
344
|
cursor: pointer;
|
|
345
|
+
transition:
|
|
346
|
+
color 0.12s var(--ease),
|
|
347
|
+
background 0.12s var(--ease),
|
|
348
|
+
border-color 0.12s var(--ease);
|
|
349
|
+
}
|
|
350
|
+
.collapse-control.top {
|
|
351
|
+
top: var(--sp-2);
|
|
352
|
+
}
|
|
353
|
+
.collapse-control.bottom {
|
|
354
|
+
bottom: var(--sp-2);
|
|
281
355
|
}
|
|
282
356
|
.right .collapse-control {
|
|
283
|
-
|
|
284
|
-
|
|
357
|
+
left: auto;
|
|
358
|
+
right: var(--sp-1);
|
|
285
359
|
}
|
|
286
360
|
.collapse-control:hover {
|
|
287
361
|
color: var(--text);
|
|
288
|
-
background: var(--
|
|
362
|
+
background: var(--bg-elevated-2);
|
|
363
|
+
border-color: var(--border);
|
|
289
364
|
}
|
|
290
365
|
.collapse-control:focus-visible,
|
|
291
366
|
.resize-handle:focus-visible {
|
|
@@ -20,6 +20,11 @@ type $$ComponentProps = {
|
|
|
20
20
|
persistCollapsed?: boolean;
|
|
21
21
|
/** Pixels added or removed by each resize-separator arrow key press. */
|
|
22
22
|
resizeStep?: number;
|
|
23
|
+
/** Anchor the collapse handle at the top or bottom of the panel edge. */
|
|
24
|
+
handlePlacement?: 'top' | 'bottom';
|
|
25
|
+
/** Keep the collapse handle in view when the panel scrolls past the
|
|
26
|
+
* viewport, repositioning on scroll/resize via requestAnimationFrame. */
|
|
27
|
+
stickyHandle?: boolean;
|
|
23
28
|
};
|
|
24
29
|
declare const ResizablePanel: import("svelte").Component<$$ComponentProps, {}, "collapsed">;
|
|
25
30
|
type ResizablePanel = ReturnType<typeof ResizablePanel>;
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
// the glyph with the accent when on. Override the tint per-instance with
|
|
31
31
|
// `style="--btn-on: var(--warn)"`.
|
|
32
32
|
pressed?: boolean;
|
|
33
|
+
// Render as a link (`<a href>`) while keeping icon-button chrome — e.g. an
|
|
34
|
+
// open-on-GitHub action. Forwarded to Button; `href` implies `as="a"`.
|
|
35
|
+
as?: 'button' | 'a';
|
|
36
|
+
href?: string;
|
|
33
37
|
class?: string;
|
|
34
38
|
};
|
|
35
39
|
|
|
@@ -18,6 +18,8 @@ type IconButtonProps = HTMLButtonAttributes & {
|
|
|
18
18
|
inline?: boolean;
|
|
19
19
|
hoverDanger?: boolean;
|
|
20
20
|
pressed?: boolean;
|
|
21
|
+
as?: 'button' | 'a';
|
|
22
|
+
href?: string;
|
|
21
23
|
class?: string;
|
|
22
24
|
};
|
|
23
25
|
declare const IconButton: import("svelte").Component<IconButtonProps, {}, "">;
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
let {
|
|
14
14
|
selected = false,
|
|
15
15
|
row = false,
|
|
16
|
+
align = 'center',
|
|
17
|
+
block = false,
|
|
16
18
|
type = 'button',
|
|
17
19
|
disabled = false,
|
|
18
20
|
class: klass = '',
|
|
@@ -21,6 +23,11 @@
|
|
|
21
23
|
}: HTMLButtonAttributes & {
|
|
22
24
|
selected?: boolean;
|
|
23
25
|
row?: boolean;
|
|
26
|
+
/** Horizontal content alignment. `start` gives a left-aligned list-row
|
|
27
|
+
* variant (pair with `block` for a full-width menu item). */
|
|
28
|
+
align?: 'center' | 'start';
|
|
29
|
+
/** Stretch to fill the container width (full-width list/menu row). */
|
|
30
|
+
block?: boolean;
|
|
24
31
|
children?: Snippet;
|
|
25
32
|
} = $props();
|
|
26
33
|
</script>
|
|
@@ -32,6 +39,8 @@
|
|
|
32
39
|
{disabled}
|
|
33
40
|
class="opt {klass}"
|
|
34
41
|
class:row
|
|
42
|
+
class:align-start={align === 'start'}
|
|
43
|
+
class:block
|
|
35
44
|
class:selected
|
|
36
45
|
aria-pressed={selected}
|
|
37
46
|
>
|
|
@@ -71,6 +80,16 @@
|
|
|
71
80
|
color: var(--text-muted);
|
|
72
81
|
font-weight: var(--fw-medium);
|
|
73
82
|
}
|
|
83
|
+
.opt.block {
|
|
84
|
+
width: 100%;
|
|
85
|
+
}
|
|
86
|
+
/* Left-aligned list-row variant: content packs to the start on both axes. */
|
|
87
|
+
.opt.align-start {
|
|
88
|
+
align-items: flex-start;
|
|
89
|
+
}
|
|
90
|
+
.opt.row.align-start {
|
|
91
|
+
justify-content: flex-start;
|
|
92
|
+
}
|
|
74
93
|
.opt.selected {
|
|
75
94
|
--oc: var(--opt-accent, var(--accent));
|
|
76
95
|
/* Retint slotted `.faint` hint text toward the accent — via an inherited
|
|
@@ -3,6 +3,11 @@ import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
|
3
3
|
type $$ComponentProps = HTMLButtonAttributes & {
|
|
4
4
|
selected?: boolean;
|
|
5
5
|
row?: boolean;
|
|
6
|
+
/** Horizontal content alignment. `start` gives a left-aligned list-row
|
|
7
|
+
* variant (pair with `block` for a full-width menu item). */
|
|
8
|
+
align?: 'center' | 'start';
|
|
9
|
+
/** Stretch to fill the container width (full-width list/menu row). */
|
|
10
|
+
block?: boolean;
|
|
6
11
|
children?: Snippet;
|
|
7
12
|
};
|
|
8
13
|
declare const OptionButton: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
// accessible name only); `pill` keeps the labelled filter-pill layout.
|
|
32
32
|
variant = 'pill',
|
|
33
33
|
size = 'md',
|
|
34
|
+
control = false,
|
|
34
35
|
label = 'Options',
|
|
35
36
|
// `mobile` hides segment labels below the mobile breakpoint, collapsing
|
|
36
37
|
// labelled segments to centered icon-only squares (aria-label preserved).
|
|
@@ -43,6 +44,9 @@
|
|
|
43
44
|
value?: string;
|
|
44
45
|
variant?: 'pill' | 'icon';
|
|
45
46
|
size?: 'sm' | 'md';
|
|
47
|
+
/** Adopt the shared `--control-height` toolbar contract (like Popover/Button
|
|
48
|
+
* `control`) so the whole segmented control height-matches its siblings. */
|
|
49
|
+
control?: boolean;
|
|
46
50
|
label?: string;
|
|
47
51
|
collapseLabels?: 'never' | 'mobile';
|
|
48
52
|
class?: string;
|
|
@@ -84,6 +88,7 @@
|
|
|
84
88
|
aria-label={label}
|
|
85
89
|
tabindex="-1"
|
|
86
90
|
class="seg seg-{variant} seg-{size} {klass}"
|
|
91
|
+
class:seg-control={control}
|
|
87
92
|
class:seg-collapse-mobile={collapseLabels === 'mobile'}
|
|
88
93
|
data-tsu="SegmentedControl"
|
|
89
94
|
{onkeydown}
|
|
@@ -160,6 +165,14 @@
|
|
|
160
165
|
.seg-sm .seg-item {
|
|
161
166
|
height: 100%;
|
|
162
167
|
}
|
|
168
|
+
/* Shared toolbar/composer height contract — matches Button/Popover `control`. */
|
|
169
|
+
.seg-control {
|
|
170
|
+
height: var(--control-height);
|
|
171
|
+
min-height: var(--control-height);
|
|
172
|
+
}
|
|
173
|
+
.seg-control .seg-item {
|
|
174
|
+
height: 100%;
|
|
175
|
+
}
|
|
163
176
|
.seg-icon .seg-item {
|
|
164
177
|
gap: 0;
|
|
165
178
|
padding: 0.35rem;
|
|
@@ -15,6 +15,9 @@ type $$ComponentProps = {
|
|
|
15
15
|
value?: string;
|
|
16
16
|
variant?: 'pill' | 'icon';
|
|
17
17
|
size?: 'sm' | 'md';
|
|
18
|
+
/** Adopt the shared `--control-height` toolbar contract (like Popover/Button
|
|
19
|
+
* `control`) so the whole segmented control height-matches its siblings. */
|
|
20
|
+
control?: boolean;
|
|
18
21
|
label?: string;
|
|
19
22
|
collapseLabels?: 'never' | 'mobile';
|
|
20
23
|
class?: string;
|
package/package.json
CHANGED