summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-26 13:52:30 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-29 15:49:30 +0200
commit722dcdd88face3724c77d23bc36a00524e552551 (patch)
tree9dbf561de29592d5a346b8c7bfd27b5d31345bc8 /apps/web/src
parent8db3bead13441a5882d8689dfc6a9f09c82375d9 (diff)
downloadopencut-722dcdd88face3724c77d23bc36a00524e552551.tar.gz
fix: resolve typescript/lint errors
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/app/layout.tsx2
-rw-r--r--apps/web/src/app/projects/page.tsx7
-rw-r--r--apps/web/src/components/editor/panels/timeline/index.tsx1
-rw-r--r--apps/web/src/components/ui/number-field.tsx47
-rw-r--r--apps/web/src/hooks/use-keyboard-shortcuts-help.ts26
-rw-r--r--apps/web/src/lib/actions/definitions.ts87
-rw-r--r--apps/web/src/lib/animation/effect-param-channel.ts10
-rw-r--r--apps/web/src/lib/animation/graphic-param-channel.ts10
-rw-r--r--apps/web/src/lib/animation/keyframe-query.ts2
-rw-r--r--apps/web/src/lib/animation/keyframes.ts4
-rw-r--r--apps/web/src/lib/animation/property-registry.ts10
-rw-r--r--apps/web/src/lib/animation/target-resolver.ts16
-rw-r--r--apps/web/src/lib/timeline/audio-state.ts2
-rw-r--r--apps/web/src/lib/timeline/drop-utils.ts15
14 files changed, 127 insertions, 112 deletions
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index f8e6040f..b4ef36c7 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -35,7 +35,7 @@ export default function RootLayout({
crossOrigin="anonymous"
strategy="beforeInteractive"
/>
-
+
{/* code to figma */}
{/* <script
dangerouslySetInnerHTML={{
diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx
index 9618de27..f32ce2b9 100644
--- a/apps/web/src/app/projects/page.tsx
+++ b/apps/web/src/app/projects/page.tsx
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
import type { KeyboardEvent, MouseEvent } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
+import type { EditorCore } from "@/core";
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
import { StoragePersistenceDialog } from "@/components/editor/dialogs/storage-persistence-dialog";
import { Button } from "@/components/ui/button";
@@ -359,7 +360,7 @@ async function deleteProjects({
editor,
ids,
}: {
- editor: ReturnType<typeof useEditor>;
+ editor: EditorCore;
ids: string[];
}) {
await editor.project.deleteProjects({ ids });
@@ -369,7 +370,7 @@ async function duplicateProjects({
editor,
ids,
}: {
- editor: ReturnType<typeof useEditor>;
+ editor: EditorCore;
ids: string[];
}) {
await editor.project.duplicateProjects({ ids });
@@ -380,7 +381,7 @@ async function renameProject({
id,
name,
}: {
- editor: ReturnType<typeof useEditor>;
+ editor: EditorCore;
id: string;
name: string;
}) {
diff --git a/apps/web/src/components/editor/panels/timeline/index.tsx b/apps/web/src/components/editor/panels/timeline/index.tsx
index f1ecd9f4..f02f8461 100644
--- a/apps/web/src/components/editor/panels/timeline/index.tsx
+++ b/apps/web/src/components/editor/panels/timeline/index.tsx
@@ -551,7 +551,6 @@ export function Timeline() {
timelineRef={timelineRef}
tracksScrollRef={tracksScrollRef}
isVisible={showSnapIndicator}
- tracks={tracks}
/>
</div>
</section>
diff --git a/apps/web/src/components/ui/number-field.tsx b/apps/web/src/components/ui/number-field.tsx
index 18d71591..acedff10 100644
--- a/apps/web/src/components/ui/number-field.tsx
+++ b/apps/web/src/components/ui/number-field.tsx
@@ -134,20 +134,28 @@ function NumberField({
ref,
...props
}: NumberFieldProps & { ref?: React.Ref<HTMLInputElement> }) {
- const iconRef = useRef<HTMLSpanElement>(null);
+ const iconRef = useRef<HTMLButtonElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const ghostRef = useRef<HTMLSpanElement>(null);
const startValueRef = useRef(0);
const cumulativeDeltaRef = useRef(0);
const [isInputFocused, setIsInputFocused] = useState(false);
const [suffixLeft, setSuffixLeft] = useState(0);
+ const ghostValue = Array.isArray(value) ? value.join(", ") : String(value ?? "");
useLayoutEffect(() => {
- if (!suffix || !ghostRef.current || !inputRef.current) return;
+ if (!suffix) {
+ setSuffixLeft(0);
+ return;
+ }
+ if (!ghostRef.current || !inputRef.current) return;
+ if (ghostRef.current.textContent !== ghostValue) {
+ ghostRef.current.textContent = ghostValue;
+ }
const paddingLeft =
parseFloat(getComputedStyle(inputRef.current).paddingLeft) || 0;
setSuffixLeft(paddingLeft + ghostRef.current.offsetWidth);
- }, [value, suffix]);
+ }, [ghostValue, suffix]);
const { containerRef: wrapperRef } = useFocusLock<HTMLDivElement>({
isActive: isInputFocused,
@@ -243,19 +251,24 @@ function NumberField({
className,
)}
>
- {icon && (
- <span
- ref={iconRef}
- className={cn(
- "text-muted-foreground [&_svg]:size-3.5! shrink-0 select-none pl-2.5 text-sm leading-none",
- canScrub && "cursor-ew-resize",
- )}
- onMouseDown={canScrub ? (event) => event.preventDefault() : undefined}
- onPointerDown={canScrub ? handleIconPointerDown : undefined}
- >
- {icon}
- </span>
- )}
+ {icon &&
+ (canScrub ? (
+ <button
+ ref={iconRef}
+ type="button"
+ aria-label="Drag to adjust value"
+ disabled={disabled}
+ className="text-muted-foreground [&_svg]:size-3.5! shrink-0 select-none pl-2.5 text-sm leading-none cursor-ew-resize"
+ onMouseDown={(event) => event.preventDefault()}
+ onPointerDown={handleIconPointerDown}
+ >
+ {icon}
+ </button>
+ ) : (
+ <span className="text-muted-foreground [&_svg]:size-3.5! shrink-0 select-none pl-2.5 text-sm leading-none">
+ {icon}
+ </span>
+ ))}
<span
className={cn(
"relative flex flex-1 min-w-0 items-center",
@@ -272,7 +285,7 @@ function NumberField({
className="invisible absolute text-sm leading-none whitespace-pre pointer-events-none"
aria-hidden="true"
>
- {value}
+ {ghostValue}
</span>
<span
className={cn(
diff --git a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
index 80f7f07a..7ea3a555 100644
--- a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
+++ b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
@@ -2,7 +2,7 @@
import { useMemo } from "react";
import { useKeybindingsStore } from "@/stores/keybindings-store";
-import { ACTIONS, type TAction } from "@/lib/actions";
+import { ACTIONS, type TActionWithOptionalArgs } from "@/lib/actions";
import {
getPlatformAlternateKey,
getPlatformSpecialKey,
@@ -13,7 +13,7 @@ export interface KeyboardShortcut {
keys: string[];
description: string;
category: string;
- action: TAction;
+ action: TActionWithOptionalArgs;
icon?: React.ReactNode;
}
@@ -40,9 +40,11 @@ export function useKeyboardShortcutsHelp() {
const shortcuts = useMemo(() => {
const result: KeyboardShortcut[] = [];
- const actionToKeys: Record<string, string[]> = {};
+ const actionToKeys: Partial<Record<TActionWithOptionalArgs, string[]>> = {};
- for (const [key, action] of Object.entries(keybindings)) {
+ for (const [key, action] of Object.entries(keybindings) as Array<
+ [string, TActionWithOptionalArgs | undefined]
+ >) {
if (action) {
if (!actionToKeys[action]) {
actionToKeys[action] = [];
@@ -51,16 +53,16 @@ export function useKeyboardShortcutsHelp() {
}
}
- for (const [actionId, keys] of Object.entries(actionToKeys)) {
- if (!isAction(actionId)) continue;
-
- const actionDef = ACTIONS[actionId];
+ for (const [action, keys] of Object.entries(actionToKeys) as Array<
+ [TActionWithOptionalArgs, string[]]
+ >) {
+ const actionDef = ACTIONS[action];
result.push({
- id: actionId,
+ id: action,
keys,
description: actionDef.description,
category: actionDef.category,
- action: actionId,
+ action,
});
}
@@ -76,7 +78,3 @@ export function useKeyboardShortcutsHelp() {
shortcuts,
};
}
-
-function isAction(id: string): id is TAction {
- return id in ACTIONS;
-}
diff --git a/apps/web/src/lib/actions/definitions.ts b/apps/web/src/lib/actions/definitions.ts
index 4d67c1ef..f6bdd65c 100644
--- a/apps/web/src/lib/actions/definitions.ts
+++ b/apps/web/src/lib/actions/definitions.ts
@@ -1,4 +1,8 @@
-import type { ShortcutKey } from "@/lib/actions/keybinding";
+import type {
+ KeybindingConfig,
+ ShortcutKey,
+} from "@/lib/actions/keybinding";
+import type { TActionWithOptionalArgs } from "./types";
export type TActionCategory =
| "playback"
@@ -10,18 +14,20 @@ export type TActionCategory =
| "controls"
| "assets";
-export interface TActionDefinition {
+export interface TActionBaseDefinition {
description: string;
category: TActionCategory;
- defaultShortcuts?: ShortcutKey[];
args?: Record<string, unknown>;
}
+export interface TActionDefinition extends TActionBaseDefinition {
+ defaultShortcuts?: readonly ShortcutKey[];
+}
+
export const ACTIONS = {
"toggle-play": {
description: "Play/Pause",
category: "playback",
- defaultShortcuts: ["space", "k"],
},
"stop-playback": {
description: "Stop playback",
@@ -30,81 +36,66 @@ export const ACTIONS = {
"seek-forward": {
description: "Seek forward 1 second",
category: "playback",
- defaultShortcuts: ["l"],
args: { seconds: "number" },
},
"seek-backward": {
description: "Seek backward 1 second",
category: "playback",
- defaultShortcuts: ["j"],
args: { seconds: "number" },
},
"frame-step-forward": {
description: "Frame step forward",
category: "navigation",
- defaultShortcuts: ["right"],
},
"frame-step-backward": {
description: "Frame step backward",
category: "navigation",
- defaultShortcuts: ["left"],
},
"jump-forward": {
description: "Jump forward 5 seconds",
category: "navigation",
- defaultShortcuts: ["shift+right"],
args: { seconds: "number" },
},
"jump-backward": {
description: "Jump backward 5 seconds",
category: "navigation",
- defaultShortcuts: ["shift+left"],
args: { seconds: "number" },
},
"goto-start": {
description: "Go to timeline start",
category: "navigation",
- defaultShortcuts: ["home", "enter"],
},
"goto-end": {
description: "Go to timeline end",
category: "navigation",
- defaultShortcuts: ["end"],
},
split: {
description: "Split elements at playhead",
category: "editing",
- defaultShortcuts: ["s"],
},
"split-left": {
description: "Split and remove left",
category: "editing",
- defaultShortcuts: ["q"],
},
"split-right": {
description: "Split and remove right",
category: "editing",
- defaultShortcuts: ["w"],
},
"delete-selected": {
description: "Delete selected elements",
category: "editing",
- defaultShortcuts: ["backspace", "delete"],
},
"copy-selected": {
description: "Copy selected elements",
category: "editing",
- defaultShortcuts: ["ctrl+c"],
},
"paste-copied": {
description: "Paste elements at playhead",
category: "editing",
- defaultShortcuts: ["ctrl+v"],
},
"toggle-snapping": {
description: "Toggle snapping",
category: "editing",
- defaultShortcuts: ["n"],
},
"toggle-ripple-editing": {
description: "Toggle ripple editing",
@@ -113,12 +104,10 @@ export const ACTIONS = {
"select-all": {
description: "Select all elements",
category: "selection",
- defaultShortcuts: ["ctrl+a"],
},
"cancel-interaction": {
description: "Cancel current interaction",
category: "controls",
- defaultShortcuts: ["escape"],
},
"deselect-all": {
description: "Deselect all elements",
@@ -127,7 +116,6 @@ export const ACTIONS = {
"duplicate-selected": {
description: "Duplicate selected element",
category: "selection",
- defaultShortcuts: ["ctrl+d"],
},
"toggle-elements-muted-selected": {
description: "Mute/unmute selected elements",
@@ -144,12 +132,10 @@ export const ACTIONS = {
undo: {
description: "Undo",
category: "history",
- defaultShortcuts: ["ctrl+z"],
},
redo: {
description: "Redo",
category: "history",
- defaultShortcuts: ["ctrl+shift+z", "ctrl+y"],
},
"remove-media-asset": {
description: "Remove media asset",
@@ -161,30 +147,59 @@ export const ACTIONS = {
category: "assets",
args: { projectId: "string", assetIds: "string[]" },
},
-} as const satisfies Record<string, TActionDefinition>;
+} as const satisfies Record<string, TActionBaseDefinition>;
export type TAction = keyof typeof ACTIONS;
+const ACTION_DEFAULT_SHORTCUTS = {
+ "toggle-play": ["space", "k"],
+ "seek-forward": ["l"],
+ "seek-backward": ["j"],
+ "frame-step-forward": ["right"],
+ "frame-step-backward": ["left"],
+ "jump-forward": ["shift+right"],
+ "jump-backward": ["shift+left"],
+ "goto-start": ["home", "enter"],
+ "goto-end": ["end"],
+ split: ["s"],
+ "split-left": ["q"],
+ "split-right": ["w"],
+ "delete-selected": ["backspace", "delete"],
+ "copy-selected": ["ctrl+c"],
+ "paste-copied": ["ctrl+v"],
+ "toggle-snapping": ["n"],
+ "select-all": ["ctrl+a"],
+ "cancel-interaction": ["escape"],
+ "duplicate-selected": ["ctrl+d"],
+ undo: ["ctrl+z"],
+ redo: ["ctrl+shift+z", "ctrl+y"],
+} as const satisfies Partial<Record<TActionWithOptionalArgs, readonly ShortcutKey[]>>;
+
+const ACTION_DEFAULT_SHORTCUTS_BY_ACTION: Partial<
+ Record<TAction, readonly ShortcutKey[]>
+> = ACTION_DEFAULT_SHORTCUTS;
+
export function getActionDefinition({
action,
}: {
action: TAction;
}): TActionDefinition {
- return ACTIONS[action];
+ return {
+ ...ACTIONS[action],
+ defaultShortcuts: ACTION_DEFAULT_SHORTCUTS_BY_ACTION[action],
+ };
}
-export function getDefaultShortcuts(): Record<ShortcutKey, TAction> {
- const shortcuts: Record<string, TAction> = {};
+export function getDefaultShortcuts(): KeybindingConfig {
+ const shortcuts: KeybindingConfig = {};
- for (const [action, def] of Object.entries(ACTIONS) as Array<
- [TAction, TActionDefinition]
- >) {
- if (def.defaultShortcuts) {
- for (const shortcut of def.defaultShortcuts) {
- shortcuts[shortcut] = action;
- }
+ for (const [action, defaultShortcuts] of Object.entries(
+ ACTION_DEFAULT_SHORTCUTS,
+ ) as Array<[TActionWithOptionalArgs, readonly ShortcutKey[]]>) {
+ for (const shortcut of defaultShortcuts) {
+ shortcuts[shortcut] = action;
}
}
- return shortcuts as Record<ShortcutKey, TAction>;
+ return shortcuts;
}
diff --git a/apps/web/src/lib/animation/effect-param-channel.ts b/apps/web/src/lib/animation/effect-param-channel.ts
index 323d9f54..a614c14f 100644
--- a/apps/web/src/lib/animation/effect-param-channel.ts
+++ b/apps/web/src/lib/animation/effect-param-channel.ts
@@ -26,11 +26,9 @@ export function buildEffectParamPath({
return `${EFFECT_PARAM_PATH_PREFIX}${effectId}${EFFECT_PARAM_PATH_SUFFIX}${paramKey}`;
}
-export function isEffectParamPath({
- propertyPath,
-}: {
- propertyPath: string;
-}): propertyPath is EffectParamPath {
+export function isEffectParamPath(
+ propertyPath: string,
+): propertyPath is EffectParamPath {
return (
propertyPath.startsWith(EFFECT_PARAM_PATH_PREFIX) &&
propertyPath.includes(EFFECT_PARAM_PATH_SUFFIX)
@@ -42,7 +40,7 @@ export function parseEffectParamPath({
}: {
propertyPath: string;
}): { effectId: string; paramKey: string } | null {
- if (!isEffectParamPath({ propertyPath })) {
+ if (!isEffectParamPath(propertyPath)) {
return null;
}
diff --git a/apps/web/src/lib/animation/graphic-param-channel.ts b/apps/web/src/lib/animation/graphic-param-channel.ts
index e77e4c73..7db544e3 100644
--- a/apps/web/src/lib/animation/graphic-param-channel.ts
+++ b/apps/web/src/lib/animation/graphic-param-channel.ts
@@ -20,11 +20,9 @@ export function buildGraphicParamPath({
return `${GRAPHIC_PARAM_PATH_PREFIX}${paramKey}`;
}
-export function isGraphicParamPath({
- propertyPath,
-}: {
- propertyPath: string;
-}): propertyPath is GraphicParamPath {
+export function isGraphicParamPath(
+ propertyPath: string,
+): propertyPath is GraphicParamPath {
return propertyPath.startsWith(GRAPHIC_PARAM_PATH_PREFIX);
}
@@ -33,7 +31,7 @@ export function parseGraphicParamPath({
}: {
propertyPath: string;
}): { paramKey: string } | null {
- if (!isGraphicParamPath({ propertyPath })) {
+ if (!isGraphicParamPath(propertyPath)) {
return null;
}
diff --git a/apps/web/src/lib/animation/keyframe-query.ts b/apps/web/src/lib/animation/keyframe-query.ts
index 753e31d0..637b0289 100644
--- a/apps/web/src/lib/animation/keyframe-query.ts
+++ b/apps/web/src/lib/animation/keyframe-query.ts
@@ -20,7 +20,7 @@ export function getElementKeyframes({
if (
!channel ||
channel.keyframes.length === 0 ||
- !isAnimationPath({ propertyPath })
+ !isAnimationPath(propertyPath)
) {
return [];
}
diff --git a/apps/web/src/lib/animation/keyframes.ts b/apps/web/src/lib/animation/keyframes.ts
index 5fa99fb8..13cdf238 100644
--- a/apps/web/src/lib/animation/keyframes.ts
+++ b/apps/web/src/lib/animation/keyframes.ts
@@ -616,8 +616,8 @@ export function splitAnimationsAtTime({
time: splitTime,
fallbackValue: normalizedChannel.keyframes[0].value,
});
- const knownPropertyPath = isAnimationPropertyPath({ propertyPath })
- ? (propertyPath as AnimationPropertyPath)
+ const knownPropertyPath = isAnimationPropertyPath(propertyPath)
+ ? propertyPath
: null;
const boundaryInterpolation = knownPropertyPath
? getDefaultInterpolationForProperty({
diff --git a/apps/web/src/lib/animation/property-registry.ts b/apps/web/src/lib/animation/property-registry.ts
index 0b28e783..46990754 100644
--- a/apps/web/src/lib/animation/property-registry.ts
+++ b/apps/web/src/lib/animation/property-registry.ts
@@ -245,12 +245,10 @@ const ANIMATION_PROPERTY_REGISTRY: Record<
},
};
-export function isAnimationPropertyPath({
- propertyPath,
-}: {
- propertyPath: string;
-}): boolean {
- return propertyPath in ANIMATION_PROPERTY_REGISTRY;
+export function isAnimationPropertyPath(
+ propertyPath: string,
+): propertyPath is AnimationPropertyPath {
+ return Object.hasOwn(ANIMATION_PROPERTY_REGISTRY, propertyPath);
}
export function getAnimationPropertyDefinition({
diff --git a/apps/web/src/lib/animation/target-resolver.ts b/apps/web/src/lib/animation/target-resolver.ts
index e801bc68..10b83775 100644
--- a/apps/web/src/lib/animation/target-resolver.ts
+++ b/apps/web/src/lib/animation/target-resolver.ts
@@ -204,15 +204,13 @@ function buildEffectParamDescriptor({
};
}
-export function isAnimationPath({
- propertyPath,
-}: {
- propertyPath: string;
-}): propertyPath is AnimationPath {
+export function isAnimationPath(
+ propertyPath: string,
+): propertyPath is AnimationPath {
return (
- isAnimationPropertyPath({ propertyPath }) ||
- isGraphicParamPath({ propertyPath }) ||
- isEffectParamPath({ propertyPath })
+ isAnimationPropertyPath(propertyPath) ||
+ isGraphicParamPath(propertyPath) ||
+ isEffectParamPath(propertyPath)
);
}
@@ -223,7 +221,7 @@ export function resolveAnimationTarget({
element: TimelineElement;
path: AnimationPath;
}): AnimationPathDescriptor | null {
- if (isAnimationPropertyPath({ propertyPath: path })) {
+ if (isAnimationPropertyPath(path)) {
const propertyDefinition = getAnimationPropertyDefinition({
propertyPath: path,
});
diff --git a/apps/web/src/lib/timeline/audio-state.ts b/apps/web/src/lib/timeline/audio-state.ts
index 90d34668..4bc3a7e1 100644
--- a/apps/web/src/lib/timeline/audio-state.ts
+++ b/apps/web/src/lib/timeline/audio-state.ts
@@ -18,7 +18,7 @@ export function clampDb(value: number): number {
}
export function dBToLinear(db: number): number {
- return Math.pow(10, clampDb(db) / 20);
+ return 10 ** (clampDb(db) / 20);
}
export function hasAnimatedVolume({
diff --git a/apps/web/src/lib/timeline/drop-utils.ts b/apps/web/src/lib/timeline/drop-utils.ts
index b4f53948..d8206474 100644
--- a/apps/web/src/lib/timeline/drop-utils.ts
+++ b/apps/web/src/lib/timeline/drop-utils.ts
@@ -6,7 +6,11 @@ import type {
import { TRACK_CONFIG, TRACK_GAP } from "@/constants/timeline-constants";
import { wouldElementOverlap } from "./element-utils";
import type { ComputeDropTargetParams, DropTarget } from "@/lib/timeline";
-import { isMainTrack, enforceMainTrackStart } from "./track-utils";
+import {
+ canElementGoOnTrack,
+ isMainTrack,
+ enforceMainTrackStart,
+} from "./track-utils";
function findElementAtPosition({
mouseX,
@@ -85,14 +89,7 @@ function isCompatible({
elementType: ElementType;
trackType: TimelineTrack["type"];
}): boolean {
- if (elementType === "text") return trackType === "text";
- if (elementType === "audio") return trackType === "audio";
- if (elementType === "sticker") return trackType === "sticker";
- if (elementType === "effect") return trackType === "effect";
- if (elementType === "video" || elementType === "image") {
- return trackType === "video";
- }
- return false;
+ return canElementGoOnTrack({ elementType, trackType });
}
function getMainTrackIndex({ tracks }: { tracks: TimelineTrack[] }): number {