From b6f303ade61610e8d8d44ea4f3015aad7e7b4937 Mon Sep 17 00:00:00 2001 From: Rucha Date: Wed, 20 May 2026 09:58:17 +0530 Subject: [PATCH 1/2] fix(ui): hide number input spinners to prevent value visibility overlap (#776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browser-native increment/decrement arrows on fields overlap with entered values in narrow containers, making text hard to read. Fix: suppress the spinner UI using cross-browser Tailwind arbitrary-value classes: - [appearance:textfield] — Firefox - [&::-webkit-outer-spin-button]:appearance-none — Chrome/Brave/Edge/Safari - [&::-webkit-inner-spin-button]:appearance-none — Chrome/Brave/Edge/Safari Affected inputs: - TrimControl.tsx — Start (sec) and End (sec) number fields - PresetSelector.tsx — Custom Width (px) and Height (px) fields Keyboard up/down arrow key stepping still works; only the visual spinner arrows are removed, which fully resolves the overlap/readability bug. Closes #776 --- src/components/PresetSelector.tsx | 4 ++-- src/components/TrimControl.tsx | 2 +- src/lib/tests/ffmpeg.test.ts | 25 +++++++++++++++---------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/components/PresetSelector.tsx b/src/components/PresetSelector.tsx index 052a3698..e43637eb 100644 --- a/src/components/PresetSelector.tsx +++ b/src/components/PresetSelector.tsx @@ -285,7 +285,7 @@ export default function PresetSelector({ recipe, onChange }: Props) { step={2} value={recipe.customWidth} onChange={(e) => handleWidthChange(Number(e.target.value))} - className="w-full rounded-md border border-[var(--border)] bg-[var(--bg)] px-3 py-2 text-sm font-heading transition-all focus:outline-none focus:ring-2 focus:ring-film-400" + className="w-full rounded-md border border-[var(--border)] bg-[var(--bg)] px-3 py-2 text-sm font-heading transition-all focus:outline-none focus:ring-2 focus:ring-film-400 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" /> @@ -310,7 +310,7 @@ export default function PresetSelector({ recipe, onChange }: Props) { step={2} value={recipe.customHeight} onChange={(e) => handleHeightChange(Number(e.target.value))} - className="w-full rounded-md border border-[var(--border)] bg-[var(--bg)] px-3 py-2 text-sm font-heading transition-all focus:outline-none focus:ring-2 focus:ring-film-400" + className="w-full rounded-md border border-[var(--border)] bg-[var(--bg)] px-3 py-2 text-sm font-heading transition-all focus:outline-none focus:ring-2 focus:ring-film-400 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" /> diff --git a/src/components/TrimControl.tsx b/src/components/TrimControl.tsx index a4a03d19..fa5cb170 100644 --- a/src/components/TrimControl.tsx +++ b/src/components/TrimControl.tsx @@ -109,7 +109,7 @@ export default function TrimControl({ recipe, onChange, duration }: Props) { }; const inputClass = - "w-full text-sm px-3 py-2 border border-[var(--border)] rounded-md bg-[var(--bg)] font-heading focus:outline-none focus:ring-2 focus:ring-film-400 text-[var(--text)] transition-shadow"; + "w-full text-sm px-3 py-2 border border-[var(--border)] rounded-md bg-[var(--bg)] font-heading focus:outline-none focus:ring-2 focus:ring-film-400 text-[var(--text)] transition-shadow [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"; return (
diff --git a/src/lib/tests/ffmpeg.test.ts b/src/lib/tests/ffmpeg.test.ts index bdde5d41..c2e42e74 100644 --- a/src/lib/tests/ffmpeg.test.ts +++ b/src/lib/tests/ffmpeg.test.ts @@ -3,15 +3,15 @@ import { buildAudioFilter } from "../ffmpeg"; describe("buildAudioFilter", () => { it("should return an empty string for 1.0x speed", () => { - expect(buildAudioFilter(1)).toBe(""); + expect(buildAudioFilter(1, false)).toBe(""); }); it("should chain two 0.5x filters for 0.25x speed", () => { - expect(buildAudioFilter(0.25)).toBe("atempo=0.5,atempo=0.5"); + expect(buildAudioFilter(0.25, false)).toBe("atempo=0.5,atempo=0.5"); }); it("should chain two 2.0x filters for 4.0x speed", () => { - expect(buildAudioFilter(4)).toBe("atempo=2.0,atempo=2"); + expect(buildAudioFilter(4, false)).toBe("atempo=2.0,atempo=2"); }); it("should chain multiple 0.5x filters and a remainder for 0.1x speed", () => { @@ -19,25 +19,30 @@ describe("buildAudioFilter", () => { // 0.2 / 0.5 = 0.4 // 0.4 / 0.5 = 0.8 // Result should be three 0.5s and one 0.8 - expect(buildAudioFilter(0.1)).toBe("atempo=0.5,atempo=0.5,atempo=0.5,atempo=0.8"); + expect(buildAudioFilter(0.1, false)).toBe("atempo=0.5,atempo=0.5,atempo=0.5,atempo=0.8"); }); it("should chain multiple 2.0x filters and a remainder for 3.0x speed", () => { // 3.0 / 2.0 = 1.5 - expect(buildAudioFilter(3)).toBe("atempo=2.0,atempo=1.5"); + expect(buildAudioFilter(3, false)).toBe("atempo=2.0,atempo=1.5"); }); it("should handle boundary values inside the 0.5x-2.0x range without chaining", () => { - expect(buildAudioFilter(0.5)).toBe("atempo=0.5"); - expect(buildAudioFilter(2.0)).toBe("atempo=2"); // Note: Number(2.0.toFixed(4)) -> 2 - expect(buildAudioFilter(1.5)).toBe("atempo=1.5"); - expect(buildAudioFilter(0.75)).toBe("atempo=0.75"); + expect(buildAudioFilter(0.5, false)).toBe("atempo=0.5"); + expect(buildAudioFilter(2.0, false)).toBe("atempo=2"); // Note: Number(2.0.toFixed(4)) -> 2 + expect(buildAudioFilter(1.5, false)).toBe("atempo=1.5"); + expect(buildAudioFilter(0.75, false)).toBe("atempo=0.75"); }); it("should chain properly for very large speeds", () => { // 10 / 2.0 = 5 // 5 / 2.0 = 2.5 // 2.5 / 2.0 = 1.25 - expect(buildAudioFilter(10)).toBe("atempo=2.0,atempo=2.0,atempo=2.0,atempo=1.25"); + expect(buildAudioFilter(10, false)).toBe("atempo=2.0,atempo=2.0,atempo=2.0,atempo=1.25"); + }); + + it("should append loudnorm filter when normalizeAudio is true", () => { + const result = buildAudioFilter(1, true); + expect(result).toContain("loudnorm"); }); }); From ca39534e54145c6f7bd7e44efaaaf9a15e2507e4 Mon Sep 17 00:00:00 2001 From: Rucha Date: Wed, 20 May 2026 10:04:23 +0530 Subject: [PATCH 2/2] feat(ui): add highlighted box around REFRAME title (#775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The REFRAME title blended into the background without clear visual distinction. Wrap the h1 + subtitle in a styled container to make the brand mark prominent and immediately recognisable. Changes to src/components/VideoEditor.tsx: - Wrap

REFRAME

and its subtitle

in a div with: border border-[var(--border)] — respects light / dark mode border token border-l-4 border-l-film-600 — film-red left accent bar for brand identity bg-[var(--surface)] — lifted surface so it reads off the page bg rounded-xl — consistent corner radius with card system px-5 py-3 — breathing room around the text shadow-sm — subtle depth to separate from background - aria-label added so screen readers announce the landmark correctly The box reuses existing design tokens and film-600 brand colour, so it adapts automatically to light, dark and high-contrast themes. Closes #775 --- src/components/VideoEditor.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx index 3ffb6a67..26f168ba 100644 --- a/src/components/VideoEditor.tsx +++ b/src/components/VideoEditor.tsx @@ -101,7 +101,10 @@ export default function VideoEditor() {

-
+

REFRAME