mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
cleanup (this doesn't work)
This commit is contained in:
@@ -37,7 +37,7 @@
|
|||||||
"embla-carousel-react": "^8.5.1",
|
"embla-carousel-react": "^8.5.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.468.0",
|
||||||
"motion": "^12.18.1",
|
"motion": "^12.18.1",
|
||||||
"next": "^15.4.1",
|
"next": "^15.4.1",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { ChevronLeft, Loader2 } from "lucide-react";
|
import { ArrowLeft, Loader2 } from "lucide-react";
|
||||||
import { GoogleIcon } from "@/components/icons";
|
import { GoogleIcon } from "@/components/icons";
|
||||||
import { useLogin } from "@/hooks/auth/useLogin";
|
import { useLogin } from "@/hooks/auth/useLogin";
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ const LoginPage = () => {
|
|||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
className="absolute top-6 left-6"
|
className="absolute top-6 left-6"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-5 w-5" /> Back
|
<ArrowLeft className="h-5 w-5" /> Back
|
||||||
</Button>
|
</Button>
|
||||||
<Card className="w-[400px] shadow-lg border-0">
|
<Card className="w-[400px] shadow-lg border-0">
|
||||||
<CardHeader className="text-center pb-4">
|
<CardHeader className="text-center pb-4">
|
||||||
@@ -119,7 +119,11 @@ const LoginPage = () => {
|
|||||||
className="w-full h-11"
|
className="w-full h-11"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
{isEmailLoading ? <Loader2 className="animate-spin" /> : "Sign in"}
|
{isEmailLoading ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Sign in"
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,6 +141,6 @@ const LoginPage = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default memo(LoginPage);
|
export default memo(LoginPage);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { ChevronLeft, Loader2 } from "lucide-react";
|
import { ArrowLeft, Loader2 } from "lucide-react";
|
||||||
import { GoogleIcon } from "@/components/icons";
|
import { GoogleIcon } from "@/components/icons";
|
||||||
import { useSignUp } from "@/hooks/auth/useSignUp";
|
import { useSignUp } from "@/hooks/auth/useSignUp";
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ const SignUpPage = () => {
|
|||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
className="absolute top-6 left-6"
|
className="absolute top-6 left-6"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-5 w-5" /> Back
|
<ArrowLeft className="h-5 w-5" /> Back
|
||||||
</Button>
|
</Button>
|
||||||
<Card className="w-[400px] shadow-lg border-0">
|
<Card className="w-[400px] shadow-lg border-0">
|
||||||
<CardHeader className="text-center pb-4">
|
<CardHeader className="text-center pb-4">
|
||||||
@@ -157,6 +157,6 @@ const SignUpPage = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default memo(SignUpPage);
|
export default memo(SignUpPage);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEditorStore } from "@/stores/editor-store";
|
import { useEditorStore } from "@/stores/editor-store";
|
||||||
|
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
|
||||||
|
|
||||||
interface EditorProviderProps {
|
interface EditorProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -11,6 +12,11 @@ interface EditorProviderProps {
|
|||||||
export function EditorProvider({ children }: EditorProviderProps) {
|
export function EditorProvider({ children }: EditorProviderProps) {
|
||||||
const { isInitializing, isPanelsReady, initializeApp } = useEditorStore();
|
const { isInitializing, isPanelsReady, initializeApp } = useEditorStore();
|
||||||
|
|
||||||
|
useKeyboardShortcuts({
|
||||||
|
enabled: !isInitializing && isPanelsReady,
|
||||||
|
context: "editor",
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeApp();
|
initializeApp();
|
||||||
}, [initializeApp]);
|
}, [initializeApp]);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
// @ts-ignore - IDE TypeScript configuration issue (React modules compile fine in Docker)
|
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +15,6 @@ import {
|
|||||||
Video,
|
Video,
|
||||||
Music,
|
Music,
|
||||||
TypeIcon,
|
TypeIcon,
|
||||||
Magnet,
|
|
||||||
Lock,
|
Lock,
|
||||||
LockOpen,
|
LockOpen,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -39,9 +37,6 @@ import { useProjectStore } from "@/stores/project-store";
|
|||||||
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
|
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
|
||||||
import { processMediaFiles } from "@/lib/media-processing";
|
import { processMediaFiles } from "@/lib/media-processing";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
// @ts-ignore - React type definitions
|
|
||||||
import * as React from "react";
|
|
||||||
// @ts-ignore - React type definitions
|
|
||||||
import { useState, useRef, useEffect, useCallback } from "react";
|
import { useState, useRef, useEffect, useCallback } from "react";
|
||||||
import { TimelineTrackContent } from "./timeline-track";
|
import { TimelineTrackContent } from "./timeline-track";
|
||||||
import {
|
import {
|
||||||
@@ -66,8 +61,6 @@ export function Timeline() {
|
|||||||
// You can drag media here to add it to your project.
|
// You can drag media here to add it to your project.
|
||||||
// elements can be trimmed, deleted, and moved.
|
// elements can be trimmed, deleted, and moved.
|
||||||
|
|
||||||
// Note: Some parameter types are inferred as 'any' due to store interface definitions
|
|
||||||
// This doesn't affect runtime safety as the stores provide proper type checking
|
|
||||||
const {
|
const {
|
||||||
tracks,
|
tracks,
|
||||||
addTrack,
|
addTrack,
|
||||||
@@ -265,259 +258,6 @@ export function Timeline() {
|
|||||||
setDuration(Math.max(totalDuration, 10)); // Minimum 10 seconds for empty timeline
|
setDuration(Math.max(totalDuration, 10)); // Minimum 10 seconds for empty timeline
|
||||||
}, [tracks, setDuration, getTotalDuration]);
|
}, [tracks, setDuration, getTotalDuration]);
|
||||||
|
|
||||||
// Keyboard event for deleting selected elements
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
// Don't trigger when typing in input fields or textareas
|
|
||||||
if (
|
|
||||||
e.target instanceof HTMLInputElement ||
|
|
||||||
e.target instanceof HTMLTextAreaElement
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only trigger when timeline is focused or mouse is over timeline
|
|
||||||
if (
|
|
||||||
!isInTimeline &&
|
|
||||||
!timelineRef.current?.contains(document.activeElement)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(e.key === "Delete" || e.key === "Backspace") &&
|
|
||||||
selectedElements.length > 0
|
|
||||||
) {
|
|
||||||
selectedElements.forEach(({ trackId, elementId }: { trackId: string; elementId: string }) => {
|
|
||||||
removeElementFromTrack(trackId, elementId);
|
|
||||||
});
|
|
||||||
clearSelectedElements();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [
|
|
||||||
selectedElements,
|
|
||||||
removeElementFromTrack,
|
|
||||||
clearSelectedElements,
|
|
||||||
isInTimeline,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Keyboard event for undo (Cmd+Z)
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "z" && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
undo();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [undo]);
|
|
||||||
|
|
||||||
// Keyboard event for redo (Cmd+Shift+Z or Cmd+Y)
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "z" && e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
redo();
|
|
||||||
} else if ((e.metaKey || e.ctrlKey) && e.key === "y") {
|
|
||||||
e.preventDefault();
|
|
||||||
redo();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [redo]);
|
|
||||||
|
|
||||||
// Core video editor keyboard shortcuts
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
// Don't interfere with typing in inputs
|
|
||||||
const activeElement = document.activeElement as HTMLElement;
|
|
||||||
const isInputFocused = activeElement && (
|
|
||||||
activeElement.tagName === 'INPUT' ||
|
|
||||||
activeElement.tagName === 'TEXTAREA' ||
|
|
||||||
activeElement.contentEditable === 'true'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isInputFocused) return;
|
|
||||||
|
|
||||||
const { key, metaKey, ctrlKey, shiftKey } = e;
|
|
||||||
const isModified = metaKey || ctrlKey || shiftKey;
|
|
||||||
|
|
||||||
switch (key) {
|
|
||||||
case ' ': // Spacebar - Play/Pause
|
|
||||||
if (!isModified) {
|
|
||||||
e.preventDefault();
|
|
||||||
toggle();
|
|
||||||
toast.info(isPlaying ? 'Paused' : 'Playing', { duration: 1000 });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'j':
|
|
||||||
case 'J':
|
|
||||||
if (!isModified) {
|
|
||||||
e.preventDefault();
|
|
||||||
// J - Rewind 1 second
|
|
||||||
seek(Math.max(0, currentTime - 1));
|
|
||||||
toast.info('Rewind 1s', { duration: 1000 });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'k':
|
|
||||||
case 'K':
|
|
||||||
if (!isModified) {
|
|
||||||
e.preventDefault();
|
|
||||||
// K - Pause/Play toggle
|
|
||||||
toggle();
|
|
||||||
toast.info(isPlaying ? 'Paused' : 'Playing', { duration: 1000 });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'l':
|
|
||||||
case 'L':
|
|
||||||
if (!isModified) {
|
|
||||||
e.preventDefault();
|
|
||||||
// L - Fast forward 1 second
|
|
||||||
seek(Math.min(duration, currentTime + 1));
|
|
||||||
toast.info('Forward 1s', { duration: 1000 });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ArrowLeft':
|
|
||||||
if (shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
// Shift+Left - Jump back 5 seconds
|
|
||||||
seek(Math.max(0, currentTime - 5));
|
|
||||||
} else if (!isModified) {
|
|
||||||
e.preventDefault();
|
|
||||||
// Left - Frame step backward (1 frame)
|
|
||||||
const projectFps = activeProject?.fps || 30;
|
|
||||||
seek(Math.max(0, currentTime - (1/projectFps)));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ArrowRight':
|
|
||||||
if (shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
// Shift+Right - Jump forward 5 seconds
|
|
||||||
seek(Math.min(duration, currentTime + 5));
|
|
||||||
} else if (!isModified) {
|
|
||||||
e.preventDefault();
|
|
||||||
// Right - Frame step forward (1 frame)
|
|
||||||
const projectFps = activeProject?.fps || 30;
|
|
||||||
seek(Math.min(duration, currentTime + (1/projectFps)));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Home':
|
|
||||||
if (!isModified) {
|
|
||||||
e.preventDefault();
|
|
||||||
seek(0);
|
|
||||||
toast.info('Start of timeline', { duration: 1000 });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'End':
|
|
||||||
if (!isModified) {
|
|
||||||
e.preventDefault();
|
|
||||||
seek(duration);
|
|
||||||
toast.info('End of timeline', { duration: 1000 });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 's':
|
|
||||||
case 'S':
|
|
||||||
if (!isModified && selectedElements.length === 1) {
|
|
||||||
e.preventDefault();
|
|
||||||
// S - Split element at playhead
|
|
||||||
const { trackId, elementId } = selectedElements[0];
|
|
||||||
const track = tracks.find((t: any) => t.id === trackId);
|
|
||||||
const element = track?.elements.find((el: any) => el.id === elementId);
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
const effectiveStart = element.startTime;
|
|
||||||
const effectiveEnd = element.startTime + (element.duration - element.trimStart - element.trimEnd);
|
|
||||||
|
|
||||||
if (currentTime > effectiveStart && currentTime < effectiveEnd) {
|
|
||||||
splitElement(trackId, elementId, currentTime);
|
|
||||||
toast.success('Element split at playhead');
|
|
||||||
} else {
|
|
||||||
toast.error('Playhead must be within selected element');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'n':
|
|
||||||
case 'N':
|
|
||||||
if (!isModified) {
|
|
||||||
e.preventDefault();
|
|
||||||
toggleSnapping();
|
|
||||||
toast.info(`Snapping ${snappingEnabled ? 'disabled' : 'enabled'}`, { duration: 1000 });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi-key shortcuts
|
|
||||||
if ((metaKey || ctrlKey) && !shiftKey) {
|
|
||||||
switch (key.toLowerCase()) {
|
|
||||||
case 'a':
|
|
||||||
e.preventDefault();
|
|
||||||
// Cmd/Ctrl+A - Select all elements
|
|
||||||
const allElements = tracks.flatMap((track: any) =>
|
|
||||||
track.elements.map((element: any) => ({
|
|
||||||
trackId: track.id,
|
|
||||||
elementId: element.id
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
setSelectedElements(allElements);
|
|
||||||
toast.info(`Selected ${allElements.length} elements`, { duration: 1000 });
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'd':
|
|
||||||
if (selectedElements.length === 1) {
|
|
||||||
e.preventDefault();
|
|
||||||
// Cmd/Ctrl+D - Duplicate selected element
|
|
||||||
const { trackId, elementId } = selectedElements[0];
|
|
||||||
const track = tracks.find((t: any) => t.id === trackId);
|
|
||||||
const element = track?.elements.find((el: any) => el.id === elementId);
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
const newStartTime = element.startTime + (element.duration - element.trimStart - element.trimEnd) + 0.1;
|
|
||||||
const { id, ...elementWithoutId } = element;
|
|
||||||
|
|
||||||
addElementToTrack(trackId, {
|
|
||||||
...elementWithoutId,
|
|
||||||
startTime: newStartTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success('Element duplicated');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [
|
|
||||||
isPlaying,
|
|
||||||
currentTime,
|
|
||||||
duration,
|
|
||||||
selectedElements,
|
|
||||||
tracks,
|
|
||||||
toggle,
|
|
||||||
seek,
|
|
||||||
splitElement,
|
|
||||||
snappingEnabled,
|
|
||||||
toggleSnapping,
|
|
||||||
setSelectedElements,
|
|
||||||
addElementToTrack
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Old marquee system removed - using new SelectionBox component instead
|
// Old marquee system removed - using new SelectionBox component instead
|
||||||
|
|
||||||
const handleDragEnter = (e: React.DragEvent) => {
|
const handleDragEnter = (e: React.DragEvent) => {
|
||||||
@@ -575,7 +315,9 @@ export function Timeline() {
|
|||||||
useTimelineStore.getState().addTextToNewTrack(dragData);
|
useTimelineStore.getState().addTextToNewTrack(dragData);
|
||||||
} else {
|
} else {
|
||||||
// Handle media items
|
// Handle media items
|
||||||
const mediaItem = mediaItems.find((item: any) => item.id === dragData.id);
|
const mediaItem = mediaItems.find(
|
||||||
|
(item: any) => item.id === dragData.id
|
||||||
|
);
|
||||||
if (!mediaItem) {
|
if (!mediaItem) {
|
||||||
toast.error("Media item not found");
|
toast.error("Media item not found");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -11,120 +11,8 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "./ui/dialog";
|
} from "./ui/dialog";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { Keyboard, Play, Pause, SkipBack, SkipForward } from "lucide-react";
|
import { Keyboard } from "lucide-react";
|
||||||
|
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
|
||||||
interface Shortcut {
|
|
||||||
keys: string[];
|
|
||||||
description: string;
|
|
||||||
category: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortcuts: Shortcut[] = [
|
|
||||||
// Playback Controls
|
|
||||||
{
|
|
||||||
keys: ["Space"],
|
|
||||||
description: "Play/Pause",
|
|
||||||
category: "Playback",
|
|
||||||
icon: <Play className="w-3 h-3" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["J"],
|
|
||||||
description: "Rewind 1 second",
|
|
||||||
category: "Playback",
|
|
||||||
icon: <SkipBack className="w-3 h-3" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["K"],
|
|
||||||
description: "Play/Pause (alternative)",
|
|
||||||
category: "Playback",
|
|
||||||
icon: <Pause className="w-3 h-3" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["L"],
|
|
||||||
description: "Fast forward 1 second",
|
|
||||||
category: "Playback",
|
|
||||||
icon: <SkipForward className="w-3 h-3" />
|
|
||||||
},
|
|
||||||
|
|
||||||
// Navigation
|
|
||||||
{
|
|
||||||
keys: ["←"],
|
|
||||||
description: "Frame step backward",
|
|
||||||
category: "Navigation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["→"],
|
|
||||||
description: "Frame step forward",
|
|
||||||
category: "Navigation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["Shift", "←"],
|
|
||||||
description: "Jump back 5 seconds",
|
|
||||||
category: "Navigation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["Shift", "→"],
|
|
||||||
description: "Jump forward 5 seconds",
|
|
||||||
category: "Navigation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["Home"],
|
|
||||||
description: "Go to timeline start",
|
|
||||||
category: "Navigation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["End"],
|
|
||||||
description: "Go to timeline end",
|
|
||||||
category: "Navigation"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Editing
|
|
||||||
{
|
|
||||||
keys: ["S"],
|
|
||||||
description: "Split element at playhead",
|
|
||||||
category: "Editing"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["Delete", "Backspace"],
|
|
||||||
description: "Delete selected elements",
|
|
||||||
category: "Editing"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["N"],
|
|
||||||
description: "Toggle snapping",
|
|
||||||
category: "Editing"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Selection & Organization
|
|
||||||
{
|
|
||||||
keys: ["Cmd", "A"],
|
|
||||||
description: "Select all elements",
|
|
||||||
category: "Selection"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["Cmd", "D"],
|
|
||||||
description: "Duplicate selected element",
|
|
||||||
category: "Selection"
|
|
||||||
},
|
|
||||||
|
|
||||||
// History
|
|
||||||
{
|
|
||||||
keys: ["Cmd", "Z"],
|
|
||||||
description: "Undo",
|
|
||||||
category: "History"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["Cmd", "Shift", "Z"],
|
|
||||||
description: "Redo",
|
|
||||||
category: "History"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["Cmd", "Y"],
|
|
||||||
description: "Redo (alternative)",
|
|
||||||
category: "History"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const KeyBadge = ({ keyName }: { keyName: string }) => {
|
const KeyBadge = ({ keyName }: { keyName: string }) => {
|
||||||
// Replace common key names with symbols
|
// Replace common key names with symbols
|
||||||
@@ -142,7 +30,7 @@ const KeyBadge = ({ keyName }: { keyName: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ShortcutItem = ({ shortcut }: { shortcut: Shortcut }) => (
|
const ShortcutItem = ({ shortcut }: { shortcut: any }) => (
|
||||||
<div className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-muted/50">
|
<div className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-muted/50">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{shortcut.icon && (
|
{shortcut.icon && (
|
||||||
@@ -151,7 +39,7 @@ const ShortcutItem = ({ shortcut }: { shortcut: Shortcut }) => (
|
|||||||
<span className="text-sm">{shortcut.description}</span>
|
<span className="text-sm">{shortcut.description}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{shortcut.keys.map((key, index) => (
|
{shortcut.keys.map((key: string, index: number) => (
|
||||||
<div key={index} className="flex items-center gap-1">
|
<div key={index} className="flex items-center gap-1">
|
||||||
<KeyBadge keyName={key} />
|
<KeyBadge keyName={key} />
|
||||||
{index < shortcut.keys.length - 1 && (
|
{index < shortcut.keys.length - 1 && (
|
||||||
@@ -166,7 +54,10 @@ const ShortcutItem = ({ shortcut }: { shortcut: Shortcut }) => (
|
|||||||
export const KeyboardShortcutsHelp = () => {
|
export const KeyboardShortcutsHelp = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const categories = Array.from(new Set(shortcuts.map(s => s.category)));
|
// Get shortcuts from centralized hook (disabled so it doesn't add event listeners)
|
||||||
|
const { shortcuts } = useKeyboardShortcuts({ enabled: false });
|
||||||
|
|
||||||
|
const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@@ -189,14 +80,14 @@ export const KeyboardShortcutsHelp = () => {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{categories.map(category => (
|
{categories.map((category) => (
|
||||||
<div key={category}>
|
<div key={category}>
|
||||||
<h3 className="font-semibold text-sm text-muted-foreground uppercase tracking-wide mb-3">
|
<h3 className="font-semibold text-sm text-muted-foreground uppercase tracking-wide mb-3">
|
||||||
{category}
|
{category}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{shortcuts
|
{shortcuts
|
||||||
.filter(shortcut => shortcut.category === category)
|
.filter((shortcut) => shortcut.category === category)
|
||||||
.map((shortcut, index) => (
|
.map((shortcut, index) => (
|
||||||
<ShortcutItem key={index} shortcut={shortcut} />
|
<ShortcutItem key={index} shortcut={shortcut} />
|
||||||
))}
|
))}
|
||||||
@@ -208,7 +99,9 @@ export const KeyboardShortcutsHelp = () => {
|
|||||||
<div className="mt-6 p-4 bg-muted/30 rounded-lg">
|
<div className="mt-6 p-4 bg-muted/30 rounded-lg">
|
||||||
<h4 className="font-medium text-sm mb-2">Tips:</h4>
|
<h4 className="font-medium text-sm mb-2">Tips:</h4>
|
||||||
<ul className="text-sm text-muted-foreground space-y-1">
|
<ul className="text-sm text-muted-foreground space-y-1">
|
||||||
<li>• Shortcuts work when the editor is focused (not typing in inputs)</li>
|
<li>
|
||||||
|
• Shortcuts work when the editor is focused (not typing in inputs)
|
||||||
|
</li>
|
||||||
<li>• J/K/L are industry-standard video editing shortcuts</li>
|
<li>• J/K/L are industry-standard video editing shortcuts</li>
|
||||||
<li>• Use arrow keys for frame-perfect positioning</li>
|
<li>• Use arrow keys for frame-perfect positioning</li>
|
||||||
<li>• Hold Shift with arrow keys for larger jumps</li>
|
<li>• Hold Shift with arrow keys for larger jumps</li>
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useCallback } from "react";
|
||||||
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
|
import { usePlaybackStore } from "@/stores/playback-store";
|
||||||
|
import { useProjectStore } from "@/stores/project-store";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export interface KeyboardShortcut {
|
||||||
|
id: string;
|
||||||
|
keys: string[];
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
action: () => void;
|
||||||
|
enabled?: boolean;
|
||||||
|
requiresSelection?: boolean;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseKeyboardShortcutsOptions {
|
||||||
|
enabled?: boolean;
|
||||||
|
context?: "global" | "timeline" | "editor";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useKeyboardShortcuts = (
|
||||||
|
options: UseKeyboardShortcutsOptions = {}
|
||||||
|
) => {
|
||||||
|
const { enabled = true, context = "editor" } = options;
|
||||||
|
|
||||||
|
const {
|
||||||
|
tracks,
|
||||||
|
selectedElements,
|
||||||
|
clearSelectedElements,
|
||||||
|
setSelectedElements,
|
||||||
|
removeElementFromTrack,
|
||||||
|
splitElement,
|
||||||
|
addElementToTrack,
|
||||||
|
snappingEnabled,
|
||||||
|
toggleSnapping,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
} = useTimelineStore();
|
||||||
|
|
||||||
|
const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore();
|
||||||
|
|
||||||
|
const { activeProject } = useProjectStore();
|
||||||
|
|
||||||
|
// Check if user is typing in an input field
|
||||||
|
const isInputFocused = useCallback(() => {
|
||||||
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
|
return (
|
||||||
|
activeElement &&
|
||||||
|
(activeElement.tagName === "INPUT" ||
|
||||||
|
activeElement.tagName === "TEXTAREA" ||
|
||||||
|
activeElement.contentEditable === "true")
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Define all shortcuts in one place
|
||||||
|
const shortcuts: KeyboardShortcut[] = [
|
||||||
|
// Playback Controls
|
||||||
|
{
|
||||||
|
id: "play-pause",
|
||||||
|
keys: ["Space"],
|
||||||
|
description: "Play/Pause",
|
||||||
|
category: "Playback",
|
||||||
|
action: () => {
|
||||||
|
toggle();
|
||||||
|
toast.info(isPlaying ? "Paused" : "Playing", { duration: 1000 });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rewind",
|
||||||
|
keys: ["j", "J"],
|
||||||
|
description: "Rewind 1 second",
|
||||||
|
category: "Playback",
|
||||||
|
action: () => {
|
||||||
|
seek(Math.max(0, currentTime - 1));
|
||||||
|
toast.info("Rewind 1s", { duration: 1000 });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "play-pause-alt",
|
||||||
|
keys: ["k", "K"],
|
||||||
|
description: "Play/Pause (alternative)",
|
||||||
|
category: "Playback",
|
||||||
|
action: () => {
|
||||||
|
toggle();
|
||||||
|
toast.info(isPlaying ? "Paused" : "Playing", { duration: 1000 });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fast-forward",
|
||||||
|
keys: ["l", "L"],
|
||||||
|
description: "Fast forward 1 second",
|
||||||
|
category: "Playback",
|
||||||
|
action: () => {
|
||||||
|
seek(Math.min(duration, currentTime + 1));
|
||||||
|
toast.info("Forward 1s", { duration: 1000 });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
{
|
||||||
|
id: "frame-backward",
|
||||||
|
keys: ["ArrowLeft"],
|
||||||
|
description: "Frame step backward",
|
||||||
|
category: "Navigation",
|
||||||
|
action: () => {
|
||||||
|
const projectFps = activeProject?.fps || 30;
|
||||||
|
seek(Math.max(0, currentTime - 1 / projectFps));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "frame-forward",
|
||||||
|
keys: ["ArrowRight"],
|
||||||
|
description: "Frame step forward",
|
||||||
|
category: "Navigation",
|
||||||
|
action: () => {
|
||||||
|
const projectFps = activeProject?.fps || 30;
|
||||||
|
seek(Math.min(duration, currentTime + 1 / projectFps));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "jump-backward",
|
||||||
|
keys: ["Shift+ArrowLeft"],
|
||||||
|
description: "Jump back 5 seconds",
|
||||||
|
category: "Navigation",
|
||||||
|
action: () => {
|
||||||
|
seek(Math.max(0, currentTime - 5));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "jump-forward",
|
||||||
|
keys: ["Shift+ArrowRight"],
|
||||||
|
description: "Jump forward 5 seconds",
|
||||||
|
category: "Navigation",
|
||||||
|
action: () => {
|
||||||
|
seek(Math.min(duration, currentTime + 5));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "goto-start",
|
||||||
|
keys: ["Home"],
|
||||||
|
description: "Go to timeline start",
|
||||||
|
category: "Navigation",
|
||||||
|
action: () => {
|
||||||
|
seek(0);
|
||||||
|
toast.info("Start of timeline", { duration: 1000 });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "goto-end",
|
||||||
|
keys: ["End"],
|
||||||
|
description: "Go to timeline end",
|
||||||
|
category: "Navigation",
|
||||||
|
action: () => {
|
||||||
|
seek(duration);
|
||||||
|
toast.info("End of timeline", { duration: 1000 });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Editing
|
||||||
|
{
|
||||||
|
id: "split-element",
|
||||||
|
keys: ["s", "S"],
|
||||||
|
description: "Split element at playhead",
|
||||||
|
category: "Editing",
|
||||||
|
requiresSelection: true,
|
||||||
|
action: () => {
|
||||||
|
if (selectedElements.length !== 1) {
|
||||||
|
toast.error("Select exactly one element to split");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { trackId, elementId } = selectedElements[0];
|
||||||
|
const track = tracks.find((t: any) => t.id === trackId);
|
||||||
|
const element = track?.elements.find((el: any) => el.id === elementId);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
const effectiveStart = element.startTime;
|
||||||
|
const effectiveEnd =
|
||||||
|
element.startTime +
|
||||||
|
(element.duration - element.trimStart - element.trimEnd);
|
||||||
|
|
||||||
|
if (currentTime > effectiveStart && currentTime < effectiveEnd) {
|
||||||
|
splitElement(trackId, elementId, currentTime);
|
||||||
|
toast.success("Element split at playhead");
|
||||||
|
} else {
|
||||||
|
toast.error("Playhead must be within selected element");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "delete-elements",
|
||||||
|
keys: ["Delete", "Backspace"],
|
||||||
|
description: "Delete selected elements",
|
||||||
|
category: "Editing",
|
||||||
|
requiresSelection: true,
|
||||||
|
action: () => {
|
||||||
|
if (selectedElements.length === 0) {
|
||||||
|
toast.error("No elements selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedElements.forEach(
|
||||||
|
({ trackId, elementId }: { trackId: string; elementId: string }) => {
|
||||||
|
removeElementFromTrack(trackId, elementId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
clearSelectedElements();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "toggle-snapping",
|
||||||
|
keys: ["n", "N"],
|
||||||
|
description: "Toggle snapping",
|
||||||
|
category: "Editing",
|
||||||
|
action: () => {
|
||||||
|
toggleSnapping();
|
||||||
|
toast.info(`Snapping ${snappingEnabled ? "disabled" : "enabled"}`, {
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Selection & Organization
|
||||||
|
{
|
||||||
|
id: "select-all",
|
||||||
|
keys: ["Cmd+a", "Ctrl+a"],
|
||||||
|
description: "Select all elements",
|
||||||
|
category: "Selection",
|
||||||
|
action: () => {
|
||||||
|
const allElements = tracks.flatMap((track: any) =>
|
||||||
|
track.elements.map((element: any) => ({
|
||||||
|
trackId: track.id,
|
||||||
|
elementId: element.id,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
setSelectedElements(allElements);
|
||||||
|
toast.info(`Selected ${allElements.length} elements`, {
|
||||||
|
duration: 1000,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "duplicate-element",
|
||||||
|
keys: ["Cmd+d", "Ctrl+d"],
|
||||||
|
description: "Duplicate selected element",
|
||||||
|
category: "Selection",
|
||||||
|
requiresSelection: true,
|
||||||
|
action: () => {
|
||||||
|
if (selectedElements.length !== 1) {
|
||||||
|
toast.error("Select exactly one element to duplicate");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { trackId, elementId } = selectedElements[0];
|
||||||
|
const track = tracks.find((t: any) => t.id === trackId);
|
||||||
|
const element = track?.elements.find((el: any) => el.id === elementId);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
const newStartTime =
|
||||||
|
element.startTime +
|
||||||
|
(element.duration - element.trimStart - element.trimEnd) +
|
||||||
|
0.1;
|
||||||
|
const { id, ...elementWithoutId } = element;
|
||||||
|
|
||||||
|
addElementToTrack(trackId, {
|
||||||
|
...elementWithoutId,
|
||||||
|
startTime: newStartTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Element duplicated");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// History
|
||||||
|
{
|
||||||
|
id: "undo",
|
||||||
|
keys: ["Cmd+z", "Ctrl+z"],
|
||||||
|
description: "Undo",
|
||||||
|
category: "History",
|
||||||
|
action: () => {
|
||||||
|
undo();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "redo",
|
||||||
|
keys: ["Cmd+Shift+z", "Ctrl+Shift+z", "Cmd+y", "Ctrl+y"],
|
||||||
|
description: "Redo",
|
||||||
|
category: "History",
|
||||||
|
action: () => {
|
||||||
|
redo();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Parse keyboard event to match against shortcuts
|
||||||
|
const parseKeyboardEvent = useCallback((e: KeyboardEvent): string => {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (e.metaKey || e.ctrlKey) parts.push(e.metaKey ? "Cmd" : "Ctrl");
|
||||||
|
if (e.shiftKey) parts.push("Shift");
|
||||||
|
if (e.altKey) parts.push("Alt");
|
||||||
|
|
||||||
|
parts.push(e.key);
|
||||||
|
|
||||||
|
return parts.join("+");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle keyboard events
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (!enabled || isInputFocused()) return;
|
||||||
|
|
||||||
|
const keyCombo = parseKeyboardEvent(e);
|
||||||
|
const shortcut = shortcuts.find((s) =>
|
||||||
|
s.keys.some((key) => key === keyCombo || key === e.key)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shortcut) {
|
||||||
|
// Check if shortcut requires selection
|
||||||
|
if (shortcut.requiresSelection && selectedElements.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
shortcut.action();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[enabled, shortcuts, selectedElements, parseKeyboardEvent, isInputFocused]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up event listener
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [enabled, handleKeyDown]);
|
||||||
|
|
||||||
|
// Return shortcuts for help component
|
||||||
|
return {
|
||||||
|
shortcuts: shortcuts.filter((s) => s.enabled !== false),
|
||||||
|
enabled,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -106,68 +106,4 @@ export const usePlaybackControls = () => {
|
|||||||
|
|
||||||
separateAudio(trackId, elementId);
|
separateAudio(trackId, elementId);
|
||||||
}, [selectedElements, tracks, separateAudio]);
|
}, [selectedElements, tracks, separateAudio]);
|
||||||
|
|
||||||
const handleKeyPress = useCallback(
|
|
||||||
(e: KeyboardEvent) => {
|
|
||||||
if (
|
|
||||||
e.target instanceof HTMLInputElement ||
|
|
||||||
e.target instanceof HTMLTextAreaElement
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (e.key) {
|
|
||||||
case " ":
|
|
||||||
e.preventDefault();
|
|
||||||
if (isPlaying) {
|
|
||||||
pause();
|
|
||||||
} else {
|
|
||||||
play();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "s":
|
|
||||||
if (e.ctrlKey || e.metaKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSplitSelectedElement();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "q":
|
|
||||||
if (e.ctrlKey || e.metaKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSplitAndKeepLeftCallback();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "w":
|
|
||||||
if (e.ctrlKey || e.metaKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSplitAndKeepRightCallback();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "d":
|
|
||||||
if (e.ctrlKey || e.metaKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSeparateAudioCallback();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
isPlaying,
|
|
||||||
play,
|
|
||||||
pause,
|
|
||||||
handleSplitSelectedElement,
|
|
||||||
handleSplitAndKeepLeftCallback,
|
|
||||||
handleSplitAndKeepRightCallback,
|
|
||||||
handleSeparateAudioCallback,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener("keydown", handleKeyPress);
|
|
||||||
return () => document.removeEventListener("keydown", handleKeyPress);
|
|
||||||
}, [handleKeyPress]);
|
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
-19
@@ -1,19 +0,0 @@
|
|||||||
/// <reference types="react" />
|
|
||||||
/// <reference types="react-dom" />
|
|
||||||
|
|
||||||
// Global React types
|
|
||||||
declare global {
|
|
||||||
namespace JSX {
|
|
||||||
interface IntrinsicElements {
|
|
||||||
[elemName: string]: any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// React module declarations
|
|
||||||
declare module 'react' {
|
|
||||||
export * from '@types/react';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure this file is treated as a module
|
|
||||||
export {};
|
|
||||||
Vendored
-100
@@ -1,100 +0,0 @@
|
|||||||
// Type declarations for external modules without @types packages
|
|
||||||
|
|
||||||
declare module 'lucide-react' {
|
|
||||||
import { FC, SVGProps } from 'react';
|
|
||||||
|
|
||||||
export interface IconProps extends SVGProps<SVGSVGElement> {
|
|
||||||
size?: string | number;
|
|
||||||
strokeWidth?: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Scissors: FC<IconProps>;
|
|
||||||
export const ArrowLeftToLine: FC<IconProps>;
|
|
||||||
export const ArrowRightToLine: FC<IconProps>;
|
|
||||||
export const Trash2: FC<IconProps>;
|
|
||||||
export const Snowflake: FC<IconProps>;
|
|
||||||
export const Copy: FC<IconProps>;
|
|
||||||
export const SplitSquareHorizontal: FC<IconProps>;
|
|
||||||
export const Pause: FC<IconProps>;
|
|
||||||
export const Play: FC<IconProps>;
|
|
||||||
export const Video: FC<IconProps>;
|
|
||||||
export const Music: FC<IconProps>;
|
|
||||||
export const TypeIcon: FC<IconProps>;
|
|
||||||
export const Magnet: FC<IconProps>;
|
|
||||||
export const Lock: FC<IconProps>;
|
|
||||||
export const ChevronLeft: FC<IconProps>;
|
|
||||||
export const ChevronRight: FC<IconProps>;
|
|
||||||
export const ChevronDown: FC<IconProps>;
|
|
||||||
export const ChevronUp: FC<IconProps>;
|
|
||||||
export const Download: FC<IconProps>;
|
|
||||||
export const Keyboard: FC<IconProps>;
|
|
||||||
export const SkipBack: FC<IconProps>;
|
|
||||||
export const SkipForward: FC<IconProps>;
|
|
||||||
export const Loader2: FC<IconProps>;
|
|
||||||
export const ExternalLink: FC<IconProps>;
|
|
||||||
export const ArrowRight: FC<IconProps>;
|
|
||||||
export const ArrowLeft: FC<IconProps>;
|
|
||||||
export const Expand: FC<IconProps>;
|
|
||||||
export const Plus: FC<IconProps>;
|
|
||||||
export const Upload: FC<IconProps>;
|
|
||||||
export const Image: FC<IconProps>;
|
|
||||||
export const MoreVertical: FC<IconProps>;
|
|
||||||
export const MoreHorizontal: FC<IconProps>;
|
|
||||||
export const Eye: FC<IconProps>;
|
|
||||||
export const EyeOff: FC<IconProps>;
|
|
||||||
export const Check: FC<IconProps>;
|
|
||||||
export const Circle: FC<IconProps>;
|
|
||||||
export const Search: FC<IconProps>;
|
|
||||||
export const X: FC<IconProps>;
|
|
||||||
export const PanelLeft: FC<IconProps>;
|
|
||||||
export const ChevronsUpDown: FC<IconProps>;
|
|
||||||
export const CheckIcon: FC<IconProps>;
|
|
||||||
export const Minus: FC<IconProps>;
|
|
||||||
export const RefreshCw: FC<IconProps>;
|
|
||||||
export const PipetteIcon: FC<IconProps>;
|
|
||||||
export const Type: FC<IconProps>;
|
|
||||||
export const Calendar: FC<IconProps>;
|
|
||||||
export const CaptionsIcon: FC<IconProps>;
|
|
||||||
export const ArrowLeftRightIcon: FC<IconProps>;
|
|
||||||
export const SparklesIcon: FC<IconProps>;
|
|
||||||
export const StickerIcon: FC<IconProps>;
|
|
||||||
export const MusicIcon: FC<IconProps>;
|
|
||||||
export const VideoIcon: FC<IconProps>;
|
|
||||||
export const BlendIcon: FC<IconProps>;
|
|
||||||
export const SlidersHorizontalIcon: FC<IconProps>;
|
|
||||||
export type LucideIcon = FC<IconProps>;
|
|
||||||
// Add other icons as needed
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'sonner' {
|
|
||||||
export interface ToastOptions {
|
|
||||||
duration?: number;
|
|
||||||
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center';
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
className?: string;
|
|
||||||
description?: string;
|
|
||||||
action?: {
|
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
};
|
|
||||||
cancel?: {
|
|
||||||
label: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
id?: string | number;
|
|
||||||
onDismiss?: (toast: any) => void;
|
|
||||||
onAutoClose?: (toast: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Toast {
|
|
||||||
success: (message: string, options?: ToastOptions) => void;
|
|
||||||
error: (message: string, options?: ToastOptions) => void;
|
|
||||||
info: (message: string, options?: ToastOptions) => void;
|
|
||||||
warning: (message: string, options?: ToastOptions) => void;
|
|
||||||
loading: (message: string, options?: ToastOptions) => void;
|
|
||||||
custom: (jsx: React.ReactNode, options?: ToastOptions) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const toast: Toast;
|
|
||||||
export const Toaster: React.FC<any>;
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"embla-carousel-react": "^8.5.1",
|
"embla-carousel-react": "^8.5.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.468.0",
|
||||||
"motion": "^12.18.1",
|
"motion": "^12.18.1",
|
||||||
"next": "^15.4.1",
|
"next": "^15.4.1",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
@@ -734,7 +734,7 @@
|
|||||||
|
|
||||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.525.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ=="],
|
"lucide-react": ["lucide-react@0.468.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA=="],
|
||||||
|
|
||||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
|
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user