diff --git a/.github/workflows/claude-manager.yml b/.github/workflows/claude-manager.yml
index 07fef221..8c8e81be 100644
--- a/.github/workflows/claude-manager.yml
+++ b/.github/workflows/claude-manager.yml
@@ -35,7 +35,6 @@ jobs:
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
- allowed_non_write_users: "*"
trigger_phrase: ""
direct_prompt: |
You are the automated manager for magic-peach/reframe, a GSSoC'26 open-source project.
@@ -75,7 +74,6 @@ jobs:
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
- allowed_non_write_users: "*"
trigger_phrase: ""
direct_prompt: |
You are the automated manager for magic-peach/reframe, a GSSoC'26 open-source project.
diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx
index a12c1f41..c1f1ab32 100644
--- a/src/components/VideoEditor.tsx
+++ b/src/components/VideoEditor.tsx
@@ -27,6 +27,10 @@ import {
import OnboardingTour from "./OnboardingTour";
import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts";
import { loadOverlayState, persistOverlayState } from "@/lib/editorPersistence";
+import {
+ getExportButtonAnimationClass,
+ isReadyToExport,
+} from "@/lib/exportButtonAnimation";
interface SectionProps {
icon: React.ReactNode;
@@ -313,6 +317,7 @@ return () => {
}, [file]);
const isProcessing = status === "loading-engine" || status === "exporting";
+ const isReadyToExportState = isReadyToExport(Boolean(file), status);
const isMac = typeof navigator !== "undefined" && /Mac/i.test(navigator.platform);
const intervalSeconds = useMemo(() => {
@@ -741,16 +746,17 @@ return () => {
className={cn(
"w-full flex items-center justify-center gap-3 py-5 min-h-[44px] rounded-xl",
"font-display text-2xl tracking-widest transition-all duration-200",
+ getExportButtonAnimationClass(Boolean(file), status),
file && !isProcessing
? "bg-[var(--accent)] hover:bg-[var(--accent-hover)] hover:scale-[1.02] text-white shadow-[var(--shadow)] active:scale-[0.98] cursor-pointer"
: "bg-[var(--border)] text-[var(--muted)] cursor-not-allowed"
)}
>
-
{isMac ? "⌘" : "Ctrl"} + Enter to export
diff --git a/src/lib/exportButtonAnimation.ts b/src/lib/exportButtonAnimation.ts new file mode 100644 index 00000000..b91b483c --- /dev/null +++ b/src/lib/exportButtonAnimation.ts @@ -0,0 +1,17 @@ +import { ExportStatus } from "@/lib/types"; + +export const READY_EXPORT_BUTTON_ANIMATION_CLASS = + "motion-safe:animate-export-ready-pulse motion-reduce:animate-none"; + +export function isReadyToExport(hasFile: boolean, status: ExportStatus): boolean { + return hasFile && status === "idle"; +} + +export function getExportButtonAnimationClass( + hasFile: boolean, + status: ExportStatus +): string { + return isReadyToExport(hasFile, status) + ? READY_EXPORT_BUTTON_ANIMATION_CLASS + : ""; +} diff --git a/src/lib/tests/exportButtonAnimation.test.ts b/src/lib/tests/exportButtonAnimation.test.ts new file mode 100644 index 00000000..c29ff583 --- /dev/null +++ b/src/lib/tests/exportButtonAnimation.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { + getExportButtonAnimationClass, + isReadyToExport, + READY_EXPORT_BUTTON_ANIMATION_CLASS, +} from "../exportButtonAnimation"; + +describe("exportButtonAnimation", () => { + it("marks export as ready only when a file is loaded and status is idle", () => { + expect(isReadyToExport(true, "idle")).toBe(true); + expect(isReadyToExport(false, "idle")).toBe(false); + expect(isReadyToExport(true, "loading-engine")).toBe(false); + expect(isReadyToExport(true, "exporting")).toBe(false); + expect(isReadyToExport(true, "done")).toBe(false); + expect(isReadyToExport(true, "error")).toBe(false); + }); + + it("returns the reduced-motion-safe animation class only in the ready state", () => { + expect(getExportButtonAnimationClass(true, "idle")).toBe( + READY_EXPORT_BUTTON_ANIMATION_CLASS + ); + expect(getExportButtonAnimationClass(false, "idle")).toBe(""); + expect(getExportButtonAnimationClass(true, "exporting")).toBe(""); + expect(getExportButtonAnimationClass(true, "done")).toBe(""); + }); +}); diff --git a/tailwind.config.ts b/tailwind.config.ts index c79da9a2..5dca82da 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -34,9 +34,20 @@ const config: Config = { shimmer: { "100%": { transform: "translateX(100%)" }, }, + exportReadyPulse: { + "0%, 100%": { + boxShadow: "var(--shadow)", + filter: "brightness(1)", + }, + "50%": { + boxShadow: "0 0 0 3px var(--accent-muted), var(--shadow)", + filter: "brightness(1.03)", + }, + }, }, animation: { shimmer: "shimmer 2s infinite", + "export-ready-pulse": "exportReadyPulse 2.4s ease-in-out infinite", }, }, },