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" )} > - + {isProcessing ? "PROCESSING" : "EXPORT"} - {file && !isProcessing && ( + {isReadyToExportState && (

{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", }, }, },