@dorsk/tsumikit 0.73.1 → 0.74.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 +2 -1
- package/dist/components/atoms/Progress.svelte +9 -5
- package/dist/components/atoms/Select.svelte +1 -1
- package/dist/components/atoms/Skeleton.svelte +14 -8
- package/dist/components/atoms/Slider.svelte +2 -2
- package/dist/components/atoms/Spinner.svelte +464 -5
- package/dist/components/atoms/Spinner.svelte.d.ts +2 -0
- package/dist/components/molecules/EmojiPicker.svelte +1 -1
- package/dist/components/molecules/FilterInput.svelte +1 -1
- package/dist/components/molecules/Menu.svelte +3 -1
- package/dist/components/molecules/Menu.svelte.d.ts +2 -0
- package/dist/components/molecules/RadialMenu.svelte +1 -1
- package/dist/components/molecules/SelectButton.svelte +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -1
- package/dist/sound-synth.d.ts +3 -0
- package/dist/sound-synth.js +86 -0
- package/dist/sound.d.ts +1 -0
- package/dist/sound.js +3 -0
- package/dist/stores/sound.svelte.d.ts +20 -0
- package/dist/stores/sound.svelte.js +150 -0
- package/dist/styles/reset.css +19 -2
- package/dist/styles/tokens.css +89 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -286,7 +286,8 @@ its icon leading and shows its check trailing, or uses the leading check slot wh
|
|
|
286
286
|
no icon; selecting closes the menu unless `closeOnSelect={false}` or the item sets `keepOpen`;
|
|
287
287
|
rest props land on the trigger, `count`/`countMax` paint its corner badge and `panelStyle`
|
|
288
288
|
sizes the panel; an item with a `control` snippet renders as a non-button row hosting a native
|
|
289
|
-
`<select>` or nested Popover trigger, keeping the icon column, row height and ↑/↓ navigation
|
|
289
|
+
`<select>` or nested Popover trigger, keeping the icon column, row height and ↑/↓ navigation;
|
|
290
|
+
labels may repeat, and an optional `id` keeps a row's identity when the list reorders),
|
|
290
291
|
Tabs (omit `panel` for a strip-only tablist of document tabs: `closable` + `onclose(id)`
|
|
291
292
|
close through the ✕, Delete and middle-click, per-tab `leading` snippet, `title`,
|
|
292
293
|
trailing `actions` snippet; `--tab-max-width` truncates long labels),
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
aria-valuenow={indeterminate ? undefined : value}
|
|
97
97
|
>
|
|
98
98
|
<div class="bar" style={indeterminate ? undefined : `width: ${pct}%`}></div>
|
|
99
|
-
{#each markers as m (m.at)}
|
|
99
|
+
{#each markers as m, i (`${i}:${m.at}`)}
|
|
100
100
|
<span
|
|
101
101
|
class="marker tone-{m.tone ? canonicalTone(m.tone) : 'text'}"
|
|
102
102
|
style="left: {markerPct(m.at)}%"
|
|
@@ -233,22 +233,22 @@
|
|
|
233
233
|
}
|
|
234
234
|
.progress.indeterminate .bar {
|
|
235
235
|
width: 40%;
|
|
236
|
-
animation: kt-progress-slide 1.
|
|
236
|
+
animation: kt-progress-slide 1.3s cubic-bezier(0.65, 0, 0.35, 1) infinite;
|
|
237
237
|
}
|
|
238
238
|
.progress.striped .bar {
|
|
239
239
|
animation: kt-stripe 0.6s linear infinite;
|
|
240
240
|
}
|
|
241
241
|
.progress.striped.indeterminate .bar {
|
|
242
242
|
animation:
|
|
243
|
-
kt-progress-slide 1.
|
|
243
|
+
kt-progress-slide 1.3s cubic-bezier(0.65, 0, 0.35, 1) infinite,
|
|
244
244
|
kt-stripe 0.6s linear infinite;
|
|
245
245
|
}
|
|
246
246
|
@keyframes kt-progress-slide {
|
|
247
247
|
0% {
|
|
248
|
-
transform: translateX(-
|
|
248
|
+
transform: translateX(-100%);
|
|
249
249
|
}
|
|
250
250
|
100% {
|
|
251
|
-
transform: translateX(
|
|
251
|
+
transform: translateX(250%);
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
@keyframes kt-stripe {
|
|
@@ -265,5 +265,9 @@
|
|
|
265
265
|
.progress.striped.indeterminate .bar {
|
|
266
266
|
animation: none;
|
|
267
267
|
}
|
|
268
|
+
.progress.indeterminate .bar {
|
|
269
|
+
width: 100%;
|
|
270
|
+
opacity: 0.35;
|
|
271
|
+
}
|
|
268
272
|
}
|
|
269
273
|
</style>
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
|
|
113
113
|
{#snippet optionList()}
|
|
114
114
|
{#if options}
|
|
115
|
-
{#each sections as section, i (section.group ??
|
|
115
|
+
{#each sections as section, i (`${i}:${section.group ?? ''}`)}
|
|
116
116
|
{#if section.group === undefined}
|
|
117
117
|
{#each section.options as o (o.value)}
|
|
118
118
|
<option value={o.value} disabled={o.disabled}>{optionText(o)}</option>
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
// Shimmering placeholder bar(s) standing in for content that is still
|
|
4
4
|
// loading. Decorative only — the surrounding region should carry aria-busy.
|
|
5
|
+
// Bars are tinted from --text so they read on any surface; the sweep is
|
|
6
|
+
// viewport-anchored so every bar on screen shimmers in sync.
|
|
5
7
|
type Own = {
|
|
6
8
|
width?: string;
|
|
7
9
|
height?: string;
|
|
@@ -46,24 +48,28 @@
|
|
|
46
48
|
max-width: 100%;
|
|
47
49
|
}
|
|
48
50
|
.skeleton-bar {
|
|
51
|
+
--sk-base: var(--skeleton, color-mix(in srgb, var(--text) 7%, transparent));
|
|
52
|
+
--sk-hi: var(--skeleton-highlight, color-mix(in srgb, var(--text) 13%, transparent));
|
|
49
53
|
display: block;
|
|
50
54
|
width: 100%;
|
|
51
|
-
background: linear-gradient(
|
|
52
|
-
background-size:
|
|
53
|
-
|
|
55
|
+
background: linear-gradient(100deg, var(--sk-base) 42%, var(--sk-hi) 50%, var(--sk-base) 58%) var(--sk-base);
|
|
56
|
+
background-size: 200vw 100%;
|
|
57
|
+
background-attachment: fixed;
|
|
58
|
+
animation: tsu-shimmer 1.8s linear infinite;
|
|
54
59
|
}
|
|
55
60
|
@keyframes tsu-shimmer {
|
|
56
|
-
|
|
57
|
-
background-position:
|
|
61
|
+
0% {
|
|
62
|
+
background-position: 100% 0;
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
75%,
|
|
65
|
+
100% {
|
|
66
|
+
background-position: 0% 0;
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
@media (prefers-reduced-motion: reduce) {
|
|
64
70
|
.skeleton-bar {
|
|
65
71
|
animation: none;
|
|
66
|
-
background: var(--
|
|
72
|
+
background: var(--sk-base);
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
</style>
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
/>
|
|
90
90
|
{#if tickValues.length > 0}
|
|
91
91
|
<span class="ticks" aria-hidden="true">
|
|
92
|
-
{#each tickValues as t (t)}
|
|
92
|
+
{#each tickValues as t, i (`${i}:${t}`)}
|
|
93
93
|
<i class="tick" class:reached={t <= Number(value)} style="--f: {frac(t)}"></i>
|
|
94
94
|
{/each}
|
|
95
95
|
</span>
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
{/if}
|
|
101
101
|
{#if marks.length > 0}
|
|
102
102
|
<span class="marks" aria-hidden="true">
|
|
103
|
-
{#each marks as m (m.value)}
|
|
103
|
+
{#each marks as m, i (`${i}:${m.value}`)}
|
|
104
104
|
<button type="button" class="mark" class:on={Number(value) === m.value} style="--f: {frac(m.value)}" tabindex="-1" onclick={() => (value = m.value)}>{m.label}</button>
|
|
105
105
|
{/each}
|
|
106
106
|
</span>
|
|
@@ -1,15 +1,46 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type SpinnerVariant =
|
|
3
|
+
| 'icon'
|
|
4
|
+
| 'arc'
|
|
5
|
+
| 'dots'
|
|
6
|
+
| 'orbit'
|
|
7
|
+
| 'pulse'
|
|
8
|
+
| 'bars'
|
|
9
|
+
| 'grid'
|
|
10
|
+
| 'clock'
|
|
11
|
+
| 'bounce';
|
|
12
|
+
|
|
13
|
+
// Inner spans per variant; `.in` wraps the flex/grid ones in an absolute
|
|
14
|
+
// layer so the root keeps the same inline baseline as the others.
|
|
15
|
+
const PARTS: Record<Exclude<SpinnerVariant, 'icon'>, { count: number; wrap: boolean }> = {
|
|
16
|
+
arc: { count: 0, wrap: false },
|
|
17
|
+
dots: { count: 3, wrap: true },
|
|
18
|
+
orbit: { count: 2, wrap: false },
|
|
19
|
+
pulse: { count: 1, wrap: false },
|
|
20
|
+
bars: { count: 4, wrap: true },
|
|
21
|
+
grid: { count: 9, wrap: true },
|
|
22
|
+
clock: { count: 0, wrap: false },
|
|
23
|
+
bounce: { count: 2, wrap: false }
|
|
24
|
+
};
|
|
25
|
+
</script>
|
|
26
|
+
|
|
1
27
|
<script lang="ts">
|
|
2
|
-
// Inline loading
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
28
|
+
// Inline loading indicator. `icon` (default) spins the `loader` glyph; the
|
|
29
|
+
// other variants are pure-CSS pictograms drawn in currentColor inside a 1em
|
|
30
|
+
// box. Every variant's base style is a meaningful static pose, which is what
|
|
31
|
+
// shows under prefers-reduced-motion. `--spinner-speed` scales the tempo.
|
|
32
|
+
// `clock` and `bounce` read best at 20px and up.
|
|
6
33
|
import Icon from './Icon.svelte';
|
|
7
34
|
|
|
8
35
|
let {
|
|
36
|
+
variant = 'icon',
|
|
9
37
|
size,
|
|
10
38
|
label = 'Loading',
|
|
39
|
+
class: klass = '',
|
|
40
|
+
style: styleProp = '',
|
|
11
41
|
...rest
|
|
12
42
|
}: {
|
|
43
|
+
variant?: SpinnerVariant;
|
|
13
44
|
/** Explicit pixel size. Omit to track the surrounding text (1em). */
|
|
14
45
|
size?: number;
|
|
15
46
|
/** AT label announcing the busy state. Set to undefined to make the
|
|
@@ -19,6 +50,434 @@
|
|
|
19
50
|
style?: string;
|
|
20
51
|
[key: string]: unknown;
|
|
21
52
|
} = $props();
|
|
53
|
+
|
|
54
|
+
const parts = $derived(variant === 'icon' ? undefined : PARTS[variant]);
|
|
22
55
|
</script>
|
|
23
56
|
|
|
24
|
-
|
|
57
|
+
{#if !parts}
|
|
58
|
+
<Icon name="loader" spin {size} {label} data-tsu="Spinner" class={klass} style={styleProp} {...rest} />
|
|
59
|
+
{:else}
|
|
60
|
+
<span
|
|
61
|
+
data-tsu="Spinner"
|
|
62
|
+
class="spinner v-{variant} {klass}"
|
|
63
|
+
style="{size ? `font-size: ${size}px;` : ''}{styleProp}"
|
|
64
|
+
role={label ? 'img' : undefined}
|
|
65
|
+
aria-label={label}
|
|
66
|
+
aria-hidden={label ? undefined : 'true'}
|
|
67
|
+
{...rest}
|
|
68
|
+
>
|
|
69
|
+
{#if parts.wrap}
|
|
70
|
+
<span class="in">
|
|
71
|
+
{#each { length: parts.count } as _, i (i)}<span></span>{/each}
|
|
72
|
+
</span>
|
|
73
|
+
{:else}
|
|
74
|
+
{#each { length: parts.count } as _, i (i)}<span></span>{/each}
|
|
75
|
+
{/if}
|
|
76
|
+
</span>
|
|
77
|
+
{/if}
|
|
78
|
+
|
|
79
|
+
<style>
|
|
80
|
+
@property --tsu-spin-arc {
|
|
81
|
+
syntax: '<angle>';
|
|
82
|
+
inherits: false;
|
|
83
|
+
initial-value: 100deg;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.spinner {
|
|
87
|
+
--sw: max(1.5px, 0.11em);
|
|
88
|
+
--t: calc(1s * var(--spinner-speed, 1));
|
|
89
|
+
position: relative;
|
|
90
|
+
display: inline-block;
|
|
91
|
+
flex: none;
|
|
92
|
+
width: 1em;
|
|
93
|
+
height: 1em;
|
|
94
|
+
vertical-align: -0.125em;
|
|
95
|
+
color: inherit;
|
|
96
|
+
}
|
|
97
|
+
.spinner span,
|
|
98
|
+
.spinner::before,
|
|
99
|
+
.spinner::after {
|
|
100
|
+
box-sizing: border-box;
|
|
101
|
+
}
|
|
102
|
+
.in {
|
|
103
|
+
position: absolute;
|
|
104
|
+
inset: 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.v-arc::before,
|
|
108
|
+
.v-arc::after {
|
|
109
|
+
content: '';
|
|
110
|
+
position: absolute;
|
|
111
|
+
inset: 0;
|
|
112
|
+
border-radius: 50%;
|
|
113
|
+
background: currentColor;
|
|
114
|
+
-webkit-mask: radial-gradient(
|
|
115
|
+
farthest-side,
|
|
116
|
+
transparent calc(100% - var(--sw)),
|
|
117
|
+
#000 calc(100% - var(--sw) + 0.5px)
|
|
118
|
+
);
|
|
119
|
+
mask: radial-gradient(
|
|
120
|
+
farthest-side,
|
|
121
|
+
transparent calc(100% - var(--sw)),
|
|
122
|
+
#000 calc(100% - var(--sw) + 0.5px)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
.v-arc::before {
|
|
126
|
+
opacity: 0.18;
|
|
127
|
+
}
|
|
128
|
+
.v-arc::after {
|
|
129
|
+
--tsu-spin-arc: 100deg;
|
|
130
|
+
-webkit-mask:
|
|
131
|
+
radial-gradient(farthest-side, transparent calc(100% - var(--sw)), #000 calc(100% - var(--sw) + 0.5px)),
|
|
132
|
+
conic-gradient(#000 var(--tsu-spin-arc), transparent 0);
|
|
133
|
+
-webkit-mask-composite: source-in;
|
|
134
|
+
mask:
|
|
135
|
+
radial-gradient(farthest-side, transparent calc(100% - var(--sw)), #000 calc(100% - var(--sw) + 0.5px))
|
|
136
|
+
intersect,
|
|
137
|
+
conic-gradient(#000 var(--tsu-spin-arc), transparent 0);
|
|
138
|
+
mask-composite: intersect;
|
|
139
|
+
animation:
|
|
140
|
+
tsu-spin-arc calc(var(--t) * 1.4) cubic-bezier(0.5, 0, 0.5, 1) infinite,
|
|
141
|
+
tsu-spin-rot calc(var(--t) * 1.4) linear infinite;
|
|
142
|
+
}
|
|
143
|
+
@keyframes tsu-spin-arc {
|
|
144
|
+
0% {
|
|
145
|
+
--tsu-spin-arc: 20deg;
|
|
146
|
+
}
|
|
147
|
+
50% {
|
|
148
|
+
--tsu-spin-arc: 280deg;
|
|
149
|
+
}
|
|
150
|
+
100% {
|
|
151
|
+
--tsu-spin-arc: 20deg;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
@keyframes tsu-spin-rot {
|
|
155
|
+
0% {
|
|
156
|
+
transform: rotate(0deg);
|
|
157
|
+
}
|
|
158
|
+
50% {
|
|
159
|
+
transform: rotate(160deg);
|
|
160
|
+
}
|
|
161
|
+
100% {
|
|
162
|
+
transform: rotate(720deg);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.v-dots .in {
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
justify-content: space-between;
|
|
170
|
+
padding: 0 0.04em;
|
|
171
|
+
}
|
|
172
|
+
.v-dots .in span {
|
|
173
|
+
width: 0.24em;
|
|
174
|
+
height: 0.24em;
|
|
175
|
+
border-radius: 50%;
|
|
176
|
+
background: currentColor;
|
|
177
|
+
animation: tsu-spin-dot calc(var(--t) * 1.1) cubic-bezier(0.45, 0, 0.55, 1) infinite;
|
|
178
|
+
}
|
|
179
|
+
.v-dots .in span:nth-child(1) {
|
|
180
|
+
opacity: 1;
|
|
181
|
+
}
|
|
182
|
+
.v-dots .in span:nth-child(2) {
|
|
183
|
+
opacity: 0.6;
|
|
184
|
+
animation-delay: calc(var(--t) * 0.14);
|
|
185
|
+
}
|
|
186
|
+
.v-dots .in span:nth-child(3) {
|
|
187
|
+
opacity: 0.32;
|
|
188
|
+
animation-delay: calc(var(--t) * 0.28);
|
|
189
|
+
}
|
|
190
|
+
@keyframes tsu-spin-dot {
|
|
191
|
+
0%,
|
|
192
|
+
60%,
|
|
193
|
+
100% {
|
|
194
|
+
transform: translateY(0);
|
|
195
|
+
opacity: 0.32;
|
|
196
|
+
}
|
|
197
|
+
25% {
|
|
198
|
+
transform: translateY(-0.22em);
|
|
199
|
+
opacity: 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.v-orbit::before {
|
|
204
|
+
content: '';
|
|
205
|
+
position: absolute;
|
|
206
|
+
inset: 0.08em;
|
|
207
|
+
border-radius: 50%;
|
|
208
|
+
border: max(1px, 0.06em) solid currentColor;
|
|
209
|
+
opacity: 0.22;
|
|
210
|
+
}
|
|
211
|
+
.v-orbit > span:nth-child(1) {
|
|
212
|
+
position: absolute;
|
|
213
|
+
left: 50%;
|
|
214
|
+
top: 50%;
|
|
215
|
+
width: 0.3em;
|
|
216
|
+
height: 0.3em;
|
|
217
|
+
margin: -0.15em;
|
|
218
|
+
border-radius: 50%;
|
|
219
|
+
background: currentColor;
|
|
220
|
+
animation: tsu-spin-core calc(var(--t) * 1.2) ease-in-out infinite;
|
|
221
|
+
}
|
|
222
|
+
.v-orbit > span:nth-child(2) {
|
|
223
|
+
position: absolute;
|
|
224
|
+
inset: 0;
|
|
225
|
+
transform: rotate(45deg);
|
|
226
|
+
animation: tsu-spin-orbit calc(var(--t) * 1.2) cubic-bezier(0.6, 0.15, 0.4, 0.85) infinite;
|
|
227
|
+
}
|
|
228
|
+
.v-orbit > span:nth-child(2)::before {
|
|
229
|
+
content: '';
|
|
230
|
+
position: absolute;
|
|
231
|
+
left: 50%;
|
|
232
|
+
top: 0;
|
|
233
|
+
width: 0.24em;
|
|
234
|
+
height: 0.24em;
|
|
235
|
+
margin-left: -0.12em;
|
|
236
|
+
border-radius: 50%;
|
|
237
|
+
background: currentColor;
|
|
238
|
+
}
|
|
239
|
+
@keyframes tsu-spin-orbit {
|
|
240
|
+
from {
|
|
241
|
+
transform: rotate(45deg);
|
|
242
|
+
}
|
|
243
|
+
to {
|
|
244
|
+
transform: rotate(405deg);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
@keyframes tsu-spin-core {
|
|
248
|
+
0%,
|
|
249
|
+
100% {
|
|
250
|
+
transform: scale(1);
|
|
251
|
+
}
|
|
252
|
+
50% {
|
|
253
|
+
transform: scale(0.7);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.v-pulse > span {
|
|
258
|
+
position: absolute;
|
|
259
|
+
left: 50%;
|
|
260
|
+
top: 50%;
|
|
261
|
+
width: 0.3em;
|
|
262
|
+
height: 0.3em;
|
|
263
|
+
margin: -0.15em;
|
|
264
|
+
border-radius: 50%;
|
|
265
|
+
background: currentColor;
|
|
266
|
+
}
|
|
267
|
+
.v-pulse::before,
|
|
268
|
+
.v-pulse::after {
|
|
269
|
+
content: '';
|
|
270
|
+
position: absolute;
|
|
271
|
+
inset: 0;
|
|
272
|
+
border-radius: 50%;
|
|
273
|
+
border: var(--sw) solid currentColor;
|
|
274
|
+
transform: scale(0.72);
|
|
275
|
+
opacity: 0.45;
|
|
276
|
+
animation: tsu-spin-ripple calc(var(--t) * 1.6) cubic-bezier(0.2, 0.6, 0.35, 1) infinite;
|
|
277
|
+
}
|
|
278
|
+
.v-pulse::after {
|
|
279
|
+
animation-delay: calc(var(--t) * -0.8);
|
|
280
|
+
}
|
|
281
|
+
@keyframes tsu-spin-ripple {
|
|
282
|
+
0% {
|
|
283
|
+
transform: scale(0.3);
|
|
284
|
+
opacity: 1;
|
|
285
|
+
}
|
|
286
|
+
100% {
|
|
287
|
+
transform: scale(1);
|
|
288
|
+
opacity: 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.v-bars .in {
|
|
293
|
+
display: flex;
|
|
294
|
+
align-items: center;
|
|
295
|
+
justify-content: space-between;
|
|
296
|
+
padding: 0.06em 0.08em;
|
|
297
|
+
}
|
|
298
|
+
.v-bars .in span {
|
|
299
|
+
width: 0.13em;
|
|
300
|
+
height: 100%;
|
|
301
|
+
border-radius: 0.1em;
|
|
302
|
+
background: currentColor;
|
|
303
|
+
transform-origin: 50% 50%;
|
|
304
|
+
animation: tsu-spin-bar var(--t) ease-in-out infinite;
|
|
305
|
+
}
|
|
306
|
+
.v-bars .in span:nth-child(1) {
|
|
307
|
+
transform: scaleY(0.4);
|
|
308
|
+
animation-delay: calc(var(--t) * -0.9);
|
|
309
|
+
}
|
|
310
|
+
.v-bars .in span:nth-child(2) {
|
|
311
|
+
transform: scaleY(0.85);
|
|
312
|
+
animation-delay: calc(var(--t) * -0.6);
|
|
313
|
+
}
|
|
314
|
+
.v-bars .in span:nth-child(3) {
|
|
315
|
+
transform: scaleY(0.6);
|
|
316
|
+
animation-delay: calc(var(--t) * -0.3);
|
|
317
|
+
}
|
|
318
|
+
.v-bars .in span:nth-child(4) {
|
|
319
|
+
transform: scaleY(1);
|
|
320
|
+
animation-delay: calc(var(--t) * -0.75);
|
|
321
|
+
}
|
|
322
|
+
@keyframes tsu-spin-bar {
|
|
323
|
+
0%,
|
|
324
|
+
100% {
|
|
325
|
+
transform: scaleY(0.3);
|
|
326
|
+
}
|
|
327
|
+
50% {
|
|
328
|
+
transform: scaleY(1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.v-grid .in {
|
|
333
|
+
display: grid;
|
|
334
|
+
grid-template-columns: repeat(3, 1fr);
|
|
335
|
+
place-items: center;
|
|
336
|
+
padding: 0.04em;
|
|
337
|
+
}
|
|
338
|
+
.v-grid .in span {
|
|
339
|
+
width: 0.2em;
|
|
340
|
+
height: 0.2em;
|
|
341
|
+
border-radius: 0.05em;
|
|
342
|
+
background: currentColor;
|
|
343
|
+
opacity: calc(1 - var(--d) * 0.18);
|
|
344
|
+
animation: tsu-spin-cell calc(var(--t) * 1.2) ease-in-out infinite;
|
|
345
|
+
animation-delay: calc(var(--t) * 0.1 * var(--d) - var(--t) * 1.2);
|
|
346
|
+
}
|
|
347
|
+
.v-grid .in span:nth-child(1) {
|
|
348
|
+
--d: 0;
|
|
349
|
+
}
|
|
350
|
+
.v-grid .in span:nth-child(2),
|
|
351
|
+
.v-grid .in span:nth-child(4) {
|
|
352
|
+
--d: 1;
|
|
353
|
+
}
|
|
354
|
+
.v-grid .in span:nth-child(3),
|
|
355
|
+
.v-grid .in span:nth-child(5),
|
|
356
|
+
.v-grid .in span:nth-child(7) {
|
|
357
|
+
--d: 2;
|
|
358
|
+
}
|
|
359
|
+
.v-grid .in span:nth-child(6),
|
|
360
|
+
.v-grid .in span:nth-child(8) {
|
|
361
|
+
--d: 3;
|
|
362
|
+
}
|
|
363
|
+
.v-grid .in span:nth-child(9) {
|
|
364
|
+
--d: 4;
|
|
365
|
+
}
|
|
366
|
+
@keyframes tsu-spin-cell {
|
|
367
|
+
0%,
|
|
368
|
+
100% {
|
|
369
|
+
opacity: 0.2;
|
|
370
|
+
transform: scale(0.7);
|
|
371
|
+
}
|
|
372
|
+
35% {
|
|
373
|
+
opacity: 1;
|
|
374
|
+
transform: scale(1);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.v-clock {
|
|
379
|
+
border-radius: 50%;
|
|
380
|
+
border: var(--sw) solid currentColor;
|
|
381
|
+
}
|
|
382
|
+
.v-clock::before,
|
|
383
|
+
.v-clock::after {
|
|
384
|
+
content: '';
|
|
385
|
+
position: absolute;
|
|
386
|
+
left: 50%;
|
|
387
|
+
bottom: 50%;
|
|
388
|
+
width: var(--sw);
|
|
389
|
+
margin-left: calc(var(--sw) / -2);
|
|
390
|
+
border-radius: var(--sw);
|
|
391
|
+
background: currentColor;
|
|
392
|
+
transform-origin: 50% 100%;
|
|
393
|
+
translate: 0 calc(var(--sw) / 2);
|
|
394
|
+
}
|
|
395
|
+
.v-clock::before {
|
|
396
|
+
height: 0.32em;
|
|
397
|
+
transform: rotate(0deg);
|
|
398
|
+
animation: tsu-spin-hand-m var(--t) linear infinite;
|
|
399
|
+
}
|
|
400
|
+
.v-clock::after {
|
|
401
|
+
height: 0.22em;
|
|
402
|
+
transform: rotate(90deg);
|
|
403
|
+
animation: tsu-spin-hand-h calc(var(--t) * 12) linear infinite;
|
|
404
|
+
}
|
|
405
|
+
@keyframes tsu-spin-hand-m {
|
|
406
|
+
from {
|
|
407
|
+
transform: rotate(0deg);
|
|
408
|
+
}
|
|
409
|
+
to {
|
|
410
|
+
transform: rotate(360deg);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
@keyframes tsu-spin-hand-h {
|
|
414
|
+
from {
|
|
415
|
+
transform: rotate(90deg);
|
|
416
|
+
}
|
|
417
|
+
to {
|
|
418
|
+
transform: rotate(450deg);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.v-bounce > span:nth-child(1) {
|
|
423
|
+
position: absolute;
|
|
424
|
+
left: 50%;
|
|
425
|
+
bottom: 0.16em;
|
|
426
|
+
width: 0.34em;
|
|
427
|
+
height: 0.34em;
|
|
428
|
+
margin-left: -0.17em;
|
|
429
|
+
border-radius: 50%;
|
|
430
|
+
background: currentColor;
|
|
431
|
+
transform-origin: 50% 100%;
|
|
432
|
+
animation: tsu-spin-ball calc(var(--t) * 0.9) infinite;
|
|
433
|
+
}
|
|
434
|
+
.v-bounce > span:nth-child(2) {
|
|
435
|
+
position: absolute;
|
|
436
|
+
left: 50%;
|
|
437
|
+
bottom: 0.06em;
|
|
438
|
+
width: 0.46em;
|
|
439
|
+
height: 0.08em;
|
|
440
|
+
margin-left: -0.23em;
|
|
441
|
+
border-radius: 50%;
|
|
442
|
+
background: currentColor;
|
|
443
|
+
opacity: 0.3;
|
|
444
|
+
animation: tsu-spin-shadow calc(var(--t) * 0.9) infinite;
|
|
445
|
+
}
|
|
446
|
+
@keyframes tsu-spin-ball {
|
|
447
|
+
0% {
|
|
448
|
+
transform: translateY(0) scale(1.18, 0.82);
|
|
449
|
+
animation-timing-function: cubic-bezier(0.2, 0.7, 0.4, 1);
|
|
450
|
+
}
|
|
451
|
+
12% {
|
|
452
|
+
transform: translateY(0) scale(1, 1);
|
|
453
|
+
animation-timing-function: cubic-bezier(0.2, 0.7, 0.4, 1);
|
|
454
|
+
}
|
|
455
|
+
50% {
|
|
456
|
+
transform: translateY(-0.5em) scale(0.95, 1.05);
|
|
457
|
+
animation-timing-function: cubic-bezier(0.6, 0, 0.8, 0.3);
|
|
458
|
+
}
|
|
459
|
+
100% {
|
|
460
|
+
transform: translateY(0) scale(1.18, 0.82);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
@keyframes tsu-spin-shadow {
|
|
464
|
+
0%,
|
|
465
|
+
100% {
|
|
466
|
+
transform: scaleX(1);
|
|
467
|
+
opacity: 0.35;
|
|
468
|
+
}
|
|
469
|
+
50% {
|
|
470
|
+
transform: scaleX(0.5);
|
|
471
|
+
opacity: 0.12;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
@media (prefers-reduced-motion: reduce) {
|
|
476
|
+
.spinner.spinner,
|
|
477
|
+
.spinner.spinner span,
|
|
478
|
+
.spinner.spinner::before,
|
|
479
|
+
.spinner.spinner::after {
|
|
480
|
+
animation: none;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
</style>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
export type SpinnerVariant = 'icon' | 'arc' | 'dots' | 'orbit' | 'pulse' | 'bars' | 'grid' | 'clock' | 'bounce';
|
|
1
2
|
type $$ComponentProps = {
|
|
3
|
+
variant?: SpinnerVariant;
|
|
2
4
|
/** Explicit pixel size. Omit to track the surrounding text (1em). */
|
|
3
5
|
size?: number;
|
|
4
6
|
/** AT label announcing the busy state. Set to undefined to make the
|
|
@@ -429,7 +429,7 @@
|
|
|
429
429
|
{#if open && menu}
|
|
430
430
|
<div bind:this={menuEl} id={listboxId} class="fi__menu" role="listbox" aria-label={kindLabel[menu.kind]}>
|
|
431
431
|
<div class="fi__menu-head">{kindLabel[menu.kind]}</div>
|
|
432
|
-
{#each menu.items as item, i (item.insert
|
|
432
|
+
{#each menu.items as item, i (`${i}:${item.insert}`)}
|
|
433
433
|
<button
|
|
434
434
|
type="button"
|
|
435
435
|
id="{uid}-opt-{i}"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
export interface MenuItem {
|
|
3
3
|
label: string;
|
|
4
|
+
/** Stable row identity; labels may repeat. */
|
|
5
|
+
id?: string;
|
|
4
6
|
/** Required for button rows; a `control` row's hosted control acts on its own. */
|
|
5
7
|
onselect?: () => void;
|
|
6
8
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
@@ -202,7 +204,7 @@
|
|
|
202
204
|
>
|
|
203
205
|
{#snippet children({ close }: { close: () => void })}
|
|
204
206
|
<div bind:this={listEl} role="none" class="menu" data-tsu="Menu" tabindex="-1" {onkeydown}>
|
|
205
|
-
{#each items as item (item.label)}
|
|
207
|
+
{#each items as item, i (item.id ?? `${i}:${item.label}`)}
|
|
206
208
|
{#if item.control}
|
|
207
209
|
<div {...item.attrs as HTMLAttributes<HTMLDivElement>} role="none" class="menu-item menu-row" class:danger={item.danger} inert={item.disabled}>
|
|
208
210
|
{#if item.icon}<Icon name={item.icon} />{/if}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export interface MenuItem {
|
|
2
2
|
label: string;
|
|
3
|
+
/** Stable row identity; labels may repeat. */
|
|
4
|
+
id?: string;
|
|
3
5
|
/** Required for button rows; a `control` row's hosted control acts on its own. */
|
|
4
6
|
onselect?: () => void;
|
|
5
7
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
@@ -390,7 +390,7 @@
|
|
|
390
390
|
{/if}
|
|
391
391
|
<div id={menuId} role="menu" aria-label={label} class="radial-menu">
|
|
392
392
|
{#if open}
|
|
393
|
-
{#each petals as item, i (item.label)}
|
|
393
|
+
{#each petals as item, i (item.id ?? `${i}:${item.label}`)}
|
|
394
394
|
{@const p = points[i]}
|
|
395
395
|
{#if p}
|
|
396
396
|
{@const glyphs = menuItemGlyphs(item)}
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
onchange={(e) => onchange((e.currentTarget as HTMLSelectElement).value)}
|
|
93
93
|
>
|
|
94
94
|
{#if groups}
|
|
95
|
-
{#each groups as g (g.label)}
|
|
95
|
+
{#each groups as g, i (`${i}:${g.label}`)}
|
|
96
96
|
<optgroup label={g.label}>
|
|
97
97
|
{#each g.options as o (o.value)}
|
|
98
98
|
<option value={o.value}>{o.label}</option>
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type { SelectOption } from './components/atoms/Select.svelte';
|
|
|
25
25
|
export { default as Select } from './components/atoms/Select.svelte';
|
|
26
26
|
export { default as Skeleton } from './components/atoms/Skeleton.svelte';
|
|
27
27
|
export { default as Slider } from './components/atoms/Slider.svelte';
|
|
28
|
+
export type { SpinnerVariant } from './components/atoms/Spinner.svelte';
|
|
28
29
|
export { default as Spinner } from './components/atoms/Spinner.svelte';
|
|
29
30
|
export { default as Swatch } from './components/atoms/Swatch.svelte';
|
|
30
31
|
export { default as Switch } from './components/atoms/Switch.svelte';
|
|
@@ -114,7 +115,9 @@ export * as filterQuery from './query';
|
|
|
114
115
|
export { type AndNode, activeToken, compilePredicate, defaultOperator, type ExprNode, type FieldDef, type FieldType, type FilterNode, filters, findField, freeText, type LeafNode, type NotNode, OPERATORS, type Operator, type OperatorId, type OrNode, operatorByCode, operatorById, operatorsFor, parse, type Query, type QueryNode, resolveValues, type Schema, type Suggestion, type SuggestKind, type SuggestState, serialize, serializeFilter, singleQuery, suggest, suggestSingle, type TextNode, toSql, type ValueContext, type ValueOption, type ValueProvider, walk, } from './query';
|
|
115
116
|
export { type OptionSection, sectionOptions } from './select-options';
|
|
116
117
|
export type { ControlSize } from './size';
|
|
118
|
+
export { playSound, type SoundMuted, type SoundName } from './sound';
|
|
117
119
|
export { fontScale, SCALE_LEVELS, type ScaleLevel } from './stores/fontscale.svelte';
|
|
120
|
+
export { sound } from './stores/sound.svelte';
|
|
118
121
|
export { type Mode, THEMES, theme } from './stores/theme.svelte';
|
|
119
122
|
export { type Toast, type ToastAction, type ToastOptions, type ToastTone, type ToastToneInput, toasts, } from './stores/toast.svelte';
|
|
120
123
|
export { AUTO_THEME, chooseTheme, DEFAULT_THEME_PREFERENCE, pickerValue, preferenceFrom, resolveTheme, type SlotOf, type ThemeChoice, type ThemePreference, type ThemeSlot, } from './theme-mode';
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Styles are NOT auto-imported (so consumers control bundling). Import once at
|
|
4
4
|
// your app root:
|
|
5
|
-
// import '
|
|
5
|
+
// import './styles/app.css';
|
|
6
6
|
// then use the components below.
|
|
7
7
|
export { artworkGradient, artworkHue, initials } from './artwork';
|
|
8
8
|
export { autoresize } from './autoresize';
|
|
@@ -123,8 +123,10 @@ export { describeMediaTime, formatMediaTime, isLiveDuration } from './media-time
|
|
|
123
123
|
export * as filterQuery from './query';
|
|
124
124
|
export { activeToken, compilePredicate, defaultOperator, filters, findField, freeText, OPERATORS, operatorByCode, operatorById, operatorsFor, parse, resolveValues, serialize, serializeFilter, singleQuery, suggest, suggestSingle, toSql, walk, } from './query';
|
|
125
125
|
export { sectionOptions } from './select-options';
|
|
126
|
+
export { playSound } from './sound';
|
|
126
127
|
export { fontScale, SCALE_LEVELS } from './stores/fontscale.svelte';
|
|
127
128
|
// ---- stores / actions ----
|
|
129
|
+
export { sound } from './stores/sound.svelte';
|
|
128
130
|
export { THEMES, theme } from './stores/theme.svelte';
|
|
129
131
|
export { toasts, } from './stores/toast.svelte';
|
|
130
132
|
export { AUTO_THEME, chooseTheme, DEFAULT_THEME_PREFERENCE, pickerValue, preferenceFrom, resolveTheme, } from './theme-mode';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const PALETTE = {
|
|
2
|
+
click: [
|
|
3
|
+
['n', 3200, 0, 0, 0.012, 0.25],
|
|
4
|
+
['sine', 620, 380, 0, 0.04, 0.18],
|
|
5
|
+
],
|
|
6
|
+
'toggle-on': [
|
|
7
|
+
['n', 3000, 0, 0, 0.008, 0.3],
|
|
8
|
+
['triangle', 660, 880, 0.045, 0.05, 0.3],
|
|
9
|
+
],
|
|
10
|
+
'toggle-off': [
|
|
11
|
+
['n', 3000, 0, 0, 0.008, 0.3],
|
|
12
|
+
['triangle', 880, 620, 0.045, 0.05, 0.3],
|
|
13
|
+
],
|
|
14
|
+
open: [['sine', 420, 760, 0, 0.09, 0.25]],
|
|
15
|
+
close: [['sine', 700, 380, 0, 0.08, 0.25]],
|
|
16
|
+
success: [
|
|
17
|
+
['sine', 784, 784, 0, 0.07, 0.25],
|
|
18
|
+
['sine', 1175, 1175, 0.06, 0.1, 0.22],
|
|
19
|
+
],
|
|
20
|
+
error: [
|
|
21
|
+
['triangle', 330, 300, 0, 0.07, 0.28],
|
|
22
|
+
['triangle', 262, 240, 0.075, 0.1, 0.28],
|
|
23
|
+
],
|
|
24
|
+
tick: [['n', 5200, 0, 0, 0.006, 0.25]],
|
|
25
|
+
};
|
|
26
|
+
let ctx;
|
|
27
|
+
let out;
|
|
28
|
+
let noise;
|
|
29
|
+
function context() {
|
|
30
|
+
ctx ??= new AudioContext();
|
|
31
|
+
if (ctx.state === 'suspended')
|
|
32
|
+
ctx.resume().catch(() => { });
|
|
33
|
+
return ctx;
|
|
34
|
+
}
|
|
35
|
+
// Call synchronously inside a user gesture so Safari lets the context start.
|
|
36
|
+
export function unlock() {
|
|
37
|
+
context();
|
|
38
|
+
}
|
|
39
|
+
export function play(name, volume) {
|
|
40
|
+
const voices = PALETTE[name];
|
|
41
|
+
if (!voices)
|
|
42
|
+
return;
|
|
43
|
+
const c = context();
|
|
44
|
+
if (!out) {
|
|
45
|
+
out = c.createGain();
|
|
46
|
+
out.connect(c.destination);
|
|
47
|
+
}
|
|
48
|
+
out.gain.value = volume;
|
|
49
|
+
const t = c.currentTime + 0.005;
|
|
50
|
+
for (const [wave, f0, f1, at, d, g] of voices) {
|
|
51
|
+
const env = c.createGain();
|
|
52
|
+
const s = t + at;
|
|
53
|
+
env.gain.setValueAtTime(0.0001, s);
|
|
54
|
+
env.gain.exponentialRampToValueAtTime(g, s + 0.003);
|
|
55
|
+
env.gain.exponentialRampToValueAtTime(0.0001, s + d);
|
|
56
|
+
env.connect(out);
|
|
57
|
+
let src;
|
|
58
|
+
if (wave === 'n') {
|
|
59
|
+
if (!noise) {
|
|
60
|
+
noise = c.createBuffer(1, c.sampleRate * 0.05, c.sampleRate);
|
|
61
|
+
const data = noise.getChannelData(0);
|
|
62
|
+
for (let i = 0; i < data.length; i++)
|
|
63
|
+
data[i] = Math.random() * 2 - 1;
|
|
64
|
+
}
|
|
65
|
+
const buf = c.createBufferSource();
|
|
66
|
+
buf.buffer = noise;
|
|
67
|
+
const bp = c.createBiquadFilter();
|
|
68
|
+
bp.type = 'bandpass';
|
|
69
|
+
bp.frequency.value = f0;
|
|
70
|
+
bp.Q.value = 1.2;
|
|
71
|
+
buf.connect(bp).connect(env);
|
|
72
|
+
src = buf;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const osc = c.createOscillator();
|
|
76
|
+
osc.type = wave;
|
|
77
|
+
osc.frequency.setValueAtTime(f0, s);
|
|
78
|
+
osc.frequency.exponentialRampToValueAtTime(f1, s + d);
|
|
79
|
+
osc.connect(env);
|
|
80
|
+
src = osc;
|
|
81
|
+
}
|
|
82
|
+
src.start(s);
|
|
83
|
+
src.stop(s + d + 0.02);
|
|
84
|
+
src.onended = () => env.disconnect();
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/sound.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { playSound, type SoundMuted, type SoundName } from './stores/sound.svelte';
|
package/dist/sound.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type SoundName = 'click' | 'toggle-on' | 'toggle-off' | 'open' | 'close' | 'success' | 'error' | 'tick';
|
|
2
|
+
export type SoundMuted = 'off' | 'reduced-motion' | null;
|
|
3
|
+
/**
|
|
4
|
+
* Play a UI sound if the user enabled sounds. `allow` is the per-component
|
|
5
|
+
* opt-out (`sound={false}`). Safe to call during SSR: it is a no-op there.
|
|
6
|
+
*/
|
|
7
|
+
export declare function playSound(name: SoundName, allow?: boolean | undefined): void;
|
|
8
|
+
declare class Sound {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
volume: number;
|
|
11
|
+
constructor();
|
|
12
|
+
/** Why nothing would play right now, for a settings hint. */
|
|
13
|
+
get muted(): SoundMuted;
|
|
14
|
+
setEnabled(on: boolean): void;
|
|
15
|
+
setVolume(v: number): void;
|
|
16
|
+
play(name: SoundName): void;
|
|
17
|
+
private save;
|
|
18
|
+
}
|
|
19
|
+
export declare const sound: Sound;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { browser } from '../env';
|
|
2
|
+
import { readStorage, writeStorage } from './storage';
|
|
3
|
+
// OPT-IN UI sound effects. Off by default and persisted; while off nothing is
|
|
4
|
+
// imported, no AudioContext exists and no listener is attached. The synth is a
|
|
5
|
+
// separate chunk loaded on first play (or eagerly when enabling, so a gesture
|
|
6
|
+
// listener can unlock audio for Safari).
|
|
7
|
+
//
|
|
8
|
+
// With `prefers-reduced-motion: reduce` only the feedback sounds (success,
|
|
9
|
+
// error) play: they carry information, the decorative ones are muted.
|
|
10
|
+
// Calls inside one frame are coalesced and the highest priority wins
|
|
11
|
+
// (error > success > open/close > toggle > click > tick); the same sound plays
|
|
12
|
+
// at most once per 50 ms (tick: 40 ms). Hidden tabs are silent.
|
|
13
|
+
const KEY = 'tsumikit-sound';
|
|
14
|
+
const DEFAULT_VOLUME = 0.5;
|
|
15
|
+
// Lowest to highest priority; the last two are feedback and survive reduced motion.
|
|
16
|
+
const ORDER = [
|
|
17
|
+
'tick',
|
|
18
|
+
'click',
|
|
19
|
+
'toggle-on',
|
|
20
|
+
'toggle-off',
|
|
21
|
+
'open',
|
|
22
|
+
'close',
|
|
23
|
+
'success',
|
|
24
|
+
'error',
|
|
25
|
+
];
|
|
26
|
+
const GAP_MS = 50;
|
|
27
|
+
const TICK_GAP_MS = 40;
|
|
28
|
+
let synth = null;
|
|
29
|
+
let loading = null;
|
|
30
|
+
let armed = false;
|
|
31
|
+
let pending = null;
|
|
32
|
+
const last = new Map();
|
|
33
|
+
function clamp(v, fallback) {
|
|
34
|
+
return Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : fallback;
|
|
35
|
+
}
|
|
36
|
+
function reducedMotion() {
|
|
37
|
+
return !!globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
38
|
+
}
|
|
39
|
+
function load() {
|
|
40
|
+
loading ??= import('../sound-synth').then((m) => (synth = m));
|
|
41
|
+
return loading;
|
|
42
|
+
}
|
|
43
|
+
function unlock() {
|
|
44
|
+
if (!synth)
|
|
45
|
+
return;
|
|
46
|
+
synth.unlock();
|
|
47
|
+
listen(false);
|
|
48
|
+
}
|
|
49
|
+
function listen(on) {
|
|
50
|
+
if (armed === on)
|
|
51
|
+
return;
|
|
52
|
+
armed = on;
|
|
53
|
+
for (const type of ['pointerdown', 'keydown']) {
|
|
54
|
+
if (on)
|
|
55
|
+
addEventListener(type, unlock, { capture: true });
|
|
56
|
+
else
|
|
57
|
+
removeEventListener(type, unlock, { capture: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function flush() {
|
|
61
|
+
const name = pending;
|
|
62
|
+
pending = null;
|
|
63
|
+
if (!name || !sound.enabled)
|
|
64
|
+
return;
|
|
65
|
+
const now = performance.now();
|
|
66
|
+
if (now - (last.get(name) ?? -1e9) < (name === 'tick' ? TICK_GAP_MS : GAP_MS))
|
|
67
|
+
return;
|
|
68
|
+
last.set(name, now);
|
|
69
|
+
const volume = sound.volume;
|
|
70
|
+
if (synth)
|
|
71
|
+
synth.play(name, volume);
|
|
72
|
+
else
|
|
73
|
+
load().then((m) => m.play(name, volume), () => { });
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Play a UI sound if the user enabled sounds. `allow` is the per-component
|
|
77
|
+
* opt-out (`sound={false}`). Safe to call during SSR: it is a no-op there.
|
|
78
|
+
*/
|
|
79
|
+
export function playSound(name, allow = true) {
|
|
80
|
+
if (!browser || allow === false || !sound.enabled || sound.volume <= 0)
|
|
81
|
+
return;
|
|
82
|
+
if (document.hidden)
|
|
83
|
+
return;
|
|
84
|
+
if (navigator.userActivation && !navigator.userActivation.hasBeenActive)
|
|
85
|
+
return;
|
|
86
|
+
const rank = ORDER.indexOf(name);
|
|
87
|
+
if (rank < ORDER.length - 2 && reducedMotion())
|
|
88
|
+
return;
|
|
89
|
+
if (pending === null) {
|
|
90
|
+
requestAnimationFrame(flush);
|
|
91
|
+
pending = name;
|
|
92
|
+
}
|
|
93
|
+
else if (rank > ORDER.indexOf(pending))
|
|
94
|
+
pending = name;
|
|
95
|
+
}
|
|
96
|
+
class Sound {
|
|
97
|
+
enabled = $state(false);
|
|
98
|
+
volume = $state(DEFAULT_VOLUME);
|
|
99
|
+
constructor() {
|
|
100
|
+
if (!browser)
|
|
101
|
+
return;
|
|
102
|
+
const raw = readStorage(KEY);
|
|
103
|
+
if (raw) {
|
|
104
|
+
try {
|
|
105
|
+
const saved = JSON.parse(raw);
|
|
106
|
+
if (typeof saved?.enabled === 'boolean')
|
|
107
|
+
this.enabled = saved.enabled;
|
|
108
|
+
if (typeof saved?.volume === 'number')
|
|
109
|
+
this.volume = clamp(saved.volume, DEFAULT_VOLUME);
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
}
|
|
113
|
+
if (this.enabled) {
|
|
114
|
+
load().catch(() => { });
|
|
115
|
+
listen(true);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Why nothing would play right now, for a settings hint. */
|
|
119
|
+
get muted() {
|
|
120
|
+
if (!this.enabled)
|
|
121
|
+
return 'off';
|
|
122
|
+
return reducedMotion() ? 'reduced-motion' : null;
|
|
123
|
+
}
|
|
124
|
+
setEnabled(on) {
|
|
125
|
+
this.enabled = on;
|
|
126
|
+
this.save();
|
|
127
|
+
if (!browser)
|
|
128
|
+
return;
|
|
129
|
+
if (on) {
|
|
130
|
+
load().catch(() => { });
|
|
131
|
+
listen(true);
|
|
132
|
+
unlock();
|
|
133
|
+
}
|
|
134
|
+
else
|
|
135
|
+
listen(false);
|
|
136
|
+
}
|
|
137
|
+
setVolume(v) {
|
|
138
|
+
this.volume = clamp(v, this.volume);
|
|
139
|
+
this.save();
|
|
140
|
+
}
|
|
141
|
+
play(name) {
|
|
142
|
+
playSound(name);
|
|
143
|
+
}
|
|
144
|
+
save() {
|
|
145
|
+
if (!browser)
|
|
146
|
+
return;
|
|
147
|
+
writeStorage(KEY, JSON.stringify({ enabled: this.enabled, volume: this.volume }));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export const sound = new Sound();
|
package/dist/styles/reset.css
CHANGED
|
@@ -106,14 +106,31 @@ pre {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/* Reduced motion: fewer and gentler, not zero. Movement tokens go neutral and
|
|
110
|
+
transitions are limited to colour/opacity, so feedback still fades while
|
|
111
|
+
nothing moves. */
|
|
109
112
|
@media (prefers-reduced-motion: reduce) {
|
|
113
|
+
:root {
|
|
114
|
+
--press-scale: 1;
|
|
115
|
+
--press-scale-sm: 1;
|
|
116
|
+
--press-scale-lg: 1;
|
|
117
|
+
--enter-scale: 1;
|
|
118
|
+
--tip-scale: 1;
|
|
119
|
+
--shift-xs: 0px;
|
|
120
|
+
--shift-sm: 0px;
|
|
121
|
+
--shift-md: 0px;
|
|
122
|
+
--ease-spring: var(--ease-out);
|
|
123
|
+
--ease-spring-bounce: var(--ease-out);
|
|
124
|
+
}
|
|
110
125
|
*,
|
|
111
126
|
*::before,
|
|
112
127
|
*::after {
|
|
128
|
+
scroll-behavior: auto !important;
|
|
113
129
|
animation-duration: 0.01ms !important;
|
|
114
130
|
animation-iteration-count: 1 !important;
|
|
115
|
-
transition-
|
|
116
|
-
|
|
131
|
+
transition-property: opacity, color, background-color, border-color, outline-color, box-shadow, fill, stroke, text-decoration-color, visibility, display, overlay !important;
|
|
132
|
+
transition-duration: var(--dur-fast) !important;
|
|
133
|
+
transition-delay: 0ms !important;
|
|
117
134
|
}
|
|
118
135
|
}
|
|
119
136
|
|
package/dist/styles/tokens.css
CHANGED
|
@@ -123,6 +123,9 @@
|
|
|
123
123
|
ballooning UI. Default 1 = no scaling. */
|
|
124
124
|
--fs-scale: 1;
|
|
125
125
|
|
|
126
|
+
/* Type scale. 2xs is the floor (never go below 11px). 13 / 15 / 16 are
|
|
127
|
+
"UI small / UI / body": no more steps between them. */
|
|
128
|
+
--fs-2xs: calc(0.6875rem * var(--fs-scale)); /* 11 */
|
|
126
129
|
--fs-xs: calc(0.75rem * var(--fs-scale)); /* 12 */
|
|
127
130
|
--fs-sm: calc(0.8125rem * var(--fs-scale)); /* 13 */
|
|
128
131
|
--fs-base: calc(0.9375rem * var(--fs-scale)); /* 15 */
|
|
@@ -130,18 +133,28 @@
|
|
|
130
133
|
--fs-lg: calc(1.125rem * var(--fs-scale)); /* 18 */
|
|
131
134
|
--fs-xl: calc(1.375rem * var(--fs-scale)); /* 22 */
|
|
132
135
|
--fs-2xl: calc(1.75rem * var(--fs-scale)); /* 28 */
|
|
136
|
+
--fs-3xl: calc(2.25rem * var(--fs-scale)); /* 36 */
|
|
133
137
|
|
|
134
138
|
--fw-normal: 400;
|
|
135
139
|
--fw-medium: 500;
|
|
136
140
|
--fw-semibold: 600;
|
|
137
141
|
--fw-bold: 700;
|
|
138
142
|
|
|
143
|
+
/* Leading paired to the scale: none = single-line controls and glyphs,
|
|
144
|
+
snug = chips / captions / tooltips (2xs–sm), tight = fields and labels
|
|
145
|
+
(sm–md), normal = body prose (md–lg), heading = xl and up. */
|
|
146
|
+
--lh-none: 1;
|
|
147
|
+
--lh-heading: 1.2;
|
|
139
148
|
--lh-tight: 1.25;
|
|
149
|
+
--lh-snug: 1.375;
|
|
140
150
|
--lh-normal: 1.5;
|
|
141
151
|
|
|
142
152
|
/* ---- Spacing (4px base) ---- */
|
|
143
153
|
--sp-0: 0;
|
|
154
|
+
--sp-px: 1px;
|
|
155
|
+
--sp-0-5: 0.125rem;
|
|
144
156
|
--sp-1: 0.25rem;
|
|
157
|
+
--sp-1-5: 0.375rem;
|
|
145
158
|
--sp-2: 0.5rem;
|
|
146
159
|
--sp-3: 0.75rem;
|
|
147
160
|
--sp-4: 1rem;
|
|
@@ -152,6 +165,8 @@
|
|
|
152
165
|
--sp-12: 3rem;
|
|
153
166
|
|
|
154
167
|
/* ---- Radii ---- */
|
|
168
|
+
/* Nested corners stay concentric: inner = outer - (border + padding). */
|
|
169
|
+
--r-xs: 2px;
|
|
155
170
|
--r-sm: 6px;
|
|
156
171
|
--r-md: 10px;
|
|
157
172
|
--r-lg: 14px;
|
|
@@ -170,16 +185,46 @@
|
|
|
170
185
|
--control-height-default: 2.5rem;
|
|
171
186
|
--control-height-large: 3rem;
|
|
172
187
|
--control-height: 2.75rem;
|
|
173
|
-
/*
|
|
188
|
+
/* Per-tier control contract (sm 32 / md 40 / lg 48): height, label size,
|
|
189
|
+
side padding, icon size. Text buttons get roomier sides via --btn-px-*. */
|
|
190
|
+
--control-sm: var(--control-height-compact);
|
|
191
|
+
--control-md: var(--control-height-default);
|
|
192
|
+
--control-lg: var(--control-height-large);
|
|
193
|
+
--control-fs-sm: var(--fs-xs);
|
|
194
|
+
--control-fs-md: var(--fs-sm);
|
|
195
|
+
--control-fs-lg: var(--fs-base);
|
|
196
|
+
--control-px-sm: var(--sp-2);
|
|
197
|
+
--control-px-md: var(--sp-3);
|
|
198
|
+
--control-px-lg: var(--sp-4);
|
|
199
|
+
--control-icon-sm: var(--icon-sm);
|
|
200
|
+
--control-icon-md: var(--icon-md);
|
|
201
|
+
--control-icon-lg: var(--icon-lg);
|
|
202
|
+
--btn-px-sm: var(--sp-3);
|
|
203
|
+
--btn-px-md: var(--sp-4);
|
|
204
|
+
--btn-px-lg: var(--sp-5);
|
|
205
|
+
/* Chip-class heights (Badge, Switch track, Checkbox box) on the 4px grid. */
|
|
206
|
+
--chip-xs: 1rem;
|
|
207
|
+
--chip-sm: 1.25rem;
|
|
208
|
+
--chip-md: 1.5rem;
|
|
209
|
+
/* Icon glyph sizes follow the text scale, not the chrome. */
|
|
210
|
+
--icon-xs: calc(0.75rem * var(--fs-scale));
|
|
211
|
+
--icon-sm: calc(0.875rem * var(--fs-scale));
|
|
212
|
+
--icon-md: calc(1rem * var(--fs-scale));
|
|
213
|
+
--icon-lg: calc(1.25rem * var(--fs-scale));
|
|
214
|
+
--icon-xl: calc(1.5rem * var(--fs-scale));
|
|
215
|
+
--disabled-opacity: 0.45;
|
|
216
|
+
/* Keyboard focus indicator shared by every interactive atom. `-inset` is for
|
|
217
|
+
rows and clipped containers that cannot draw outside their box. */
|
|
218
|
+
--focus-ring-width: 2px;
|
|
174
219
|
--focus-ring: 2px solid var(--accent);
|
|
175
|
-
--focus-ring-offset:
|
|
220
|
+
--focus-ring-offset: 2px;
|
|
221
|
+
--focus-ring-offset-inset: calc(-1 * var(--focus-ring-width));
|
|
176
222
|
/* Square box scale for icon-only controls (IconButton, Button `box`/`square`,
|
|
177
|
-
SelectButton, Popover trigger, CopyButton, FileButton). md
|
|
178
|
-
|
|
179
|
-
lines up with text controls in the same row. */
|
|
223
|
+
SelectButton, Popover trigger, CopyButton, FileButton). sm/md/lg match the
|
|
224
|
+
control heights so a boxed control lines up with text controls in a row. */
|
|
180
225
|
--box-xs: 1.5rem;
|
|
181
226
|
--box-sm: var(--control-height-compact);
|
|
182
|
-
--box-md: 2.
|
|
227
|
+
--box-md: 2.5rem;
|
|
183
228
|
--box-lg: var(--control-height-default);
|
|
184
229
|
/* Minimum hit area on coarse pointers (WCAG 2.5.8); px so it never shrinks
|
|
185
230
|
under a small root font-size. */
|
|
@@ -212,5 +257,43 @@
|
|
|
212
257
|
--safe-left: env(safe-area-inset-left, 0px);
|
|
213
258
|
--safe-right: env(safe-area-inset-right, 0px);
|
|
214
259
|
|
|
260
|
+
/* ---- Motion ---- */
|
|
215
261
|
--ease: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
262
|
+
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
263
|
+
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
|
264
|
+
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
|
|
265
|
+
--ease-spring: var(--ease-out);
|
|
266
|
+
--ease-spring-bounce: cubic-bezier(0.34, 1.36, 0.64, 1);
|
|
267
|
+
|
|
268
|
+
--dur-instant: 0ms;
|
|
269
|
+
--dur-press: 100ms;
|
|
270
|
+
--dur-fast: 120ms;
|
|
271
|
+
--dur-base: 180ms;
|
|
272
|
+
--dur-slow: 240ms;
|
|
273
|
+
--dur-sheet: 280ms;
|
|
274
|
+
--dur-exit-fast: 80ms;
|
|
275
|
+
--dur-exit: 140ms;
|
|
276
|
+
--dur-exit-slow: 200ms;
|
|
277
|
+
--dur-spring: 250ms;
|
|
278
|
+
--dur-spring-bounce: 330ms;
|
|
279
|
+
|
|
280
|
+
--press-scale: 0.97;
|
|
281
|
+
--press-scale-sm: 0.92;
|
|
282
|
+
--press-scale-lg: 0.985;
|
|
283
|
+
--enter-scale: 0.96;
|
|
284
|
+
--tip-scale: 0.97;
|
|
285
|
+
--shift-xs: 2px;
|
|
286
|
+
--shift-sm: 4px;
|
|
287
|
+
--shift-md: 8px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@supports (animation-timing-function: linear(0, 1)) {
|
|
291
|
+
:root {
|
|
292
|
+
/* stiffness 1200, damping 56, mass 1: zeta 0.81, 1.1% overshoot, settles 250ms */
|
|
293
|
+
/* prettier-ignore */
|
|
294
|
+
--ease-spring: linear(0, 0.007 1.2%, 0.023 2.4%, 0.055 4%, 0.098 5.6%, 0.188 8.4%, 0.53 18%, 0.629 21.2%, 0.704 24%, 0.778 27.2%, 0.839 30.4%, 0.887 33.6%, 0.928 37.2%, 0.959 40.8%, 0.982 44.8%, 0.998 49.2%, 1.007 54.4%, 1.011 64.4%, 1);
|
|
295
|
+
/* stiffness 1000, damping 40, mass 1: zeta 0.63, 7.4% overshoot, settles 330ms */
|
|
296
|
+
/* prettier-ignore */
|
|
297
|
+
--ease-spring-bounce: linear(0, 0.01 1.2%, 0.033 2.4%, 0.067 3.6%, 0.122 5.2%, 0.244 7.9%, 0.502 13%, 0.618 15.5%, 0.735 18.2%, 0.824 20.6%, 0.898 23%, 0.957 25.5%, 1.007 28.2%, 1.041 30.9%, 1.067 34.8%, 1.073 39.4%, 1.064 44.2%, 1.016 58.2%, 1 66.4%, 0.995 77.3%, 1);
|
|
298
|
+
}
|
|
216
299
|
}
|
package/package.json
CHANGED