diff options
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/app/layout.tsx | 2 | ||||
| -rw-r--r-- | apps/web/src/app/projects/page.tsx | 7 | ||||
| -rw-r--r-- | apps/web/src/components/editor/panels/timeline/index.tsx | 1 | ||||
| -rw-r--r-- | apps/web/src/components/ui/number-field.tsx | 47 | ||||
| -rw-r--r-- | apps/web/src/hooks/use-keyboard-shortcuts-help.ts | 26 | ||||
| -rw-r--r-- | apps/web/src/lib/actions/definitions.ts | 87 | ||||
| -rw-r--r-- | apps/web/src/lib/animation/effect-param-channel.ts | 10 | ||||
| -rw-r--r-- | apps/web/src/lib/animation/graphic-param-channel.ts | 10 | ||||
| -rw-r--r-- | apps/web/src/lib/animation/keyframe-query.ts | 2 | ||||
| -rw-r--r-- | apps/web/src/lib/animation/keyframes.ts | 4 | ||||
| -rw-r--r-- | apps/web/src/lib/animation/property-registry.ts | 10 | ||||
| -rw-r--r-- | apps/web/src/lib/animation/target-resolver.ts | 16 | ||||
| -rw-r--r-- | apps/web/src/lib/timeline/audio-state.ts | 2 | ||||
| -rw-r--r-- | apps/web/src/lib/timeline/drop-utils.ts | 15 |
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 { |
