diff --git a/bun.lock b/bun.lock
index c5aa7903..c9dcf103 100644
--- a/bun.lock
+++ b/bun.lock
@@ -20,7 +20,6 @@
"wasm-feature-detect": "^1.8.0",
},
"devDependencies": {
- "@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/bun": "^1.3.14",
@@ -39,8 +38,6 @@
},
},
"packages": {
- "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="],
-
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
@@ -425,8 +422,6 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
- "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
-
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"cssstyle": ["cssstyle@3.0.0", "", { "dependencies": { "rrweb-cssom": "^0.6.0" } }, "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg=="],
@@ -465,7 +460,7 @@
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
- "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
+ "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
"domexception": ["domexception@4.0.0", "", { "dependencies": { "webidl-conversions": "^7.0.0" } }, "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw=="],
@@ -619,8 +614,6 @@
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
- "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
-
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
@@ -771,8 +764,6 @@
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
- "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
-
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -891,8 +882,6 @@
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
- "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
-
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"reframe": ["reframe@root:", {}],
@@ -981,8 +970,6 @@
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
- "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
-
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "@babel/core": "*", "babel-plugin-macros": "*", "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "optionalPeers": ["@babel/core", "babel-plugin-macros"] }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
@@ -1099,8 +1086,6 @@
"@testing-library/dom/aria-query": ["aria-query@5.1.3", "", { "dependencies": { "deep-equal": "^2.0.5" } }, "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ=="],
- "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
-
"@testing-library/react/@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
diff --git a/src/app/globals.css b/src/app/globals.css
index 408e2d8e..c070728d 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -5,8 +5,8 @@
/* ── Light mode tokens ── */
:root {
--bg: #ffffff;
- --surface: #f1f5f9;
- --border: #cbd5e1;
+ --surface: #f8fafc;
+ --border: #e2e8f0;
--text: #0f172a;
--muted: #64748b;
--accent: #3b82f6;
@@ -140,7 +140,7 @@ textarea:focus {
}
:focus-visible {
- outline: 0;
- box-shadow: 0 0 0 3px var(--accent-muted);
- border-radius: var(--radius);
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ border-radius: 4px;
}
diff --git a/src/components/DownloadResult.tsx b/src/components/DownloadResult.tsx
index 9cb04286..48590fe4 100644
--- a/src/components/DownloadResult.tsx
+++ b/src/components/DownloadResult.tsx
@@ -44,10 +44,7 @@ export default function DownloadResult({ result, onReset, soundOnCompletion, onT
useEffect(() => {
if (soundOnCompletion) {
const audio = new Audio("/sounds/export-complete.mp3");
- audio.play().catch((error) => {
- console.error("Failed to play completion sound:", error);
- setSoundError(true);
- });
+ audio.play().catch((err) => console.error(err));
}
}, [soundOnCompletion]);
const handleReset = () => {
diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx
index f9f2127e..cf1f16c5 100644
--- a/src/components/FileUpload.tsx
+++ b/src/components/FileUpload.tsx
@@ -127,7 +127,7 @@ export default function FileUpload({
// ── File info (shown after upload) ───────────────────
const FileInfo = () => (
-
+
@@ -136,18 +136,18 @@ export default function FileUpload({
-
+
{currentFile?.name}
{currentFile && (
-
+
{currentFile.name.includes(".")
? currentFile.name.split(".").pop()
: "VIDEO"}
)}
-
+
{formatBytes(currentFile?.size ?? 0)}
{duration > 0
@@ -168,12 +168,12 @@ export default function FileUpload({
-
+
Supports: MP4, MOV, AVI, MKV, WebM, and most video formats
{fileError && (
-
{fileError}
+
{fileError}
)}
{error && (
-
+
{error}
)}
{warning && (
-
+
{warning}
)}
@@ -309,4 +309,4 @@ export default function FileUpload({
>
);
-}
+}
\ No newline at end of file
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
index c6006f0d..475f28a6 100644
--- a/src/components/Footer.tsx
+++ b/src/components/Footer.tsx
@@ -41,7 +41,7 @@ export default function Footer() {
].map((tag) => (
{tag.icon} {tag.label}
@@ -49,6 +49,7 @@ export default function Footer() {
+
{/* Links Section */}
@@ -59,19 +60,19 @@ export default function Footer() {
href="https://github.com/magic-peach/reframe"
target="_blank"
rel="noopener"
- className="opacity-70 hover:opacity-100 hover:text-[var(--accent)] hover:scale-110 transition-all duration-500 ease-in-out w-fit flex items-center gap-2 group"
+ className="opacity-70 hover:opacity-100 hover:text-red-400 hover:scale-110 transition-all duration-500 ease-in-out w-fit flex items-center gap-2 group"
>
GitHub
Contact
Privacy Policy
@@ -102,7 +103,7 @@ export default function Footer() {
) : (
-
{/* Bottom Bar */}
© {new Date().getFullYear()} Reframe · MIT License
diff --git a/src/components/ImageOverlay.tsx b/src/components/ImageOverlay.tsx
index ac928206..09152858 100644
--- a/src/components/ImageOverlay.tsx
+++ b/src/components/ImageOverlay.tsx
@@ -118,7 +118,7 @@ export default function ImageOverlayPanel({
type="button"
onClick={() => setOverlayFile(null)}
aria-label="Remove overlay image"
- className="w-6 h-6 rounded flex items-center justify-center bg-[var(--error-bg)] hover:bg-[var(--error-hover)] text-[var(--error)] border border-[var(--error-border)] transition shrink-0"
+ className="w-6 h-6 rounded flex items-center justify-center bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 transition shrink-0"
>
diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx
index c1cef066..c181fa7c 100644
--- a/src/components/OnboardingTour.tsx
+++ b/src/components/OnboardingTour.tsx
@@ -232,7 +232,7 @@ export default function OnboardingTour() {
const [visible, setVisible] = useState(false);
const [targetRect, setTargetRect] = useState
(null);
const tooltipRef = useRef(null);
- const isFirstRender = useRef(true);
+ const isFirstRender = useRef(true);
const currentStep = TOUR_STEPS[stepIndex];
const dismiss = useCallback(() => {
@@ -269,16 +269,41 @@ export default function OnboardingTour() {
// Initialise on mount
useEffect(() => {
- if (localStorage.getItem(TOUR_KEY)) return;
- const t = setTimeout(async () => {
- const rect = await measureTarget(TOUR_STEPS[0]?.targetId ?? "");
- if (rect) {
- setTargetRect(rect);
- setVisible(true);
+ if (localStorage.getItem(TOUR_KEY)) return;
+ const t = setTimeout(async () => {
+ const rect = await measureTarget(TOUR_STEPS[0]?.targetId ?? "");
+ if (rect) {
+ setTargetRect(rect);
+ setVisible(true);
+ }
+ }, 600);
+ return () => clearTimeout(t);
+}, [measureTarget]);
+
+// Measure target whenever step changes (skip on first render — init effect handles that)
+useEffect(() => {
+ if (!visible) return;
+ if (isFirstRender.current) {
+ isFirstRender.current = false;
+ return;
+ }
+ if (!currentStep) {
+ dismiss();
+ return;
+ }
+ measureTarget(currentStep.targetId).then((rect) => {
+ if (rect) {
+ setTargetRect(rect);
+ setTimeout(() => tooltipRef.current?.focus(), 50);
+ } else {
+ if (stepIndex < TOUR_STEPS.length - 1) {
+ setStepIndex((i) => i + 1);
+ } else {
+ dismiss();
}
- }, 600);
- return () => clearTimeout(t);
- }, [measureTarget]);
+ }
+ });
+ }, [stepIndex, visible, measureTarget, dismiss, currentStep]);
// Measure target whenever step changes (skip on first render — init effect handles that)
useEffect(() => {
diff --git a/src/components/PresetSelector.tsx b/src/components/PresetSelector.tsx
index fd129ab2..75006812 100644
--- a/src/components/PresetSelector.tsx
+++ b/src/components/PresetSelector.tsx
@@ -141,7 +141,7 @@ export default function PresetSelector({ recipe, onChange }: Props) {
return (
{/* Quick-action row */}
-
+
{QUICK_ACTIONS.map(({ preset, label, platform, icon }) => {
const isActive = recipe.preset === preset;
return (
diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx
index 03e32bef..b94b2b13 100644
--- a/src/components/ThemeToggle.tsx
+++ b/src/components/ThemeToggle.tsx
@@ -17,10 +17,9 @@ export function ThemeToggle() {
bg-[var(--surface)]
text-[var(--text)]
border border-[var(--border)]
- hover:border-[var(--accent)] hover:bg-[var(--accent-muted)]
- focus:outline-none focus:ring-2 focus:ring-[var(--accent)] focus:ring-offset-2
- focus:ring-offset-[var(--bg)]
- transition-all duration-200
+ hover:opacity-90
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
+ transition-colors duration-200
"
>
{isDark ? (
diff --git a/src/components/ThumbnailStrip.tsx b/src/components/ThumbnailStrip.tsx
index f5094722..649f6dd9 100644
--- a/src/components/ThumbnailStrip.tsx
+++ b/src/components/ThumbnailStrip.tsx
@@ -147,8 +147,10 @@ export default function ThumbnailStrip({
if (videoSrc && duration > 0) {
generateThumbnails();
}
+
+
return () => {
- cancelThumbnailRun();
+ lastRunIdRef.current = lastRunIdRef.current + 1;
revokeAllObjectUrls();
};
}, [cancelThumbnailRun, generateThumbnails, revokeAllObjectUrls, videoSrc, duration]);
@@ -214,9 +216,8 @@ export default function ThumbnailStrip({
return (