fix: resolve typescript/lint errors

This commit is contained in:
Maze Winther
2026-03-26 13:52:30 +01:00
parent 08f5b92c3d
commit b62833384d
14 changed files with 127 additions and 112 deletions
+4 -3
View File
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
import type { KeyboardEvent, MouseEvent } from "react"; import type { KeyboardEvent, MouseEvent } from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import type { EditorCore } from "@/core";
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog"; import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
import { StoragePersistenceDialog } from "@/components/editor/dialogs/storage-persistence-dialog"; import { StoragePersistenceDialog } from "@/components/editor/dialogs/storage-persistence-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -359,7 +360,7 @@ async function deleteProjects({
editor, editor,
ids, ids,
}: { }: {
editor: ReturnType<typeof useEditor>; editor: EditorCore;
ids: string[]; ids: string[];
}) { }) {
await editor.project.deleteProjects({ ids }); await editor.project.deleteProjects({ ids });
@@ -369,7 +370,7 @@ async function duplicateProjects({
editor, editor,
ids, ids,
}: { }: {
editor: ReturnType<typeof useEditor>; editor: EditorCore;
ids: string[]; ids: string[];
}) { }) {
await editor.project.duplicateProjects({ ids }); await editor.project.duplicateProjects({ ids });
@@ -380,7 +381,7 @@ async function renameProject({
id, id,
name, name,
}: { }: {
editor: ReturnType<typeof useEditor>; editor: EditorCore;
id: string; id: string;
name: string; name: string;
}) { }) {
@@ -551,7 +551,6 @@ export function Timeline() {
timelineRef={timelineRef} timelineRef={timelineRef}
tracksScrollRef={tracksScrollRef} tracksScrollRef={tracksScrollRef}
isVisible={showSnapIndicator} isVisible={showSnapIndicator}
tracks={tracks}
/> />
</div> </div>
</section> </section>
+30 -17
View File
@@ -134,20 +134,28 @@ function NumberField({
ref, ref,
...props ...props
}: NumberFieldProps & { ref?: React.Ref<HTMLInputElement> }) { }: NumberFieldProps & { ref?: React.Ref<HTMLInputElement> }) {
const iconRef = useRef<HTMLSpanElement>(null); const iconRef = useRef<HTMLButtonElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const ghostRef = useRef<HTMLSpanElement>(null); const ghostRef = useRef<HTMLSpanElement>(null);
const startValueRef = useRef(0); const startValueRef = useRef(0);
const cumulativeDeltaRef = useRef(0); const cumulativeDeltaRef = useRef(0);
const [isInputFocused, setIsInputFocused] = useState(false); const [isInputFocused, setIsInputFocused] = useState(false);
const [suffixLeft, setSuffixLeft] = useState(0); const [suffixLeft, setSuffixLeft] = useState(0);
const ghostValue = Array.isArray(value) ? value.join(", ") : String(value ?? "");
useLayoutEffect(() => { 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 = const paddingLeft =
parseFloat(getComputedStyle(inputRef.current).paddingLeft) || 0; parseFloat(getComputedStyle(inputRef.current).paddingLeft) || 0;
setSuffixLeft(paddingLeft + ghostRef.current.offsetWidth); setSuffixLeft(paddingLeft + ghostRef.current.offsetWidth);
}, [value, suffix]); }, [ghostValue, suffix]);
const { containerRef: wrapperRef } = useFocusLock<HTMLDivElement>({ const { containerRef: wrapperRef } = useFocusLock<HTMLDivElement>({
isActive: isInputFocused, isActive: isInputFocused,
@@ -243,19 +251,24 @@ function NumberField({
className, className,
)} )}
> >
{icon && ( {icon &&
<span (canScrub ? (
ref={iconRef} <button
className={cn( ref={iconRef}
"text-muted-foreground [&_svg]:size-3.5! shrink-0 select-none pl-2.5 text-sm leading-none", type="button"
canScrub && "cursor-ew-resize", aria-label="Drag to adjust value"
)} disabled={disabled}
onMouseDown={canScrub ? (event) => event.preventDefault() : undefined} className="text-muted-foreground [&_svg]:size-3.5! shrink-0 select-none pl-2.5 text-sm leading-none cursor-ew-resize"
onPointerDown={canScrub ? handleIconPointerDown : undefined} onMouseDown={(event) => event.preventDefault()}
> onPointerDown={handleIconPointerDown}
{icon} >
</span> {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 <span
className={cn( className={cn(
"relative flex flex-1 min-w-0 items-center", "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" className="invisible absolute text-sm leading-none whitespace-pre pointer-events-none"
aria-hidden="true" aria-hidden="true"
> >
{value} {ghostValue}
</span> </span>
<span <span
className={cn( className={cn(
@@ -2,7 +2,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useKeybindingsStore } from "@/stores/keybindings-store"; import { useKeybindingsStore } from "@/stores/keybindings-store";
import { ACTIONS, type TAction } from "@/lib/actions"; import { ACTIONS, type TActionWithOptionalArgs } from "@/lib/actions";
import { import {
getPlatformAlternateKey, getPlatformAlternateKey,
getPlatformSpecialKey, getPlatformSpecialKey,
@@ -13,7 +13,7 @@ export interface KeyboardShortcut {
keys: string[]; keys: string[];
description: string; description: string;
category: string; category: string;
action: TAction; action: TActionWithOptionalArgs;
icon?: React.ReactNode; icon?: React.ReactNode;
} }
@@ -40,9 +40,11 @@ export function useKeyboardShortcutsHelp() {
const shortcuts = useMemo(() => { const shortcuts = useMemo(() => {
const result: KeyboardShortcut[] = []; 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 (action) {
if (!actionToKeys[action]) { if (!actionToKeys[action]) {
actionToKeys[action] = []; actionToKeys[action] = [];
@@ -51,16 +53,16 @@ export function useKeyboardShortcutsHelp() {
} }
} }
for (const [actionId, keys] of Object.entries(actionToKeys)) { for (const [action, keys] of Object.entries(actionToKeys) as Array<
if (!isAction(actionId)) continue; [TActionWithOptionalArgs, string[]]
>) {
const actionDef = ACTIONS[actionId]; const actionDef = ACTIONS[action];
result.push({ result.push({
id: actionId, id: action,
keys, keys,
description: actionDef.description, description: actionDef.description,
category: actionDef.category, category: actionDef.category,
action: actionId, action,
}); });
} }
@@ -76,7 +78,3 @@ export function useKeyboardShortcutsHelp() {
shortcuts, shortcuts,
}; };
} }
function isAction(id: string): id is TAction {
return id in ACTIONS;
}
+51 -36
View File
@@ -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 = export type TActionCategory =
| "playback" | "playback"
@@ -10,18 +14,20 @@ export type TActionCategory =
| "controls" | "controls"
| "assets"; | "assets";
export interface TActionDefinition { export interface TActionBaseDefinition {
description: string; description: string;
category: TActionCategory; category: TActionCategory;
defaultShortcuts?: ShortcutKey[];
args?: Record<string, unknown>; args?: Record<string, unknown>;
} }
export interface TActionDefinition extends TActionBaseDefinition {
defaultShortcuts?: readonly ShortcutKey[];
}
export const ACTIONS = { export const ACTIONS = {
"toggle-play": { "toggle-play": {
description: "Play/Pause", description: "Play/Pause",
category: "playback", category: "playback",
defaultShortcuts: ["space", "k"],
}, },
"stop-playback": { "stop-playback": {
description: "Stop playback", description: "Stop playback",
@@ -30,81 +36,66 @@ export const ACTIONS = {
"seek-forward": { "seek-forward": {
description: "Seek forward 1 second", description: "Seek forward 1 second",
category: "playback", category: "playback",
defaultShortcuts: ["l"],
args: { seconds: "number" }, args: { seconds: "number" },
}, },
"seek-backward": { "seek-backward": {
description: "Seek backward 1 second", description: "Seek backward 1 second",
category: "playback", category: "playback",
defaultShortcuts: ["j"],
args: { seconds: "number" }, args: { seconds: "number" },
}, },
"frame-step-forward": { "frame-step-forward": {
description: "Frame step forward", description: "Frame step forward",
category: "navigation", category: "navigation",
defaultShortcuts: ["right"],
}, },
"frame-step-backward": { "frame-step-backward": {
description: "Frame step backward", description: "Frame step backward",
category: "navigation", category: "navigation",
defaultShortcuts: ["left"],
}, },
"jump-forward": { "jump-forward": {
description: "Jump forward 5 seconds", description: "Jump forward 5 seconds",
category: "navigation", category: "navigation",
defaultShortcuts: ["shift+right"],
args: { seconds: "number" }, args: { seconds: "number" },
}, },
"jump-backward": { "jump-backward": {
description: "Jump backward 5 seconds", description: "Jump backward 5 seconds",
category: "navigation", category: "navigation",
defaultShortcuts: ["shift+left"],
args: { seconds: "number" }, args: { seconds: "number" },
}, },
"goto-start": { "goto-start": {
description: "Go to timeline start", description: "Go to timeline start",
category: "navigation", category: "navigation",
defaultShortcuts: ["home", "enter"],
}, },
"goto-end": { "goto-end": {
description: "Go to timeline end", description: "Go to timeline end",
category: "navigation", category: "navigation",
defaultShortcuts: ["end"],
}, },
split: { split: {
description: "Split elements at playhead", description: "Split elements at playhead",
category: "editing", category: "editing",
defaultShortcuts: ["s"],
}, },
"split-left": { "split-left": {
description: "Split and remove left", description: "Split and remove left",
category: "editing", category: "editing",
defaultShortcuts: ["q"],
}, },
"split-right": { "split-right": {
description: "Split and remove right", description: "Split and remove right",
category: "editing", category: "editing",
defaultShortcuts: ["w"],
}, },
"delete-selected": { "delete-selected": {
description: "Delete selected elements", description: "Delete selected elements",
category: "editing", category: "editing",
defaultShortcuts: ["backspace", "delete"],
}, },
"copy-selected": { "copy-selected": {
description: "Copy selected elements", description: "Copy selected elements",
category: "editing", category: "editing",
defaultShortcuts: ["ctrl+c"],
}, },
"paste-copied": { "paste-copied": {
description: "Paste elements at playhead", description: "Paste elements at playhead",
category: "editing", category: "editing",
defaultShortcuts: ["ctrl+v"],
}, },
"toggle-snapping": { "toggle-snapping": {
description: "Toggle snapping", description: "Toggle snapping",
category: "editing", category: "editing",
defaultShortcuts: ["n"],
}, },
"toggle-ripple-editing": { "toggle-ripple-editing": {
description: "Toggle ripple editing", description: "Toggle ripple editing",
@@ -113,12 +104,10 @@ export const ACTIONS = {
"select-all": { "select-all": {
description: "Select all elements", description: "Select all elements",
category: "selection", category: "selection",
defaultShortcuts: ["ctrl+a"],
}, },
"cancel-interaction": { "cancel-interaction": {
description: "Cancel current interaction", description: "Cancel current interaction",
category: "controls", category: "controls",
defaultShortcuts: ["escape"],
}, },
"deselect-all": { "deselect-all": {
description: "Deselect all elements", description: "Deselect all elements",
@@ -127,7 +116,6 @@ export const ACTIONS = {
"duplicate-selected": { "duplicate-selected": {
description: "Duplicate selected element", description: "Duplicate selected element",
category: "selection", category: "selection",
defaultShortcuts: ["ctrl+d"],
}, },
"toggle-elements-muted-selected": { "toggle-elements-muted-selected": {
description: "Mute/unmute selected elements", description: "Mute/unmute selected elements",
@@ -144,12 +132,10 @@ export const ACTIONS = {
undo: { undo: {
description: "Undo", description: "Undo",
category: "history", category: "history",
defaultShortcuts: ["ctrl+z"],
}, },
redo: { redo: {
description: "Redo", description: "Redo",
category: "history", category: "history",
defaultShortcuts: ["ctrl+shift+z", "ctrl+y"],
}, },
"remove-media-asset": { "remove-media-asset": {
description: "Remove media asset", description: "Remove media asset",
@@ -161,30 +147,59 @@ export const ACTIONS = {
category: "assets", category: "assets",
args: { projectId: "string", assetIds: "string[]" }, args: { projectId: "string", assetIds: "string[]" },
}, },
} as const satisfies Record<string, TActionDefinition>; } as const satisfies Record<string, TActionBaseDefinition>;
export type TAction = keyof typeof ACTIONS; 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({ export function getActionDefinition({
action, action,
}: { }: {
action: TAction; action: TAction;
}): TActionDefinition { }): TActionDefinition {
return ACTIONS[action]; return {
...ACTIONS[action],
defaultShortcuts: ACTION_DEFAULT_SHORTCUTS_BY_ACTION[action],
};
} }
export function getDefaultShortcuts(): Record<ShortcutKey, TAction> { export function getDefaultShortcuts(): KeybindingConfig {
const shortcuts: Record<string, TAction> = {}; const shortcuts: KeybindingConfig = {};
for (const [action, def] of Object.entries(ACTIONS) as Array< for (const [action, defaultShortcuts] of Object.entries(
[TAction, TActionDefinition] ACTION_DEFAULT_SHORTCUTS,
>) { ) as Array<[TActionWithOptionalArgs, readonly ShortcutKey[]]>) {
if (def.defaultShortcuts) { for (const shortcut of defaultShortcuts) {
for (const shortcut of def.defaultShortcuts) { shortcuts[shortcut] = action;
shortcuts[shortcut] = action;
}
} }
} }
return shortcuts as Record<ShortcutKey, TAction>; return shortcuts;
} }
@@ -26,11 +26,9 @@ export function buildEffectParamPath({
return `${EFFECT_PARAM_PATH_PREFIX}${effectId}${EFFECT_PARAM_PATH_SUFFIX}${paramKey}`; return `${EFFECT_PARAM_PATH_PREFIX}${effectId}${EFFECT_PARAM_PATH_SUFFIX}${paramKey}`;
} }
export function isEffectParamPath({ export function isEffectParamPath(
propertyPath, propertyPath: string,
}: { ): propertyPath is EffectParamPath {
propertyPath: string;
}): propertyPath is EffectParamPath {
return ( return (
propertyPath.startsWith(EFFECT_PARAM_PATH_PREFIX) && propertyPath.startsWith(EFFECT_PARAM_PATH_PREFIX) &&
propertyPath.includes(EFFECT_PARAM_PATH_SUFFIX) propertyPath.includes(EFFECT_PARAM_PATH_SUFFIX)
@@ -42,7 +40,7 @@ export function parseEffectParamPath({
}: { }: {
propertyPath: string; propertyPath: string;
}): { effectId: string; paramKey: string } | null { }): { effectId: string; paramKey: string } | null {
if (!isEffectParamPath({ propertyPath })) { if (!isEffectParamPath(propertyPath)) {
return null; return null;
} }
@@ -20,11 +20,9 @@ export function buildGraphicParamPath({
return `${GRAPHIC_PARAM_PATH_PREFIX}${paramKey}`; return `${GRAPHIC_PARAM_PATH_PREFIX}${paramKey}`;
} }
export function isGraphicParamPath({ export function isGraphicParamPath(
propertyPath, propertyPath: string,
}: { ): propertyPath is GraphicParamPath {
propertyPath: string;
}): propertyPath is GraphicParamPath {
return propertyPath.startsWith(GRAPHIC_PARAM_PATH_PREFIX); return propertyPath.startsWith(GRAPHIC_PARAM_PATH_PREFIX);
} }
@@ -33,7 +31,7 @@ export function parseGraphicParamPath({
}: { }: {
propertyPath: string; propertyPath: string;
}): { paramKey: string } | null { }): { paramKey: string } | null {
if (!isGraphicParamPath({ propertyPath })) { if (!isGraphicParamPath(propertyPath)) {
return null; return null;
} }
+1 -1
View File
@@ -20,7 +20,7 @@ export function getElementKeyframes({
if ( if (
!channel || !channel ||
channel.keyframes.length === 0 || channel.keyframes.length === 0 ||
!isAnimationPath({ propertyPath }) !isAnimationPath(propertyPath)
) { ) {
return []; return [];
} }
+2 -2
View File
@@ -616,8 +616,8 @@ export function splitAnimationsAtTime({
time: splitTime, time: splitTime,
fallbackValue: normalizedChannel.keyframes[0].value, fallbackValue: normalizedChannel.keyframes[0].value,
}); });
const knownPropertyPath = isAnimationPropertyPath({ propertyPath }) const knownPropertyPath = isAnimationPropertyPath(propertyPath)
? (propertyPath as AnimationPropertyPath) ? propertyPath
: null; : null;
const boundaryInterpolation = knownPropertyPath const boundaryInterpolation = knownPropertyPath
? getDefaultInterpolationForProperty({ ? getDefaultInterpolationForProperty({
@@ -245,12 +245,10 @@ const ANIMATION_PROPERTY_REGISTRY: Record<
}, },
}; };
export function isAnimationPropertyPath({ export function isAnimationPropertyPath(
propertyPath, propertyPath: string,
}: { ): propertyPath is AnimationPropertyPath {
propertyPath: string; return Object.hasOwn(ANIMATION_PROPERTY_REGISTRY, propertyPath);
}): boolean {
return propertyPath in ANIMATION_PROPERTY_REGISTRY;
} }
export function getAnimationPropertyDefinition({ export function getAnimationPropertyDefinition({
@@ -204,15 +204,13 @@ function buildEffectParamDescriptor({
}; };
} }
export function isAnimationPath({ export function isAnimationPath(
propertyPath, propertyPath: string,
}: { ): propertyPath is AnimationPath {
propertyPath: string;
}): propertyPath is AnimationPath {
return ( return (
isAnimationPropertyPath({ propertyPath }) || isAnimationPropertyPath(propertyPath) ||
isGraphicParamPath({ propertyPath }) || isGraphicParamPath(propertyPath) ||
isEffectParamPath({ propertyPath }) isEffectParamPath(propertyPath)
); );
} }
@@ -223,7 +221,7 @@ export function resolveAnimationTarget({
element: TimelineElement; element: TimelineElement;
path: AnimationPath; path: AnimationPath;
}): AnimationPathDescriptor | null { }): AnimationPathDescriptor | null {
if (isAnimationPropertyPath({ propertyPath: path })) { if (isAnimationPropertyPath(path)) {
const propertyDefinition = getAnimationPropertyDefinition({ const propertyDefinition = getAnimationPropertyDefinition({
propertyPath: path, propertyPath: path,
}); });
+1 -1
View File
@@ -18,7 +18,7 @@ export function clampDb(value: number): number {
} }
export function dBToLinear(db: number): number { export function dBToLinear(db: number): number {
return Math.pow(10, clampDb(db) / 20); return 10 ** (clampDb(db) / 20);
} }
export function hasAnimatedVolume({ export function hasAnimatedVolume({
+6 -9
View File
@@ -6,7 +6,11 @@ import type {
import { TRACK_CONFIG, TRACK_GAP } from "@/constants/timeline-constants"; import { TRACK_CONFIG, TRACK_GAP } from "@/constants/timeline-constants";
import { wouldElementOverlap } from "./element-utils"; import { wouldElementOverlap } from "./element-utils";
import type { ComputeDropTargetParams, DropTarget } from "@/lib/timeline"; import type { ComputeDropTargetParams, DropTarget } from "@/lib/timeline";
import { isMainTrack, enforceMainTrackStart } from "./track-utils"; import {
canElementGoOnTrack,
isMainTrack,
enforceMainTrackStart,
} from "./track-utils";
function findElementAtPosition({ function findElementAtPosition({
mouseX, mouseX,
@@ -85,14 +89,7 @@ function isCompatible({
elementType: ElementType; elementType: ElementType;
trackType: TimelineTrack["type"]; trackType: TimelineTrack["type"];
}): boolean { }): boolean {
if (elementType === "text") return trackType === "text"; return canElementGoOnTrack({ elementType, trackType });
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;
} }
function getMainTrackIndex({ tracks }: { tracks: TimelineTrack[] }): number { function getMainTrackIndex({ tracks }: { tracks: TimelineTrack[] }): number {