diff --git a/.cursor/rules/codebase-index.mdc b/.cursor/rules/codebase-index.mdc index d168069d..fe0b4c0b 100644 --- a/.cursor/rules/codebase-index.mdc +++ b/.cursor/rules/codebase-index.mdc @@ -1,1715 +1,1713 @@ ---- -alwaysApply: true ---- - -# video-editor-oss Codebase Index - -**This file provides an index of exported functions, types, interfaces, classes, and constants in your codebase.** - -Updated in real-time by Twiggy. Use this to discover existing utilities and avoid duplicating code. - -## How to Use - -When implementing new features: -1. Check if similar functionality already exists -2. Reuse existing types and utilities -3. Understand the API surface of your codebase - -```typescript -## apps/web/src/constants - -editor-constants.ts - export const PLATFORM_LAYOUTS: Record - export const PANEL_CONFIG - -export-constants.ts - export const DEFAULT_EXPORT_OPTIONS - export const EXPORT_MIME_TYPES - -font-constants.ts - export interface FontOption { - value: string - label: string - category: "system" | "google" | "custom" - weights?: number[] - hasClassName?: boolean - } - export const FONT_OPTIONS: FontOption[] - export const DEFAULT_FONT - export type FontFamily = (typeof FONT_OPTIONS)[number]["value"] - export const getFontByValue = (value: string): FontOption | undefined => ... - export const getGoogleFonts = (): FontOption[] => ... - export const getSystemFonts = (): FontOption[] => ... - -language-constants.ts - export const LANGUAGES - -project-constants.ts - export const DEFAULT_CANVAS_PRESETS: TCanvasSize[] - export const FPS_PRESETS - export const BLUR_INTENSITY_PRESETS: { label: string; value: number }[] - export const DEFAULT_CANVAS_SIZE: TCanvasSize - export const DEFAULT_FPS - export const DEFAULT_BLUR_INTENSITY - export const DEFAULT_COLOR - -site-constants.ts - export const SITE_URL - export const SITE_INFO - export type ExternalTool = { - name: string; - description: string; - url: string; - icon: React.ElementType; - } - export const EXTERNAL_TOOLS: ExternalTool[] - export const DEFAULT_LOGO_URL - export const SOCIAL_LINKS - export type Sponsor = { - name: string; - url: string; - logo: string; - description: string; - } - export const SPONSORS: Sponsor[] - -stickers-constants.ts - export const STICKER_CATEGORIES - export const STICKER_CATEGORY_CONFIG: Record< - (typeof STICKER_CATEGORIES)[number], - string | undefined - > - -text-constants.ts - export const DEFAULT_TEXT_ELEMENT: Omit - -timeline-constants.tsx - export const TRACK_COLORS: Record - export const TRACK_HEIGHTS: Record - export const TRACK_GAP - export const TIMELINE_CONSTANTS - export const TRACK_ICONS: Record - -transcription-constants.ts - export const TRANSCRIPTION_LANGUAGES - export const TRANSCRIPTION_MODELS: TranscriptionModel[] - export const DEFAULT_TRANSCRIPTION_MODEL: TranscriptionModelId - export const DEFAULT_CHUNK_LENGTH_SECONDS - export const DEFAULT_STRIDE_SECONDS - export const DEFAULT_WORDS_PER_CAPTION - export const MIN_CAPTION_DURATION_SECONDS - -## apps/web/src/core - -index.ts - export class EditorCore { - instance: EditorCore | null - command: CommandManager - playback: PlaybackManager - timeline: TimelineManager - scenes: ScenesManager - project: ProjectManager - media: MediaManager - renderer: RendererManager - save: SaveManager - static getInstance(): EditorCore - static reset(): void - } - -## apps/web/src/hooks - -use-editor.ts - export function useEditor(): EditorCore - -use-file-upload.ts - export function useFileUpload({ - accept, - multiple, - onFilesSelected, - }: UseFileUploadOptions = {}) - -use-infinite-scroll.ts - export function useInfiniteScroll({ - onLoadMore, - hasMore, - isLoading, - threshold = 200, - enabled = true, - }: UseInfiniteScrollOptions) - -use-keybindings.ts - export function useKeybindingsListener() - export function useKeybindingDisabler() - export const bindings - -use-keyboard-shortcuts-help.ts - export interface KeyboardShortcut { - id: string - keys: string[] - description: string - category: string - action: TAction - icon?: React.ReactNode - } - export function useKeyboardShortcutsHelp() - -use-mobile.ts - export function useIsMobile() - -use-raf-loop.ts - export function useRafLoop(callback: ({ time }: { time: number }) => void) - -use-reveal-item.ts - export function useRevealItem( - highlightId: string | null, - onClearHighlight: () => void, - highlightDuration = 1000, - ) - -use-sound-search.ts - export function useSoundSearch({ - query, - commercialOnly, - }: { - query: string; - commercialOnly: boolean; - }) - -## apps/web/src/hooks/actions - -use-action-handler.ts - export function useActionHandler( - action: A, - handler: TActionFunc, - isActive: TActionHandlerOptions, - ) - -use-editor-actions.ts - export function useEditorActions() - -## apps/web/src/hooks/timeline - -use-edge-auto-scroll.ts - export function useEdgeAutoScroll({ - isActive, - getMouseClientX, - rulerScrollRef, - tracksScrollRef, - contentWidth, - edgeThreshold = 100, - maxScrollSpeed = 15, - }: UseEdgeAutoScrollParams): void - -use-scroll-sync.ts - export function useScrollSync({ - rulerScrollRef, - tracksScrollRef, - trackLabelsScrollRef, - bookmarksScrollRef, - }: UseScrollSyncProps) - -use-selection-box.ts - export function useSelectionBox({ - containerRef, - onSelectionComplete, - isEnabled = true, - tracksScrollRef, - zoomLevel, - }: UseSelectionBoxProps) - -use-snap-indicator-position.ts - export function useSnapIndicatorPosition({ - snapPoint, - zoomLevel, - tracks, - timelineRef, - trackLabelsRef, - tracksScrollRef, - }: UseSnapIndicatorPositionParams): SnapIndicatorPosition - -use-timeline-drag-drop.ts - export function useTimelineDragDrop({ - containerRef, - zoomLevel, - isSnappingEnabled = true, - }: UseTimelineDragDropProps) - -use-timeline-playhead.ts - export function useTimelinePlayhead({ - zoomLevel, - rulerRef, - rulerScrollRef, - tracksScrollRef, - playheadRef, - }: UseTimelinePlayheadProps) - -use-timeline-seek.ts - export function useTimelineSeek({ - playheadRef, - trackLabelsRef, - rulerScrollRef, - tracksScrollRef, - zoomLevel, - duration, - isSelecting, - clearSelectedElements, - seek, - }: UseTimelineSeekProps) - -use-timeline-snapping.ts - export interface SnapPoint { - time: number - type: "element-start" | "element-end" | "playhead" - elementId?: string - trackId?: string - } - export interface SnapResult { - snappedTime: number - snapPoint: SnapPoint | null - snapDistance: number - } - export interface UseTimelineSnappingOptions { - snapThreshold?: number - enableElementSnapping?: boolean - enablePlayheadSnapping?: boolean - } - export function useTimelineSnapping({ - snapThreshold = 10, - enableElementSnapping = true, - enablePlayheadSnapping = true, - }: UseTimelineSnappingOptions = {}) - -use-timeline-zoom.ts - export function useTimelineZoom({ - containerRef, - isInTimeline = false, - minZoom = TIMELINE_CONSTANTS.ZOOM_MIN, - }: UseTimelineZoomProps): UseTimelineZoomReturn - -## apps/web/src/hooks/timeline/element - -use-element-interaction.ts - export function useElementInteraction({ - zoomLevel, - timelineRef, - tracksContainerRef, - tracksScrollRef, - snappingEnabled, - onSnapPointChange, - }: UseElementInteractionProps) - -use-element-resize.ts - export interface ResizeState { - elementId: string - side: "left" | "right" - startX: number - initialTrimStart: number - initialTrimEnd: number - initialStartTime: number - initialDuration: number - } - export function useTimelineElementResize({ - element, - track, - zoomLevel, - onSnapPointChange, - onResizeStateChange, - }: UseTimelineElementResizeProps) - -use-element-selection.ts - export function useElementSelection() - -## apps/web/src/lib - -drag-data.ts - export function setDragData({ - dataTransfer, - dragData, - }: { - dataTransfer: DataTransfer; - dragData: TimelineDragData; - }): void - export function getDragData({ - dataTransfer, - }: { - dataTransfer: DataTransfer; - }): TimelineDragData | null - export function hasDragData({ - dataTransfer, - }: { - dataTransfer: DataTransfer; - }): boolean - export function clearDragData(): void - -export.ts - export function getExportMimeType({ - format, - }: { - format: ExportFormat; - }): string - export function getExportFileExtension({ - format, - }: { - format: ExportFormat; - }): string - -iconify-api.ts - export const ICONIFY_HOSTS - export interface IconSet { - prefix: string - name: string - total: number - author?: { - name: string; - url?: string; - } - license?: { - title: string; - spdx?: string; - url?: string; - } - samples?: string[] - category?: string - palette?: boolean - } - export interface IconSearchResult { - icons: string[] - total: number - limit: number - start: number - collections: Record - } - export interface CollectionInfo { - prefix: string - total: number - title?: string - uncategorized?: string[] - categories?: Record - hidden?: string[] - aliases?: Record - } - export function getCollections( - category?: string, - ): Promise> - export function getCollection( - prefix: string, - ): Promise - export function searchIcons( - query: string, - limit: number = 64, - prefixes?: string[], - category?: string, - ): Promise - export function buildIconSvgUrl( - host: string, - iconName: string, - params?: { - color?: string; - width?: number; - height?: number; - flip?: "horizontal" | "vertical" | "horizontal,vertical"; - rotate?: number | string; - }, - ): string - export function getIconSvgUrl( - iconName: string, - params?: Parameters[2], - ): string - export function downloadSvgAsText( - iconName: string, - params?: Parameters[1], - ): Promise - export function svgToFile(svgText: string, fileName: string): File - export const POPULAR_COLLECTIONS - export function getCategoriesFromCollections( - collections: Record, - ): string[] - -rate-limit.ts - export const baseRateLimit - export function checkRateLimit({ request }: { request: Request }) - -scenes.ts - export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null - export function ensureMainScene({ scenes }: { scenes: TScene[] }): TScene[] - export function buildDefaultScene({ - name, - isMain, - }: { - name: string; - isMain: boolean; - }): TScene - export function canDeleteScene({ scene }: { scene: TScene }): { - canDelete: boolean; - reason?: string; - } - export function getFallbackSceneAfterDelete({ - scenes, - deletedSceneId, - currentSceneId, - }: { - scenes: TScene[]; - deletedSceneId: string; - currentSceneId: string | null; - }): TScene | null - export function findCurrentScene({ - scenes, - currentSceneId, - }: { - scenes: TScene[]; - currentSceneId: string; - }): TScene | null - export function updateSceneInArray({ - scenes, - sceneId, - updates, - }: { - scenes: TScene[]; - sceneId: string; - updates: Partial; - }): TScene[] - -time.ts - export function roundToFrame({ - time, - fps, - }: { - time: number; - fps: number; - }): number - export function formatTimeCode({ - timeInSeconds, - format = "HH:MM:SS:CS", - fps, - }: { - timeInSeconds: number; - format?: TTimeCode; - fps?: number; - }): string - export function parseTimeCode({ - timeCode, - format = "HH:MM:SS:CS", - fps, - }: { - timeCode: string; - format?: TTimeCode; - fps: number; - }): number | null - export function guessTimeCodeFormat({ - timeCode, - }: { - timeCode: string; - }): TTimeCode | null - export function timeToFrame({ - time, - fps, - }: { - time: number; - fps: number; - }): number - export function frameToTime({ - frame, - fps, - }: { - frame: number; - fps: number; - }): number - export function snapTimeToFrame({ - time, - fps, - }: { - time: number; - fps: number; - }): number - export function getSnappedSeekTime({ - rawTime, - duration, - fps, - }: { - rawTime: number; - duration: number; - fps: number; - }): number - -## apps/web/src/lib/actions - -definitions.ts - export type TActionCategory = | "playback" - | "navigation" - | "editing" - | "selection" - | "history" - | "timeline" - | "controls" - export interface TActionDefinition { - description: string - category: TActionCategory - defaultShortcuts?: ShortcutKey[] - args?: Record - } - export const ACTIONS - export type TAction = keyof typeof ACTIONS - export function getActionDefinition(action: TAction): TActionDefinition - export function getDefaultShortcuts(): Record - -registry.ts - export function bindAction( - action: A, - handler: TActionFunc, - ) - export function unbindAction( - action: A, - handler: TActionFunc, - ) - export const invokeAction = ( - action: A, - args?: TArgOfAction, - trigger?: TInvocationTrigger, - ) => ... - -types.ts - export type TActionArgsMap = { - "seek-forward": { seconds: number } | undefined; - "seek-backward": { seconds: number } | undef... - export type TActionWithArgs = keyof TActionArgsMap - export type TActionWithOptionalArgs = | TActionWithNoArgs - | TKeysWithValueUndefined - export type TActionWithNoArgs = Exclude - export type TArgOfAction = A extends TActionWithArgs - ? TActionArgsMap[A] - : undefined - export type TActionFunc = A extends TActionWithArgs - ? (arg: TArgOfAction, trigger?: TInvocationTrigger) => void - : (_?:... - export type TInvocationTrigger = "keypress" | "mouseclick" - export type TBoundActionList = { - [A in TAction]?: Array>; - } - export type TActionHandlerOptions = | MutableRefObject - | boolean - | undefined - -## apps/web/src/lib/auth - -server.ts - export const auth - export type Auth = typeof auth - -## apps/web/src/lib/blog - -query.ts - export function getPosts() - export function getTags() - export function getSinglePost({ slug }: { slug: string }) - export function getCategories() - export function getAuthors() - export function processHtmlContent({ - html, - }: { - html: string; - }): Promise - -## apps/web/src/lib/db - -index.ts - export const db - -schema.ts - export const users - export const sessions - export const accounts - export const verifications - -## apps/web/src/lib/gradients - -canvas.ts - export function drawCssBackground({ - ctx, - width, - height, - css, - }: { - ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; - width: number; - height: number; - css: string; - }): void - -parser.ts - export type GradientOrientation = LinearOrientation | Array - export type Color = | { type: "hex"; value: string } - | { type: "literal"; value: string } - | { type: "rgb"; value: A... - export type ColorStop = Color & { length?: Distance } - export type GradientAst = { - type: GradientType; - orientation: GradientOrientation | undefined; - colorStops: Array => ... - export const GradientParser - -## apps/web/src/lib/media - -audio.ts - export type CollectedAudioElement = Omit< - AudioElement, - "type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl" - ... - export function createAudioContext(): AudioContext - export interface DecodedAudio { - samples: Float32Array - sampleRate: number - } - export function decodeAudioToFloat32({ - audioBlob, - }: { - audioBlob: Blob; - }): Promise - export function collectAudioElements({ - tracks, - mediaAssets, - audioContext, - }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - audioContext: AudioContext; - }): Promise - export function collectAudioMixSources({ - tracks, - mediaAssets, - }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - }): Promise - export function createTimelineAudioBuffer({ - tracks, - mediaAssets, - duration, - sampleRate = 44100, - }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - duration: number; - sampleRate?: number; - }): Promise - -media-utils.ts - export const SUPPORTS_AUDIO: readonly MediaType[] - export function mediaSupportsAudio({ - media, - }: { - media: MediaAsset | null | undefined; - }): boolean - export const getMediaTypeFromFile = ({ - file, - }: { - file: File; - }): MediaType | null => ... - -mediabunny.ts - export function getVideoInfo({ - videoFile, - }: { - videoFile: File; - }): Promise<{ - duration: number; - width: number; - height: number; - fps: number; - }> - export const extractTimelineAudio = ({ - tracks, - mediaAssets, - totalDuration, - onProgress, - }: { - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - totalDuration: number; - onProgress?: (progress: number) => void; - }): Promise => ... - -processing.ts - export interface ProcessedMediaAsset extends Omit - export function generateThumbnail({ - videoFile, - timeInSeconds, - }: { - videoFile: File; - timeInSeconds: number; - }): Promise - export function processMediaAssets({ - files, - onProgress, - }: { - files: FileList | File[]; - onProgress?: ({ progress }: { progress: number }) => void; - }): Promise - -## apps/web/src/lib/timeline - -bookmarks.ts - export function findBookmarkIndex({ - bookmarks, - frameTime, - }: { - bookmarks: number[]; - frameTime: number; - }): number - export function isBookmarkAtTime({ - bookmarks, - frameTime, - }: { - bookmarks: number[]; - frameTime: number; - }): boolean - export function toggleBookmarkInArray({ - bookmarks, - frameTime, - }: { - bookmarks: number[]; - frameTime: number; - }): number[] - export function removeBookmarkFromArray({ - bookmarks, - frameTime, - }: { - bookmarks: number[]; - frameTime: number; - }): number[] - export function getFrameTime({ - time, - fps, - }: { - time: number; - fps: number; - }): number - -drop-utils.ts - export function computeDropTarget({ - elementType, - mouseX, - mouseY, - tracks, - playheadTime, - isExternalDrop, - elementDuration, - pixelsPerSecond, - zoomLevel, - verticalDragDirection, - startTimeOverride, - excludeElementId, - }: ComputeDropTargetParams): DropTarget - export function getDropLineY({ - dropTarget, - tracks, - }: { - dropTarget: DropTarget; - tracks: TimelineTrack[]; - }): number - -element-utils.ts - export function canElementHaveAudio( - element: TimelineElement, - ) - export function canElementBeHidden( - element: TimelineElement, - ) - export function hasMediaId( - element: TimelineElement, - ) - export function requiresMediaId({ - element, - }: { - element: CreateTimelineElement; - }): boolean - export function checkElementOverlaps({ - elements, - }: { - elements: TimelineElement[]; - }): boolean - export function resolveElementOverlaps({ - elements, - }: { - elements: TimelineElement[]; - }): TimelineElement[] - export function wouldElementOverlap({ - elements, - startTime, - endTime, - excludeElementId, - }: { - elements: TimelineElement[]; - startTime: number; - endTime: number; - excludeElementId?: string; - }): boolean - export function buildTextElement({ - raw, - startTime, - }: { - raw: Partial>; - startTime: number; - }): CreateTimelineElement - export function buildStickerElement({ - iconName, - startTime, - }: { - iconName: string; - startTime: number; - }): CreateStickerElement - export function buildUploadAudioElement({ - mediaId, - name, - duration, - startTime, - buffer, - }: { - mediaId: string; - name: string; - duration: number; - startTime: number; - buffer?: AudioBuffer; - }): CreateUploadAudioElement - export function buildLibraryAudioElement({ - sourceUrl, - name, - duration, - startTime, - buffer, - }: { - sourceUrl: string; - name: string; - duration: number; - startTime: number; - buffer?: AudioBuffer; - }): CreateLibraryAudioElement - -index.ts - export function calculateTotalDuration({ - tracks, - }: { - tracks: TimelineTrack[]; - }): number - -track-utils.ts - export function canTracktHaveAudio( - track: TimelineTrack, - ) - export function canTrackBeHidden( - track: TimelineTrack, - ) - export function getTrackColor({ type }: { type: TrackType }) - export function getTrackClasses({ type }: { type: TrackType }) - export function getTrackHeight({ type }: { type: TrackType }): number - export function getCumulativeHeightBefore({ - tracks, - trackIndex, - }: { - tracks: Array<{ type: TrackType }>; - trackIndex: number; - }): number - export function getTotalTracksHeight({ - tracks, - }: { - tracks: Array<{ type: TrackType }>; - }): number - export function buildEmptyTrack({ - id, - type, - name, - }: { - id: string; - type: TrackType; - name?: string; - }): TimelineTrack - export function getDefaultInsertIndexForTrack({ - tracks, - trackType, - }: { - tracks: TimelineTrack[]; - trackType: TrackType; - }): number - export function getHighestInsertIndexForTrack({ - tracks, - trackType, - }: { - tracks: TimelineTrack[]; - trackType: TrackType; - }): number - export function isMainTrack(track: TimelineTrack) - export function getMainTrack({ - tracks, - }: { - tracks: TimelineTrack[]; - }): TimelineTrack | null - export function ensureMainTrack({ - tracks, - }: { - tracks: TimelineTrack[]; - }): TimelineTrack[] - export function canElementGoOnTrack({ - elementType, - trackType, - }: { - elementType: ElementType; - trackType: TrackType; - }): boolean - export function validateElementTrackCompatibility({ - element, - track, - }: { - element: { type: ElementType }; - track: { type: TrackType }; - }): { isValid: boolean; errorMessage?: string } - -zoom-utils.ts - export function getTimelineZoomMin({ - duration, - containerWidth, - }: { - duration: number; - containerWidth: number | null | undefined; - }): number - -## apps/web/src/lib/transcription - -caption.ts - export function buildCaptionChunks({ - segments, - wordsPerChunk = DEFAULT_WORDS_PER_CAPTION, - minDuration = MIN_CAPTION_DURATION_SECONDS, - }: { - segments: TranscriptionSegment[]; - wordsPerChunk?: number; - minDuration?: number; - }): CaptionChunk[] - -## apps/web/src/services/media - -video-cache.ts - export class VideoCache { - sinks - initPromises - async getFrameAt({ - mediaId, - file, - time, - }: { - mediaId: string; - file: File; - time: number; - }): Promise - clearVideo({ mediaId }: { mediaId: string }): void - clearAll(): void - getStats() - } - export const videoCache - -## apps/web/src/services/renderer - -canvas-renderer.ts - export type CanvasRendererParams = { - width: number; - height: number; - fps: number; - } - export class CanvasRenderer { - canvas: OffscreenCanvas | HTMLCanvasElement - context: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D - width: number - height: number - fps: number - constructor({ width, height, fps }: CanvasRendererParams) - setSize({ width, height }: { width: number; height: number }) - async render({ node, time }: { node: BaseNode; time: number }) - async renderToCanvas({ - node, - time, - targetCanvas, - }: { - node: BaseNode; - time: number; - targetCanvas: HTMLCanvasElement; - }) - } - -scene-builder.ts - export type BuildSceneParams = { - canvasSize: TCanvasSize; - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - duration: numb... - export function buildScene(params: BuildSceneParams) - -scene-exporter.ts - export type ExportFormat = "mp4" | "webm" - export type ExportQuality = "low" | "medium" | "high" | "very_high" - export type SceneExporterEvents = { - progress: [progress: number]; - complete: [buffer: ArrayBuffer]; - error: [error: Error]; - cance... - export class SceneExporter extends EventEmitter { - renderer: CanvasRenderer - format: ExportFormat - quality: ExportQuality - includeAudio: boolean - audioBuffer: AudioBuffer - cancelled - constructor(params: ExportParams) - cancel() - async export(rootNode: RootNode) - } - -## apps/web/src/services/storage - -indexeddb-adapter.ts - export class IndexedDBAdapter implements StorageAdapter { - dbName: string - storeName: string - version: number - constructor(dbName: string, storeName: string, version = 1) - async get(key: string): Promise - async set(key: string, value: T): Promise - async remove(key: string): Promise - async list(): Promise - async getAll(): Promise - async clear(): Promise - } - -opfs-adapter.ts - export class OPFSAdapter implements StorageAdapter { - directoryName: string - constructor(directoryName = "media") - async get(key: string): Promise - async set(key: string, file: File): Promise - async remove(key: string): Promise - async list(): Promise - async clear(): Promise - static isSupported(): boolean - } - -storage-service.ts - export const storageService - -types.ts - export interface StorageAdapter { - get(key: string): Promise - set(key: string, value: T): Promise - remove(key: string): Promise - list(): Promise - clear(): Promise - } - export interface MediaAssetData { - id: string - name: string - type: MediaType - size: number - lastModified: number - width?: number - height?: number - duration?: number - fps?: number - ephemeral?: boolean - thumbnailUrl?: string - sourceStickerIconName?: string - } - export type SerializedScene = Omit & { - createdAt: string; - updatedAt: string; - } - export type SerializedProjectMetadata = Omit< - TProjectMetadata, - "createdAt" | "updatedAt" - > & { - createdAt: string; - updatedAt: string; - } - export type SerializedProject = Omit & { - metadata: SerializedProjectMetadata; - scenes: Serializ... - export interface StorageConfig { - projectsDb: string - mediaDb: string - savedSoundsDb: string - version: number - } - -## apps/web/src/services/transcription - -index.ts - export const transcriptionService - -worker.ts - export type WorkerMessage = | { type: "init"; modelId: string } - | { type: "transcribe"; audio: Float32Array; language: strin... - export type WorkerResponse = | { type: "init-progress"; progress: number } - | { type: "init-complete" } - | { type: "init-error... - -## apps/web/src/stores - -assets-panel-store.ts - export const TAB_KEYS - export type Tab = (typeof TAB_KEYS)[number] - export const tabs - export const useAssetsPanelStore - -editor-store.ts - export const useEditorStore - -keybindings-store.ts - export const defaultKeybindings: KeybindingConfig - export interface KeybindingConflict { - key: ShortcutKey - existingAction: TActionWithOptionalArgs - newAction: TActionWithOptionalArgs - } - export const useKeybindingsStore - -panel-store.ts - export interface PanelSizes { - tools: number - preview: number - properties: number - mainContent: number - timeline: number - } - export type PanelId = keyof PanelSizes - export const usePanelStore - -sounds-store.ts - export const useSoundsStore - -stickers-store.ts - export const useStickersStore - -text-properties-store.ts - export type TextPropertiesTab = "text" | "transform" - export interface TextPropertiesTabMeta { - value: TextPropertiesTab - label: string - } - export const TEXT_PROPERTIES_TABS: ReadonlyArray - export function isTextPropertiesTab(value: string) - export const useTextPropertiesStore - -timeline-store.ts - export const useTimelineStore - -## apps/web/src/types - -assets.ts - export type MediaType = "image" | "video" | "audio" - export interface MediaAsset extends Omit { - file: File - url?: string - } - -blog.ts - export type Post = { - id: string; - slug: string; - title: string; - content: string; - description: string; - coverImage... - export type Pagination = { - limit: number; - currpage: number; - nextPage: number | null; - prevPage: number | null; - totalIt... - export type MarblePostList = { - posts: Post[]; - pagination: Pagination; - } - export type MarblePost = { - post: Post; - } - export type Tag = { - id: string; - name: string; - slug: string; - } - export type MarbleTag = { - tag: Tag; - } - export type MarbleTagList = { - tags: Tag[]; - pagination: Pagination; - } - export type Category = { - id: string; - name: string; - slug: string; - } - export type MarbleCategory = { - category: Category; - } - export type MarbleCategoryList = { - categories: Category[]; - pagination: Pagination; - } - export type Author = { - id: string; - name: string; - image: string; - } - export type MarbleAuthor = { - author: Author; - } - export type MarbleAuthorList = { - authors: Author[]; - pagination: Pagination; - } - -drag.ts - export interface MediaDragData extends BaseDragData { - type: "media" - mediaType: "image" | "video" | "audio" - } - export interface TextDragData extends BaseDragData { - type: "text" - content: string - } - export interface StickerDragData extends BaseDragData { - type: "sticker" - iconName: string - } - export type TimelineDragData = MediaDragData | TextDragData | StickerDragData - -editor.ts - export type TPlatformLayout = "tiktok" - -export.ts - export const EXPORT_QUALITY_VALUES - export const EXPORT_FORMAT_VALUES - export type ExportFormat = (typeof EXPORT_FORMAT_VALUES)[number] - export type ExportQuality = (typeof EXPORT_QUALITY_VALUES)[number] - export interface ExportOptions { - format: ExportFormat - quality: ExportQuality - fps?: number - includeAudio?: boolean - onProgress?: ({ progress }: { progress: number }) => void - onCancel?: () => boolean - } - export interface ExportResult { - success: boolean - buffer?: ArrayBuffer - error?: string - cancelled?: boolean - } - -keybinding.ts - export type ModifierKeys = | "ctrl" - | "alt" - | "shift" - | "ctrl+shift" - | "alt+shift" - | "ctrl+alt" - | "ctrl+alt+shift" - export type Key = | "a" - | "b" - | "c" - | "d" - | "e" - | "f" - | "g" - | "h" - | "i" - | "j" - | "k" - | "l" - | "m" - | "n" - ... - export type ModifierBasedShortcutKey = `${ModifierKeys}+${Key}` - export type SingleCharacterShortcutKey = `${Key}` - export type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey - export type KeybindingConfig = { - [key in ShortcutKey]?: TActionWithOptionalArgs; - } - -language.ts - export type Language = (typeof LANGUAGES)[number] - export type LanguageCode = Language["code"] - -project.ts - export type TBackground = | { - type: "color"; - color: string; - } - | { - type: "blur"; - blurIntensity: number; - } - export interface TCanvasSize { - width: number - height: number - } - export interface TProjectMetadata { - id: string - name: string - thumbnail?: string - createdAt: Date - updatedAt: Date - } - export interface TProjectSettings { - fps: number - canvasSize: TCanvasSize - originalCanvasSize?: TCanvasSize | null - background: TBackground - } - export interface TProject { - metadata: TProjectMetadata - scenes: TScene[] - currentSceneId: string - settings: TProjectSettings - version: number - } - -sounds.ts - export interface SoundEffect { - id: number - name: string - description: string - url: string - previewUrl?: string - downloadUrl?: string - duration: number - filesize: number - type: string - channels: number - bitrate: number - bitdepth: number - samplerate: number - username: string - tags: string[] - license: string - created: string - downloads: number - rating: number - ratingCount: number - } - export interface SavedSound { - id: number - name: string - username: string - previewUrl?: string - downloadUrl?: string - duration: number - tags: string[] - license: string - savedAt: string - } - export interface SavedSoundsData { - sounds: SavedSound[] - lastModified: string - } - -stickers.ts - export type StickerCategory = (typeof STICKER_CATEGORIES)[number] - -time.ts - export type TTimeCode = "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF" - -timeline.ts - export interface TScene { - id: string - name: string - isMain: boolean - tracks: TimelineTrack[] - bookmarks: number[] - createdAt: Date - updatedAt: Date - } - export type TrackType = "video" | "text" | "audio" | "sticker" - export interface VideoTrack extends BaseTrack { - type: "video" - elements: (VideoElement | ImageElement)[] - isMain: boolean - muted: boolean - hidden: boolean - } - export interface TextTrack extends BaseTrack { - type: "text" - elements: TextElement[] - hidden: boolean - } - export interface AudioTrack extends BaseTrack { - type: "audio" - elements: AudioElement[] - muted: boolean - } - export interface StickerTrack extends BaseTrack { - type: "sticker" - elements: StickerElement[] - hidden: boolean - } - export type TimelineTrack = VideoTrack | TextTrack | AudioTrack | StickerTrack - export interface Transform { - scale: number - position: { - x: number; - y: number; - } - rotate: number - } - export interface UploadAudioElement extends BaseAudioElement { - sourceType: "upload" - mediaId: string - } - export interface LibraryAudioElement extends BaseAudioElement { - sourceType: "library" - sourceUrl: string - } - export type AudioElement = UploadAudioElement | LibraryAudioElement - export interface VideoElement extends BaseTimelineElement { - type: "video" - mediaId: string - muted?: boolean - hidden?: boolean - transform: Transform - opacity: number - } - export interface ImageElement extends BaseTimelineElement { - type: "image" - mediaId: string - hidden?: boolean - transform: Transform - opacity: number - } - export interface TextElement extends BaseTimelineElement { - type: "text" - content: string - fontSize: number - fontFamily: string - color: string - backgroundColor: string - textAlign: "left" | "center" | "right" - fontWeight: "normal" | "bold" - fontStyle: "normal" | "italic" - textDecoration: "none" | "underline" | "line-through" - hidden?: boolean - transform: Transform - opacity: number - } - export interface StickerElement extends BaseTimelineElement { - type: "sticker" - iconName: string - hidden?: boolean - transform: Transform - opacity: number - color?: string - } - export type TimelineElement = | AudioElement - | VideoElement - | ImageElement - | TextElement - | StickerElement - export type ElementType = TimelineElement["type"] - export type CreateUploadAudioElement = Omit - export type CreateLibraryAudioElement = Omit - export type CreateAudioElement = | CreateUploadAudioElement - | CreateLibraryAudioElement - export type CreateVideoElement = Omit - export type CreateImageElement = Omit - export type CreateTextElement = Omit - export type CreateStickerElement = Omit - export type CreateTimelineElement = | CreateAudioElement - | CreateVideoElement - | CreateImageElement - | CreateTextElement - | CreateSt... - export interface ElementDragState { - isDragging: boolean - elementId: string | null - trackId: string | null - startMouseX: number - startMouseY: number - startElementTime: number - clickOffsetTime: number - currentTime: number - currentMouseY: number - } - export interface DropTarget { - trackIndex: number - isNewTrack: boolean - insertPosition: "above" | "below" | null - xPosition: number - } - export interface ComputeDropTargetParams { - elementType: ElementType - mouseX: number - mouseY: number - tracks: TimelineTrack[] - playheadTime: number - isExternalDrop: boolean - elementDuration: number - pixelsPerSecond: number - zoomLevel: number - verticalDragDirection?: "up" | "down" | null - startTimeOverride?: number - excludeElementId?: string - } - export interface ClipboardItem { - trackId: string - trackType: TrackType - element: CreateTimelineElement - } - -transcription.ts - export type TranscriptionLanguage = LanguageCode | "auto" - export interface TranscriptionSegment { - text: string - start: number - end: number - } - export interface TranscriptionResult { - text: string - segments: TranscriptionSegment[] - language: string - } - export type TranscriptionStatus = | "idle" - | "loading-model" - | "transcribing" - | "complete" - | "error" - export interface TranscriptionProgress { - status: TranscriptionStatus - progress: number - message?: string - } - export type TranscriptionModelId = | "whisper-tiny" - | "whisper-small" - | "whisper-medium" - | "whisper-large-v3-turbo" - export interface TranscriptionModel { - id: TranscriptionModelId - name: string - huggingFaceId: string - description: string - } - export interface CaptionChunk { - text: string - startTime: number - duration: number - } - -## apps/web/src/utils - -browser.ts - export function isTypableDOMElement({ - element, - }: { - element: HTMLElement; - }): boolean - -date.ts - export function formatDate({ date }: { date: Date }): string - -geometry.ts - export function dimensionToAspectRatio({ - width, - height, - }: { - width: number; - height: number; - }): string - -id.ts - export function generateUUID(): string - -math.ts - export function clamp({ - value, - min, - max, - }: { - value: number; - min: number; - max: number; - }): number - -platform.ts - export function getPlatformSpecialKey(): string - export function getPlatformAlternateKey(): string - export function isAppleDevice(): boolean - -strings.ts - export function capitalizeFirstLetter({ string }: { string: string }) - export function uppercase({ string }: { string: string }) - -ui.ts - export function cn(...inputs: ClassValue[]): string - -## packages/ui/src/icons - -base-icon.tsx - export function BaseIcon({ - title, - size = 24, - viewBox = "0 0 24 24", - children, - ...props - }: BaseIconProps) - -create-icon.tsx - export function createIcon({ definition }: CreateIconParams) - -index.tsx - export const OcGoogleIcon - export const OcGithubIcon - export const OcVercelIcon - export const OcDataBuddyIcon - export const OcMarbleIcon - export const OcBackgroundIcon - export const OcSocialsIcon - export const OcTransitionUpIcon - export const OcMenuIcon - export const OcPencilIcon - export const OcLeftArrowIcon - export const OcTrashIcon - -types.ts - export type IconProps = Omit< - SVGProps, - "children" | "width" | "height" - > & { - size?: number; - title?: s... - export type IconNode = Array<{ - element: keyof JSX.IntrinsicElements; - props: Record + export const PANEL_CONFIG + +export-constants.ts + export const DEFAULT_EXPORT_OPTIONS + export const EXPORT_MIME_TYPES + +font-constants.ts + export interface FontOption { + value: string + label: string + category: "system" | "google" | "custom" + weights?: number[] + hasClassName?: boolean + } + export const FONT_OPTIONS: FontOption[] + export const DEFAULT_FONT + export type FontFamily = (typeof FONT_OPTIONS)[number]["value"] + export const getFontByValue = (value: string): FontOption | undefined => ... + export const getGoogleFonts = (): FontOption[] => ... + export const getSystemFonts = (): FontOption[] => ... + +language-constants.ts + export const LANGUAGES + +project-constants.ts + export const DEFAULT_CANVAS_PRESETS: TCanvasSize[] + export const FPS_PRESETS + export const BLUR_INTENSITY_PRESETS: { label: string; value: number }[] + export const DEFAULT_CANVAS_SIZE: TCanvasSize + export const DEFAULT_FPS + export const DEFAULT_BLUR_INTENSITY + export const DEFAULT_COLOR + +site-constants.ts + export const SITE_URL + export const SITE_INFO + export type ExternalTool = { + name: string; + description: string; + url: string; + icon: React.ElementType; + } + export const EXTERNAL_TOOLS: ExternalTool[] + export const DEFAULT_LOGO_URL + export const SOCIAL_LINKS + export type Sponsor = { + name: string; + url: string; + logo: string; + description: string; + } + export const SPONSORS: Sponsor[] + +stickers-constants.ts + export const STICKER_CATEGORIES + export const STICKER_CATEGORY_CONFIG: Record< + (typeof STICKER_CATEGORIES)[number], + string | undefined + > + +text-constants.ts + export const DEFAULT_TEXT_ELEMENT: Omit + +timeline-constants.tsx + export const TRACK_COLORS: Record + export const TRACK_HEIGHTS: Record + export const TRACK_GAP + export const TIMELINE_CONSTANTS + export const TRACK_ICONS: Record + +transcription-constants.ts + export const TRANSCRIPTION_LANGUAGES + export const TRANSCRIPTION_MODELS: TranscriptionModel[] + export const DEFAULT_TRANSCRIPTION_MODEL: TranscriptionModelId + export const DEFAULT_CHUNK_LENGTH_SECONDS + export const DEFAULT_STRIDE_SECONDS + export const DEFAULT_WORDS_PER_CAPTION + export const MIN_CAPTION_DURATION_SECONDS + +## apps/web/src/core + +index.ts + export class EditorCore { + instance: EditorCore | null + command: CommandManager + playback: PlaybackManager + timeline: TimelineManager + scenes: ScenesManager + project: ProjectManager + media: MediaManager + renderer: RendererManager + save: SaveManager + static getInstance(): EditorCore + static reset(): void + } + +## apps/web/src/hooks + +use-editor.ts + export function useEditor(): EditorCore + +use-file-upload.ts + export function useFileUpload({ + accept, + multiple, + onFilesSelected, + }: UseFileUploadOptions = {}) + +use-infinite-scroll.ts + export function useInfiniteScroll({ + onLoadMore, + hasMore, + isLoading, + threshold = 200, + enabled = true, + }: UseInfiniteScrollOptions) + +use-keybindings.ts + export function useKeybindingsListener() + export function useKeybindingDisabler() + +use-keyboard-shortcuts-help.ts + export interface KeyboardShortcut { + id: string + keys: string[] + description: string + category: string + action: TAction + icon?: React.ReactNode + } + export function useKeyboardShortcutsHelp() + +use-mobile.ts + export function useIsMobile() + +use-raf-loop.ts + export function useRafLoop(callback: ({ time }: { time: number }) => void) + +use-reveal-item.ts + export function useRevealItem( + highlightId: string | null, + onClearHighlight: () => void, + highlightDuration = 1000, + ) + +use-sound-search.ts + export function useSoundSearch({ + query, + commercialOnly, + }: { + query: string; + commercialOnly: boolean; + }) + +## apps/web/src/hooks/actions + +use-action-handler.ts + export function useActionHandler( + action: A, + handler: TActionFunc, + isActive: TActionHandlerOptions, + ) + +use-editor-actions.ts + export function useEditorActions() + +## apps/web/src/hooks/timeline + +use-edge-auto-scroll.ts + export function useEdgeAutoScroll({ + isActive, + getMouseClientX, + rulerScrollRef, + tracksScrollRef, + contentWidth, + edgeThreshold = 100, + maxScrollSpeed = 15, + }: UseEdgeAutoScrollParams): void + +use-scroll-sync.ts + export function useScrollSync({ + rulerScrollRef, + tracksScrollRef, + trackLabelsScrollRef, + bookmarksScrollRef, + }: UseScrollSyncProps) + +use-selection-box.ts + export function useSelectionBox({ + containerRef, + onSelectionComplete, + isEnabled = true, + tracksScrollRef, + zoomLevel, + }: UseSelectionBoxProps) + +use-snap-indicator-position.ts + export function useSnapIndicatorPosition({ + snapPoint, + zoomLevel, + tracks, + timelineRef, + trackLabelsRef, + tracksScrollRef, + }: UseSnapIndicatorPositionParams): SnapIndicatorPosition + +use-timeline-drag-drop.ts + export function useTimelineDragDrop({ + containerRef, + zoomLevel, + }: UseTimelineDragDropProps) + +use-timeline-playhead.ts + export function useTimelinePlayhead({ + zoomLevel, + rulerRef, + rulerScrollRef, + tracksScrollRef, + playheadRef, + }: UseTimelinePlayheadProps) + +use-timeline-seek.ts + export function useTimelineSeek({ + playheadRef, + trackLabelsRef, + rulerScrollRef, + tracksScrollRef, + zoomLevel, + duration, + isSelecting, + clearSelectedElements, + seek, + }: UseTimelineSeekProps) + +use-timeline-snapping.ts + export interface SnapPoint { + time: number + type: "element-start" | "element-end" | "playhead" + elementId?: string + trackId?: string + } + export interface SnapResult { + snappedTime: number + snapPoint: SnapPoint | null + snapDistance: number + } + export interface UseTimelineSnappingOptions { + snapThreshold?: number + enableElementSnapping?: boolean + enablePlayheadSnapping?: boolean + } + export function useTimelineSnapping({ + snapThreshold = 10, + enableElementSnapping = true, + enablePlayheadSnapping = true, + }: UseTimelineSnappingOptions = {}) + +use-timeline-zoom.ts + export function useTimelineZoom({ + containerRef, + isInTimeline = false, + minZoom = TIMELINE_CONSTANTS.ZOOM_MIN, + }: UseTimelineZoomProps): UseTimelineZoomReturn + +## apps/web/src/hooks/timeline/element + +use-element-interaction.ts + export function useElementInteraction({ + zoomLevel, + timelineRef, + tracksContainerRef, + tracksScrollRef, + snappingEnabled, + onSnapPointChange, + }: UseElementInteractionProps) + +use-element-resize.ts + export interface ResizeState { + elementId: string + side: "left" | "right" + startX: number + initialTrimStart: number + initialTrimEnd: number + initialStartTime: number + initialDuration: number + } + export function useTimelineElementResize({ + element, + track, + zoomLevel, + onSnapPointChange, + onResizeStateChange, + }: UseTimelineElementResizeProps) + +use-element-selection.ts + export function useElementSelection() + +## apps/web/src/lib + +drag-data.ts + export function setDragData({ + dataTransfer, + dragData, + }: { + dataTransfer: DataTransfer; + dragData: TimelineDragData; + }): void + export function getDragData({ + dataTransfer, + }: { + dataTransfer: DataTransfer; + }): TimelineDragData | null + export function hasDragData({ + dataTransfer, + }: { + dataTransfer: DataTransfer; + }): boolean + export function clearDragData(): void + +export.ts + export function getExportMimeType({ + format, + }: { + format: ExportFormat; + }): string + export function getExportFileExtension({ + format, + }: { + format: ExportFormat; + }): string + +iconify-api.ts + export const ICONIFY_HOSTS + export interface IconSet { + prefix: string + name: string + total: number + author?: { + name: string; + url?: string; + } + license?: { + title: string; + spdx?: string; + url?: string; + } + samples?: string[] + category?: string + palette?: boolean + } + export interface IconSearchResult { + icons: string[] + total: number + limit: number + start: number + collections: Record + } + export interface CollectionInfo { + prefix: string + total: number + title?: string + uncategorized?: string[] + categories?: Record + hidden?: string[] + aliases?: Record + } + export function getCollections( + category?: string, + ): Promise> + export function getCollection( + prefix: string, + ): Promise + export function searchIcons( + query: string, + limit: number = 64, + prefixes?: string[], + category?: string, + ): Promise + export function buildIconSvgUrl( + host: string, + iconName: string, + params?: { + color?: string; + width?: number; + height?: number; + flip?: "horizontal" | "vertical" | "horizontal,vertical"; + rotate?: number | string; + }, + ): string + export function getIconSvgUrl( + iconName: string, + params?: Parameters[2], + ): string + export function downloadSvgAsText( + iconName: string, + params?: Parameters[1], + ): Promise + export function svgToFile(svgText: string, fileName: string): File + export const POPULAR_COLLECTIONS + export function getCategoriesFromCollections( + collections: Record, + ): string[] + +rate-limit.ts + export const baseRateLimit + export function checkRateLimit({ request }: { request: Request }) + +scenes.ts + export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null + export function ensureMainScene({ scenes }: { scenes: TScene[] }): TScene[] + export function buildDefaultScene({ + name, + isMain, + }: { + name: string; + isMain: boolean; + }): TScene + export function canDeleteScene({ scene }: { scene: TScene }): { + canDelete: boolean; + reason?: string; + } + export function getFallbackSceneAfterDelete({ + scenes, + deletedSceneId, + currentSceneId, + }: { + scenes: TScene[]; + deletedSceneId: string; + currentSceneId: string | null; + }): TScene | null + export function findCurrentScene({ + scenes, + currentSceneId, + }: { + scenes: TScene[]; + currentSceneId: string; + }): TScene | null + export function updateSceneInArray({ + scenes, + sceneId, + updates, + }: { + scenes: TScene[]; + sceneId: string; + updates: Partial; + }): TScene[] + +time.ts + export function roundToFrame({ + time, + fps, + }: { + time: number; + fps: number; + }): number + export function formatTimeCode({ + timeInSeconds, + format = "HH:MM:SS:CS", + fps, + }: { + timeInSeconds: number; + format?: TTimeCode; + fps?: number; + }): string + export function parseTimeCode({ + timeCode, + format = "HH:MM:SS:CS", + fps, + }: { + timeCode: string; + format?: TTimeCode; + fps: number; + }): number | null + export function guessTimeCodeFormat({ + timeCode, + }: { + timeCode: string; + }): TTimeCode | null + export function timeToFrame({ + time, + fps, + }: { + time: number; + fps: number; + }): number + export function frameToTime({ + frame, + fps, + }: { + frame: number; + fps: number; + }): number + export function snapTimeToFrame({ + time, + fps, + }: { + time: number; + fps: number; + }): number + export function getSnappedSeekTime({ + rawTime, + duration, + fps, + }: { + rawTime: number; + duration: number; + fps: number; + }): number + +## apps/web/src/lib/actions + +definitions.ts + export type TActionCategory = | "playback" + | "navigation" + | "editing" + | "selection" + | "history" + | "timeline" + | "controls" + export interface TActionDefinition { + description: string + category: TActionCategory + defaultShortcuts?: ShortcutKey[] + args?: Record + } + export const ACTIONS + export type TAction = keyof typeof ACTIONS + export function getActionDefinition(action: TAction): TActionDefinition + export function getDefaultShortcuts(): Record + +registry.ts + export function bindAction( + action: A, + handler: TActionFunc, + ) + export function unbindAction( + action: A, + handler: TActionFunc, + ) + export const invokeAction = ( + action: A, + args?: TArgOfAction, + trigger?: TInvocationTrigger, + ) => ... + +types.ts + export type TActionArgsMap = { + "seek-forward": { seconds: number } | undefined; + "seek-backward": { seconds: number } | undef... + export type TActionWithArgs = keyof TActionArgsMap + export type TActionWithOptionalArgs = | TActionWithNoArgs + | TKeysWithValueUndefined + export type TActionWithNoArgs = Exclude + export type TArgOfAction = A extends TActionWithArgs + ? TActionArgsMap[A] + : undefined + export type TActionFunc = A extends TActionWithArgs + ? (arg: TArgOfAction, trigger?: TInvocationTrigger) => void + : (_?:... + export type TInvocationTrigger = "keypress" | "mouseclick" + export type TBoundActionList = { + [A in TAction]?: Array>; + } + export type TActionHandlerOptions = | MutableRefObject + | boolean + | undefined + +## apps/web/src/lib/auth + +server.ts + export const auth + export type Auth = typeof auth + +## apps/web/src/lib/blog + +query.ts + export function getPosts() + export function getTags() + export function getSinglePost({ slug }: { slug: string }) + export function getCategories() + export function getAuthors() + export function processHtmlContent({ + html, + }: { + html: string; + }): Promise + +## apps/web/src/lib/db + +index.ts + export const db + +schema.ts + export const users + export const sessions + export const accounts + export const verifications + +## apps/web/src/lib/gradients + +canvas.ts + export function drawCssBackground({ + ctx, + width, + height, + css, + }: { + ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; + width: number; + height: number; + css: string; + }): void + +parser.ts + export type GradientOrientation = LinearOrientation | Array + export type Color = | { type: "hex"; value: string } + | { type: "literal"; value: string } + | { type: "rgb"; value: A... + export type ColorStop = Color & { length?: Distance } + export type GradientAst = { + type: GradientType; + orientation: GradientOrientation | undefined; + colorStops: Array => ... + export const GradientParser + +## apps/web/src/lib/media + +audio.ts + export type CollectedAudioElement = Omit< + AudioElement, + "type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl" + ... + export function createAudioContext(): AudioContext + export interface DecodedAudio { + samples: Float32Array + sampleRate: number + } + export function decodeAudioToFloat32({ + audioBlob, + }: { + audioBlob: Blob; + }): Promise + export function collectAudioElements({ + tracks, + mediaAssets, + audioContext, + }: { + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + audioContext: AudioContext; + }): Promise + export function collectAudioMixSources({ + tracks, + mediaAssets, + }: { + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + }): Promise + export function createTimelineAudioBuffer({ + tracks, + mediaAssets, + duration, + sampleRate = 44100, + }: { + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + duration: number; + sampleRate?: number; + }): Promise + +media-utils.ts + export const SUPPORTS_AUDIO: readonly MediaType[] + export function mediaSupportsAudio({ + media, + }: { + media: MediaAsset | null | undefined; + }): boolean + export const getMediaTypeFromFile = ({ + file, + }: { + file: File; + }): MediaType | null => ... + +mediabunny.ts + export function getVideoInfo({ + videoFile, + }: { + videoFile: File; + }): Promise<{ + duration: number; + width: number; + height: number; + fps: number; + }> + export const extractTimelineAudio = ({ + tracks, + mediaAssets, + totalDuration, + onProgress, + }: { + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + totalDuration: number; + onProgress?: (progress: number) => void; + }): Promise => ... + +processing.ts + export interface ProcessedMediaAsset extends Omit + export function generateThumbnail({ + videoFile, + timeInSeconds, + }: { + videoFile: File; + timeInSeconds: number; + }): Promise + export function processMediaAssets({ + files, + onProgress, + }: { + files: FileList | File[]; + onProgress?: ({ progress }: { progress: number }) => void; + }): Promise + +## apps/web/src/lib/timeline + +bookmarks.ts + export function findBookmarkIndex({ + bookmarks, + frameTime, + }: { + bookmarks: number[]; + frameTime: number; + }): number + export function isBookmarkAtTime({ + bookmarks, + frameTime, + }: { + bookmarks: number[]; + frameTime: number; + }): boolean + export function toggleBookmarkInArray({ + bookmarks, + frameTime, + }: { + bookmarks: number[]; + frameTime: number; + }): number[] + export function removeBookmarkFromArray({ + bookmarks, + frameTime, + }: { + bookmarks: number[]; + frameTime: number; + }): number[] + export function getFrameTime({ + time, + fps, + }: { + time: number; + fps: number; + }): number + +drop-utils.ts + export function computeDropTarget({ + elementType, + mouseX, + mouseY, + tracks, + playheadTime, + isExternalDrop, + elementDuration, + pixelsPerSecond, + zoomLevel, + verticalDragDirection, + startTimeOverride, + excludeElementId, + }: ComputeDropTargetParams): DropTarget + export function getDropLineY({ + dropTarget, + tracks, + }: { + dropTarget: DropTarget; + tracks: TimelineTrack[]; + }): number + +element-utils.ts + export function canElementHaveAudio( + element: TimelineElement, + ) + export function canElementBeHidden( + element: TimelineElement, + ) + export function hasMediaId( + element: TimelineElement, + ) + export function requiresMediaId({ + element, + }: { + element: CreateTimelineElement; + }): boolean + export function checkElementOverlaps({ + elements, + }: { + elements: TimelineElement[]; + }): boolean + export function resolveElementOverlaps({ + elements, + }: { + elements: TimelineElement[]; + }): TimelineElement[] + export function wouldElementOverlap({ + elements, + startTime, + endTime, + excludeElementId, + }: { + elements: TimelineElement[]; + startTime: number; + endTime: number; + excludeElementId?: string; + }): boolean + export function buildTextElement({ + raw, + startTime, + }: { + raw: Partial>; + startTime: number; + }): CreateTimelineElement + export function buildStickerElement({ + iconName, + startTime, + }: { + iconName: string; + startTime: number; + }): CreateStickerElement + export function buildUploadAudioElement({ + mediaId, + name, + duration, + startTime, + buffer, + }: { + mediaId: string; + name: string; + duration: number; + startTime: number; + buffer?: AudioBuffer; + }): CreateUploadAudioElement + export function buildLibraryAudioElement({ + sourceUrl, + name, + duration, + startTime, + buffer, + }: { + sourceUrl: string; + name: string; + duration: number; + startTime: number; + buffer?: AudioBuffer; + }): CreateLibraryAudioElement + +index.ts + export function calculateTotalDuration({ + tracks, + }: { + tracks: TimelineTrack[]; + }): number + +track-utils.ts + export function canTracktHaveAudio( + track: TimelineTrack, + ) + export function canTrackBeHidden( + track: TimelineTrack, + ) + export function getTrackColor({ type }: { type: TrackType }) + export function getTrackClasses({ type }: { type: TrackType }) + export function getTrackHeight({ type }: { type: TrackType }): number + export function getCumulativeHeightBefore({ + tracks, + trackIndex, + }: { + tracks: Array<{ type: TrackType }>; + trackIndex: number; + }): number + export function getTotalTracksHeight({ + tracks, + }: { + tracks: Array<{ type: TrackType }>; + }): number + export function buildEmptyTrack({ + id, + type, + name, + }: { + id: string; + type: TrackType; + name?: string; + }): TimelineTrack + export function getDefaultInsertIndexForTrack({ + tracks, + trackType, + }: { + tracks: TimelineTrack[]; + trackType: TrackType; + }): number + export function getHighestInsertIndexForTrack({ + tracks, + trackType, + }: { + tracks: TimelineTrack[]; + trackType: TrackType; + }): number + export function isMainTrack(track: TimelineTrack) + export function getMainTrack({ + tracks, + }: { + tracks: TimelineTrack[]; + }): TimelineTrack | null + export function ensureMainTrack({ + tracks, + }: { + tracks: TimelineTrack[]; + }): TimelineTrack[] + export function canElementGoOnTrack({ + elementType, + trackType, + }: { + elementType: ElementType; + trackType: TrackType; + }): boolean + export function validateElementTrackCompatibility({ + element, + track, + }: { + element: { type: ElementType }; + track: { type: TrackType }; + }): { isValid: boolean; errorMessage?: string } + +zoom-utils.ts + export function getTimelineZoomMin({ + duration, + containerWidth, + }: { + duration: number; + containerWidth: number | null | undefined; + }): number + +## apps/web/src/lib/transcription + +caption.ts + export function buildCaptionChunks({ + segments, + wordsPerChunk = DEFAULT_WORDS_PER_CAPTION, + minDuration = MIN_CAPTION_DURATION_SECONDS, + }: { + segments: TranscriptionSegment[]; + wordsPerChunk?: number; + minDuration?: number; + }): CaptionChunk[] + +## apps/web/src/services/media + +video-cache.ts + export class VideoCache { + sinks + initPromises + async getFrameAt({ + mediaId, + file, + time, + }: { + mediaId: string; + file: File; + time: number; + }): Promise + clearVideo({ mediaId }: { mediaId: string }): void + clearAll(): void + getStats() + } + export const videoCache + +## apps/web/src/services/renderer + +canvas-renderer.ts + export type CanvasRendererParams = { + width: number; + height: number; + fps: number; + } + export class CanvasRenderer { + canvas: OffscreenCanvas | HTMLCanvasElement + context: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D + width: number + height: number + fps: number + constructor({ width, height, fps }: CanvasRendererParams) + setSize({ width, height }: { width: number; height: number }) + async render({ node, time }: { node: BaseNode; time: number }) + async renderToCanvas({ + node, + time, + targetCanvas, + }: { + node: BaseNode; + time: number; + targetCanvas: HTMLCanvasElement; + }) + } + +scene-builder.ts + export type BuildSceneParams = { + canvasSize: TCanvasSize; + tracks: TimelineTrack[]; + mediaAssets: MediaAsset[]; + duration: numb... + export function buildScene(params: BuildSceneParams) + +scene-exporter.ts + export type ExportFormat = "mp4" | "webm" + export type ExportQuality = "low" | "medium" | "high" | "very_high" + export type SceneExporterEvents = { + progress: [progress: number]; + complete: [buffer: ArrayBuffer]; + error: [error: Error]; + c... + export class SceneExporter extends EventEmitter { + renderer: CanvasRenderer + format: ExportFormat + quality: ExportQuality + includeAudio: boolean + audioBuffer: AudioBuffer + cancelled + constructor(params: ExportParams) + cancel() + async export(rootNode: RootNode) + } + +## apps/web/src/services/storage + +indexeddb-adapter.ts + export class IndexedDBAdapter implements StorageAdapter { + dbName: string + storeName: string + version: number + constructor(dbName: string, storeName: string, version = 1) + async get(key: string): Promise + async set(key: string, value: T): Promise + async remove(key: string): Promise + async list(): Promise + async getAll(): Promise + async clear(): Promise + } + +opfs-adapter.ts + export class OPFSAdapter implements StorageAdapter { + directoryName: string + constructor(directoryName = "media") + async get(key: string): Promise + async set(key: string, file: File): Promise + async remove(key: string): Promise + async list(): Promise + async clear(): Promise + static isSupported(): boolean + } + +storage-service.ts + export const storageService + +types.ts + export interface StorageAdapter { + get(key: string): Promise + set(key: string, value: T): Promise + remove(key: string): Promise + list(): Promise + clear(): Promise + } + export interface MediaAssetData { + id: string + name: string + type: MediaType + size: number + lastModified: number + width?: number + height?: number + duration?: number + fps?: number + ephemeral?: boolean + thumbnailUrl?: string + sourceStickerIconName?: string + } + export type SerializedScene = Omit & { + createdAt: string; + updatedAt: string; + } + export type SerializedProjectMetadata = Omit< + TProjectMetadata, + "createdAt" | "updatedAt" + > & { + createdAt: string; + updatedAt: string; + } + export type SerializedProject = Omit & { + metadata: SerializedProjectMetadata; + scenes: Serializ... + export interface StorageConfig { + projectsDb: string + mediaDb: string + savedSoundsDb: string + version: number + } + +## apps/web/src/services/transcription + +index.ts + export const transcriptionService + +worker.ts + export type WorkerMessage = | { type: "init"; modelId: string } + | { type: "transcribe"; audio: Float32Array; language: strin... + export type WorkerResponse = | { type: "init-progress"; progress: number } + | { type: "init-complete" } + | { type: "init-error... + +## apps/web/src/stores + +assets-panel-store.ts + export const TAB_KEYS + export type Tab = (typeof TAB_KEYS)[number] + export const tabs + export const useAssetsPanelStore + +editor-store.ts + export const useEditorStore + +keybindings-store.ts + export const defaultKeybindings: KeybindingConfig + export interface KeybindingConflict { + key: ShortcutKey + existingAction: TActionWithOptionalArgs + newAction: TActionWithOptionalArgs + } + export const useKeybindingsStore + +panel-store.ts + export interface PanelSizes { + tools: number + preview: number + properties: number + mainContent: number + timeline: number + } + export type PanelId = keyof PanelSizes + export const usePanelStore + +sounds-store.ts + export const useSoundsStore + +stickers-store.ts + export const useStickersStore + +text-properties-store.ts + export type TextPropertiesTab = "text" | "transform" + export interface TextPropertiesTabMeta { + value: TextPropertiesTab + label: string + } + export const TEXT_PROPERTIES_TABS: ReadonlyArray + export function isTextPropertiesTab(value: string) + export const useTextPropertiesStore + +timeline-store.ts + export const useTimelineStore + +## apps/web/src/types + +assets.ts + export type MediaType = "image" | "video" | "audio" + export interface MediaAsset extends Omit { + file: File + url?: string + } + +blog.ts + export type Post = { + id: string; + slug: string; + title: string; + content: string; + description: string; + coverImage... + export type Pagination = { + limit: number; + currpage: number; + nextPage: number | null; + prevPage: number | null; + totalIt... + export type MarblePostList = { + posts: Post[]; + pagination: Pagination; + } + export type MarblePost = { + post: Post; + } + export type Tag = { + id: string; + name: string; + slug: string; + } + export type MarbleTag = { + tag: Tag; + } + export type MarbleTagList = { + tags: Tag[]; + pagination: Pagination; + } + export type Category = { + id: string; + name: string; + slug: string; + } + export type MarbleCategory = { + category: Category; + } + export type MarbleCategoryList = { + categories: Category[]; + pagination: Pagination; + } + export type Author = { + id: string; + name: string; + image: string; + } + export type MarbleAuthor = { + author: Author; + } + export type MarbleAuthorList = { + authors: Author[]; + pagination: Pagination; + } + +drag.ts + export interface MediaDragData extends BaseDragData { + type: "media" + mediaType: "image" | "video" | "audio" + } + export interface TextDragData extends BaseDragData { + type: "text" + content: string + } + export interface StickerDragData extends BaseDragData { + type: "sticker" + iconName: string + } + export type TimelineDragData = MediaDragData | TextDragData | StickerDragData + +editor.ts + export type TPlatformLayout = "tiktok" + +export.ts + export const EXPORT_QUALITY_VALUES + export const EXPORT_FORMAT_VALUES + export type ExportFormat = (typeof EXPORT_FORMAT_VALUES)[number] + export type ExportQuality = (typeof EXPORT_QUALITY_VALUES)[number] + export interface ExportOptions { + format: ExportFormat + quality: ExportQuality + fps?: number + includeAudio?: boolean + onProgress?: ({ progress }: { progress: number }) => void + onCancel?: () => boolean + } + export interface ExportResult { + success: boolean + buffer?: ArrayBuffer + error?: string + cancelled?: boolean + } + +keybinding.ts + export type ModifierKeys = | "ctrl" + | "alt" + | "shift" + | "ctrl+shift" + | "alt+shift" + | "ctrl+alt" + | "ctrl+alt+shift" + export type Key = | "a" + | "b" + | "c" + | "d" + | "e" + | "f" + | "g" + | "h" + | "i" + | "j" + | "k" + | "l" + | "m" + | "n" + ... + export type ModifierBasedShortcutKey = `${ModifierKeys}+${Key}` + export type SingleCharacterShortcutKey = `${Key}` + export type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey + export type KeybindingConfig = { + [key in ShortcutKey]?: TActionWithOptionalArgs; + } + +language.ts + export type Language = (typeof LANGUAGES)[number] + export type LanguageCode = Language["code"] + +project.ts + export type TBackground = | { + type: "color"; + color: string; + } + | { + type: "blur"; + blurIntensity: number; + } + export interface TCanvasSize { + width: number + height: number + } + export interface TProjectMetadata { + id: string + name: string + thumbnail?: string + createdAt: Date + updatedAt: Date + } + export interface TProjectSettings { + fps: number + canvasSize: TCanvasSize + originalCanvasSize?: TCanvasSize | null + background: TBackground + } + export interface TProject { + metadata: TProjectMetadata + scenes: TScene[] + currentSceneId: string + settings: TProjectSettings + version: number + } + +sounds.ts + export interface SoundEffect { + id: number + name: string + description: string + url: string + previewUrl?: string + downloadUrl?: string + duration: number + filesize: number + type: string + channels: number + bitrate: number + bitdepth: number + samplerate: number + username: string + tags: string[] + license: string + created: string + downloads: number + rating: number + ratingCount: number + } + export interface SavedSound { + id: number + name: string + username: string + previewUrl?: string + downloadUrl?: string + duration: number + tags: string[] + license: string + savedAt: string + } + export interface SavedSoundsData { + sounds: SavedSound[] + lastModified: string + } + +stickers.ts + export type StickerCategory = (typeof STICKER_CATEGORIES)[number] + +time.ts + export type TTimeCode = "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF" + +timeline.ts + export interface TScene { + id: string + name: string + isMain: boolean + tracks: TimelineTrack[] + bookmarks: number[] + createdAt: Date + updatedAt: Date + } + export type TrackType = "video" | "text" | "audio" | "sticker" + export interface VideoTrack extends BaseTrack { + type: "video" + elements: (VideoElement | ImageElement)[] + isMain: boolean + muted: boolean + hidden: boolean + } + export interface TextTrack extends BaseTrack { + type: "text" + elements: TextElement[] + hidden: boolean + } + export interface AudioTrack extends BaseTrack { + type: "audio" + elements: AudioElement[] + muted: boolean + } + export interface StickerTrack extends BaseTrack { + type: "sticker" + elements: StickerElement[] + hidden: boolean + } + export type TimelineTrack = VideoTrack | TextTrack | AudioTrack | StickerTrack + export interface Transform { + scale: number + position: { + x: number; + y: number; + } + rotate: number + } + export interface UploadAudioElement extends BaseAudioElement { + sourceType: "upload" + mediaId: string + } + export interface LibraryAudioElement extends BaseAudioElement { + sourceType: "library" + sourceUrl: string + } + export type AudioElement = UploadAudioElement | LibraryAudioElement + export interface VideoElement extends BaseTimelineElement { + type: "video" + mediaId: string + muted?: boolean + hidden?: boolean + transform: Transform + opacity: number + } + export interface ImageElement extends BaseTimelineElement { + type: "image" + mediaId: string + hidden?: boolean + transform: Transform + opacity: number + } + export interface TextElement extends BaseTimelineElement { + type: "text" + content: string + fontSize: number + fontFamily: string + color: string + backgroundColor: string + textAlign: "left" | "center" | "right" + fontWeight: "normal" | "bold" + fontStyle: "normal" | "italic" + textDecoration: "none" | "underline" | "line-through" + hidden?: boolean + transform: Transform + opacity: number + } + export interface StickerElement extends BaseTimelineElement { + type: "sticker" + iconName: string + hidden?: boolean + transform: Transform + opacity: number + color?: string + } + export type TimelineElement = | AudioElement + | VideoElement + | ImageElement + | TextElement + | StickerElement + export type ElementType = TimelineElement["type"] + export type CreateUploadAudioElement = Omit + export type CreateLibraryAudioElement = Omit + export type CreateAudioElement = | CreateUploadAudioElement + | CreateLibraryAudioElement + export type CreateVideoElement = Omit + export type CreateImageElement = Omit + export type CreateTextElement = Omit + export type CreateStickerElement = Omit + export type CreateTimelineElement = | CreateAudioElement + | CreateVideoElement + | CreateImageElement + | CreateTextElement + | CreateSt... + export interface ElementDragState { + isDragging: boolean + elementId: string | null + trackId: string | null + startMouseX: number + startMouseY: number + startElementTime: number + clickOffsetTime: number + currentTime: number + currentMouseY: number + } + export interface DropTarget { + trackIndex: number + isNewTrack: boolean + insertPosition: "above" | "below" | null + xPosition: number + } + export interface ComputeDropTargetParams { + elementType: ElementType + mouseX: number + mouseY: number + tracks: TimelineTrack[] + playheadTime: number + isExternalDrop: boolean + elementDuration: number + pixelsPerSecond: number + zoomLevel: number + verticalDragDirection?: "up" | "down" | null + startTimeOverride?: number + excludeElementId?: string + } + export interface ClipboardItem { + trackId: string + trackType: TrackType + element: CreateTimelineElement + } + +transcription.ts + export type TranscriptionLanguage = LanguageCode | "auto" + export interface TranscriptionSegment { + text: string + start: number + end: number + } + export interface TranscriptionResult { + text: string + segments: TranscriptionSegment[] + language: string + } + export type TranscriptionStatus = | "idle" + | "loading-model" + | "transcribing" + | "complete" + | "error" + export interface TranscriptionProgress { + status: TranscriptionStatus + progress: number + message?: string + } + export type TranscriptionModelId = | "whisper-tiny" + | "whisper-small" + | "whisper-medium" + | "whisper-large-v3-turbo" + export interface TranscriptionModel { + id: TranscriptionModelId + name: string + huggingFaceId: string + description: string + } + export interface CaptionChunk { + text: string + startTime: number + duration: number + } + +## apps/web/src/utils + +browser.ts + export function isTypableDOMElement({ + element, + }: { + element: HTMLElement; + }): boolean + +date.ts + export function formatDate({ date }: { date: Date }): string + +geometry.ts + export function dimensionToAspectRatio({ + width, + height, + }: { + width: number; + height: number; + }): string + +id.ts + export function generateUUID(): string + +math.ts + export function clamp({ + value, + min, + max, + }: { + value: number; + min: number; + max: number; + }): number + +platform.ts + export function getPlatformSpecialKey(): string + export function getPlatformAlternateKey(): string + export function isAppleDevice(): boolean + +strings.ts + export function capitalizeFirstLetter({ string }: { string: string }) + export function uppercase({ string }: { string: string }) + +ui.ts + export function cn(...inputs: ClassValue[]): string + +## packages/ui/src/icons + +base-icon.tsx + export function BaseIcon({ + title, + size = 24, + viewBox = "0 0 24 24", + children, + ...props + }: BaseIconProps) + +create-icon.tsx + export function createIcon({ definition }: CreateIconParams) + +index.tsx + export const OcGoogleIcon + export const OcGithubIcon + export const OcVercelIcon + export const OcDataBuddyIcon + export const OcMarbleIcon + export const OcBackgroundIcon + export const OcSocialsIcon + export const OcTransitionUpIcon + export const OcMenuIcon + export const OcPencilIcon + export const OcLeftArrowIcon + export const OcTrashIcon + +types.ts + export type IconProps = Omit< + SVGProps, + "children" | "width" | "height" + > & { + size?: number; + title?: s... + export type IconNode = Array<{ + element: keyof JSX.IntrinsicElements; + props: Record { + const _handleSelectAll = ({ checked }: { checked: boolean }) => { if (checked) { setSelectedProjects( new Set(projectsToDisplay.map((project) => project.id)), @@ -141,11 +141,11 @@ export default function ProjectsPage() { sortOption, }); - const isAllSelected = + const _isAllSelected = projectsToDisplay.length > 0 && selectedProjects.size === projectsToDisplay.length; - const hasSomeSelected = + const _hasSomeSelected = selectedProjects.size > 0 && selectedProjects.size < projectsToDisplay.length; diff --git a/apps/web/src/components/editor/draggable-item.tsx b/apps/web/src/components/editor/draggable-item.tsx index 2c3d253a..07beab69 100644 --- a/apps/web/src/components/editor/draggable-item.tsx +++ b/apps/web/src/components/editor/draggable-item.tsx @@ -129,12 +129,14 @@ export function DraggableItem({ {shouldShowLabel && ( - {name.length > 8 - ? `${name.slice(0, 16)}...${name.slice(-3)}` - : name} + {name} + )} @@ -147,7 +149,8 @@ export function DraggableItem({ isHighlighted && highlightClassName, )} > -
{name} -
+ )} diff --git a/apps/web/src/components/editor/timeline/bookmarks.tsx b/apps/web/src/components/editor/timeline/bookmarks.tsx index da170aa3..a21a3db0 100644 --- a/apps/web/src/components/editor/timeline/bookmarks.tsx +++ b/apps/web/src/components/editor/timeline/bookmarks.tsx @@ -26,28 +26,30 @@ export function TimelineBookmarksRow({ const activeScene = editor.scenes.getActiveScene(); return ( -
+
-
{ + handleRulerMouseDown(event); + handleRulerTrackingMouseDown(event); + }} > - {activeScene.bookmarks.map((time: number, index: number) => ( + {activeScene.bookmarks.map((time: number) => ( ))} -
+
); @@ -65,23 +67,29 @@ export function TimelineBookmark({ const handleBookmarkClick = ({ event, }: { - event: React.MouseEvent; + event: React.MouseEvent; }) => { event.stopPropagation(); editor.playback.seek({ time }); }; return ( -
handleBookmarkClick({ event })} >
- +
-
+ ); } diff --git a/apps/web/src/components/editor/timeline/timeline-ruler.tsx b/apps/web/src/components/editor/timeline/timeline-ruler.tsx index fc6118ee..eb23f57d 100644 --- a/apps/web/src/components/editor/timeline/timeline-ruler.tsx +++ b/apps/web/src/components/editor/timeline/timeline-ruler.tsx @@ -47,13 +47,21 @@ export function TimelineRuler({ return (
{}} >
; +type CategoryLinks = Record; const links: CategoryLinks = { resources: [ diff --git a/apps/web/src/components/providers/editor-provider.tsx b/apps/web/src/components/providers/editor-provider.tsx index 698dd2b8..5f672e01 100644 --- a/apps/web/src/components/providers/editor-provider.tsx +++ b/apps/web/src/components/providers/editor-provider.tsx @@ -56,7 +56,7 @@ export function EditorProvider({ projectId, children }: EditorProviderProps) { name: "Untitled Project", }); router.replace(`/editor/${newProjectId}`); - } catch (createErr) { + } catch (_createErr) { setError("Failed to create project"); setIsLoading(false); } diff --git a/apps/web/src/components/storage-provider.tsx b/apps/web/src/components/storage-provider.tsx index 6439e44c..144790dc 100644 --- a/apps/web/src/components/storage-provider.tsx +++ b/apps/web/src/components/storage-provider.tsx @@ -73,7 +73,7 @@ export function StorageProvider({ children }: StorageProviderProps) { }; initializeStorage(); - }, []); + }, [editor.project.loadAllProjects]); return ( {children} diff --git a/apps/web/src/core/managers/timeline-manager.ts b/apps/web/src/core/managers/timeline-manager.ts index 2771b896..db81094d 100644 --- a/apps/web/src/core/managers/timeline-manager.ts +++ b/apps/web/src/core/managers/timeline-manager.ts @@ -1,7 +1,6 @@ import type { EditorCore } from "@/core"; import type { TrackType, - CreateTimelineElement, TimelineTrack, TextElement, TimelineElement, diff --git a/apps/web/src/hooks/actions/use-editor-actions.ts b/apps/web/src/hooks/actions/use-editor-actions.ts index ba294efb..b5aa2213 100644 --- a/apps/web/src/hooks/actions/use-editor-actions.ts +++ b/apps/web/src/hooks/actions/use-editor-actions.ts @@ -224,7 +224,7 @@ export function useEditorActions() { elements: selectedElements, }); const items = results.map(({ track, element }) => { - const { id, ...elementWithoutId } = element; + const { ...elementWithoutId } = element; return { trackId: track.id, trackType: track.type, diff --git a/apps/web/src/hooks/timeline/element/use-element-resize.ts b/apps/web/src/hooks/timeline/element/use-element-resize.ts index 151e8cab..803d9788 100644 --- a/apps/web/src/hooks/timeline/element/use-element-resize.ts +++ b/apps/web/src/hooks/timeline/element/use-element-resize.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import type { TimelineElement, TimelineTrack } from "@/types/timeline"; import { snapTimeToFrame } from "@/lib/time"; import { EditorCore } from "@/core"; @@ -48,26 +48,6 @@ export function useTimelineElementResize({ const currentStartTimeRef = useRef(element.startTime); const currentDurationRef = useRef(element.duration); - useEffect(() => { - if (!resizing) return; - - const handleDocumentMouseMove = ({ clientX }: MouseEvent) => { - updateTrimFromMouseMove({ clientX }); - }; - - const handleDocumentMouseUp = () => { - handleResizeEnd(); - }; - - document.addEventListener("mousemove", handleDocumentMouseMove); - document.addEventListener("mouseup", handleDocumentMouseUp); - - return () => { - document.removeEventListener("mousemove", handleDocumentMouseMove); - document.removeEventListener("mouseup", handleDocumentMouseUp); - }; - }, [resizing]); - const handleResizeStart = ({ e, elementId, @@ -101,15 +81,15 @@ export function useTimelineElementResize({ onResizeStateChange?.({ isResizing: true }); }; - const canExtendElementDuration = () => { + const canExtendElementDuration = useCallback(() => { if (element.type === "text" || element.type === "image") { return true; } return false; - }; + }, [element.type]); - const updateTrimFromMouseMove = ({ clientX }: { clientX: number }) => { + const updateTrimFromMouseMove = useCallback(({ clientX }: { clientX: number }) => { if (!resizing) return; const deltaX = clientX - resizing.startX; @@ -277,9 +257,9 @@ export function useTimelineElementResize({ currentDurationRef.current = newDuration; } } - }; + }, [resizing, zoomLevel, activeProject.settings.fps, snappingEnabled, editor, findSnapPoints, snapToNearestPoint, element.id, onSnapPointChange, canExtendElementDuration]); - const handleResizeEnd = () => { + const handleResizeEnd = useCallback(() => { if (!resizing) return; const finalTrimStart = currentTrimStartRef.current; @@ -317,7 +297,27 @@ export function useTimelineElementResize({ setResizing(null); onResizeStateChange?.({ isResizing: false }); onSnapPointChange?.(null); - }; + }, [resizing, editor.timeline, element.id, track.id, onResizeStateChange, onSnapPointChange]); + + useEffect(() => { + if (!resizing) return; + + const handleDocumentMouseMove = ({ clientX }: MouseEvent) => { + updateTrimFromMouseMove({ clientX }); + }; + + const handleDocumentMouseUp = () => { + handleResizeEnd(); + }; + + document.addEventListener("mousemove", handleDocumentMouseMove); + document.addEventListener("mouseup", handleDocumentMouseUp); + + return () => { + document.removeEventListener("mousemove", handleDocumentMouseMove); + document.removeEventListener("mouseup", handleDocumentMouseUp); + }; + }, [resizing, handleResizeEnd, updateTrimFromMouseMove]); return { resizing, diff --git a/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts b/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts index 5649e1d0..01c43172 100644 --- a/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts +++ b/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts @@ -16,13 +16,11 @@ import type { MediaDragData, StickerDragData } from "@/types/drag"; interface UseTimelineDragDropProps { containerRef: RefObject; zoomLevel: number; - isSnappingEnabled?: boolean; } export function useTimelineDragDrop({ containerRef, zoomLevel, - isSnappingEnabled = true, }: UseTimelineDragDropProps) { const editor = useEditor(); const [isDragOver, setIsDragOver] = useState(false); @@ -488,11 +486,12 @@ export function useTimelineDragDrop({ } }, [ - dropTarget, - executeTextDrop, - executeStickerDrop, - executeMediaDrop, + dropTarget, + executeTextDrop, + executeStickerDrop, + executeMediaDrop, executeFileDrop, + containerRef, ], ); diff --git a/apps/web/src/hooks/timeline/use-timeline-playhead.ts b/apps/web/src/hooks/timeline/use-timeline-playhead.ts index ec32103a..4b8a648e 100644 --- a/apps/web/src/hooks/timeline/use-timeline-playhead.ts +++ b/apps/web/src/hooks/timeline/use-timeline-playhead.ts @@ -21,15 +21,18 @@ export function useTimelinePlayhead({ }: UseTimelinePlayheadProps) { const editor = useEditor(); const activeProject = editor.project.getActive(); - const seek = (time: number) => editor.playback.seek({ time }); const currentTime = editor.playback.getCurrentTime(); const duration = editor.timeline.getTotalDuration(); + const isPlaying = editor.playback.getIsPlaying(); + + const seek = useCallback( + ({ time }: { time: number }) => editor.playback.seek({ time }), + [editor.playback], + ); - // Playhead scrubbing state const [isScrubbing, setIsScrubbing] = useState(false); const [scrubTime, setScrubTime] = useState(null); - // Ruler drag detection state const [isDraggingRuler, setIsDraggingRuler] = useState(false); const [hasDraggedRuler, setHasDraggedRuler] = useState(false); const lastMouseXRef = useRef(0); @@ -37,67 +40,78 @@ export function useTimelinePlayhead({ const playheadPosition = isScrubbing && scrubTime !== null ? scrubTime : currentTime; - const handlePlayheadMouseDown = useCallback( - (event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); // prevent ruler drag from triggering - setIsScrubbing(true); - handleScrub(event); + const handleScrub = useCallback( + ({ event }: { event: MouseEvent | React.MouseEvent }) => { + const ruler = rulerRef.current; + if (!ruler) return; + const rulerRect = ruler.getBoundingClientRect(); + const relativeMouseX = event.clientX - rulerRect.left; + + const timelineContentWidth = + duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; + + const clampedMouseX = Math.max( + 0, + Math.min(timelineContentWidth, relativeMouseX), + ); + + const rawTime = Math.max( + 0, + Math.min( + duration, + clampedMouseX / + (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel), + ), + ); + const framesPerSecond = activeProject.settings.fps; + const time = getSnappedSeekTime({ + rawTime, + duration, + fps: framesPerSecond, + }); + + setScrubTime(time); + seek({ time }); + + lastMouseXRef.current = event.clientX; }, - [duration, zoomLevel], + [duration, zoomLevel, seek, rulerRef, activeProject.settings.fps], + ); + + const handlePlayheadMouseDown = useCallback( + ({ event }: { event: React.MouseEvent }) => { + event.preventDefault(); + event.stopPropagation(); + setIsScrubbing(true); + handleScrub({ event }); + }, + [handleScrub], ); const handleRulerMouseDown = useCallback( - (event: React.MouseEvent) => { - // only handle left mouse button + ({ event }: { event: React.MouseEvent }) => { if (event.button !== 0) return; - // don't interfere if clicking on the playhead itself if (playheadRef?.current?.contains(event.target as Node)) return; event.preventDefault(); setIsDraggingRuler(true); setHasDraggedRuler(false); - // start scrubbing immediately setIsScrubbing(true); - handleScrub(event); + handleScrub({ event }); }, - [duration, zoomLevel], + [handleScrub, playheadRef], ); - const handleScrub = useCallback( - (event: MouseEvent | React.MouseEvent) => { - const ruler = rulerRef.current; - if (!ruler) return; - const rect = ruler.getBoundingClientRect(); - const rawX = event.clientX - rect.left; + const handlePlayheadMouseDownEvent = useCallback( + (event: React.MouseEvent) => handlePlayheadMouseDown({ event }), + [handlePlayheadMouseDown], + ); - // get the timeline content width based on duration and zoom - const timelineContentWidth = - duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; - - // constrain x to be within the timeline content bounds - const x = Math.max(0, Math.min(timelineContentWidth, rawX)); - - const rawTime = Math.max( - 0, - Math.min( - duration, - x / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel), - ), - ); - // use frame snapping for playhead scrubbing - const fps = activeProject.settings.fps; - const time = getSnappedSeekTime({ rawTime, duration, fps }); - - setScrubTime(time); - seek(time); // update video preview in real time - - // store mouse position for auto-scrolling - lastMouseXRef.current = event.clientX; - }, - [duration, zoomLevel, seek, rulerRef, activeProject.settings.fps], + const handleRulerMouseDownEvent = useCallback( + (event: React.MouseEvent) => handleRulerMouseDown({ event }), + [handleRulerMouseDown], ); useEdgeAutoScroll({ @@ -111,30 +125,30 @@ export function useTimelinePlayhead({ useEffect(() => { if (!isScrubbing) return; - const onMouseMove = (event: MouseEvent) => { - handleScrub(event); - // mark that we've dragged if ruler drag is active + const handleMouseMove = ({ event }: { event: MouseEvent }) => { + handleScrub({ event }); if (isDraggingRuler) { setHasDraggedRuler(true); } }; - const onMouseUp = (event: MouseEvent) => { + const handleMouseUp = ({ event }: { event: MouseEvent }) => { setIsScrubbing(false); - if (scrubTime !== null) seek(scrubTime); // finalize seek + if (scrubTime !== null) seek({ time: scrubTime }); setScrubTime(null); - // handle ruler click vs drag if (isDraggingRuler) { setIsDraggingRuler(false); - // if we didn't drag, treat it as a click-to-seek if (!hasDraggedRuler) { - handleScrub(event); + handleScrub({ event }); } setHasDraggedRuler(false); } }; + const onMouseMove = (event: MouseEvent) => handleMouseMove({ event }); + const onMouseUp = (event: MouseEvent) => handleMouseUp({ event }); + window.addEventListener("mousemove", onMouseMove); window.addEventListener("mouseup", onMouseUp); @@ -149,49 +163,45 @@ export function useTimelinePlayhead({ handleScrub, isDraggingRuler, hasDraggedRuler, - // edge auto scroll hook is independent ]); useEffect(() => { - // only auto-scroll during playback, not during manual interactions - if (!editor.playback.getIsPlaying() || isScrubbing) return; + if (!isPlaying || isScrubbing) return; const rulerViewport = rulerScrollRef.current; const tracksViewport = tracksScrollRef.current; if (!rulerViewport || !tracksViewport) return; - const playheadPx = + const playheadPixels = playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; const viewportWidth = rulerViewport.clientWidth; - const scrollMin = 0; - const scrollMax = rulerViewport.scrollWidth - viewportWidth; + const scrollMinimum = 0; + const scrollMaximum = rulerViewport.scrollWidth - viewportWidth; - // only auto-scroll if playhead is completely out of view (no buffer) const needsScroll = - playheadPx < rulerViewport.scrollLeft || - playheadPx > rulerViewport.scrollLeft + viewportWidth; + playheadPixels < rulerViewport.scrollLeft || + playheadPixels > rulerViewport.scrollLeft + viewportWidth; if (needsScroll) { - // center the playhead in the viewport const desiredScroll = Math.max( - scrollMin, - Math.min(scrollMax, playheadPx - viewportWidth / 2), + scrollMinimum, + Math.min(scrollMaximum, playheadPixels - viewportWidth / 2), ); rulerViewport.scrollLeft = tracksViewport.scrollLeft = desiredScroll; } }, [ playheadPosition, - duration, zoomLevel, rulerScrollRef, tracksScrollRef, isScrubbing, + isPlaying, ]); return { playheadPosition, - handlePlayheadMouseDown, - handleRulerMouseDown, + handlePlayheadMouseDown: handlePlayheadMouseDownEvent, + handleRulerMouseDown: handleRulerMouseDownEvent, isDraggingRuler, }; } diff --git a/apps/web/src/hooks/use-keybindings.ts b/apps/web/src/hooks/use-keybindings.ts index 6b33e166..42539c19 100644 --- a/apps/web/src/hooks/use-keybindings.ts +++ b/apps/web/src/hooks/use-keybindings.ts @@ -3,7 +3,7 @@ import { invokeAction } from "@/lib/actions"; import { useKeybindingsStore } from "@/stores/keybindings-store"; /** - * A composable that hooks to the caller component's + * a composable that hooks to the caller component's * lifecycle and hooks to the keyboard events to fire * the appropriate actions based on keybindings */ @@ -13,7 +13,7 @@ export function useKeybindingsListener() { useEffect(() => { const handleKeyDown = (ev: KeyboardEvent) => { - // Do not check keybinds if the mode is disabled + // do not check keybinds if the mode is disabled if (!keybindingsEnabled) return; // ignore key events if user is changing keybindings if (isRecording) return; @@ -35,20 +35,22 @@ export function useKeybindingsListener() { ev.preventDefault(); - // Handle actions with default arguments - let actionArgs: any; - - if (boundAction === "seek-forward") { - actionArgs = { seconds: 1 }; - } else if (boundAction === "seek-backward") { - actionArgs = { seconds: 1 }; - } else if (boundAction === "jump-forward") { - actionArgs = { seconds: 5 }; - } else if (boundAction === "jump-backward") { - actionArgs = { seconds: 5 }; - } - - invokeAction(boundAction, actionArgs, "keypress"); + switch (boundAction) { + case "seek-forward": + invokeAction("seek-forward", { seconds: 1 }, "keypress"); + break; + case "seek-backward": + invokeAction("seek-backward", { seconds: 1 }, "keypress"); + break; + case "jump-forward": + invokeAction("jump-forward", { seconds: 5 }, "keypress"); + break; + case "jump-backward": + invokeAction("jump-backward", { seconds: 5 }, "keypress"); + break; + default: + invokeAction(boundAction, undefined, "keypress"); + } }; document.addEventListener("keydown", handleKeyDown); @@ -60,7 +62,7 @@ export function useKeybindingsListener() { } /** - * This composable allows for the UI component to be disabled if the component in question is mounted + * this composable allows for the UI component to be disabled if the component in question is mounted */ export function useKeybindingDisabler() { const { disableKeybindings, enableKeybindings } = useKeybindingsStore(); @@ -70,6 +72,3 @@ export function useKeybindingDisabler() { enableKeybindings, }; } - -// Export the bindings for backward compatibility -export const bindings = {}; diff --git a/apps/web/src/lib/actions/registry.ts b/apps/web/src/lib/actions/registry.ts index 86f59d0f..b9c84529 100644 --- a/apps/web/src/lib/actions/registry.ts +++ b/apps/web/src/lib/actions/registry.ts @@ -6,19 +6,21 @@ import type { TActionArgsMap, TArgOfAction, TInvocationTrigger, - TBoundActionList, } from "./types"; -const boundActions: TBoundActionList = {}; +type ActionHandler = (arg: unknown, trigger?: TInvocationTrigger) => void; +const boundActions: Partial> = {}; export function bindAction( action: A, handler: TActionFunc, ) { - if (boundActions[action]) { - boundActions[action]?.push(handler); + const handlers = boundActions[action]; + const typedHandler = handler as ActionHandler; + if (handlers) { + handlers.push(typedHandler); } else { - boundActions[action] = [handler] as any; + boundActions[action] = [typedHandler]; } } @@ -26,9 +28,11 @@ export function unbindAction( action: A, handler: TActionFunc, ) { - boundActions[action] = boundActions[action]?.filter( - (x) => x !== handler, - ) as any; + const handlers = boundActions[action]; + if (!handlers) return; + + const typedHandler = handler as ActionHandler; + boundActions[action] = handlers.filter((h) => h !== typedHandler); if (boundActions[action]?.length === 0) { delete boundActions[action]; @@ -41,7 +45,11 @@ type InvokeActionFunc = { args?: undefined, trigger?: TInvocationTrigger, ): void; - (action: A, args: TActionArgsMap[A]): void; + ( + action: A, + args: TActionArgsMap[A], + trigger?: TInvocationTrigger, + ): void; }; export const invokeAction: InvokeActionFunc = ( @@ -49,5 +57,5 @@ export const invokeAction: InvokeActionFunc = ( args?: TArgOfAction, trigger?: TInvocationTrigger, ) => { - boundActions[action]?.forEach((handler) => (handler as any)(args, trigger)); + boundActions[action]?.forEach((handler) => handler(args, trigger)); }; diff --git a/apps/web/src/lib/time.ts b/apps/web/src/lib/time.ts index 6b6f2037..d6526ffb 100644 --- a/apps/web/src/lib/time.ts +++ b/apps/web/src/lib/time.ts @@ -57,7 +57,7 @@ export function parseTimeCode({ const parts = cleanTimeCode.split(":"); if (parts.length !== 2) return null; const [minutes, seconds] = parts.map((part) => parseInt(part, 10)); - if (isNaN(minutes) || isNaN(seconds)) return null; + if (Number.isNaN(minutes) || Number.isNaN(seconds)) return null; if (minutes < 0 || seconds < 0 || seconds >= 60) return null; return minutes * 60 + seconds; } @@ -68,7 +68,12 @@ export function parseTimeCode({ const [hours, minutes, seconds] = parts.map((part) => parseInt(part, 10), ); - if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) return null; + if ( + Number.isNaN(hours) || + Number.isNaN(minutes) || + Number.isNaN(seconds) + ) + return null; if ( hours < 0 || minutes < 0 || @@ -87,10 +92,10 @@ export function parseTimeCode({ parseInt(part, 10), ); if ( - isNaN(hours) || - isNaN(minutes) || - isNaN(seconds) || - isNaN(centiseconds) + Number.isNaN(hours) || + Number.isNaN(minutes) || + Number.isNaN(seconds) || + Number.isNaN(centiseconds) ) return null; if ( @@ -112,7 +117,12 @@ export function parseTimeCode({ const [hours, minutes, seconds, frames] = parts.map((part) => parseInt(part, 10), ); - if (isNaN(hours) || isNaN(minutes) || isNaN(seconds) || isNaN(frames)) + if ( + Number.isNaN(hours) || + Number.isNaN(minutes) || + Number.isNaN(seconds) || + Number.isNaN(frames) + ) return null; if ( hours < 0 || @@ -141,7 +151,7 @@ export function guessTimeCodeFormat({ const numbers = timeCode.split(":"); - if (!numbers.every((n) => !isNaN(Number(n)))) return null; + if (!numbers.every((n) => !Number.isNaN(Number(n)))) return null; if (numbers.length === 2) return "MM:SS"; if (numbers.length === 3) return "HH:MM:SS"; diff --git a/apps/web/src/services/renderer/nodes/image-node.ts b/apps/web/src/services/renderer/nodes/image-node.ts index 2d396913..2a684d5f 100644 --- a/apps/web/src/services/renderer/nodes/image-node.ts +++ b/apps/web/src/services/renderer/nodes/image-node.ts @@ -16,13 +16,14 @@ export class ImageNode extends BaseNode { } private async load() { - this.image = new Image(); + const image = new Image(); + this.image = image; const url = URL.createObjectURL(this.params.file); await new Promise((resolve, reject) => { - this.image!.onload = () => resolve(); - this.image!.onerror = () => reject(new Error("Image load failed")); - this.image!.src = url; + image.onload = () => resolve(); + image.onerror = () => reject(new Error("Image load failed")); + image.src = url; }); URL.revokeObjectURL(url); diff --git a/apps/web/src/services/renderer/nodes/sticker-node.ts b/apps/web/src/services/renderer/nodes/sticker-node.ts index 7d078825..5af13a6f 100644 --- a/apps/web/src/services/renderer/nodes/sticker-node.ts +++ b/apps/web/src/services/renderer/nodes/sticker-node.ts @@ -25,17 +25,18 @@ export class StickerNode extends BaseNode { } private async load() { - this.image = new Image(); + const image = new Image(); + this.image = image; const color = this.params.color ? `&color=${encodeURIComponent(this.params.color)}` : ""; const url = `https://api.iconify.design/${this.params.iconName}.svg?width=200&height=200${color}`; await new Promise((resolve, reject) => { - this.image!.onload = () => resolve(); - this.image!.onerror = () => + image.onload = () => resolve(); + image.onerror = () => reject(new Error(`Failed to load sticker: ${this.params.iconName}`)); - this.image!.src = url; + image.src = url; }); } diff --git a/apps/web/src/services/renderer/scene-exporter.ts b/apps/web/src/services/renderer/scene-exporter.ts index 1db1daf3..05f06fd5 100644 --- a/apps/web/src/services/renderer/scene-exporter.ts +++ b/apps/web/src/services/renderer/scene-exporter.ts @@ -81,7 +81,7 @@ export class SceneExporter extends EventEmitter { target: new BufferTarget(), }); - const videoSource = new CanvasSource(this.renderer.canvas as any, { + const videoSource = new CanvasSource(this.renderer.canvas, { codec: this.format === "webm" ? "vp9" : "avc", bitrate: qualityMap[this.quality], }); diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/stores/keybindings-store.ts index a1d9ff4c..137cf5e9 100644 --- a/apps/web/src/stores/keybindings-store.ts +++ b/apps/web/src/stores/keybindings-store.ts @@ -93,7 +93,7 @@ export const useKeybindingsStore = create()( importKeybindings: (config: KeybindingConfig) => { // Validate all keys and actions - for (const [key, action] of Object.entries(config)) { + for (const [key] of Object.entries(config)) { // Validate the key format if (typeof key !== "string" || key.length === 0) { throw new Error(`Invalid key format: ${key}`);