Merge branch 'main' into codex/editor-defaults-ssot

This commit is contained in:
Sid
2026-05-22 19:44:37 -07:00
committed by GitHub
17 changed files with 805 additions and 126 deletions
+18 -15
View File
@@ -61,6 +61,7 @@ import {
DEFAULT_SOURCE_DIMENSIONS,
DEFAULT_WEBCAM_SETTINGS,
} from "./editorDefaults";
import { parseCustomPlaybackSpeedInput } from "./customPlaybackSpeed";
import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp";
import type {
AnnotationRegion,
@@ -79,6 +80,7 @@ import type {
} from "./types";
import {
MAX_PLAYBACK_SPEED,
DEFAULT_WEBCAM_SIZE_PRESET,
MAX_ZOOM_SCALE,
MIN_ZOOM_SCALE,
ROTATION_3D_PRESET_ORDER,
@@ -97,37 +99,38 @@ function CustomSpeedInput({
onError: () => void;
}) {
const isPreset = SPEED_OPTIONS.some((o) => o.speed === value);
const [draft, setDraft] = useState(isPreset ? "" : String(Math.round(value)));
const [draft, setDraft] = useState(isPreset ? "" : String(value));
const [isFocused, setIsFocused] = useState(false);
const prevValue = useRef(value);
if (!isFocused && prevValue.current !== value) {
prevValue.current = value;
setDraft(isPreset ? "" : String(Math.round(value)));
setDraft(isPreset ? "" : String(value));
}
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const digits = e.target.value.replace(/\D/g, "");
if (digits === "") {
setDraft("");
return;
}
const num = Number(digits);
if (num > MAX_PLAYBACK_SPEED) {
const result = parseCustomPlaybackSpeedInput(e.target.value);
if (result.status === "too-fast") {
onError();
return;
}
setDraft(digits);
if (num >= 1) onChange(num);
setDraft(result.draft);
if (result.status === "valid") {
onChange(result.speed);
}
},
[onChange, onError],
);
const handleBlur = useCallback(() => {
setIsFocused(false);
if (!draft || Number(draft) < 1) {
setDraft(isPreset ? "" : String(Math.round(value)));
const result = parseCustomPlaybackSpeedInput(draft);
if (result.status === "valid") {
setDraft(String(result.speed));
} else {
setDraft(isPreset ? "" : String(value));
}
}, [draft, isPreset, value]);
@@ -135,8 +138,8 @@ function CustomSpeedInput({
<div className="flex items-center gap-1">
<input
type="text"
inputMode="numeric"
pattern="[0-9]*"
inputMode="decimal"
pattern="[0-9]*[.]?[0-9]*"
placeholder="--"
value={draft}
onFocus={() => setIsFocused(true)}
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { parseCustomPlaybackSpeedInput } from "./customPlaybackSpeed";
describe("parseCustomPlaybackSpeedInput", () => {
it("accepts decimal playback speeds", () => {
expect(parseCustomPlaybackSpeedInput("1.1")).toEqual({
status: "valid",
draft: "1.1",
speed: 1.1,
});
});
it("keeps a single decimal point while typing", () => {
expect(parseCustomPlaybackSpeedInput("1.2.3")).toEqual({
status: "valid",
draft: "1.23",
speed: 1.23,
});
});
it("allows sub-1 custom speeds down to the editor minimum", () => {
expect(parseCustomPlaybackSpeedInput("0.1")).toEqual({
status: "valid",
draft: "0.1",
speed: 0.1,
});
});
it("rejects speeds below the editor minimum", () => {
expect(parseCustomPlaybackSpeedInput("0.09")).toEqual({
status: "too-slow",
draft: "0.09",
});
});
it("accepts comma decimal input by normalizing to a dot", () => {
expect(parseCustomPlaybackSpeedInput("1,1")).toEqual({
status: "valid",
draft: "1.1",
speed: 1.1,
});
});
it("rejects speeds above the editor maximum", () => {
expect(parseCustomPlaybackSpeedInput("16.1")).toEqual({
status: "too-fast",
draft: "16.1",
});
});
});
@@ -0,0 +1,37 @@
import {
clampPlaybackSpeed,
MAX_PLAYBACK_SPEED,
MIN_PLAYBACK_SPEED,
type PlaybackSpeed,
} from "./types";
export type CustomPlaybackSpeedInputResult =
| { status: "empty"; draft: string }
| { status: "too-fast"; draft: string }
| { status: "too-slow"; draft: string }
| { status: "valid"; draft: string; speed: PlaybackSpeed };
export function parseCustomPlaybackSpeedInput(rawValue: string): CustomPlaybackSpeedInputResult {
const decimalDraft = rawValue.replace(/,/g, ".").replace(/[^\d.]/g, "");
const [whole = "", ...fractionParts] = decimalDraft.split(".");
const draft = fractionParts.length > 0 ? `${whole}.${fractionParts.join("")}` : whole;
if (draft === "" || draft === ".") {
return { status: "empty", draft };
}
const speed = Number(draft);
if (!Number.isFinite(speed)) {
return { status: "empty", draft };
}
if (speed > MAX_PLAYBACK_SPEED) {
return { status: "too-fast", draft };
}
if (speed < MIN_PLAYBACK_SPEED) {
return { status: "too-slow", draft };
}
return { status: "valid", draft, speed: clampPlaybackSpeed(speed) };
}