sh3-core 0.12.0 → 0.13.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/__test__/reset.js +2 -0
- package/dist/actions/MenuButton.svelte +2 -1
- package/dist/actions/contextMenuModel.js +8 -0
- package/dist/actions/contextMenuModel.test.js +22 -2
- package/dist/actions/listeners.js +17 -6
- package/dist/actions/listeners.test.js +42 -2
- package/dist/api.d.ts +16 -0
- package/dist/api.js +14 -0
- package/dist/apps/lifecycle.js +3 -0
- package/dist/apps/lifecycle.test.js +45 -0
- package/dist/host.js +12 -0
- package/dist/navigation/back-stack.d.ts +29 -0
- package/dist/navigation/back-stack.js +87 -0
- package/dist/navigation/back-stack.test.d.ts +1 -0
- package/dist/navigation/back-stack.test.js +145 -0
- package/dist/navigation/index.d.ts +2 -0
- package/dist/navigation/index.js +6 -0
- package/dist/navigation/platform-web.d.ts +3 -0
- package/dist/navigation/platform-web.js +54 -0
- package/dist/navigation/platform-web.test.d.ts +1 -0
- package/dist/navigation/platform-web.test.js +96 -0
- package/dist/overlays/modal.js +7 -0
- package/dist/overlays/modal.test.js +35 -0
- package/dist/overlays/popup.js +7 -0
- package/dist/overlays/popup.test.js +33 -0
- package/dist/platform/index.d.ts +15 -0
- package/dist/platform/index.js +47 -0
- package/dist/primitives/base.css +17 -6
- package/dist/primitives/widgets/ColorSwatch.svelte +66 -0
- package/dist/primitives/widgets/ColorSwatch.svelte.d.ts +9 -0
- package/dist/primitives/widgets/Field.svelte +124 -0
- package/dist/primitives/widgets/Field.svelte.d.ts +19 -0
- package/dist/primitives/widgets/FilePicker.d.ts +3 -0
- package/dist/primitives/widgets/FilePicker.js +19 -0
- package/dist/primitives/widgets/FilePicker.svelte +79 -0
- package/dist/primitives/widgets/FilePicker.svelte.d.ts +13 -0
- package/dist/primitives/widgets/FilePicker.test.d.ts +1 -0
- package/dist/primitives/widgets/FilePicker.test.js +44 -0
- package/dist/primitives/widgets/IconToggleGroup.d.ts +2 -0
- package/dist/primitives/widgets/IconToggleGroup.js +8 -0
- package/dist/primitives/widgets/IconToggleGroup.svelte +86 -0
- package/dist/primitives/widgets/IconToggleGroup.svelte.d.ts +16 -0
- package/dist/primitives/widgets/IconToggleGroup.test.d.ts +1 -0
- package/dist/primitives/widgets/IconToggleGroup.test.js +19 -0
- package/dist/primitives/widgets/NumberInput.d.ts +6 -0
- package/dist/primitives/widgets/NumberInput.js +19 -0
- package/dist/primitives/widgets/NumberInput.svelte +167 -0
- package/dist/primitives/widgets/NumberInput.svelte.d.ts +17 -0
- package/dist/primitives/widgets/NumberInput.test.d.ts +1 -0
- package/dist/primitives/widgets/NumberInput.test.js +28 -0
- package/dist/primitives/widgets/RangeSlider.d.ts +2 -0
- package/dist/primitives/widgets/RangeSlider.js +7 -0
- package/dist/primitives/widgets/RangeSlider.svelte +124 -0
- package/dist/primitives/widgets/RangeSlider.svelte.d.ts +13 -0
- package/dist/primitives/widgets/RangeSlider.test.d.ts +1 -0
- package/dist/primitives/widgets/RangeSlider.test.js +14 -0
- package/dist/primitives/widgets/Segmented.d.ts +9 -0
- package/dist/primitives/widgets/Segmented.js +28 -0
- package/dist/primitives/widgets/Segmented.svelte +82 -0
- package/dist/primitives/widgets/Segmented.svelte.d.ts +10 -0
- package/dist/primitives/widgets/Segmented.test.d.ts +1 -0
- package/dist/primitives/widgets/Segmented.test.js +24 -0
- package/dist/primitives/widgets/Select.d.ts +11 -0
- package/dist/primitives/widgets/Select.js +42 -0
- package/dist/primitives/widgets/Select.svelte +163 -0
- package/dist/primitives/widgets/Select.svelte.d.ts +14 -0
- package/dist/primitives/widgets/Select.test.d.ts +1 -0
- package/dist/primitives/widgets/Select.test.js +68 -0
- package/dist/primitives/widgets/Slider.d.ts +6 -0
- package/dist/primitives/widgets/Slider.js +19 -0
- package/dist/primitives/widgets/Slider.svelte +205 -0
- package/dist/primitives/widgets/Slider.svelte.d.ts +15 -0
- package/dist/primitives/widgets/Slider.test.d.ts +1 -0
- package/dist/primitives/widgets/Slider.test.js +31 -0
- package/dist/primitives/widgets/SliderGroup.svelte +58 -0
- package/dist/primitives/widgets/SliderGroup.svelte.d.ts +18 -0
- package/dist/primitives/widgets/Textarea.svelte +81 -0
- package/dist/primitives/widgets/Textarea.svelte.d.ts +16 -0
- package/dist/primitives/widgets/_select-listbox.svelte +228 -0
- package/dist/primitives/widgets/_select-listbox.svelte.d.ts +18 -0
- package/dist/shell-shard/Terminal.svelte +1 -4
- package/dist/shell-shard/verbs/index.js +2 -0
- package/dist/shell-shard/verbs/reset.d.ts +2 -0
- package/dist/shell-shard/verbs/reset.js +26 -0
- package/dist/tokens.css +32 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { valueToPercent } from './Slider';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
value = $bindable(0),
|
|
6
|
+
min = 0,
|
|
7
|
+
max = 100,
|
|
8
|
+
step = 1,
|
|
9
|
+
ticks,
|
|
10
|
+
showValue = false,
|
|
11
|
+
disabled = false,
|
|
12
|
+
invalid = false,
|
|
13
|
+
size = 'md',
|
|
14
|
+
orientation = 'horizontal',
|
|
15
|
+
}: {
|
|
16
|
+
value?: number;
|
|
17
|
+
min?: number;
|
|
18
|
+
max?: number;
|
|
19
|
+
step?: number;
|
|
20
|
+
ticks?: number[];
|
|
21
|
+
showValue?: boolean;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
invalid?: boolean;
|
|
24
|
+
size?: 'sm' | 'md';
|
|
25
|
+
orientation?: 'horizontal' | 'vertical';
|
|
26
|
+
} = $props();
|
|
27
|
+
|
|
28
|
+
const pct = $derived(valueToPercent(value, min, max));
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<div class="sh3-slider sh3-slider--{orientation}"
|
|
32
|
+
class:sh3-slider--sm={size === 'sm'}
|
|
33
|
+
class:sh3-slider--invalid={invalid}>
|
|
34
|
+
<div class="sh3-slider__track">
|
|
35
|
+
<div class="sh3-slider__fill" style:--pct="{pct}%"></div>
|
|
36
|
+
{#if ticks}
|
|
37
|
+
{#each ticks as t}
|
|
38
|
+
<span class="sh3-slider__tick" style:--tick-pct="{valueToPercent(t, min, max)}%"></span>
|
|
39
|
+
{/each}
|
|
40
|
+
{/if}
|
|
41
|
+
</div>
|
|
42
|
+
<input
|
|
43
|
+
type="range"
|
|
44
|
+
class="sh3-slider__native"
|
|
45
|
+
{min}
|
|
46
|
+
{max}
|
|
47
|
+
{step}
|
|
48
|
+
{disabled}
|
|
49
|
+
aria-invalid={invalid || undefined}
|
|
50
|
+
bind:value
|
|
51
|
+
/>
|
|
52
|
+
{#if showValue}
|
|
53
|
+
<output class="sh3-slider__value" style:--pct="{pct}%">{value}</output>
|
|
54
|
+
{/if}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<style>
|
|
58
|
+
.sh3-slider {
|
|
59
|
+
--thumb-size: 14px;
|
|
60
|
+
position: relative;
|
|
61
|
+
display: inline-block;
|
|
62
|
+
}
|
|
63
|
+
.sh3-slider--sm { --thumb-size: 12px; }
|
|
64
|
+
.sh3-slider--horizontal { width: 200px; height: var(--thumb-size); }
|
|
65
|
+
.sh3-slider--vertical { width: var(--thumb-size); height: 200px; }
|
|
66
|
+
|
|
67
|
+
.sh3-slider__track {
|
|
68
|
+
position: absolute;
|
|
69
|
+
background: var(--shell-track-bg);
|
|
70
|
+
border: 1px solid var(--shell-track-border);
|
|
71
|
+
border-radius: var(--shell-widget-radius-pill);
|
|
72
|
+
pointer-events: none;
|
|
73
|
+
}
|
|
74
|
+
.sh3-slider--horizontal .sh3-slider__track {
|
|
75
|
+
top: 50%; left: 0; right: 0;
|
|
76
|
+
height: 4px;
|
|
77
|
+
transform: translateY(-50%);
|
|
78
|
+
}
|
|
79
|
+
.sh3-slider--vertical .sh3-slider__track {
|
|
80
|
+
left: 50%; top: 0; bottom: 0;
|
|
81
|
+
width: 4px;
|
|
82
|
+
transform: translateX(-50%);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.sh3-slider__fill {
|
|
86
|
+
position: absolute;
|
|
87
|
+
background: var(--shell-track-fill);
|
|
88
|
+
border-radius: inherit;
|
|
89
|
+
}
|
|
90
|
+
.sh3-slider--horizontal .sh3-slider__fill {
|
|
91
|
+
left: 0; top: 0; bottom: 0;
|
|
92
|
+
width: var(--pct);
|
|
93
|
+
}
|
|
94
|
+
.sh3-slider--vertical .sh3-slider__fill {
|
|
95
|
+
bottom: 0; left: 0; right: 0;
|
|
96
|
+
height: var(--pct);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.sh3-slider__tick {
|
|
100
|
+
position: absolute;
|
|
101
|
+
background: var(--shell-tick-bg);
|
|
102
|
+
}
|
|
103
|
+
.sh3-slider--horizontal .sh3-slider__tick {
|
|
104
|
+
width: 1px; height: 8px; top: 50%;
|
|
105
|
+
transform: translate(-50%, -50%);
|
|
106
|
+
left: var(--tick-pct);
|
|
107
|
+
}
|
|
108
|
+
.sh3-slider--vertical .sh3-slider__tick {
|
|
109
|
+
width: 8px; height: 1px; left: 50%;
|
|
110
|
+
transform: translate(-50%, 50%);
|
|
111
|
+
bottom: var(--tick-pct);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.sh3-slider__native {
|
|
115
|
+
position: absolute;
|
|
116
|
+
inset: 0;
|
|
117
|
+
width: 100%;
|
|
118
|
+
height: 100%;
|
|
119
|
+
margin: 0;
|
|
120
|
+
background: transparent;
|
|
121
|
+
-webkit-appearance: none;
|
|
122
|
+
appearance: none;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
}
|
|
125
|
+
.sh3-slider--vertical .sh3-slider__native {
|
|
126
|
+
writing-mode: vertical-lr;
|
|
127
|
+
direction: rtl;
|
|
128
|
+
}
|
|
129
|
+
/* Browser default applies opacity:0.5 to <input>:disabled at the
|
|
130
|
+
element level; that composites onto the thumb regardless of any
|
|
131
|
+
pseudo-element opacity overrides. Pin to 1 here so the disabled
|
|
132
|
+
thumb stays opaque and the muted-token recolor is what shows. */
|
|
133
|
+
.sh3-slider__native:disabled {
|
|
134
|
+
cursor: not-allowed;
|
|
135
|
+
opacity: 1;
|
|
136
|
+
}
|
|
137
|
+
.sh3-slider--invalid .sh3-slider__track { border-color: var(--shell-error); }
|
|
138
|
+
|
|
139
|
+
/* Disabled state: shift the fill + thumb to muted/border tokens
|
|
140
|
+
instead of using opacity, so the thumb stays opaque (no see-through). */
|
|
141
|
+
.sh3-slider:has(.sh3-slider__native:disabled) .sh3-slider__fill {
|
|
142
|
+
background: var(--shell-border-strong);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.sh3-slider__native::-webkit-slider-thumb {
|
|
146
|
+
-webkit-appearance: none;
|
|
147
|
+
width: var(--thumb-size);
|
|
148
|
+
height: var(--thumb-size);
|
|
149
|
+
border-radius: 50%;
|
|
150
|
+
background: var(--shell-thumb-bg);
|
|
151
|
+
border: 2px solid var(--shell-thumb-border);
|
|
152
|
+
box-shadow: var(--shell-thumb-shadow);
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
opacity: 1;
|
|
155
|
+
}
|
|
156
|
+
.sh3-slider__native::-moz-range-thumb {
|
|
157
|
+
width: var(--thumb-size);
|
|
158
|
+
height: var(--thumb-size);
|
|
159
|
+
border-radius: 50%;
|
|
160
|
+
background: var(--shell-thumb-bg);
|
|
161
|
+
border: 2px solid var(--shell-thumb-border);
|
|
162
|
+
box-shadow: var(--shell-thumb-shadow);
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
opacity: 1;
|
|
165
|
+
}
|
|
166
|
+
.sh3-slider__native:disabled::-webkit-slider-thumb {
|
|
167
|
+
background: var(--shell-fg-subtle);
|
|
168
|
+
border-color: var(--shell-border-strong);
|
|
169
|
+
cursor: not-allowed;
|
|
170
|
+
opacity: 1;
|
|
171
|
+
}
|
|
172
|
+
.sh3-slider__native:disabled::-moz-range-thumb {
|
|
173
|
+
background: var(--shell-fg-subtle);
|
|
174
|
+
border-color: var(--shell-border-strong);
|
|
175
|
+
cursor: not-allowed;
|
|
176
|
+
opacity: 1;
|
|
177
|
+
}
|
|
178
|
+
.sh3-slider__native::-webkit-slider-runnable-track,
|
|
179
|
+
.sh3-slider__native::-moz-range-track {
|
|
180
|
+
background: transparent;
|
|
181
|
+
}
|
|
182
|
+
.sh3-slider__native:focus-visible::-webkit-slider-thumb {
|
|
183
|
+
box-shadow: var(--shell-focus-ring), var(--shell-thumb-shadow);
|
|
184
|
+
}
|
|
185
|
+
.sh3-slider__native:focus-visible::-moz-range-thumb {
|
|
186
|
+
box-shadow: var(--shell-focus-ring), var(--shell-thumb-shadow);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.sh3-slider__value {
|
|
190
|
+
position: absolute;
|
|
191
|
+
font-size: 0.75rem;
|
|
192
|
+
color: var(--shell-fg-muted);
|
|
193
|
+
pointer-events: none;
|
|
194
|
+
transform: translate(-50%, 0);
|
|
195
|
+
}
|
|
196
|
+
.sh3-slider--horizontal .sh3-slider__value {
|
|
197
|
+
top: calc(100% + 4px);
|
|
198
|
+
left: var(--pct);
|
|
199
|
+
}
|
|
200
|
+
.sh3-slider--vertical .sh3-slider__value {
|
|
201
|
+
left: calc(100% + 6px);
|
|
202
|
+
bottom: var(--pct);
|
|
203
|
+
transform: translate(0, 50%);
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value?: number;
|
|
3
|
+
min?: number;
|
|
4
|
+
max?: number;
|
|
5
|
+
step?: number;
|
|
6
|
+
ticks?: number[];
|
|
7
|
+
showValue?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
invalid?: boolean;
|
|
10
|
+
size?: 'sm' | 'md';
|
|
11
|
+
orientation?: 'horizontal' | 'vertical';
|
|
12
|
+
};
|
|
13
|
+
declare const Slider: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
14
|
+
type Slider = ReturnType<typeof Slider>;
|
|
15
|
+
export default Slider;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { valueToPercent, percentToValue, snapToStep } from './Slider';
|
|
3
|
+
describe('Slider value-position math', () => {
|
|
4
|
+
it('valueToPercent at min', () => {
|
|
5
|
+
expect(valueToPercent(0, 0, 100)).toBe(0);
|
|
6
|
+
});
|
|
7
|
+
it('valueToPercent at max', () => {
|
|
8
|
+
expect(valueToPercent(100, 0, 100)).toBe(100);
|
|
9
|
+
});
|
|
10
|
+
it('valueToPercent in middle', () => {
|
|
11
|
+
expect(valueToPercent(50, 0, 100)).toBe(50);
|
|
12
|
+
});
|
|
13
|
+
it('valueToPercent with negative range', () => {
|
|
14
|
+
expect(valueToPercent(0, -10, 10)).toBe(50);
|
|
15
|
+
});
|
|
16
|
+
it('percentToValue at 50%', () => {
|
|
17
|
+
expect(percentToValue(50, 0, 100)).toBe(50);
|
|
18
|
+
});
|
|
19
|
+
it('percentToValue at 0%', () => {
|
|
20
|
+
expect(percentToValue(0, -10, 10)).toBe(-10);
|
|
21
|
+
});
|
|
22
|
+
it('snapToStep snaps to grid', () => {
|
|
23
|
+
expect(snapToStep(2.7, 0, 1)).toBe(3);
|
|
24
|
+
expect(snapToStep(2.3, 0, 1)).toBe(2);
|
|
25
|
+
expect(snapToStep(2.7, 0, 0.5)).toBe(2.5);
|
|
26
|
+
});
|
|
27
|
+
it('snapToStep clamps to min/max', () => {
|
|
28
|
+
expect(snapToStep(-5, 0, 1, { min: 0, max: 10 })).toBe(0);
|
|
29
|
+
expect(snapToStep(15, 0, 1, { min: 0, max: 10 })).toBe(10);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Slider from './Slider.svelte';
|
|
3
|
+
|
|
4
|
+
type Channel = { id: string; label: string; min?: number; max?: number; step?: number };
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
orientation = 'horizontal',
|
|
8
|
+
channels,
|
|
9
|
+
values = $bindable<Record<string, number>>({}),
|
|
10
|
+
showValues = false,
|
|
11
|
+
disabled = false,
|
|
12
|
+
size = 'md',
|
|
13
|
+
}: {
|
|
14
|
+
orientation?: 'horizontal' | 'vertical';
|
|
15
|
+
channels: Channel[];
|
|
16
|
+
values?: Record<string, number>;
|
|
17
|
+
showValues?: boolean;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
size?: 'sm' | 'md';
|
|
20
|
+
} = $props();
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<div class="sh3-sg sh3-sg--{orientation}">
|
|
24
|
+
{#each channels as ch (ch.id)}
|
|
25
|
+
<div class="sh3-sg__channel">
|
|
26
|
+
<span class="sh3-sg__label">{ch.label}</span>
|
|
27
|
+
<Slider
|
|
28
|
+
{orientation}
|
|
29
|
+
min={ch.min ?? 0}
|
|
30
|
+
max={ch.max ?? 100}
|
|
31
|
+
step={ch.step ?? 1}
|
|
32
|
+
{disabled}
|
|
33
|
+
{size}
|
|
34
|
+
showValue={showValues}
|
|
35
|
+
bind:value={
|
|
36
|
+
() => values[ch.id] ?? 0,
|
|
37
|
+
(n: number) => values = { ...values, [ch.id]: n }
|
|
38
|
+
}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
{/each}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
.sh3-sg { display: inline-flex; gap: var(--shell-pad-md); }
|
|
46
|
+
.sh3-sg--vertical { flex-direction: row; align-items: end; }
|
|
47
|
+
.sh3-sg--horizontal { flex-direction: column; }
|
|
48
|
+
.sh3-sg__channel {
|
|
49
|
+
display: flex; gap: 4px; align-items: center;
|
|
50
|
+
}
|
|
51
|
+
.sh3-sg--vertical .sh3-sg__channel { flex-direction: column-reverse; align-items: center; }
|
|
52
|
+
.sh3-sg__label {
|
|
53
|
+
font-size: 0.75rem;
|
|
54
|
+
color: var(--shell-fg-muted);
|
|
55
|
+
min-width: 60px;
|
|
56
|
+
}
|
|
57
|
+
.sh3-sg--vertical .sh3-sg__label { min-width: 0; text-align: center; }
|
|
58
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type Channel = {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
min?: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
step?: number;
|
|
7
|
+
};
|
|
8
|
+
type $$ComponentProps = {
|
|
9
|
+
orientation?: 'horizontal' | 'vertical';
|
|
10
|
+
channels: Channel[];
|
|
11
|
+
values?: Record<string, number>;
|
|
12
|
+
showValues?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
size?: 'sm' | 'md';
|
|
15
|
+
};
|
|
16
|
+
declare const SliderGroup: import("svelte").Component<$$ComponentProps, {}, "values">;
|
|
17
|
+
type SliderGroup = ReturnType<typeof SliderGroup>;
|
|
18
|
+
export default SliderGroup;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
value = $bindable(''),
|
|
4
|
+
label,
|
|
5
|
+
placeholder,
|
|
6
|
+
helper,
|
|
7
|
+
error,
|
|
8
|
+
disabled = false,
|
|
9
|
+
invalid = false,
|
|
10
|
+
size = 'md',
|
|
11
|
+
required = false,
|
|
12
|
+
rows = 3,
|
|
13
|
+
resize = 'vertical',
|
|
14
|
+
}: {
|
|
15
|
+
value?: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
helper?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
invalid?: boolean;
|
|
22
|
+
size?: 'sm' | 'md';
|
|
23
|
+
required?: boolean;
|
|
24
|
+
rows?: number;
|
|
25
|
+
resize?: 'none' | 'vertical' | 'both';
|
|
26
|
+
} = $props();
|
|
27
|
+
|
|
28
|
+
const showError = $derived(invalid && !!error);
|
|
29
|
+
const helperText = $derived(showError ? error : helper);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<label class="sh3-textarea" class:sh3-textarea--invalid={invalid} class:sh3-textarea--sm={size === 'sm'}>
|
|
33
|
+
{#if label}<span class="sh3-textarea__label">{label}{#if required}<span aria-hidden="true"> *</span>{/if}</span>{/if}
|
|
34
|
+
<textarea
|
|
35
|
+
class="sh3-textarea__input"
|
|
36
|
+
style:resize
|
|
37
|
+
{placeholder}
|
|
38
|
+
{disabled}
|
|
39
|
+
{required}
|
|
40
|
+
{rows}
|
|
41
|
+
aria-invalid={invalid || undefined}
|
|
42
|
+
bind:value
|
|
43
|
+
></textarea>
|
|
44
|
+
{#if helperText}<span class="sh3-textarea__helper" class:sh3-textarea__helper--error={showError}>{helperText}</span>{/if}
|
|
45
|
+
</label>
|
|
46
|
+
|
|
47
|
+
<style>
|
|
48
|
+
.sh3-textarea {
|
|
49
|
+
display: inline-flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: 4px;
|
|
52
|
+
font-family: var(--shell-font-ui);
|
|
53
|
+
font-size: 0.8125rem;
|
|
54
|
+
}
|
|
55
|
+
.sh3-textarea__label {
|
|
56
|
+
color: var(--shell-fg-muted);
|
|
57
|
+
font-size: 0.75rem;
|
|
58
|
+
}
|
|
59
|
+
.sh3-textarea__input {
|
|
60
|
+
background: var(--shell-input-bg);
|
|
61
|
+
color: var(--shell-fg);
|
|
62
|
+
border: 1px solid var(--shell-border);
|
|
63
|
+
border-radius: var(--shell-widget-radius);
|
|
64
|
+
padding: var(--shell-pad-sm) var(--shell-field-pad-x);
|
|
65
|
+
font: inherit;
|
|
66
|
+
outline: none;
|
|
67
|
+
transition: border-color var(--shell-motion-fast) var(--shell-ease-standard);
|
|
68
|
+
}
|
|
69
|
+
.sh3-textarea__input:focus {
|
|
70
|
+
border-color: var(--shell-input-border-focus);
|
|
71
|
+
box-shadow: var(--shell-focus-ring);
|
|
72
|
+
}
|
|
73
|
+
.sh3-textarea--invalid .sh3-textarea__input {
|
|
74
|
+
border-color: var(--shell-error);
|
|
75
|
+
}
|
|
76
|
+
.sh3-textarea__helper {
|
|
77
|
+
color: var(--shell-fg-muted);
|
|
78
|
+
font-size: 0.75rem;
|
|
79
|
+
}
|
|
80
|
+
.sh3-textarea__helper--error { color: var(--shell-error); }
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value?: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
helper?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
invalid?: boolean;
|
|
9
|
+
size?: 'sm' | 'md';
|
|
10
|
+
required?: boolean;
|
|
11
|
+
rows?: number;
|
|
12
|
+
resize?: 'none' | 'vertical' | 'both';
|
|
13
|
+
};
|
|
14
|
+
declare const Textarea: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
15
|
+
type Textarea = ReturnType<typeof Textarea>;
|
|
16
|
+
export default Textarea;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
type SelectOption,
|
|
5
|
+
shouldShowSearch,
|
|
6
|
+
filterOptions,
|
|
7
|
+
matchTypeAhead,
|
|
8
|
+
} from './Select';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
options,
|
|
12
|
+
getValue,
|
|
13
|
+
multiple,
|
|
14
|
+
onSelect,
|
|
15
|
+
onClose,
|
|
16
|
+
close,
|
|
17
|
+
}: {
|
|
18
|
+
options: SelectOption[];
|
|
19
|
+
/** Live read of the selected value(s). Returning a closure (rather
|
|
20
|
+
* than passing value as a snapshot) is what lets the listbox
|
|
21
|
+
* re-render checkmarks across multiple selections without unmounting. */
|
|
22
|
+
getValue: () => string | string[];
|
|
23
|
+
multiple: boolean;
|
|
24
|
+
onSelect: (v: string) => void;
|
|
25
|
+
/** Called by listbox-initiated close paths so the trigger can
|
|
26
|
+
* restore its open-flag and focus. Outside-click and Escape
|
|
27
|
+
* paths are handled by the popup manager via Select's wrap. */
|
|
28
|
+
onClose?: () => void;
|
|
29
|
+
close: () => void;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
const value = $derived(getValue());
|
|
33
|
+
|
|
34
|
+
function dismiss() {
|
|
35
|
+
onClose?.();
|
|
36
|
+
close();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const showSearch = $derived(shouldShowSearch(options));
|
|
40
|
+
|
|
41
|
+
function initialActiveIdx(): number {
|
|
42
|
+
if (multiple) return 0;
|
|
43
|
+
const v = typeof value === 'string' ? value : '';
|
|
44
|
+
const idx = options.findIndex((o) => o.value === v && !o.disabled);
|
|
45
|
+
return idx >= 0 ? idx : 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let query = $state('');
|
|
49
|
+
let activeIdx = $state(initialActiveIdx());
|
|
50
|
+
let listEl = $state<HTMLDivElement | undefined>(undefined);
|
|
51
|
+
let searchEl = $state<HTMLInputElement | undefined>(undefined);
|
|
52
|
+
let typeAheadBuffer = '';
|
|
53
|
+
let typeAheadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
54
|
+
|
|
55
|
+
const filtered = $derived(filterOptions(options, query));
|
|
56
|
+
|
|
57
|
+
$effect(() => {
|
|
58
|
+
if (activeIdx >= filtered.length) activeIdx = 0;
|
|
59
|
+
void scrollActiveIntoView();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
async function scrollActiveIntoView() {
|
|
63
|
+
await tick();
|
|
64
|
+
if (!listEl) return;
|
|
65
|
+
const el = listEl.querySelector<HTMLElement>(`[data-idx="${activeIdx}"]`);
|
|
66
|
+
el?.scrollIntoView({ block: 'nearest' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isSelected(o: SelectOption): boolean {
|
|
70
|
+
if (multiple && Array.isArray(value)) return value.includes(o.value);
|
|
71
|
+
return value === o.value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function commit(idx: number) {
|
|
75
|
+
const o = filtered[idx];
|
|
76
|
+
if (!o || o.disabled) return;
|
|
77
|
+
onSelect(o.value);
|
|
78
|
+
if (!multiple) dismiss();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onListKey(e: KeyboardEvent) {
|
|
82
|
+
if (filtered.length === 0) return;
|
|
83
|
+
switch (e.key) {
|
|
84
|
+
case 'ArrowDown':
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
activeIdx = (activeIdx + 1) % filtered.length;
|
|
87
|
+
break;
|
|
88
|
+
case 'ArrowUp':
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
activeIdx = (activeIdx - 1 + filtered.length) % filtered.length;
|
|
91
|
+
break;
|
|
92
|
+
case 'Home':
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
activeIdx = 0;
|
|
95
|
+
break;
|
|
96
|
+
case 'End':
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
activeIdx = filtered.length - 1;
|
|
99
|
+
break;
|
|
100
|
+
case 'Enter':
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
commit(activeIdx);
|
|
103
|
+
break;
|
|
104
|
+
case ' ':
|
|
105
|
+
case 'Space':
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
commit(activeIdx);
|
|
108
|
+
break;
|
|
109
|
+
case 'Escape':
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
dismiss();
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
if (showSearch && document.activeElement === searchEl) return;
|
|
115
|
+
if (e.key.length !== 1 || e.metaKey || e.ctrlKey || e.altKey) return;
|
|
116
|
+
typeAheadBuffer += e.key;
|
|
117
|
+
if (typeAheadTimer) clearTimeout(typeAheadTimer);
|
|
118
|
+
typeAheadTimer = setTimeout(() => { typeAheadBuffer = ''; }, 500);
|
|
119
|
+
const idx = matchTypeAhead(filtered, typeAheadBuffer, activeIdx);
|
|
120
|
+
if (idx >= 0) activeIdx = idx;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<div
|
|
126
|
+
class="sh3-listbox"
|
|
127
|
+
role="listbox"
|
|
128
|
+
aria-multiselectable={multiple}
|
|
129
|
+
tabindex="-1"
|
|
130
|
+
onkeydown={onListKey}
|
|
131
|
+
>
|
|
132
|
+
{#if showSearch}
|
|
133
|
+
<input
|
|
134
|
+
bind:this={searchEl}
|
|
135
|
+
class="sh3-listbox__search"
|
|
136
|
+
type="search"
|
|
137
|
+
placeholder="Filter…"
|
|
138
|
+
bind:value={query}
|
|
139
|
+
/>
|
|
140
|
+
{/if}
|
|
141
|
+
<div class="sh3-listbox__list" bind:this={listEl}>
|
|
142
|
+
{#each filtered as opt, i}
|
|
143
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
144
|
+
<div
|
|
145
|
+
role="option"
|
|
146
|
+
tabindex="-1"
|
|
147
|
+
aria-selected={isSelected(opt)}
|
|
148
|
+
aria-disabled={opt.disabled}
|
|
149
|
+
data-idx={i}
|
|
150
|
+
class:sh3-listbox__opt--active={i === activeIdx}
|
|
151
|
+
class:sh3-listbox__opt--selected={isSelected(opt)}
|
|
152
|
+
onclick={() => commit(i)}
|
|
153
|
+
onmouseenter={() => activeIdx = i}
|
|
154
|
+
>
|
|
155
|
+
{#if multiple}
|
|
156
|
+
<span class="sh3-listbox__check" class:sh3-listbox__check--on={isSelected(opt)} aria-hidden="true"></span>
|
|
157
|
+
{/if}
|
|
158
|
+
<span class="sh3-listbox__label">{opt.label}</span>
|
|
159
|
+
</div>
|
|
160
|
+
{/each}
|
|
161
|
+
{#if filtered.length === 0}
|
|
162
|
+
<div class="sh3-listbox__empty">No matches</div>
|
|
163
|
+
{/if}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<style>
|
|
168
|
+
.sh3-listbox {
|
|
169
|
+
background: var(--shell-bg-elevated);
|
|
170
|
+
border: 1px solid var(--shell-border);
|
|
171
|
+
border-radius: var(--shell-widget-radius);
|
|
172
|
+
box-shadow: var(--shell-shadow-sm);
|
|
173
|
+
min-width: 180px;
|
|
174
|
+
max-width: 320px;
|
|
175
|
+
color: var(--shell-fg);
|
|
176
|
+
font-size: 0.8125rem;
|
|
177
|
+
overflow: hidden;
|
|
178
|
+
}
|
|
179
|
+
.sh3-listbox__search {
|
|
180
|
+
width: 100%;
|
|
181
|
+
height: 26px;
|
|
182
|
+
padding: 0 8px;
|
|
183
|
+
background: var(--shell-input-bg);
|
|
184
|
+
border: none;
|
|
185
|
+
border-bottom: 1px solid var(--shell-border);
|
|
186
|
+
color: var(--shell-fg);
|
|
187
|
+
font: inherit;
|
|
188
|
+
outline: none;
|
|
189
|
+
}
|
|
190
|
+
.sh3-listbox__list {
|
|
191
|
+
max-height: 280px;
|
|
192
|
+
overflow-y: auto;
|
|
193
|
+
padding: 4px 0;
|
|
194
|
+
}
|
|
195
|
+
.sh3-listbox__list > div {
|
|
196
|
+
display: flex; align-items: center; gap: 6px;
|
|
197
|
+
padding: 4px 10px;
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
}
|
|
200
|
+
.sh3-listbox__opt--active { background: var(--shell-bg); }
|
|
201
|
+
.sh3-listbox__opt--selected { color: var(--shell-accent); }
|
|
202
|
+
.sh3-listbox__opt--active.sh3-listbox__opt--selected { background: var(--shell-accent); color: var(--shell-fg-on-accent); }
|
|
203
|
+
.sh3-listbox__list > div[aria-disabled="true"] {
|
|
204
|
+
color: var(--shell-fg-subtle);
|
|
205
|
+
cursor: not-allowed;
|
|
206
|
+
}
|
|
207
|
+
.sh3-listbox__check {
|
|
208
|
+
flex-shrink: 0;
|
|
209
|
+
width: 12px;
|
|
210
|
+
height: 12px;
|
|
211
|
+
}
|
|
212
|
+
.sh3-listbox__check--on::before {
|
|
213
|
+
content: "";
|
|
214
|
+
display: block;
|
|
215
|
+
width: 100%;
|
|
216
|
+
height: 100%;
|
|
217
|
+
background: var(--shell-accent);
|
|
218
|
+
clip-path: polygon(14% 44%, 0 60%, 40% 100%, 100% 20%, 85% 8%, 38% 70%);
|
|
219
|
+
}
|
|
220
|
+
.sh3-listbox__opt--active.sh3-listbox__opt--selected .sh3-listbox__check--on::before {
|
|
221
|
+
background: var(--shell-fg-on-accent);
|
|
222
|
+
}
|
|
223
|
+
.sh3-listbox__empty {
|
|
224
|
+
padding: 6px 10px;
|
|
225
|
+
color: var(--shell-fg-muted);
|
|
226
|
+
font-style: italic;
|
|
227
|
+
}
|
|
228
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type SelectOption } from './Select';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
options: SelectOption[];
|
|
4
|
+
/** Live read of the selected value(s). Returning a closure (rather
|
|
5
|
+
* than passing value as a snapshot) is what lets the listbox
|
|
6
|
+
* re-render checkmarks across multiple selections without unmounting. */
|
|
7
|
+
getValue: () => string | string[];
|
|
8
|
+
multiple: boolean;
|
|
9
|
+
onSelect: (v: string) => void;
|
|
10
|
+
/** Called by listbox-initiated close paths so the trigger can
|
|
11
|
+
* restore its open-flag and focus. Outside-click and Escape
|
|
12
|
+
* paths are handled by the popup manager via Select's wrap. */
|
|
13
|
+
onClose?: () => void;
|
|
14
|
+
close: () => void;
|
|
15
|
+
};
|
|
16
|
+
declare const SelectListbox: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
17
|
+
type SelectListbox = ReturnType<typeof SelectListbox>;
|
|
18
|
+
export default SelectListbox;
|
|
@@ -78,10 +78,6 @@
|
|
|
78
78
|
let focusLocked = $state(false);
|
|
79
79
|
let targetShard = $state<string | null>(null);
|
|
80
80
|
|
|
81
|
-
function toggleFocusLock(): void {
|
|
82
|
-
focusLocked = !focusLocked;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
81
|
// Toolbar slot registry
|
|
86
82
|
const toolbarRegistry = new ToolbarSlotRegistry();
|
|
87
83
|
toolbarRegistry.register({ id: 'mode', order: 10, visible: () => true, component: ModeSlot });
|
|
@@ -101,6 +97,7 @@
|
|
|
101
97
|
const found = getActiveViewId(child);
|
|
102
98
|
if (found !== null) return found;
|
|
103
99
|
}
|
|
100
|
+
return null;
|
|
104
101
|
}
|
|
105
102
|
// slot node
|
|
106
103
|
return node.viewId ?? null;
|