@crievo/omni 0.1.2 → 0.1.4
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/docs/onboarding.md +69 -161
- package/omnivideo.config.example.json +1 -1
- package/package.json +2 -1
- package/reference/learnings.md +12 -0
- package/reference/omnichat.md +5 -4
- package/reference/publish.md +9 -9
- package/reference/studio.md +1 -1
- package/studio/api/app.ts +2 -2
- package/studio/api/routes/sound.ts +22 -3
- package/studio/api/services/onboarding.ts +242 -21
- package/studio/api/services/settings.ts +4 -2
- package/studio/api/services/sound.ts +45 -0
- package/studio/shared/types/api.ts +26 -1
- package/studio/studio.ts +1 -0
- package/studio/web/components/AppShell.tsx +3 -1
- package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +3 -0
- package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +37 -20
- package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +39 -12
- package/studio/web/components/MissingDmsPanel/BatchSend.tsx +13 -4
- package/studio/web/components/Omni/OmniWidget.tsx +14 -5
- package/studio/web/components/Omni/PendingActions.tsx +25 -6
- package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +13 -3
- package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +14 -4
- package/studio/web/components/ResultMessage.tsx +12 -3
- package/studio/web/components/Sidebar.tsx +63 -24
- package/studio/web/components/StageStepper.tsx +105 -34
- package/studio/web/components/Toaster.tsx +25 -0
- package/studio/web/components/VoiceCarousel.tsx +19 -16
- package/studio/web/components/VoiceProfilesSettings.tsx +8 -3
- package/studio/web/components/bencho/ActionNode.tsx +456 -0
- package/studio/web/components/bencho/AssigneePicker.tsx +246 -0
- package/studio/web/components/bencho/BenchoScope.tsx +8 -0
- package/studio/web/components/bencho/Checklist.tsx +421 -0
- package/studio/web/components/bencho/CreateMenu.tsx +258 -0
- package/studio/web/components/bencho/GenerateImage.tsx +290 -0
- package/studio/web/components/bencho/ImageAccordion.tsx +273 -0
- package/studio/web/components/bencho/LiquidToggle.tsx +289 -0
- package/studio/web/components/bencho/MagnifyDock.tsx +200 -0
- package/studio/web/components/bencho/NowPlaying.tsx +403 -0
- package/studio/web/components/bencho/OneTimeCode.tsx +374 -0
- package/studio/web/components/bencho/PullToRefresh.tsx +527 -0
- package/studio/web/components/bencho/SlideToConfirm.tsx +385 -0
- package/studio/web/components/bencho/SloshSlider.tsx +394 -0
- package/studio/web/components/bencho/StackedBars.tsx +342 -0
- package/studio/web/components/bencho/ToastStack.tsx +263 -0
- package/studio/web/components/bencho/UploadDropzone.tsx +394 -0
- package/studio/web/components/bencho/VoiceNote.tsx +705 -0
- package/studio/web/components/icons.tsx +27 -0
- package/studio/web/components/ui/slider.tsx +34 -1
- package/studio/web/components/ui/switch.tsx +18 -5
- package/studio/web/hooks/useOnboarding.ts +24 -6
- package/studio/web/index.html +1 -1
- package/studio/web/lib/alerts.ts +10 -0
- package/studio/web/lib/omni-panel.ts +4 -1
- package/studio/web/lib/toast.ts +196 -0
- package/studio/web/lib/upload.ts +24 -0
- package/studio/web/pages/BenchoPage/index.tsx +74 -0
- package/studio/web/pages/BenchoPage/registry.ts +174 -0
- package/studio/web/pages/GettingStartedPage/components/ComputerCard.tsx +102 -0
- package/studio/web/pages/GettingStartedPage/index.tsx +84 -58
- package/studio/web/pages/NewVideoPage/index.tsx +6 -2
- package/studio/web/pages/ReferencesPage/index.tsx +4 -1
- package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +30 -3
- package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +14 -17
- package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +1 -1
- package/studio/web/pages/VideoPage/components/PublishForm.tsx +24 -17
- package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +2 -1
- package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +12 -14
- package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +27 -11
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +22 -18
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +3 -4
- package/studio/web/pages/VideoPage/components/SoundTimeline/UploadTrack.tsx +59 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +1 -0
- package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +4 -6
- package/studio/web/pages/VideoPage/index.tsx +4 -0
- package/studio/web/pages/VideosPage/components/VideoRow.tsx +2 -4
- package/studio/web/router.tsx +8 -0
- package/studio/web/styles/components/bencho/action-node.css +227 -0
- package/studio/web/styles/components/bencho/checklist.css +182 -0
- package/studio/web/styles/components/bencho/dock.css +188 -0
- package/studio/web/styles/components/bencho/gallery.css +109 -0
- package/studio/web/styles/components/bencho/generate.css +151 -0
- package/studio/web/styles/components/bencho/image-accordion.css +37 -0
- package/studio/web/styles/components/bencho/liq-create.css +279 -0
- package/studio/web/styles/components/bencho/liq-toggle.css +106 -0
- package/studio/web/styles/components/bencho/one-time-code.css +148 -0
- package/studio/web/styles/components/bencho/picker.css +204 -0
- package/studio/web/styles/components/bencho/pull.css +313 -0
- package/studio/web/styles/components/bencho/scope.css +42 -0
- package/studio/web/styles/components/bencho/slide-confirm.css +139 -0
- package/studio/web/styles/components/bencho/slosh.css +137 -0
- package/studio/web/styles/components/bencho/sound.css +260 -0
- package/studio/web/styles/components/bencho/stacked-bars.css +334 -0
- package/studio/web/styles/components/bencho/toast-stack.css +192 -0
- package/studio/web/styles/components/bencho/tokens.css +243 -0
- package/studio/web/styles/components/bencho/upload-dropzone.css +352 -0
- package/studio/web/styles/components/bencho/voice-note.css +337 -0
- package/studio/web/styles/components/clone-voice.css +12 -2
- package/studio/web/styles/components/omni.css +6 -1
- package/studio/web/styles/components/stepper.css +123 -7
- package/studio/web/styles/components/toaster.css +10 -0
- package/studio/web/styles/components/ui/slider.css +6 -16
- package/studio/web/styles/components/ui/switch.css +2 -41
- package/studio/web/styles/components/voice-carousel.css +46 -4
- package/studio/web/styles/index.css +23 -0
- package/studio/web/styles/pages/getting-started.css +46 -0
- package/studio/web/styles/pages/settings/shell.css +4 -0
- package/studio/web/styles/pages/video/layout.css +1 -4
- package/studio/web/styles/pages/video/sound-timeline.css +8 -0
- package/studio/web/styles/theme/identity/shell.css +10 -26
- package/tools/lib/agents.mjs +24 -13
- package/tools/lib/claude-settings.mjs +32 -3
- package/tools/lib/deps.mjs +24 -0
- package/tools/lib/platforms/instagram.mjs +1 -1
- package/tools/lib/up.mjs +5 -2
- package/tools/omni.mjs +7 -5
- package/tools/omnichat.mjs +5 -7
- package/tools/package.mjs +5 -3
- package/tools/publish.mjs +40 -137
- package/tools/reference.mjs +1 -1
- package/tools/setup.mjs +16 -15
- package/tools/sound.mjs +67 -3
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/* A slider whose value has mass. Ported from bencho.dev (MIT).
|
|
2
|
+
|
|
3
|
+
The handle is rigid: it is under your finger and anything under a finger that lags it is broken. The fill is a second
|
|
4
|
+
spring behind it, softer and barely damped, so a fast drag throws it past the handle, into the end stop, and rocks it
|
|
5
|
+
back two or three times before it settles. Release halfway and the handle coasts on the velocity it actually had.
|
|
6
|
+
The tilt is what sells it as liquid: a bar whose leading edge leans in the direction it is travelling is a surface
|
|
7
|
+
with a meniscus on it, where a bar that only lags is a laggy bar.
|
|
8
|
+
|
|
9
|
+
Nothing here goes through React. The loop writes width, a custom property and one attribute straight onto the
|
|
10
|
+
elements, and it stops the moment everything is at rest. */
|
|
11
|
+
|
|
12
|
+
import { type CSSProperties, type KeyboardEvent, type PointerEvent, type ReactNode, useEffect, useRef } from "react";
|
|
13
|
+
|
|
14
|
+
const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));
|
|
15
|
+
|
|
16
|
+
/* a preference, not a live input: read once */
|
|
17
|
+
const still = () => typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
18
|
+
|
|
19
|
+
/* The track's corner. 13 is what it was drawn at and 20 is half of its 40px height: the pill, as round as a 40px
|
|
20
|
+
track can be. The knob inside stays a circle: it is a ball in a slot. */
|
|
21
|
+
const SLO_CORNER = 20;
|
|
22
|
+
const SLO_MAX = 20;
|
|
23
|
+
|
|
24
|
+
/* decimals of a step (0.005 -> 3), so quantizing does not leave 0.30000000000000004 in what the caller reads */
|
|
25
|
+
const decimals = (n: number) => (String(n).split(".")[1] ?? "").length;
|
|
26
|
+
|
|
27
|
+
/* A mark within this share of the range pulls the reported value onto it, as the Radix slider here always has */
|
|
28
|
+
const SNAP = 0.03;
|
|
29
|
+
|
|
30
|
+
/* Gaps longer than this between the last pointer move and the release mean the finger had stopped: no coast */
|
|
31
|
+
const STALE_MS = 80;
|
|
32
|
+
|
|
33
|
+
export function SloshSlider({
|
|
34
|
+
/* 0 is a rigid fill, 100 is loose liquid. 15 is a little give rather than a wave in a glass. */
|
|
35
|
+
viscosity = 15,
|
|
36
|
+
/* how far it coasts after release, 0..100 */
|
|
37
|
+
momentum = 55,
|
|
38
|
+
/* lean on the leading edge, 0..100 */
|
|
39
|
+
tilt = 45,
|
|
40
|
+
/* the corner it is cut with */
|
|
41
|
+
corner = SLO_CORNER,
|
|
42
|
+
/* controlled use. Values are in the min..max scale and travel as a one-entry array, the way the Radix slider that
|
|
43
|
+
this replaces reports them. Without them it is the demo: 0..100, starting at 62, continuous, reporting nowhere. */
|
|
44
|
+
value,
|
|
45
|
+
defaultValue,
|
|
46
|
+
min = 0,
|
|
47
|
+
max = 100,
|
|
48
|
+
/* what the caller reads is quantized to this; the handle and the liquid stay smooth, and the handle settles onto the
|
|
49
|
+
quantized position once it is let go. Undefined is continuous. */
|
|
50
|
+
step,
|
|
51
|
+
disabled = false,
|
|
52
|
+
onValueChange,
|
|
53
|
+
/* on release, and again if the handle then coasts somewhere else */
|
|
54
|
+
onValueCommit,
|
|
55
|
+
/* text inside the track */
|
|
56
|
+
label,
|
|
57
|
+
/* a dashed line at this value, and with snapToMark the reported value sticks to it when the handle is close */
|
|
58
|
+
markValue,
|
|
59
|
+
snapToMark = false,
|
|
60
|
+
/* fills the width it is given instead of the demo's 320px */
|
|
61
|
+
fluid = false,
|
|
62
|
+
className,
|
|
63
|
+
id,
|
|
64
|
+
ariaLabel = "Fluxo",
|
|
65
|
+
ariaLabelledBy,
|
|
66
|
+
ariaDescribedBy,
|
|
67
|
+
...rest
|
|
68
|
+
}: {
|
|
69
|
+
viscosity?: number;
|
|
70
|
+
momentum?: number;
|
|
71
|
+
tilt?: number;
|
|
72
|
+
corner?: number;
|
|
73
|
+
value?: number[];
|
|
74
|
+
defaultValue?: number[];
|
|
75
|
+
min?: number;
|
|
76
|
+
max?: number;
|
|
77
|
+
step?: number;
|
|
78
|
+
disabled?: boolean;
|
|
79
|
+
onValueChange?: (value: number[]) => void;
|
|
80
|
+
onValueCommit?: (value: number[]) => void;
|
|
81
|
+
label?: ReactNode;
|
|
82
|
+
markValue?: number;
|
|
83
|
+
snapToMark?: boolean;
|
|
84
|
+
fluid?: boolean;
|
|
85
|
+
className?: string;
|
|
86
|
+
id?: string;
|
|
87
|
+
ariaLabel?: string;
|
|
88
|
+
ariaLabelledBy?: string;
|
|
89
|
+
ariaDescribedBy?: string;
|
|
90
|
+
/* data-* and the like, which the studio's slider wrapper puts on the root */
|
|
91
|
+
[attr: `data-${string}`]: string | undefined;
|
|
92
|
+
} = {}) {
|
|
93
|
+
const track = useRef<HTMLDivElement | null>(null);
|
|
94
|
+
const fillEl = useRef<HTMLDivElement | null>(null);
|
|
95
|
+
const knobEl = useRef<HTMLDivElement | null>(null);
|
|
96
|
+
const reduced = still();
|
|
97
|
+
|
|
98
|
+
/* what the caller has said, live: the loop and the handlers outlive any one render */
|
|
99
|
+
const props = useRef({ value: value?.[0], min, max, step, markValue, snapToMark, onValueChange, onValueCommit });
|
|
100
|
+
props.current = { value: value?.[0], min, max, step, markValue, snapToMark, onValueChange, onValueCommit };
|
|
101
|
+
|
|
102
|
+
/* the value as the caller reads it from a position on the track (0..100): the handle's true position, quantized and
|
|
103
|
+
snapped. Never the fill, which lags and overshoots on purpose. */
|
|
104
|
+
const report = (pos: number, snap = true) => {
|
|
105
|
+
const p = props.current;
|
|
106
|
+
const span = p.max - p.min;
|
|
107
|
+
let v = p.min + (pos / 100) * span;
|
|
108
|
+
if (p.step) v = Number((p.min + Math.round((v - p.min) / p.step) * p.step).toFixed(decimals(p.step)));
|
|
109
|
+
v = clamp(v, p.min, p.max);
|
|
110
|
+
if (snap && p.snapToMark && p.markValue !== undefined && Math.abs(v - p.markValue) <= span * SNAP)
|
|
111
|
+
return p.markValue;
|
|
112
|
+
return v;
|
|
113
|
+
};
|
|
114
|
+
const posOf = (v: number) => (max === min ? 0 : clamp(((v - min) / (max - min)) * 100, 0, 100));
|
|
115
|
+
|
|
116
|
+
/* the handle, its velocity after release, and the fill chasing it with a velocity of its own */
|
|
117
|
+
const start = value?.[0] ?? defaultValue?.[0] ?? (min === 0 && max === 100 ? 62 : min);
|
|
118
|
+
const val = useRef(posOf(start));
|
|
119
|
+
const hv = useRef(0);
|
|
120
|
+
const fill = useRef(posOf(start));
|
|
121
|
+
const fv = useRef(0);
|
|
122
|
+
const held = useRef(false);
|
|
123
|
+
const raf = useRef(0);
|
|
124
|
+
/* the last two samples, which is all a release velocity is */
|
|
125
|
+
const last = useRef({ v: val.current, t: 0 });
|
|
126
|
+
/* the last value the caller was told, so an echo of it from a controlled parent is not mistaken for a new one */
|
|
127
|
+
const told = useRef(start);
|
|
128
|
+
/* where the handle eases to after it is let go, so it sits on the value the caller holds and not between two steps */
|
|
129
|
+
const settle = useRef<number | null>(null);
|
|
130
|
+
/* a release that is still travelling has not committed yet */
|
|
131
|
+
const commitAtRest = useRef(false);
|
|
132
|
+
const range = useRef({ min, max });
|
|
133
|
+
|
|
134
|
+
/* live, so the loop reads what the panel is currently set to without restarting on every drag of a knob */
|
|
135
|
+
const knobs = useRef({ viscosity, momentum, tilt, reduced });
|
|
136
|
+
knobs.current = { viscosity, momentum, tilt, reduced };
|
|
137
|
+
|
|
138
|
+
/* the loop parks itself when everything is at rest, so every handler below needs a way to wake it back up */
|
|
139
|
+
const run = useRef<() => void>(() => {});
|
|
140
|
+
|
|
141
|
+
const emit = (snap = true) => {
|
|
142
|
+
const v = report(val.current, snap);
|
|
143
|
+
if (v === told.current) return;
|
|
144
|
+
told.current = v;
|
|
145
|
+
props.current.onValueChange?.([v]);
|
|
146
|
+
};
|
|
147
|
+
const commit = () => props.current.onValueCommit?.([told.current]);
|
|
148
|
+
const armSettle = () => {
|
|
149
|
+
if (!props.current.step) return;
|
|
150
|
+
const to = posOf(report(val.current));
|
|
151
|
+
settle.current = Math.abs(to - val.current) > 0.005 ? to : null;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const paint = () => {
|
|
156
|
+
const f = fillEl.current;
|
|
157
|
+
const k = knobEl.current;
|
|
158
|
+
const t = track.current;
|
|
159
|
+
if (!f || !k || !t) return;
|
|
160
|
+
f.style.width = `${fill.current.toFixed(2)}%`;
|
|
161
|
+
k.style.left = `${val.current.toFixed(2)}%`;
|
|
162
|
+
/* the meniscus: proportional to how fast the fill is actually moving, so it is upright at rest */
|
|
163
|
+
const lean = knobs.current.reduced ? 0 : clamp((knobs.current.tilt / 100) * fv.current * 3, -20, 20);
|
|
164
|
+
f.style.setProperty("--lean", `${lean.toFixed(2)}px`);
|
|
165
|
+
t.setAttribute("aria-valuenow", String(Math.round(report(val.current) * 100) / 100));
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const rest = () =>
|
|
169
|
+
!held.current &&
|
|
170
|
+
Math.abs(hv.current) < 0.01 &&
|
|
171
|
+
settle.current === null &&
|
|
172
|
+
Math.abs(fv.current) < 0.01 &&
|
|
173
|
+
Math.abs(val.current - fill.current) < 0.02;
|
|
174
|
+
|
|
175
|
+
let prev = 0;
|
|
176
|
+
const tick = (t: number) => {
|
|
177
|
+
const dt = prev ? clamp((t - prev) / 16.67, 0, 2.5) : 1;
|
|
178
|
+
prev = t;
|
|
179
|
+
const { viscosity: v, momentum: m, reduced: flat } = knobs.current;
|
|
180
|
+
|
|
181
|
+
/* the coast: friction is per frame, so it is raised to dt rather than multiplied by it, and a dropped frame
|
|
182
|
+
does not double the deceleration */
|
|
183
|
+
if (!held.current && hv.current) {
|
|
184
|
+
val.current += hv.current * dt;
|
|
185
|
+
hv.current *= (0.86 + (m / 100) * 0.115) ** dt;
|
|
186
|
+
if (val.current <= 0 || val.current >= 100) {
|
|
187
|
+
val.current = clamp(val.current, 0, 100);
|
|
188
|
+
hv.current = 0;
|
|
189
|
+
}
|
|
190
|
+
if (Math.abs(hv.current) < 0.01) hv.current = 0;
|
|
191
|
+
emit();
|
|
192
|
+
if (!hv.current) armSettle();
|
|
193
|
+
} else if (!held.current && settle.current !== null) {
|
|
194
|
+
val.current += (settle.current - val.current) * Math.min(1, 0.3 * dt);
|
|
195
|
+
if (Math.abs(settle.current - val.current) < 0.02) {
|
|
196
|
+
val.current = settle.current;
|
|
197
|
+
settle.current = null;
|
|
198
|
+
}
|
|
199
|
+
emit();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const soft = flat ? 0 : v / 100;
|
|
203
|
+
if (soft === 0) {
|
|
204
|
+
/* 0 is an ordinary slider and has to be exactly that, not a very stiff spring that still rings */
|
|
205
|
+
fill.current = val.current;
|
|
206
|
+
fv.current = 0;
|
|
207
|
+
} else {
|
|
208
|
+
/* stiffness falls and damping rises together: thin liquid is slow to answer and slow to forget, which is the
|
|
209
|
+
same thing said twice and is why these are one knob rather than two */
|
|
210
|
+
const stiff = 0.34 - soft * 0.29;
|
|
211
|
+
const damp = 0.74 + soft * 0.22;
|
|
212
|
+
fv.current += (val.current - fill.current) * stiff * dt;
|
|
213
|
+
fv.current *= damp ** dt;
|
|
214
|
+
fill.current += fv.current * dt;
|
|
215
|
+
/* the end stop is a wall, and a wall gives some back: without this the overshoot at the top of the track is
|
|
216
|
+
swallowed and the liquid stops behaving like liquid exactly where you are looking */
|
|
217
|
+
if (fill.current > 100) {
|
|
218
|
+
fill.current = 100;
|
|
219
|
+
fv.current = -fv.current * 0.42;
|
|
220
|
+
} else if (fill.current < 0) {
|
|
221
|
+
fill.current = 0;
|
|
222
|
+
fv.current = -fv.current * 0.42;
|
|
223
|
+
}
|
|
224
|
+
if (Math.abs(val.current - fill.current) < 0.02 && Math.abs(fv.current) < 0.02) {
|
|
225
|
+
fill.current = val.current;
|
|
226
|
+
fv.current = 0;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
paint();
|
|
231
|
+
if (rest()) {
|
|
232
|
+
raf.current = 0;
|
|
233
|
+
prev = 0;
|
|
234
|
+
if (commitAtRest.current) {
|
|
235
|
+
commitAtRest.current = false;
|
|
236
|
+
commit();
|
|
237
|
+
}
|
|
238
|
+
/* a parent that answered with something other than what it was told (a clamp, a refusal) has the last word */
|
|
239
|
+
const want = props.current.value;
|
|
240
|
+
if (want !== undefined && want !== told.current) {
|
|
241
|
+
told.current = want;
|
|
242
|
+
val.current = posOf(want);
|
|
243
|
+
raf.current = requestAnimationFrame(tick);
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
raf.current = requestAnimationFrame(tick);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
run.current = () => {
|
|
251
|
+
if (!raf.current) raf.current = requestAnimationFrame(tick);
|
|
252
|
+
};
|
|
253
|
+
paint();
|
|
254
|
+
return () => {
|
|
255
|
+
cancelAnimationFrame(raf.current);
|
|
256
|
+
raf.current = 0;
|
|
257
|
+
};
|
|
258
|
+
}, []);
|
|
259
|
+
|
|
260
|
+
/* A value changed from outside moves the handle there and lets the liquid catch up, the same way Home does. While a
|
|
261
|
+
finger is on it, or the handle is still travelling from one, the parent is only echoing what it was told and the
|
|
262
|
+
user is the one in charge. A new range is a new position for the same value. */
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const moved = range.current.min !== min || range.current.max !== max;
|
|
265
|
+
range.current = { min, max };
|
|
266
|
+
if (held.current || hv.current) return;
|
|
267
|
+
const want = value?.[0];
|
|
268
|
+
if (want === undefined || (want === told.current && !moved)) return;
|
|
269
|
+
told.current = want;
|
|
270
|
+
settle.current = null;
|
|
271
|
+
val.current = posOf(want);
|
|
272
|
+
run.current();
|
|
273
|
+
}, [value?.[0], min, max]);
|
|
274
|
+
|
|
275
|
+
/* offset maths, then divided by the zoom, so a scaled ancestor does not skew the reading */
|
|
276
|
+
const readAt = (clientX: number) => {
|
|
277
|
+
const t = track.current;
|
|
278
|
+
if (!t) return 0;
|
|
279
|
+
const box = t.getBoundingClientRect();
|
|
280
|
+
const k = box.width / (t.offsetWidth || box.width) || 1;
|
|
281
|
+
return clamp(((clientX - box.left) / k / t.offsetWidth) * 100, 0, 100);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const grab = (e: PointerEvent) => {
|
|
285
|
+
if (disabled || (e.pointerType === "mouse" && e.button !== 0)) return;
|
|
286
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
287
|
+
held.current = true;
|
|
288
|
+
hv.current = 0;
|
|
289
|
+
settle.current = null;
|
|
290
|
+
commitAtRest.current = false;
|
|
291
|
+
val.current = readAt(e.clientX);
|
|
292
|
+
last.current = { v: val.current, t: e.timeStamp };
|
|
293
|
+
emit();
|
|
294
|
+
run.current();
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const move = (e: PointerEvent) => {
|
|
298
|
+
if (!held.current) return;
|
|
299
|
+
const v = readAt(e.clientX);
|
|
300
|
+
const dt = Math.max(1, e.timeStamp - last.current.t);
|
|
301
|
+
/* per frame, not per ms: the whole loop is in frames and a release velocity in the other unit is a hundred times
|
|
302
|
+
too big */
|
|
303
|
+
hv.current = ((v - last.current.v) / dt) * 16.67;
|
|
304
|
+
last.current = { v, t: e.timeStamp };
|
|
305
|
+
val.current = v;
|
|
306
|
+
emit();
|
|
307
|
+
run.current();
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const drop = (e: PointerEvent) => {
|
|
311
|
+
if (!held.current) return;
|
|
312
|
+
held.current = false;
|
|
313
|
+
/* it leaves along the velocity it actually had, capped so a flick across the whole track does not simply pin it.
|
|
314
|
+
A finger that had stopped before it lifted had none: the last move is old news, and a precision control must not
|
|
315
|
+
wander off from where it was put. */
|
|
316
|
+
hv.current = clamp(hv.current, -6, 6);
|
|
317
|
+
if (knobs.current.reduced || e.timeStamp - last.current.t > STALE_MS) hv.current = 0;
|
|
318
|
+
if (!hv.current) armSettle();
|
|
319
|
+
commit();
|
|
320
|
+
commitAtRest.current = hv.current !== 0 || settle.current !== null;
|
|
321
|
+
run.current();
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const key = (e: KeyboardEvent) => {
|
|
325
|
+
if (disabled) return;
|
|
326
|
+
const p = props.current;
|
|
327
|
+
const unit = p.step ?? (p.max - p.min) / 100;
|
|
328
|
+
const big = e.shiftKey || e.key === "PageUp" || e.key === "PageDown";
|
|
329
|
+
const now = told.current;
|
|
330
|
+
const to =
|
|
331
|
+
e.key === "ArrowRight" || e.key === "ArrowUp" || e.key === "PageUp"
|
|
332
|
+
? now + unit * (big ? 10 : 1)
|
|
333
|
+
: e.key === "ArrowLeft" || e.key === "ArrowDown" || e.key === "PageDown"
|
|
334
|
+
? now - unit * (big ? 10 : 1)
|
|
335
|
+
: e.key === "Home"
|
|
336
|
+
? p.min
|
|
337
|
+
: e.key === "End"
|
|
338
|
+
? p.max
|
|
339
|
+
: null;
|
|
340
|
+
if (to === null) return;
|
|
341
|
+
e.preventDefault();
|
|
342
|
+
hv.current = 0;
|
|
343
|
+
settle.current = null;
|
|
344
|
+
/* from the value the caller holds, with no snapping: next to a mark the arrow would otherwise land on it forever */
|
|
345
|
+
val.current = posOf(to);
|
|
346
|
+
emit(false);
|
|
347
|
+
commit();
|
|
348
|
+
run.current();
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const markPos = markValue !== undefined ? posOf(markValue) : null;
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<div
|
|
355
|
+
{...rest}
|
|
356
|
+
className={`bn-slo${fluid ? " bn-slo--fluid" : ""}${className ? ` ${className}` : ""}`}
|
|
357
|
+
style={{ "--slo-r": `${clamp(corner, 0, SLO_MAX)}px` } as CSSProperties}
|
|
358
|
+
data-disabled={disabled ? "" : undefined}
|
|
359
|
+
>
|
|
360
|
+
{/* nothing but the track: a caption and a readout bracketing it is what every slider looks like, and the number
|
|
361
|
+
would read the fill, which lags, a second telling of what the liquid is already saying */}
|
|
362
|
+
<div
|
|
363
|
+
className="bn-slo-track"
|
|
364
|
+
data-slot="slider-track"
|
|
365
|
+
ref={track}
|
|
366
|
+
id={id}
|
|
367
|
+
role="slider"
|
|
368
|
+
tabIndex={disabled ? -1 : 0}
|
|
369
|
+
aria-label={ariaLabelledBy || !ariaLabel ? undefined : ariaLabel}
|
|
370
|
+
aria-labelledby={ariaLabelledBy}
|
|
371
|
+
aria-describedby={ariaDescribedBy}
|
|
372
|
+
aria-disabled={disabled || undefined}
|
|
373
|
+
aria-valuemin={min}
|
|
374
|
+
aria-valuemax={max}
|
|
375
|
+
aria-valuenow={start}
|
|
376
|
+
aria-orientation="horizontal"
|
|
377
|
+
onPointerDown={grab}
|
|
378
|
+
onPointerMove={move}
|
|
379
|
+
onPointerUp={drop}
|
|
380
|
+
onPointerCancel={drop}
|
|
381
|
+
onKeyDown={key}
|
|
382
|
+
>
|
|
383
|
+
<div className="bn-slo-fill" ref={fillEl} style={{ width: `${fill.current.toFixed(2)}%` }} />
|
|
384
|
+
{markPos !== null && <span aria-hidden className="bn-slo-mark" style={{ left: `${markPos}%` }} />}
|
|
385
|
+
<div className="bn-slo-knob" ref={knobEl} style={{ left: `${val.current.toFixed(2)}%` }} />
|
|
386
|
+
{label !== undefined && (
|
|
387
|
+
<span aria-hidden className="bn-slo-label">
|
|
388
|
+
{label}
|
|
389
|
+
</span>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
}
|