@dorsk/tsumikit 0.74.0 → 0.76.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 +38 -1
- package/dist/autoresize.d.ts +2 -0
- package/dist/autoresize.js +8 -1
- package/dist/components/atoms/Artwork.svelte +12 -5
- package/dist/components/atoms/Avatar.svelte +1 -1
- package/dist/components/atoms/Badge.svelte +59 -23
- package/dist/components/atoms/Button.svelte +130 -96
- package/dist/components/atoms/Button.svelte.d.ts +2 -0
- package/dist/components/atoms/Card.svelte +10 -4
- package/dist/components/atoms/Checkbox.svelte +36 -9
- package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
- package/dist/components/atoms/Gauge.svelte +20 -14
- package/dist/components/atoms/Heading.svelte +1 -1
- package/dist/components/atoms/Icon.svelte +1 -0
- package/dist/components/atoms/Input.svelte +67 -13
- package/dist/components/atoms/Kbd.svelte +3 -3
- package/dist/components/atoms/Link.svelte +7 -2
- package/dist/components/atoms/Progress.svelte +4 -2
- package/dist/components/atoms/Scrim.svelte +3 -8
- package/dist/components/atoms/SegmentedProgress.svelte +5 -4
- package/dist/components/atoms/Select.svelte +58 -27
- package/dist/components/atoms/Slider.svelte +79 -22
- package/dist/components/atoms/Slider.svelte.d.ts +2 -0
- package/dist/components/atoms/Swatch.svelte +19 -8
- package/dist/components/atoms/Switch.svelte +34 -21
- package/dist/components/atoms/Switch.svelte.d.ts +2 -0
- package/dist/components/atoms/Text.svelte +1 -1
- package/dist/components/atoms/Textarea.svelte +34 -12
- package/dist/components/atoms/TimeDisplay.svelte +4 -2
- package/dist/components/layouts/AppShell.svelte +26 -15
- package/dist/components/layouts/MasterDetail.svelte +23 -21
- package/dist/components/layouts/NavBar.svelte +11 -8
- package/dist/components/layouts/NavItem.svelte +15 -7
- package/dist/components/layouts/NavSection.svelte +1 -1
- package/dist/components/layouts/ResizablePanel.svelte +37 -16
- package/dist/components/layouts/TabBar.svelte +24 -14
- package/dist/components/layouts/TabBar.svelte.d.ts +2 -0
- package/dist/components/molecules/Accordion.svelte +4 -0
- package/dist/components/molecules/Accordion.svelte.d.ts +2 -0
- package/dist/components/molecules/AttachmentList.svelte +14 -11
- package/dist/components/molecules/Breadcrumb.svelte +13 -9
- package/dist/components/molecules/Callout.svelte +3 -3
- package/dist/components/molecules/CapBar.svelte +23 -22
- package/dist/components/molecules/Carousel.svelte +27 -17
- package/dist/components/molecules/ChatBubble.svelte +2 -2
- package/dist/components/molecules/CodeBlock.svelte +2 -2
- package/dist/components/molecules/Combobox.svelte +30 -1
- package/dist/components/molecules/Combobox.svelte.d.ts +4 -0
- package/dist/components/molecules/Composer.svelte +2 -2
- package/dist/components/molecules/ConfirmModal.svelte +4 -0
- package/dist/components/molecules/ConfirmModal.svelte.d.ts +2 -0
- package/dist/components/molecules/CopyButton.svelte +46 -2
- package/dist/components/molecules/CopyButton.svelte.d.ts +2 -0
- package/dist/components/molecules/Disclosure.svelte +23 -12
- package/dist/components/molecules/Disclosure.svelte.d.ts +2 -0
- package/dist/components/molecules/Drawer.svelte +43 -23
- package/dist/components/molecules/Drawer.svelte.d.ts +2 -0
- package/dist/components/molecules/Dropzone.svelte +24 -8
- package/dist/components/molecules/EmojiPicker.svelte +27 -8
- package/dist/components/molecules/EmojiPicker.svelte.d.ts +2 -0
- package/dist/components/molecules/EmptyState.svelte +5 -5
- package/dist/components/molecules/Fieldset.svelte +8 -13
- package/dist/components/molecules/FileButton.svelte +57 -31
- package/dist/components/molecules/FileButton.svelte.d.ts +2 -0
- package/dist/components/molecules/FilterInput.svelte +51 -11
- package/dist/components/molecules/FontScalePicker.svelte +20 -9
- package/dist/components/molecules/GitRef.svelte +9 -7
- package/dist/components/molecules/GlyphStack.svelte +46 -14
- package/dist/components/molecules/IconButton.svelte +6 -3
- package/dist/components/molecules/IconButton.svelte.d.ts +2 -0
- package/dist/components/molecules/InputGroup.svelte +13 -12
- package/dist/components/molecules/LoadMore.svelte +6 -1
- package/dist/components/molecules/LoadMore.svelte.d.ts +2 -0
- package/dist/components/molecules/Menu.svelte +33 -7
- package/dist/components/molecules/Menu.svelte.d.ts +2 -0
- package/dist/components/molecules/Metric.svelte +11 -9
- package/dist/components/molecules/Modal.svelte +76 -26
- package/dist/components/molecules/Modal.svelte.d.ts +2 -0
- package/dist/components/molecules/OptionButton.svelte +28 -10
- package/dist/components/molecules/OptionButton.svelte.d.ts +2 -0
- package/dist/components/molecules/Pagination.svelte +26 -9
- package/dist/components/molecules/Pagination.svelte.d.ts +2 -0
- package/dist/components/molecules/Popover.svelte +107 -51
- package/dist/components/molecules/Popover.svelte.d.ts +2 -0
- package/dist/components/molecules/Prose.svelte +1 -1
- package/dist/components/molecules/PullToRefresh.svelte +14 -10
- package/dist/components/molecules/PullToRefresh.svelte.d.ts +2 -0
- package/dist/components/molecules/RadialMenu.svelte +52 -19
- package/dist/components/molecules/RadialMenu.svelte.d.ts +2 -0
- package/dist/components/molecules/RadioGroup.svelte +38 -23
- package/dist/components/molecules/RadioGroup.svelte.d.ts +2 -0
- package/dist/components/molecules/RichSelect.svelte +583 -0
- package/dist/components/molecules/RichSelect.svelte.d.ts +68 -0
- package/dist/components/molecules/SectionHeader.svelte +13 -8
- package/dist/components/molecules/SeekSlider.svelte +38 -27
- package/dist/components/molecules/SeekSlider.svelte.d.ts +2 -0
- package/dist/components/molecules/SegmentedControl.svelte +139 -86
- package/dist/components/molecules/SegmentedControl.svelte.d.ts +2 -0
- package/dist/components/molecules/SelectButton.svelte +21 -6
- package/dist/components/molecules/SelectButton.svelte.d.ts +2 -0
- package/dist/components/molecules/SortableList.svelte +10 -11
- package/dist/components/molecules/SoundSettings.svelte +161 -0
- package/dist/components/molecules/SoundSettings.svelte.d.ts +17 -0
- package/dist/components/molecules/SplitButton.svelte +30 -3
- package/dist/components/molecules/SplitButton.svelte.d.ts +2 -0
- package/dist/components/molecules/SwipeRow.svelte +18 -15
- package/dist/components/molecules/SwipeRow.svelte.d.ts +2 -0
- package/dist/components/molecules/Tabs.svelte +92 -10
- package/dist/components/molecules/Tabs.svelte.d.ts +2 -0
- package/dist/components/molecules/ThemePicker.svelte +25 -10
- package/dist/components/molecules/Timestamp.svelte +12 -7
- package/dist/components/molecules/Toaster.svelte +65 -14
- package/dist/components/molecules/Toaster.svelte.d.ts +2 -0
- package/dist/components/molecules/Toggle.svelte +36 -13
- package/dist/components/molecules/Toggle.svelte.d.ts +2 -0
- package/dist/components/molecules/Tooltip.svelte +41 -10
- package/dist/components/molecules/Tooltip.svelte.d.ts +1 -1
- package/dist/components/molecules/VolumeSlider.svelte +34 -14
- package/dist/components/molecules/VolumeSlider.svelte.d.ts +2 -0
- package/dist/components/molecules/WorkingDir.svelte +1 -0
- package/dist/components/molecules/rich-select-keyboard.d.ts +82 -0
- package/dist/components/molecules/rich-select-keyboard.js +114 -0
- package/dist/components/organisms/ConversationFeed.svelte +24 -1
- package/dist/components/organisms/DataTable.svelte +37 -14
- package/dist/floating.js +26 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/sound-synth.js +17 -12
- package/dist/stores/toast.svelte.d.ts +9 -0
- package/dist/stores/toast.svelte.js +45 -9
- package/dist/styles/reset.css +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Opt-in UI sound control: a speaker trigger opens a popover with the
|
|
3
|
+
// on/off switch, a volume slider (disabled while off), a preview button and
|
|
4
|
+
// the reason nothing plays (off, or reduced motion muting decorative sounds).
|
|
5
|
+
import type { ComponentProps } from 'svelte';
|
|
6
|
+
import Button from '../atoms/Button.svelte';
|
|
7
|
+
import Icon from '../atoms/Icon.svelte';
|
|
8
|
+
import Slider from '../atoms/Slider.svelte';
|
|
9
|
+
import Switch from '../atoms/Switch.svelte';
|
|
10
|
+
import Popover from './Popover.svelte';
|
|
11
|
+
import { sound } from '../../stores/sound.svelte';
|
|
12
|
+
|
|
13
|
+
type TriggerChrome = Pick<
|
|
14
|
+
ComponentProps<typeof Popover>,
|
|
15
|
+
'variant' | 'tone' | 'box' | 'pill' | 'control' | 'block' | 'bare' | 'hitArea' | 'placement' | 'disabled'
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
size = 'md',
|
|
20
|
+
box = size,
|
|
21
|
+
placement = 'bottom-end',
|
|
22
|
+
variant,
|
|
23
|
+
tone,
|
|
24
|
+
pill,
|
|
25
|
+
control,
|
|
26
|
+
block,
|
|
27
|
+
bare,
|
|
28
|
+
hitArea,
|
|
29
|
+
disabled,
|
|
30
|
+
label = 'Sound',
|
|
31
|
+
switchLabel = 'Sound effects',
|
|
32
|
+
volumeLabel = 'Volume',
|
|
33
|
+
previewLabel = 'Preview',
|
|
34
|
+
offHint = 'Off. Nothing is loaded until you turn it on.',
|
|
35
|
+
reducedMotionHint = 'Decorative sounds are muted because your system asks for reduced motion.',
|
|
36
|
+
class: klass = '',
|
|
37
|
+
style: styleProp = '',
|
|
38
|
+
}: TriggerChrome & {
|
|
39
|
+
size?: 'sm' | 'md';
|
|
40
|
+
label?: string;
|
|
41
|
+
switchLabel?: string;
|
|
42
|
+
volumeLabel?: string;
|
|
43
|
+
previewLabel?: string;
|
|
44
|
+
offHint?: string;
|
|
45
|
+
reducedMotionHint?: string;
|
|
46
|
+
class?: string;
|
|
47
|
+
style?: string;
|
|
48
|
+
} = $props();
|
|
49
|
+
|
|
50
|
+
const icon = $derived(
|
|
51
|
+
!sound.enabled || sound.volume === 0 ? 'volume-off' : sound.volume < 0.5 ? 'volume-low' : 'volume-high'
|
|
52
|
+
);
|
|
53
|
+
const percent = $derived(Math.round(sound.volume * 100));
|
|
54
|
+
const hint = $derived(
|
|
55
|
+
sound.muted === 'off' ? offHint : sound.muted === 'reduced-motion' ? reducedMotionHint : null
|
|
56
|
+
);
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<Popover
|
|
60
|
+
{label}
|
|
61
|
+
{placement}
|
|
62
|
+
{box}
|
|
63
|
+
{variant}
|
|
64
|
+
{tone}
|
|
65
|
+
{pill}
|
|
66
|
+
{control}
|
|
67
|
+
{block}
|
|
68
|
+
{bare}
|
|
69
|
+
{hitArea}
|
|
70
|
+
{disabled}
|
|
71
|
+
triggerClass={klass}
|
|
72
|
+
style={styleProp}
|
|
73
|
+
sound={false}
|
|
74
|
+
>
|
|
75
|
+
{#snippet trigger()}
|
|
76
|
+
<span data-tsu="SoundSettings" class="glyph" title="{label}: {sound.enabled ? `${percent}%` : 'off'}">
|
|
77
|
+
<Icon name={icon} size={size === 'sm' ? 16 : 18} />
|
|
78
|
+
</span>
|
|
79
|
+
{/snippet}
|
|
80
|
+
<div class="panel" class:panel-sm={size === 'sm'}>
|
|
81
|
+
<div class="row">
|
|
82
|
+
<Switch
|
|
83
|
+
label={switchLabel}
|
|
84
|
+
labelVisible
|
|
85
|
+
size={size === 'sm' ? 'sm' : 'md'}
|
|
86
|
+
bind:checked={() => sound.enabled, (on) => sound.setEnabled(on)}
|
|
87
|
+
/>
|
|
88
|
+
<Button
|
|
89
|
+
size="sm"
|
|
90
|
+
variant="ghost"
|
|
91
|
+
disabled={!sound.enabled}
|
|
92
|
+
sound={false}
|
|
93
|
+
onclick={() => sound.play('success')}>{previewLabel}</Button
|
|
94
|
+
>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="row">
|
|
97
|
+
<Icon name="volume" size={16} />
|
|
98
|
+
<Slider
|
|
99
|
+
value={sound.volume}
|
|
100
|
+
min={0}
|
|
101
|
+
max={1}
|
|
102
|
+
step={0.05}
|
|
103
|
+
label={volumeLabel}
|
|
104
|
+
aria-valuetext="{percent}%"
|
|
105
|
+
disabled={!sound.enabled}
|
|
106
|
+
oninput={(e) => sound.setVolume(Number((e.currentTarget as HTMLInputElement).value))}
|
|
107
|
+
/>
|
|
108
|
+
<span class="value">{percent}%</span>
|
|
109
|
+
</div>
|
|
110
|
+
{#if hint}
|
|
111
|
+
<div class="hint" aria-live="polite">{hint}</div>
|
|
112
|
+
{/if}
|
|
113
|
+
</div>
|
|
114
|
+
</Popover>
|
|
115
|
+
|
|
116
|
+
<style>
|
|
117
|
+
.glyph {
|
|
118
|
+
display: inline-flex;
|
|
119
|
+
line-height: var(--lh-none);
|
|
120
|
+
}
|
|
121
|
+
.panel {
|
|
122
|
+
width: min(17rem, calc(100vw - var(--sp-6)));
|
|
123
|
+
box-sizing: border-box;
|
|
124
|
+
display: grid;
|
|
125
|
+
gap: var(--sp-2);
|
|
126
|
+
padding: var(--sp-3);
|
|
127
|
+
font-size: 0.875rem;
|
|
128
|
+
line-height: var(--lh-snug);
|
|
129
|
+
}
|
|
130
|
+
.panel-sm {
|
|
131
|
+
width: min(15rem, calc(100vw - var(--sp-6)));
|
|
132
|
+
padding: var(--sp-2) var(--sp-3);
|
|
133
|
+
font-size: 0.8125rem;
|
|
134
|
+
}
|
|
135
|
+
.row {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: var(--sp-2);
|
|
139
|
+
min-width: 0;
|
|
140
|
+
color: var(--text-muted);
|
|
141
|
+
}
|
|
142
|
+
.row > :global(.switch-row) {
|
|
143
|
+
flex: 1 1 auto;
|
|
144
|
+
color: var(--text);
|
|
145
|
+
}
|
|
146
|
+
.row > :global(.slider) {
|
|
147
|
+
flex: 1 1 0;
|
|
148
|
+
min-width: 0;
|
|
149
|
+
}
|
|
150
|
+
.value {
|
|
151
|
+
flex: none;
|
|
152
|
+
min-width: 2.5rem;
|
|
153
|
+
text-align: end;
|
|
154
|
+
font-size: 0.75rem;
|
|
155
|
+
font-variant-numeric: tabular-nums;
|
|
156
|
+
}
|
|
157
|
+
.hint {
|
|
158
|
+
font-size: 0.75rem;
|
|
159
|
+
color: var(--text-muted);
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ComponentProps } from 'svelte';
|
|
2
|
+
import Popover from './Popover.svelte';
|
|
3
|
+
type TriggerChrome = Pick<ComponentProps<typeof Popover>, 'variant' | 'tone' | 'box' | 'pill' | 'control' | 'block' | 'bare' | 'hitArea' | 'placement' | 'disabled'>;
|
|
4
|
+
type $$ComponentProps = TriggerChrome & {
|
|
5
|
+
size?: 'sm' | 'md';
|
|
6
|
+
label?: string;
|
|
7
|
+
switchLabel?: string;
|
|
8
|
+
volumeLabel?: string;
|
|
9
|
+
previewLabel?: string;
|
|
10
|
+
offHint?: string;
|
|
11
|
+
reducedMotionHint?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
style?: string;
|
|
14
|
+
};
|
|
15
|
+
declare const SoundSettings: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
16
|
+
type SoundSettings = ReturnType<typeof SoundSettings>;
|
|
17
|
+
export default SoundSettings;
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
loading?: boolean;
|
|
49
49
|
/** Handler of the primary action. */
|
|
50
50
|
onclick?: (e: MouseEvent) => void;
|
|
51
|
+
/** Play UI sounds when enabled globally; false silences this instance. */
|
|
52
|
+
sound?: boolean;
|
|
51
53
|
class?: string;
|
|
52
54
|
style?: string;
|
|
53
55
|
/** Content of the primary action. */
|
|
@@ -75,6 +77,7 @@
|
|
|
75
77
|
type = 'button',
|
|
76
78
|
loading = false,
|
|
77
79
|
onclick,
|
|
80
|
+
sound = true,
|
|
78
81
|
class: klass = '',
|
|
79
82
|
style: styleProp = '',
|
|
80
83
|
children,
|
|
@@ -102,6 +105,8 @@
|
|
|
102
105
|
class="split {klass}"
|
|
103
106
|
class:split-primary={variant === 'primary'}
|
|
104
107
|
class:split-half={caret === 'half'}
|
|
108
|
+
class:split-sm={size === 'sm'}
|
|
109
|
+
class:split-lg={size === 'lg'}
|
|
105
110
|
style={styleProp}
|
|
106
111
|
style:--split-height={height}
|
|
107
112
|
>
|
|
@@ -114,6 +119,7 @@
|
|
|
114
119
|
{type}
|
|
115
120
|
{loading}
|
|
116
121
|
{disabled}
|
|
122
|
+
{sound}
|
|
117
123
|
block
|
|
118
124
|
{onclick}
|
|
119
125
|
onkeydown={onPrimaryKeydown}
|
|
@@ -139,7 +145,7 @@
|
|
|
139
145
|
{onopen}
|
|
140
146
|
{onclose}
|
|
141
147
|
>
|
|
142
|
-
{#snippet trigger()}<Icon name="chevron-down"
|
|
148
|
+
{#snippet trigger()}<span class="caret-glyph"><Icon name="chevron-down" /></span>{/snippet}
|
|
143
149
|
</Menu>
|
|
144
150
|
</span>
|
|
145
151
|
</div>
|
|
@@ -169,14 +175,35 @@
|
|
|
169
175
|
.split-half .split-caret {
|
|
170
176
|
width: calc(var(--split-height) / 2);
|
|
171
177
|
}
|
|
178
|
+
.caret-glyph {
|
|
179
|
+
display: inline-flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
font-size: var(--control-icon-md);
|
|
182
|
+
}
|
|
183
|
+
.split-sm .caret-glyph {
|
|
184
|
+
font-size: var(--control-icon-sm);
|
|
185
|
+
}
|
|
186
|
+
.split-lg .caret-glyph {
|
|
187
|
+
font-size: var(--control-icon-lg);
|
|
188
|
+
}
|
|
189
|
+
/* Coarse pointers: the narrow caret keeps a 24px hit box without changing its drawn width. */
|
|
190
|
+
@media (pointer: coarse) {
|
|
191
|
+
.split-half .split-caret {
|
|
192
|
+
min-width: var(--box-xs);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
172
195
|
/* The hovered or focused segment paints above its neighbour, so its
|
|
173
196
|
accent border and focus ring are not cut by the overlap. */
|
|
174
|
-
.split-main:hover,
|
|
175
197
|
.split-main:focus-within,
|
|
176
|
-
.split-caret:hover,
|
|
177
198
|
.split-caret:focus-within {
|
|
178
199
|
z-index: 1;
|
|
179
200
|
}
|
|
201
|
+
@media (hover: hover) and (pointer: fine) {
|
|
202
|
+
.split-main:hover,
|
|
203
|
+
.split-caret:hover {
|
|
204
|
+
z-index: 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
180
207
|
/* On a filled primary the borders match the fill; paint the divider in the
|
|
181
208
|
on-accent foreground instead. */
|
|
182
209
|
.split-primary .split-caret::before {
|
|
@@ -40,6 +40,8 @@ type Own = {
|
|
|
40
40
|
loading?: boolean;
|
|
41
41
|
/** Handler of the primary action. */
|
|
42
42
|
onclick?: (e: MouseEvent) => void;
|
|
43
|
+
/** Play UI sounds when enabled globally; false silences this instance. */
|
|
44
|
+
sound?: boolean;
|
|
43
45
|
class?: string;
|
|
44
46
|
style?: string;
|
|
45
47
|
/** Content of the primary action. */
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
28
28
|
import Icon from '../atoms/Icon.svelte';
|
|
29
29
|
import { canonicalTone } from '../../tone';
|
|
30
|
+
import { playSound } from '../../sound';
|
|
30
31
|
|
|
31
32
|
type Own = {
|
|
32
33
|
children: Snippet;
|
|
@@ -40,6 +41,8 @@
|
|
|
40
41
|
/** Fraction of the row width the content must travel to fire. */
|
|
41
42
|
threshold?: number;
|
|
42
43
|
disabled?: boolean;
|
|
44
|
+
/** Play UI sounds when enabled globally; false silences this instance. */
|
|
45
|
+
sound?: boolean;
|
|
43
46
|
class?: string;
|
|
44
47
|
style?: string;
|
|
45
48
|
};
|
|
@@ -52,6 +55,7 @@
|
|
|
52
55
|
trailing,
|
|
53
56
|
threshold = 0.35,
|
|
54
57
|
disabled = false,
|
|
58
|
+
sound = true,
|
|
55
59
|
class: klass = '',
|
|
56
60
|
style: styleProp,
|
|
57
61
|
...rest
|
|
@@ -150,7 +154,12 @@
|
|
|
150
154
|
const fire = locked === 'x' && armed ? active : undefined;
|
|
151
155
|
release(el);
|
|
152
156
|
settle();
|
|
153
|
-
fire
|
|
157
|
+
if (fire) run(fire);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function run(action: SwipeAction) {
|
|
161
|
+
playSound('click', sound);
|
|
162
|
+
action.onaction();
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
function onpointercancel(e: PointerEvent) {
|
|
@@ -223,12 +232,12 @@
|
|
|
223
232
|
{#if leadingAction || trailingAction}
|
|
224
233
|
<div class="swiperow-actions" data-part="actions">
|
|
225
234
|
{#if leadingAction}
|
|
226
|
-
<button type="button" class="swiperow-sr" data-part="leading-button" {disabled} onclick={() => leadingAction
|
|
235
|
+
<button type="button" class="swiperow-sr" data-part="leading-button" {disabled} onclick={() => leadingAction && run(leadingAction)}>
|
|
227
236
|
{leadingAction.label}
|
|
228
237
|
</button>
|
|
229
238
|
{/if}
|
|
230
239
|
{#if trailingAction}
|
|
231
|
-
<button type="button" class="swiperow-sr" data-part="trailing-button" {disabled} onclick={() => trailingAction
|
|
240
|
+
<button type="button" class="swiperow-sr" data-part="trailing-button" {disabled} onclick={() => trailingAction && run(trailingAction)}>
|
|
232
241
|
{trailingAction.label}
|
|
233
242
|
</button>
|
|
234
243
|
{/if}
|
|
@@ -276,7 +285,7 @@
|
|
|
276
285
|
background: var(--swiperow-tone);
|
|
277
286
|
color: var(--swiperow-ink);
|
|
278
287
|
font-size: var(--fs-sm);
|
|
279
|
-
font-weight:
|
|
288
|
+
font-weight: var(--fw-semibold);
|
|
280
289
|
visibility: hidden;
|
|
281
290
|
pointer-events: none;
|
|
282
291
|
}
|
|
@@ -291,10 +300,10 @@
|
|
|
291
300
|
visibility: visible;
|
|
292
301
|
}
|
|
293
302
|
.swiperow-pane :global(svg) {
|
|
294
|
-
transition:
|
|
303
|
+
transition: scale var(--dur-spring) var(--ease-spring);
|
|
295
304
|
}
|
|
296
305
|
.swiperow--armed .swiperow-pane :global(svg) {
|
|
297
|
-
|
|
306
|
+
scale: 1.2;
|
|
298
307
|
}
|
|
299
308
|
.swiperow-content {
|
|
300
309
|
position: relative;
|
|
@@ -303,7 +312,7 @@
|
|
|
303
312
|
will-change: transform;
|
|
304
313
|
}
|
|
305
314
|
.swiperow--settling .swiperow-content {
|
|
306
|
-
transition: transform
|
|
315
|
+
transition: transform var(--dur-exit-slow) var(--ease-out);
|
|
307
316
|
}
|
|
308
317
|
.swiperow--dragging .swiperow-content {
|
|
309
318
|
cursor: grabbing;
|
|
@@ -341,13 +350,7 @@
|
|
|
341
350
|
color: var(--swiperow-ink);
|
|
342
351
|
background: var(--swiperow-tone);
|
|
343
352
|
border-radius: var(--r-sm);
|
|
344
|
-
outline:
|
|
345
|
-
outline-offset:
|
|
346
|
-
}
|
|
347
|
-
@media (prefers-reduced-motion: reduce) {
|
|
348
|
-
.swiperow--settling .swiperow-content,
|
|
349
|
-
.swiperow-pane :global(svg) {
|
|
350
|
-
transition: none;
|
|
351
|
-
}
|
|
353
|
+
outline: var(--focus-ring);
|
|
354
|
+
outline-offset: var(--sp-px);
|
|
352
355
|
}
|
|
353
356
|
</style>
|
|
@@ -32,6 +32,8 @@ type Own = {
|
|
|
32
32
|
/** Fraction of the row width the content must travel to fire. */
|
|
33
33
|
threshold?: number;
|
|
34
34
|
disabled?: boolean;
|
|
35
|
+
/** Play UI sounds when enabled globally; false silences this instance. */
|
|
36
|
+
sound?: boolean;
|
|
35
37
|
class?: string;
|
|
36
38
|
style?: string;
|
|
37
39
|
};
|
|
@@ -31,10 +31,12 @@
|
|
|
31
31
|
// Without `panel` the tablist stands alone as a document strip: `closable`
|
|
32
32
|
// tabs gain a close control, Delete and middle-click close the focused tab,
|
|
33
33
|
// `leading` decorates each tab and `actions` trails the strip.
|
|
34
|
+
import { tick } from 'svelte';
|
|
34
35
|
import type { Snippet } from 'svelte';
|
|
35
36
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
36
37
|
import Icon from '../atoms/Icon.svelte';
|
|
37
38
|
import IconButton from './IconButton.svelte';
|
|
39
|
+
import { playSound } from '../../sound';
|
|
38
40
|
|
|
39
41
|
let {
|
|
40
42
|
tabs,
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
style: styleProp = '',
|
|
51
53
|
panelClass = '',
|
|
52
54
|
panelPadding = 'md',
|
|
55
|
+
sound = true,
|
|
53
56
|
...rest
|
|
54
57
|
}: Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own = $props();
|
|
55
58
|
|
|
@@ -73,6 +76,8 @@
|
|
|
73
76
|
style?: string;
|
|
74
77
|
panelClass?: string;
|
|
75
78
|
panelPadding?: 'none' | 'sm' | 'md';
|
|
79
|
+
/** Play UI sounds when enabled globally; false silences this instance. */
|
|
80
|
+
sound?: boolean;
|
|
76
81
|
};
|
|
77
82
|
|
|
78
83
|
// Default to the first selectable tab when no value is supplied.
|
|
@@ -97,8 +102,59 @@
|
|
|
97
102
|
return t.closable ?? closable;
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
// One indicator slides under the selected tab on translate/scale (never
|
|
106
|
+
// width/left). It snaps on mount and on reflow, and only animates on a
|
|
107
|
+
// selection change; until it is placed the selected tab draws its own line.
|
|
108
|
+
let indicator = $state<HTMLSpanElement | null>(null);
|
|
109
|
+
let placed = $state(false);
|
|
110
|
+
let indLeft = 0;
|
|
111
|
+
let indWidth = 0;
|
|
112
|
+
function placeIndicator(animate: boolean) {
|
|
113
|
+
const el = listEl?.querySelector<HTMLElement>('.tab.selected');
|
|
114
|
+
const ind = indicator;
|
|
115
|
+
if (!el || !ind) {
|
|
116
|
+
placed = false;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const left = el.offsetLeft;
|
|
120
|
+
const width = el.offsetWidth;
|
|
121
|
+
const prevLeft = indLeft;
|
|
122
|
+
const prevWidth = indWidth;
|
|
123
|
+
indLeft = left;
|
|
124
|
+
indWidth = width;
|
|
125
|
+
ind.style.width = `${width}px`;
|
|
126
|
+
if (!animate || !placed || width === 0) {
|
|
127
|
+
ind.style.transition = 'none';
|
|
128
|
+
ind.style.translate = `${left}px 0`;
|
|
129
|
+
ind.style.scale = '1 1';
|
|
130
|
+
ind.getBoundingClientRect();
|
|
131
|
+
ind.style.transition = '';
|
|
132
|
+
placed = true;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
ind.style.transition = 'none';
|
|
136
|
+
ind.style.translate = `${prevLeft}px 0`;
|
|
137
|
+
ind.style.scale = `${prevWidth / width} 1`;
|
|
138
|
+
ind.getBoundingClientRect();
|
|
139
|
+
ind.style.transition = '';
|
|
140
|
+
ind.style.translate = `${left}px 0`;
|
|
141
|
+
ind.style.scale = '1 1';
|
|
142
|
+
}
|
|
143
|
+
$effect(() => {
|
|
144
|
+
value;
|
|
145
|
+
tabs;
|
|
146
|
+
tick().then(() => placeIndicator(true));
|
|
147
|
+
});
|
|
148
|
+
$effect(() => {
|
|
149
|
+
if (!listEl || typeof ResizeObserver === 'undefined') return;
|
|
150
|
+
const ro = new ResizeObserver(() => placeIndicator(false));
|
|
151
|
+
ro.observe(listEl);
|
|
152
|
+
return () => ro.disconnect();
|
|
153
|
+
});
|
|
154
|
+
|
|
100
155
|
function select(id: string, focus = false) {
|
|
101
156
|
if (tabs.find((t) => t.id === id)?.disabled) return;
|
|
157
|
+
if (value !== id) playSound('click', sound);
|
|
102
158
|
value = id;
|
|
103
159
|
queueMicrotask(() => {
|
|
104
160
|
const el = listEl?.ownerDocument.getElementById(tabId(id));
|
|
@@ -163,7 +219,8 @@
|
|
|
163
219
|
|
|
164
220
|
<div {...rest} class="tabs {klass}" style={styleProp} data-tsu="Tabs">
|
|
165
221
|
<div class="strip">
|
|
166
|
-
<div bind:this={listEl} role="tablist" aria-label={label} tabindex="-1" class="tablist" {onkeydown}>
|
|
222
|
+
<div bind:this={listEl} role="tablist" aria-label={label} tabindex="-1" class="tablist" class:placed {onkeydown}>
|
|
223
|
+
<span bind:this={indicator} class="indicator" aria-hidden="true"></span>
|
|
167
224
|
{#each tabs as t (t.id)}
|
|
168
225
|
{#if canClose(t)}
|
|
169
226
|
<!-- A close control cannot nest inside a <button>, so a closable tab is a div. -->
|
|
@@ -235,6 +292,7 @@
|
|
|
235
292
|
align-items: stretch;
|
|
236
293
|
}
|
|
237
294
|
.tablist {
|
|
295
|
+
position: relative;
|
|
238
296
|
display: flex;
|
|
239
297
|
flex: 1 1 auto;
|
|
240
298
|
min-width: 0;
|
|
@@ -244,42 +302,66 @@
|
|
|
244
302
|
overflow-y: hidden;
|
|
245
303
|
scrollbar-width: thin;
|
|
246
304
|
}
|
|
305
|
+
.indicator {
|
|
306
|
+
position: absolute;
|
|
307
|
+
left: 0;
|
|
308
|
+
bottom: 0;
|
|
309
|
+
width: 0;
|
|
310
|
+
height: 2px;
|
|
311
|
+
background: var(--accent);
|
|
312
|
+
transform-origin: left center;
|
|
313
|
+
pointer-events: none;
|
|
314
|
+
transition:
|
|
315
|
+
translate var(--dur-spring) var(--ease-spring),
|
|
316
|
+
scale var(--dur-spring) var(--ease-spring);
|
|
317
|
+
}
|
|
247
318
|
.tab {
|
|
248
319
|
display: inline-flex;
|
|
249
320
|
flex: 0 0 auto;
|
|
250
321
|
white-space: nowrap;
|
|
251
322
|
align-items: center;
|
|
252
323
|
gap: var(--sp-2);
|
|
253
|
-
|
|
324
|
+
min-height: var(--control-md);
|
|
325
|
+
padding: 0 var(--control-px-md);
|
|
254
326
|
border: none;
|
|
255
327
|
background: none;
|
|
256
328
|
color: var(--text-muted);
|
|
257
|
-
font-size: var(--fs-
|
|
329
|
+
font-size: var(--control-fs-md);
|
|
258
330
|
font-weight: var(--fw-medium);
|
|
331
|
+
--icon-size: var(--control-icon-md);
|
|
259
332
|
border-bottom: 2px solid transparent;
|
|
260
333
|
margin-bottom: -1px;
|
|
261
334
|
max-width: var(--tab-max-width, none);
|
|
262
335
|
transition:
|
|
263
|
-
color
|
|
264
|
-
border-color
|
|
336
|
+
color var(--dur-fast) var(--ease),
|
|
337
|
+
border-color var(--dur-fast) var(--ease);
|
|
265
338
|
}
|
|
266
339
|
.tab.closable {
|
|
267
340
|
cursor: pointer;
|
|
268
341
|
padding-inline-end: var(--sp-2);
|
|
269
342
|
user-select: none;
|
|
270
343
|
}
|
|
271
|
-
|
|
272
|
-
|
|
344
|
+
@media (hover: hover) and (pointer: fine) {
|
|
345
|
+
.tab:hover:not(:disabled, .disabled) {
|
|
346
|
+
color: var(--text);
|
|
347
|
+
}
|
|
273
348
|
}
|
|
274
349
|
.tab:disabled,
|
|
275
350
|
.tab.disabled {
|
|
276
|
-
opacity:
|
|
351
|
+
opacity: var(--disabled-opacity);
|
|
277
352
|
cursor: not-allowed;
|
|
278
353
|
}
|
|
279
354
|
.tab.selected {
|
|
280
355
|
color: var(--accent);
|
|
356
|
+
}
|
|
357
|
+
.tablist:not(.placed) .tab.selected {
|
|
281
358
|
border-bottom-color: var(--accent);
|
|
282
359
|
}
|
|
360
|
+
/* The list clips its overflow, so the ring sits inside the tab. */
|
|
361
|
+
.tab:focus-visible {
|
|
362
|
+
outline: var(--focus-ring);
|
|
363
|
+
outline-offset: var(--focus-ring-offset-inset);
|
|
364
|
+
}
|
|
283
365
|
.label {
|
|
284
366
|
min-width: 0;
|
|
285
367
|
overflow: hidden;
|
|
@@ -303,8 +385,8 @@
|
|
|
303
385
|
padding-top: 0;
|
|
304
386
|
}
|
|
305
387
|
.tabpanel:focus-visible {
|
|
306
|
-
outline:
|
|
307
|
-
outline-offset:
|
|
388
|
+
outline: var(--focus-ring);
|
|
389
|
+
outline-offset: var(--focus-ring-offset);
|
|
308
390
|
border-radius: var(--r-sm);
|
|
309
391
|
}
|
|
310
392
|
</style>
|
|
@@ -39,6 +39,8 @@ type Own = {
|
|
|
39
39
|
style?: string;
|
|
40
40
|
panelClass?: string;
|
|
41
41
|
panelPadding?: 'none' | 'sm' | 'md';
|
|
42
|
+
/** Play UI sounds when enabled globally; false silences this instance. */
|
|
43
|
+
sound?: boolean;
|
|
42
44
|
};
|
|
43
45
|
type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own;
|
|
44
46
|
declare const Tabs: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
@@ -157,11 +157,11 @@
|
|
|
157
157
|
position: absolute;
|
|
158
158
|
right: -0.35rem;
|
|
159
159
|
bottom: -0.35rem;
|
|
160
|
-
border-radius:
|
|
160
|
+
border-radius: var(--r-pill);
|
|
161
161
|
background: var(--bg);
|
|
162
162
|
color: var(--text-muted);
|
|
163
163
|
font-size: var(--fs-xs);
|
|
164
|
-
line-height:
|
|
164
|
+
line-height: var(--lh-none);
|
|
165
165
|
}
|
|
166
166
|
.swatch {
|
|
167
167
|
display: grid;
|
|
@@ -215,19 +215,27 @@
|
|
|
215
215
|
gap: var(--sp-1);
|
|
216
216
|
}
|
|
217
217
|
.cell {
|
|
218
|
+
--cell-scale-dur: var(--dur-fast);
|
|
218
219
|
display: inline-flex;
|
|
219
|
-
padding:
|
|
220
|
+
padding: var(--sp-0-5);
|
|
220
221
|
border: 2px solid transparent;
|
|
221
222
|
border-radius: var(--r-md);
|
|
222
223
|
background: none;
|
|
223
224
|
cursor: pointer;
|
|
225
|
+
transition:
|
|
226
|
+
background-color var(--dur-fast) ease,
|
|
227
|
+
border-color var(--dur-fast) ease,
|
|
228
|
+
scale var(--cell-scale-dur) var(--ease-out);
|
|
224
229
|
}
|
|
230
|
+
/* Concentric with the cell: outer radius minus border and padding. */
|
|
225
231
|
.cell .swatch {
|
|
226
232
|
width: 1.6rem;
|
|
227
233
|
height: 1.6rem;
|
|
234
|
+
border-radius: calc(var(--r-md) - var(--sp-1));
|
|
228
235
|
}
|
|
229
|
-
.cell:
|
|
230
|
-
|
|
236
|
+
.cell:active {
|
|
237
|
+
--cell-scale-dur: var(--dur-press);
|
|
238
|
+
scale: var(--press-scale-sm);
|
|
231
239
|
}
|
|
232
240
|
.cell.current {
|
|
233
241
|
border-color: var(--accent);
|
|
@@ -238,8 +246,8 @@
|
|
|
238
246
|
}
|
|
239
247
|
.cell:focus-visible,
|
|
240
248
|
.auto:focus-visible {
|
|
241
|
-
outline:
|
|
242
|
-
outline-offset:
|
|
249
|
+
outline: var(--focus-ring);
|
|
250
|
+
outline-offset: var(--focus-ring-offset-inset);
|
|
243
251
|
}
|
|
244
252
|
.auto {
|
|
245
253
|
display: flex;
|
|
@@ -255,16 +263,23 @@
|
|
|
255
263
|
color: inherit;
|
|
256
264
|
text-align: left;
|
|
257
265
|
cursor: pointer;
|
|
266
|
+
transition:
|
|
267
|
+
background-color var(--dur-fast) ease,
|
|
268
|
+
border-color var(--dur-fast) ease;
|
|
258
269
|
}
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
@media (hover: hover) and (pointer: fine) {
|
|
271
|
+
.cell:hover,
|
|
272
|
+
.auto:hover {
|
|
273
|
+
background: var(--bg-elevated-2);
|
|
274
|
+
transition-duration: var(--dur-instant);
|
|
275
|
+
}
|
|
261
276
|
}
|
|
262
277
|
.auto.current {
|
|
263
278
|
border-color: var(--accent);
|
|
264
279
|
}
|
|
265
280
|
.auto-glyph {
|
|
266
281
|
font-size: var(--fs-md);
|
|
267
|
-
line-height:
|
|
282
|
+
line-height: var(--lh-none);
|
|
268
283
|
}
|
|
269
284
|
.auto-text {
|
|
270
285
|
display: flex;
|
|
@@ -199,10 +199,19 @@
|
|
|
199
199
|
text-underline-offset: 2px;
|
|
200
200
|
text-decoration-color: var(--border-strong);
|
|
201
201
|
}
|
|
202
|
-
.ts-trigger:hover,
|
|
203
202
|
.ts-trigger:focus-visible {
|
|
204
203
|
text-decoration-color: currentColor;
|
|
205
204
|
}
|
|
205
|
+
@media (hover: hover) and (pointer: fine) {
|
|
206
|
+
.ts-trigger:hover {
|
|
207
|
+
text-decoration-color: currentColor;
|
|
208
|
+
}
|
|
209
|
+
.ts-mode:hover {
|
|
210
|
+
background: var(--bg-elevated-2);
|
|
211
|
+
color: var(--text);
|
|
212
|
+
transition-duration: var(--dur-instant);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
206
215
|
|
|
207
216
|
.ts-panel {
|
|
208
217
|
display: flex;
|
|
@@ -224,12 +233,8 @@
|
|
|
224
233
|
font-size: var(--fs-xs);
|
|
225
234
|
cursor: pointer;
|
|
226
235
|
transition:
|
|
227
|
-
background
|
|
228
|
-
color
|
|
229
|
-
}
|
|
230
|
-
.ts-mode:hover {
|
|
231
|
-
background: var(--bg-elevated-2);
|
|
232
|
-
color: var(--text);
|
|
236
|
+
background var(--dur-fast) var(--ease),
|
|
237
|
+
color var(--dur-fast) var(--ease);
|
|
233
238
|
}
|
|
234
239
|
.ts-mode.active {
|
|
235
240
|
background: var(--bg-elevated-2);
|