Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Footer from "@/components/Footer";

export default function Home() {
return (
<>
<main>
<a
href="https://github.com/magic-peach/reframe"
target="_blank"
Expand Down
2 changes: 1 addition & 1 deletion src/components/DownloadResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,4 @@ export default function DownloadResult({ result, onReset, soundOnCompletion }: P
</div>
</div>
);
}
}
2 changes: 1 addition & 1 deletion src/components/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ export default function VideoEditor() {

<Section icon={<Crop size={12} />} title="Framing" delay={100}>
<FramingControl recipe={recipe} onChange={updateRecipe} />
</Section>
</Section>https://github.com/magic-peach/reframe/pull/752/conflict?name=src%252Fcomponents%252FDownloadResult.tsx&ancestor_oid=8ed8770779f9434a70be0b71e7ca1a1ab6ee537a&base_oid=f306ac02c31626128c0373f1db080093edef60c3&head_oid=35bb94601feff9507db3fdd7ea734161799d2aa5

<div className="pt-2 flex justify-end">
<button
Expand Down
143 changes: 88 additions & 55 deletions src/components/components.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { type ReactNode } from "react";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

/**
* Utility function to merge tailwind classes without conflicts.
* It uses clsx to handle conditional classes and twMerge to handle Tailwind conflicts.
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

// --- COMPONENTS ---

interface CardProps {
title: string;
Expand All @@ -11,16 +23,16 @@ interface CardProps {
* Reusable Card component demonstrating dark: variant usage.
* All interactive and structural colors are dual-themed.
*/
export function Card({ title, description, children, className = "" }: CardProps) {
export function Card({ title, description, children, className }: CardProps) {
return (
<div
className={`
rounded-xl border p-6
bg-white dark:bg-gray-800
border-gray-200 dark:border-gray-700
shadow-sm dark:shadow-gray-900/40
${className}
`}
className={cn(
"rounded-xl border p-6 shadow-sm",
"bg-white dark:bg-gray-800",
"border-gray-200 dark:border-gray-700",
"dark:shadow-gray-900/40",
className
)}
>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
{title}
Expand All @@ -42,40 +54,25 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "ghost";
}

export function Button({ variant = "primary", className = "", children, ...props }: ButtonProps) {
export function Button({ variant = "primary", className, children, ...props }: ButtonProps) {
const variants = {
primary: `
bg-blue-600 dark:bg-blue-500
text-white
hover:bg-blue-700 dark:hover:bg-blue-600
focus:ring-blue-500
`,
secondary: `
bg-gray-100 dark:bg-gray-700
text-gray-800 dark:text-gray-100
hover:bg-gray-200 dark:hover:bg-gray-600
focus:ring-gray-400
`,
ghost: `
bg-transparent
text-gray-700 dark:text-gray-300
hover:bg-gray-100 dark:hover:bg-gray-800
focus:ring-gray-400
`,
primary:
"bg-blue-600 dark:bg-blue-500 text-white hover:bg-blue-700 dark:hover:bg-blue-600 focus:ring-blue-500",
secondary:
"bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-600 focus:ring-gray-400",
ghost:
"bg-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:ring-gray-400",
};

return (
<button
className={`
inline-flex items-center justify-center gap-2
rounded-lg px-4 py-2 text-sm font-medium
focus:outline-none focus:ring-2 focus:ring-offset-2
dark:focus:ring-offset-gray-900
transition-colors duration-200
disabled:opacity-50 disabled:cursor-not-allowed
${variants[variant]}
${className}
`}
className={cn(
"inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors duration-200",
"focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900",
"disabled:cursor-not-allowed disabled:opacity-50",
variants[variant],
className
)}
{...props}
>
{children}
Expand All @@ -86,20 +83,18 @@ export function Button({ variant = "primary", className = "", children, ...props
/**
* Input — demonstrates form field dark mode variants.
*/
export function Input({ className = "", ...props }: React.InputHTMLAttributes<HTMLInputElement>) {
export function Input({ className, ...props }: React.InputHTMLAttributes<HTMLInputElement>) {
return (
<input
className={`
w-full rounded-lg px-3 py-2 text-sm
bg-white dark:bg-gray-900
text-gray-900 dark:text-gray-100
border border-gray-300 dark:border-gray-600
placeholder-gray-400 dark:placeholder-gray-500
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
focus:border-transparent
transition-colors duration-200
${className}
`}
className={cn(
"w-full rounded-lg px-3 py-2 text-sm transition-colors duration-200",
"bg-white dark:bg-gray-900",
"text-gray-900 dark:text-gray-100",
"border border-gray-300 dark:border-gray-600",
"placeholder-gray-400 dark:placeholder-gray-500",
"focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400",
className
)}
{...props}
/>
);
Expand All @@ -111,19 +106,57 @@ export function Input({ className = "", ...props }: React.InputHTMLAttributes<HT
interface BadgeProps {
label: string;
variant?: "default" | "success" | "warning" | "error";
className?: string;
}

export function Badge({ label, variant = "default" }: BadgeProps) {
export function Badge({ label, variant = "default", className }: BadgeProps) {
const variants = {
default: "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300",
success: "bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-400",
warning: "bg-yellow-100 dark:bg-yellow-900/40 text-yellow-700 dark:text-yellow-400",
error: "bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-400",
default: "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300",
success: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400",
warning: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-400",
error: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400",
};

return (
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${variants[variant]}`}>
<span
className={cn(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
variants[variant],
className
)}
>
{label}
</span>
);
}

/**
* Toggle (Switch) — Interactive boolean input with dark mode support.
*/
interface ToggleProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
label?: string;
}

export function Toggle({ label, className, ...props }: ToggleProps) {
return (
<label className={cn("relative flex items-center gap-3 cursor-pointer", className)}>
<div className="relative inline-flex items-center">
<input type="checkbox" className="sr-only peer" {...props} />
<div className={cn(
"w-9 h-5 rounded-full transition-colors duration-200 ease-in-out",
"bg-gray-200 dark:bg-gray-700",
"peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 dark:peer-focus:ring-blue-400 peer-focus:ring-offset-2 dark:peer-focus:ring-offset-gray-900",
"peer-checked:bg-blue-600 dark:peer-checked:bg-blue-500",
"after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:h-4 after:w-4",
"after:bg-white after:border-gray-300 after:border after:rounded-full after:transition-all after:duration-200",
"peer-checked:after:translate-x-full peer-checked:after:border-white"
)}></div>
</div>
{label && (
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 select-none">
{label}
</span>
)}
</label>
);
}
33 changes: 33 additions & 0 deletions src/hooks/useVideoEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,39 @@ export function useVideoEditor() {
const [overlaySize, setOverlaySize] = useState(150);
const [overlayOpacity, setOverlayOpacity] = useState(100);

// --- LocalStorage Persistence Logic ---

// 1. Load saved settings on mount (Client-side only)
useEffect(() => {
const savedToggle = localStorage.getItem('rememberSettings') === 'true';
setRememberSettings(savedToggle);

if (savedToggle) {
const savedRecipe = localStorage.getItem('videoEditorRecipe');
if (savedRecipe) {
try {
const parsedRecipe = JSON.parse(savedRecipe);
setRecipe(parsedRecipe);
} catch (error) {
console.error("Failed to parse saved video recipe", error);
}
}
}
}, []);

// 2. Save settings when recipe or toggle changes
useEffect(() => {
localStorage.setItem('rememberSettings', String(rememberSettings));

if (rememberSettings) {
localStorage.setItem('videoEditorRecipe', JSON.stringify(recipe));
} else {
localStorage.removeItem('videoEditorRecipe');
}
}, [rememberSettings, recipe]);

// --- End LocalStorage Logic ---

const updateRecipe = useCallback((patch: Partial<EditRecipe>) => {
setRecipe((prev) => ({ ...prev, ...patch }));
}, []);
Expand Down
Loading