diff --git a/.claude/docs/files.json b/.claude/docs/files.json new file mode 100644 index 00000000..4a188919 --- /dev/null +++ b/.claude/docs/files.json @@ -0,0 +1,18450 @@ +{ + "files": { + "src/components/video-editor/videoPlayback/zoomTransform.ts": { + "short_description": "Compute and apply zoom/pan transforms and motion blur for the video camera layer", + "category": "SOURCE_CODE", + "description": "Provides utilities to compute zoom/pan transforms and to apply them to a PIXI camera container, including a motion-blur response based on camera velocity. It encapsulates logic for focus calculation, transform inversion, and configuring Blur/MotionBlur filters during playback to create smooth motion effects.", + "key_constructs": [ + { + "name": "computeZoomTransform", + "type": "function", + "purpose": "Calculate scale and translation based on zoomScale, focus, stage and mask geometry.", + "callers": [ + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 38, + "context": "computeZoomTransform," + }, + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 608, + "context": "const startTransform = computeZoomTransform({" + }, + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 616, + "context": "const endTransform = computeZoomTransform({" + }, + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 656, + "context": "const projectedTransform = computeZoomTransform({" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 63, + "context": "computeZoomTransform," + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 948, + "context": "const startTransform = computeZoomTransform({" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 956, + "context": "const endTransform = computeZoomTransform({" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 995, + "context": "const projectedTransform = computeZoomTransform({" + } + ] + }, + { + "name": "computeFocusFromTransform", + "type": "function", + "purpose": "Invert an applied transform to compute the normalized focus point within the base mask.", + "callers": [ + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 37, + "context": "computeFocusFromTransform," + }, + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 634, + "context": "targetFocus = computeFocusFromTransform({" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 62, + "context": "computeFocusFromTransform," + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 974, + "context": "targetFocus = computeFocusFromTransform({" + } + ] + }, + { + "name": "applyZoomTransform", + "type": "function", + "purpose": "Apply computed transform to PIXI camera container and adjust blur/motion-blur filters based on motion and state.", + "callers": [ + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 36, + "context": "applyZoomTransform," + }, + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 391, + "context": "applyZoomTransform({" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 61, + "context": "applyZoomTransform," + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 574, + "context": "applyZoomTransform({" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 843, + "context": "const appliedTransform = applyZoomTransform({" + } + ] + }, + { + "name": "createMotionBlurState", + "type": "function", + "purpose": "Create and initialize a MotionBlurState object used to track previous frame camera values.", + "callers": [ + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 39, + "context": "createMotionBlurState," + }, + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 123, + "context": "private motionBlurState: MotionBlurState = createMotionBlurState();" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 64, + "context": "createMotionBlurState," + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 209, + "context": "const motionBlurStateRef = useRef(createMotionBlurState());" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 550, + "context": "motionBlurStateRef.current = createMotionBlurState();" + } + ] + }, + { + "name": "MotionBlurState", + "type": "function", + "purpose": "Interface describing persisted motion blur/frame tracking state." + } + ], + "semantic_tags": [ + "rendering", + "zoom", + "motion-blur", + "pixi", + "transforms" + ], + "handles_entities": [ + "MotionBlurState", + "AppliedTransform" + ], + "key_behaviors": [ + "computes camera scale and translation for zoom effects", + "applies camera transform to PIXI container", + "calculates and configures motion blur based on camera velocity", + "provides helpers to invert transforms to focus coordinates" + ], + "insights": [ + { + "type": "bug_fix", + "category": "Edge Case", + "title": "applyZoomTransform motionBlur default alignment", + "problem": "applyZoomTransform used motionBlurEnabled default true which could produce motion blur even when disabled in settings.", + "root_cause": "Default param mismatch across modules handling transforms and rendering.", + "solution": "Set motionBlurEnabled default to false in applyZoomTransform signature so motion transforms respect the disabled-by-default setting.", + "lesson_learned": "Transformation helper functions should use parameters that mirror application-level defaults to avoid silent visual discrepancies.", + "commits": [ + "f2c6d8f" + ], + "constructs": [ + "applyZoomTransform" + ] + }, + { + "type": "bug_fix", + "category": "State Management", + "title": "Gate motion blur by motionBlurEnabled flag", + "problem": "Motion blur continued to be applied even when user toggled motion blur off (or animation used stale value).", + "root_cause": "applyZoomTransform used isPlaying and motionIntensity checks but didn't consider a motionBlurEnabled preference.", + "solution": "Added motionBlurEnabled parameter to TransformParams and used it to gate application of blurFilter and computed motionBlur amount.", + "lesson_learned": "Low-level transform functions should accept all flags that influence rendering rather than reading global state; keep transforms pure and parameterized.", + "commits": [ + "8f31bde" + ], + "constructs": [ + "applyZoomTransform" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/contexts/ShortcutsContext.tsx": { + "short_description": "React context for managing keyboard shortcuts and persistence", + "category": "SOURCE_CODE", + "description": "Provides a React context and hook to read, update, persist, and expose keyboard shortcut configuration plus platform detection (isMac). It loads persisted shortcuts on startup, merges defaults, and exposes methods to open/close the shortcuts config UI and save changes via the electron API.", + "key_constructs": [ + { + "name": "ShortcutsProvider", + "type": "function", + "purpose": "React provider component that manages shortcut state, platform info, and persistence functions.", + "callers": [ + { + "file": "src/App.tsx", + "line": 8, + "context": "import { ShortcutsProvider } from \"./contexts/ShortcutsContext\";" + }, + { + "file": "src/App.tsx", + "line": 38, + "context": "" + }, + { + "file": "src/App.tsx", + "line": 41, + "context": "" + } + ] + }, + { + "name": "useShortcuts", + "type": "function", + "purpose": "Hook to access the ShortcutsContext from components.", + "callers": [ + { + "file": "src/components/video-editor/ShortcutsConfigDialog.tsx", + "line": 13, + "context": "import { useShortcuts } from \"@/contexts/ShortcutsContext\";" + }, + { + "file": "src/components/video-editor/ShortcutsConfigDialog.tsx", + "line": 30, + "context": "useShortcuts();" + }, + { + "file": "src/components/video-editor/KeyboardShortcutsHelp.tsx", + "line": 3, + "context": "import { useShortcuts } from \"@/contexts/ShortcutsContext\";" + }, + { + "file": "src/components/video-editor/KeyboardShortcutsHelp.tsx", + "line": 7, + "context": "const { shortcuts, isMac, openConfig } = useShortcuts();" + }, + { + "file": "src/components/video-editor/timeline/TimelineEditor.tsx", + "line": 24, + "context": "import { useShortcuts } from \"@/contexts/ShortcutsContext\";" + }, + { + "file": "src/components/video-editor/timeline/TimelineEditor.tsx", + "line": 787, + "context": "const { shortcuts: keyShortcuts, isMac } = useShortcuts();" + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 15, + "context": "import { useShortcuts } from \"@/contexts/ShortcutsContext\";" + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 151, + "context": "const { shortcuts, isMac } = useShortcuts();" + } + ] + }, + { + "name": "DEFAULT_SHORTCUTS", + "type": "constant", + "purpose": "Imported default shortcut definitions used to initialize state." + } + ], + "semantic_tags": [ + "shortcuts", + "react-context", + "persistence", + "platform-detection", + "ui-configuration" + ], + "handles_entities": [ + "ShortcutsConfig" + ], + "key_behaviors": [ + "loads persisted shortcuts on app start", + "exposes API to update and persist shortcut configuration", + "tracks whether config dialog is open and whether running on macOS" + ], + "insights": [ + { + "type": "feature", + "category": "State Management", + "title": "Provide centralized shortcuts config and persistence", + "problem": "Multiple components duplicated logic to read platform and shortcuts; persistence was missing.", + "root_cause": "No scoped context for shortcuts existed.", + "solution": "Added ShortcutsContext provider that loads saved shortcuts from main via electronAPI.getShortcuts, merges with defaults, exposes isMac detection, setShortcuts, persistShortcuts and config open/close helpers.", + "lesson_learned": "Use a React context to centralize platform-aware shortcut logic and avoid prop drilling. Provide persistShortcuts wrapper to unify save behavior.", + "commits": [ + "9bc2c78" + ], + "constructs": [ + "ShortcutsProvider", + "useShortcuts" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/hooks/useCameraDevices.ts": { + "short_description": "Hook to enumerate and manage available camera devices", + "category": "SOURCE_CODE", + "description": "A React hook that enumerates video input devices when enabled, exposes the device list, selected device id, loading/error state, and handles dynamic device changes. It chooses a sensible default selection and updates when devices are added/removed via the MediaDevices devicechange event.", + "key_constructs": [ + { + "name": "useCameraDevices", + "type": "function", + "purpose": "Hook that enumerates videoinput devices, manages selection and listens for device changes.", + "callers": [ + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 25, + "context": "import { useCameraDevices } from \"../../hooks/useCameraDevices\";" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 124, + "context": "} = useCameraDevices(webcamEnabled);" + } + ] + }, + { + "name": "CameraDevice", + "type": "function", + "purpose": "Type describing deviceId, label, and groupId for a camera device." + } + ], + "semantic_tags": [ + "media-devices", + "camera", + "hooks", + "device-enumeration", + "permissions" + ], + "handles_entities": [ + "CameraDevice" + ], + "key_behaviors": [ + "enumerates available camera devices", + "selects a default camera and exposes selection setter", + "updates device list on device change events" + ], + "insights": [ + { + "type": "bug_fix", + "category": "State Management", + "title": "Resilient selected device handling and removal when devices unplug", + "problem": "If the previously selected camera was unplugged, the hook could leave selectedDeviceId pointing to a stale id or cause wrong behavior.", + "root_cause": "Effect depended on selectedDeviceId and used closures; it didn't robustly fall back to an available device when the selected ID disappeared.", + "solution": "Use a ref to track selectedDeviceId within effect and, when loading devices, pick first available device if current selected ID is missing. Remove selectedDeviceId from effect deps to avoid unnecessary re-runs; register devicechange handler to reload devices.", + "lesson_learned": "When reacting to device lists, don't rely solely on state captured by closure \u2014 use refs or event-driven updates and always validate that selected IDs are still present in the new list.", + "commits": [ + "eade280" + ], + "constructs": [ + "useCameraDevices" + ] + }, + { + "type": "error_handling", + "category": "Error Handling", + "title": "Set error state when enumerateDevices rejects", + "problem": "Enumeration failures (permission denied or other) weren't surfaced properly to consumers.", + "root_cause": "Previous tests and code paths assumed enumerateDevices resolves; actual rejection paths weren't asserted/handled.", + "solution": "Catch enumeration errors, set error state string, set devices to empty and isLoading to false. Tests were added to assert this behavior.", + "lesson_learned": "Always handle navigator.mediaDevices errors and expose understandable error information to the UI. Add tests covering rejection paths.", + "commits": [ + "baec9a7", + "eade280" + ], + "constructs": [ + "useCameraDevices" + ] + }, + { + "type": "feature", + "category": "API / Edge Case", + "title": "Enumerate camera devices with permission re-request if labels are empty", + "problem": "Device labels can be empty until camera permission is granted, making it hard to display user-friendly names.", + "root_cause": "Browser privacy model hides device labels without user permission; simply calling enumerateDevices() can return unlabeled devices.", + "solution": "Added useCameraDevices hook which enumerates devices, detects missing labels and (when enabled) requests a temporary getUserMedia({video:true}) to obtain labels, stops tracks immediately, and re-enumerates. Also listens to devicechange and cleans up event listeners on unmount.", + "commits": [ + "0a5e57c" + ], + "constructs": [ + "useCameraDevices" + ] + }, + { + "type": "bug_fix", + "category": "Edge Case", + "title": "Avoid stale selectedDeviceId on mount", + "problem": "Selected device id might remain unset if not handled correctly after enumeration.", + "root_cause": "Selected device state needed initialization after devices are loaded.", + "solution": "Set selectedDeviceId to the first discovered device when none was set yet.", + "commits": [ + "0a5e57c" + ], + "constructs": [ + "useCameraDevices" + ] + } + ], + "tests": { + "exercised_by": [ + "src/hooks/useCameraDevices.test.ts" + ], + "test_functions": [ + "useCameraDevices", + "should request permission if labels are empty", + "should set first device as default", + "should list video input devices" + ], + "source_commits": [ + "eade280", + "baec9a7", + "0a5e57c" + ] + }, + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/hooks/useMicrophoneDevices.ts": { + "short_description": "Hook to request permission and enumerate microphone devices", + "category": "SOURCE_CODE", + "description": "A React hook that requests audio permission (to obtain labels), enumerates audio input devices, tracks selection and loading/error state, and updates on device change events. It stops the temporary permission stream after enumeration and defaults selection to a discovered device when appropriate.", + "key_constructs": [ + { + "name": "useMicrophoneDevices", + "type": "function", + "purpose": "Hook that requests microphone permission, enumerates audio input devices, and manages selection.", + "callers": [ + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 26, + "context": "import { useMicrophoneDevices } from \"../../hooks/useMicrophoneDevices\";" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 117, + "context": "} = useMicrophoneDevices(microphoneEnabled);" + } + ] + }, + { + "name": "MicrophoneDevice", + "type": "function", + "purpose": "Type describing deviceId, label, and groupId for a microphone device." + } + ], + "semantic_tags": [ + "audio", + "microphone", + "hooks", + "permissions", + "device-enumeration" + ], + "handles_entities": [ + "MicrophoneDevice" + ], + "key_behaviors": [ + "requests mic permission to obtain device labels", + "enumerates audio input devices and exposes selection", + "updates list on device change events" + ], + "insights": [ + { + "type": "feature", + "category": "API", + "title": "Enumerate microphone devices and keep labels after permission", + "problem": "Need to present available microphone devices and labels to the user.", + "root_cause": "Device labels are only available after permission is granted.", + "solution": "New useMicrophoneDevices hook requests a short getUserMedia({audio:true}) to obtain labels, enumerates audioinput devices, stops permission stream, and listens to devicechange to refresh list.", + "lesson_learned": "Request permission to get device labels, always stop temporary permission streams, and handle devicechange events. Provide selectedDeviceId and isLoading/error state for UI.", + "commits": [ + "64bc261" + ], + "constructs": [ + "useMicrophoneDevices" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/i18n/config.ts": { + "short_description": "Defines supported locales, namespaces, and i18n constants", + "category": "SOURCE_CODE", + "description": "Holds constants and types for internationalization: default locale, supported locales, i18n namespaces, and a storage key for persisting user locale. It centralizes locale-related configuration used by the i18n context and UI.", + "key_constructs": [ + { + "name": "DEFAULT_LOCALE", + "type": "constant", + "purpose": "Default locale code used by the app.", + "callers": [ + { + "file": "src/i18n/loader.ts", + "line": 1, + "context": "import { DEFAULT_LOCALE, type I18nNamespace, type Locale } from \"./config\";" + }, + { + "file": "src/i18n/loader.ts", + "line": 56, + "context": "getMessageValue(messages[DEFAULT_LOCALE]?.[namespace], key);" + }, + { + "file": "src/contexts/I18nContext.tsx", + "line": 11, + "context": "DEFAULT_LOCALE," + }, + { + "file": "src/contexts/I18nContext.tsx", + "line": 54, + "context": "return DEFAULT_LOCALE;" + } + ] + }, + { + "name": "SUPPORTED_LOCALES", + "type": "constant", + "purpose": "List of locale codes that the app supports.", + "callers": [ + { + "file": "src/contexts/I18nContext.tsx", + "line": 15, + "context": "SUPPORTED_LOCALES," + }, + { + "file": "src/contexts/I18nContext.tsx", + "line": 44, + "context": "return (SUPPORTED_LOCALES as readonly string[]).includes(value);" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 21, + "context": "import { type Locale, SUPPORTED_LOCALES } from \"@/i18n/config\";" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 243, + "context": "{SUPPORTED_LOCALES.map((loc) => (" + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 17, + "context": "import { type Locale, SUPPORTED_LOCALES } from \"@/i18n/config\";" + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 1582, + "context": "{SUPPORTED_LOCALES.map((loc) => (" + } + ] + }, + { + "name": "I18N_NAMESPACES", + "type": "constant", + "purpose": "Namespaces used for translation resource organization." + } + ], + "semantic_tags": [ + "i18n", + "localization", + "config", + "locale" + ], + "handles_entities": [], + "key_behaviors": [ + "defines supported locales and translation namespaces", + "provides storage key for persisted locale selection" + ], + "insights": [ + { + "type": "feature", + "category": "Configuration", + "title": "Add supported locales (tr, fr)", + "problem": "New locale translations (Turkish, French) were added but SUPPORTED_LOCALES needed updates.", + "root_cause": "SUPPORTED_LOCALES was missing newly added locale identifiers.", + "solution": "Extended SUPPORTED_LOCALES to include 'tr' and 'fr'.", + "Lesson Learned": "When adding locale files, update central config and any validation tools to include new locales.", + "commits": [ + "c36349d", + "e739653" + ], + "constructs": [ + "SUPPORTED_LOCALES" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/lib/userPreferences.ts": { + "short_description": "Load and persist user preferences with validation", + "category": "SOURCE_CODE", + "description": "Manages user preferences (padding, aspect ratio, export quality/format) saved in localStorage. It validates loaded values against allowed ranges and falls back to defaults; saveUserPreferences updates only provided fields while preserving validity.", + "key_constructs": [ + { + "name": "loadUserPreferences", + "type": "function", + "purpose": "Read and validate persisted user preferences from localStorage, returning defaults when invalid.", + "callers": [ + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 34, + "context": "import { loadUserPreferences, saveUserPreferences } from \"@/lib/userPreferences\";" + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 388, + "context": "const prefs = loadUserPreferences();" + } + ] + }, + { + "name": "saveUserPreferences", + "type": "function", + "purpose": "Merge provided partial preferences with current and persist them to localStorage.", + "callers": [ + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 34, + "context": "import { loadUserPreferences, saveUserPreferences } from \"@/lib/userPreferences\";" + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 401, + "context": "saveUserPreferences({ padding, aspectRatio, exportQuality, exportFormat });" + } + ] + }, + { + "name": "UserPreferences", + "type": "constant", + "purpose": "Interface defining the structure of persisted user preferences." + } + ], + "semantic_tags": [ + "preferences", + "localstorage", + "validation", + "export-settings" + ], + "handles_entities": [ + "UserPreferences" + ], + "key_behaviors": [ + "loads user preferences with validation and defaults", + "persists updated preference fields to localStorage" + ], + "insights": [ + { + "type": "feature", + "category": "State Management", + "title": "Persist and validate user preferences to localStorage", + "problem": "User preferences (padding, aspect ratio, export quality/format) were not persisted across sessions.", + "root_cause": "No existing module for saving/loading preferences; naive JSON parse or invalid stored values could cause incorrect state.", + "solution": "Add userPreferences module with loadUserPreferences and saveUserPreferences. Implement safeJsonParse, default prefs, field validation (range checks for padding, whitelist aspect ratios, include 'good' and 'mp4' as valid options), and wrap localStorage access in try/catch to handle quota/private-mode errors.", + "lesson_learned": "When persisting UI settings to localStorage, validate fields and guard against localStorage failures. Use defensive merging when saving partial updates.", + "commits": [ + "7d74619", + "4f48ecd" + ], + "constructs": [ + "loadUserPreferences", + "saveUserPreferences", + "safeJsonParse" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/utils/platformUtils.ts": { + "short_description": "Detect platform and format modifier key labels for UI", + "category": "SOURCE_CODE", + "description": "Provides helpers to determine the underlying OS/platform (via Electron or navigator fallback) and to format modifier/shift keys and full keyboard shortcuts for UI display. It caches the platform result and exposes conveniences for platform-specific symbols (e.g., \u2318 on macOS).", + "key_constructs": [ + { + "name": "getPlatform", + "type": "function", + "purpose": "Async retrieval of platform string from Electron with navigator fallback and caching." + }, + { + "name": "isMac", + "type": "function", + "purpose": "Returns whether the current platform is macOS.", + "callers": [ + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 23, + "context": "import { isMac as getIsMac } from \"@/utils/platformUtils\";" + }, + { + "file": "src/contexts/ShortcutsContext.tsx", + "line": 11, + "context": "import { isMac as getIsMac } from \"@/utils/platformUtils\";" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 23, + "context": "import { isMac as getIsMac } from \"@/utils/platformUtils\";" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 79, + "context": "getIsMac().then(setIsMac);" + }, + { + "file": "src/contexts/ShortcutsContext.tsx", + "line": 11, + "context": "import { isMac as getIsMac } from \"@/utils/platformUtils\";" + }, + { + "file": "src/contexts/ShortcutsContext.tsx", + "line": 37, + "context": "getIsMac()" + } + ] + }, + { + "name": "formatShortcut", + "type": "function", + "purpose": "Format an array of key identifiers into a platform-appropriate display string.", + "callers": [ + { + "file": "src/components/video-editor/timeline/TimelineEditor.tsx", + "line": 28, + "context": "import { formatShortcut } from \"@/utils/platformUtils\";" + }, + { + "file": "src/components/video-editor/timeline/TimelineEditor.tsx", + "line": 790, + "context": "formatShortcut([\"mod\", \"Scroll\"]).then((zoom) => {" + } + ] + } + ], + "semantic_tags": [ + "platform-detection", + "shortcuts", + "electron", + "ui-formatting" + ], + "handles_entities": [ + "Platform" + ], + "key_behaviors": [ + "detects host platform (Electron or browser fallback)", + "provides modifier symbols tailored to platform", + "formats keyboard shortcuts for display" + ], + "insights": [ + { + "type": "breaking_change", + "category": "API", + "title": "platformUtils refactor: synchronous -> asynchronous APIs", + "problem": "formatShortcut/isMac/getModifierKey/getShiftKey were synchronous utilities relying on navigator, not accurate in Electron. Changing them to async breaks many callers expecting sync strings.", + "root_cause": "Need for accurate platform detection in renderer required IPC to main via electronAPI.getPlatform; that is async and prompted changing utilities to async.", + "solution": "Added getPlatform() (with caching) that invokes window.electronAPI.getPlatform(); converted isMac/getModifierKey/getShiftKey/formatShortcut to async functions that await platform. Provided a navigator-based fallback for dev.", + "lesson_learned": "Converting widely used sync utils into async is a breaking change \u2014 update all call sites (components, tests) to await or handle promises. Prefer incremental approach: add new async helpers and keep legacy sync helpers until all consumers are ported.", + "commits": [ + "f34bd19" + ], + "constructs": [ + "getPlatform", + "isMac", + "getModifierKey", + "getShiftKey", + "formatShortcut" + ] + }, + { + "type": "other", + "category": "Configuration", + "title": "Cache platform result in renderer", + "problem": "Repeated IPC calls for platform detection degrade performance.", + "root_cause": "Each call previously would invoke IPC.", + "solution": "Introduced cachedPlatform variable to memoize platform after first IPC call.", + "lesson_learned": "Cache stable environment values retrieved from main to avoid unnecessary IPC overhead.", + "commits": [ + "f34bd19" + ], + "constructs": [ + "getPlatform" + ] + }, + { + "type": "refactoring", + "category": "Other", + "title": "Platform-aware keyboard shortcut formatting utility", + "problem": "Shortcut UI snippets were duplicated and platform-specific formatting scattered.", + "root_cause": "Different places rendered modifier keys with hardcoded symbols.", + "solution": "Add formatShortcut/isMac/getModifierKey/getShiftKey helpers and replace hardcoded snippets with formatShortcut([...]).", + "lesson_learned": "Encapsulate platform specifics for UI display (modifier symbols) in a small util so all components render consistently.", + "commits": [ + "3919380" + ], + "constructs": [ + "formatShortcut", + "isMac", + "getModifierKey", + "getShiftKey" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/video-editor/AnnotationOverlay.tsx": { + "short_description": "Draggable, resizable annotation overlay component for editor canvas", + "category": "SOURCE_CODE", + "description": "Renders an annotation region (text, image, or figure) inside the editor using react-rnd for drag/resize interactions. It translates percentage-based annotation geometry to pixel coordinates, handles selection styling, and reports position/size updates back to parent state.", + "key_constructs": [ + { + "name": "AnnotationOverlay", + "type": "function", + "purpose": "React component that renders and manages a single annotation's drag/resize/selection behavior." + } + ], + "semantic_tags": [ + "annotations", + "ui", + "drag-resize", + "canvas", + "editor" + ], + "handles_entities": [ + "AnnotationRegion" + ], + "key_behaviors": [ + "renders annotations as draggable/resizable overlays", + "converts between percentage geometry and pixel coordinates", + "notifies parent about position and size changes" + ], + "insights": [ + { + "type": "refactoring", + "category": "Refactoring", + "title": "Move arrow icons to reusable SVGs and streamline figure rendering", + "problem": "Annotation overlay used React icon libraries which are not deterministic for export and caused duplication of icon-to-export logic.", + "root_cause": "UI-only icons can't be easily reproduced in export renderer; different implementations led to visual mismatch.", + "solution": "Introduced ArrowSvgs and getArrowComponent usage; simplified figure rendering to use arrow SVG components and consolidated figure types.", + "lesson_learned": "For visuals that must be exported, prefer deterministic, self-contained rendering primitives (inline SVG or canvas) rather than external icon libraries.", + "commits": [ + "6ac712e", + "262745a" + ], + "constructs": [ + "AnnotationOverlay" + ] + }, + { + "type": "bug_fix", + "category": "Edge Case", + "title": "Prevent click event after dragging an annotation", + "problem": "Click handler would fire immediately after a drag due to click events being emitted after drag stop, causing accidental selection toggles.", + "root_cause": "No differentiation between click and drag end; no isDragging flag.", + "solution": "Added isDraggingRef, set onDragStart/onDragStop with a short debounce to suppress click events that follow drag, and use pointer-events logic for selected vs non-selected items.", + "lesson_learned": "Distinguish user interactions (drag vs click) with state flags to prevent accidental click handlers firing post-drag.", + "commits": [ + "79e40cf", + "c847953" + ], + "constructs": [ + "AnnotationOverlay" + ] + }, + { + "type": "refactoring", + "category": "State Management", + "title": "Support zIndex boost for selected annotation", + "problem": "Overlapping annotations made selection/editing difficult because stacking order was static.", + "root_cause": "No runtime z-index boost for selected item.", + "solution": "Added zIndex prop and isSelectedBoost flag that increases z-index when selected so the item is easier to drag/edit.", + "lesson_learned": "Expose stacking order (zIndex) in annotation model and temporarily raise z-index for focused/selected elements to avoid hit-test friction.", + "commits": [ + "c847953" + ], + "constructs": [ + "AnnotationOverlay" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/video-editor/ShortcutsConfigDialog.tsx": { + "short_description": "Dialog UI to view, capture, resolve conflicts, and persist keyboard shortcuts", + "category": "SOURCE_CODE", + "description": "Provides a dialog for configuring app keyboard shortcuts: capturing keybindings, detecting conflicts (fixed vs configurable), swapping conflicting bindings, resetting to defaults, and persisting changes via the shortcuts context and electron API. It handles key capture lifecycle and user feedback (toasts) when saving or conflicts occur.", + "key_constructs": [ + { + "name": "ShortcutsConfigDialog", + "type": "function", + "purpose": "React component that presents the configurable and fixed shortcuts and allows capturing and resolving shortcut bindings." + }, + { + "name": "findConflict", + "type": "function", + "purpose": "Imported utility used to detect conflicts when capturing a new binding." + }, + { + "name": "formatBinding", + "type": "function", + "purpose": "Imported utility to format a ShortcutBinding for display based on platform." + } + ], + "semantic_tags": [ + "shortcuts", + "dialog", + "ui", + "key-capture", + "conflict-resolution" + ], + "handles_entities": [ + "ShortcutsConfig", + "ShortcutBinding" + ], + "key_behaviors": [ + "captures keyboard input for shortcut replacement", + "detects and resolves binding conflicts (swap/cancel)", + "saves and persists updated shortcuts or resets to defaults" + ], + "insights": [ + { + "type": "refactoring", + "category": "Localization", + "title": "Localize fixed shortcuts in the ShortcutsConfigDialog", + "problem": "Dialog listed labels directly and keyed list by label, which could be non-unique or non-localizable.", + "root_cause": "Data model lacked stable i18n identifiers.", + "solution": "Key list by i18nKey and render localized labels via t(i18nKey, { defaultValue: label }).", + "lesson_learned": "Use stable identifiers (not display strings) for keys in lists and for i18n lookups.", + "commits": [ + "cd0f2ab" + ], + "constructs": [ + "ShortcutsConfigDialog" + ] + }, + { + "type": "feature", + "category": "API", + "title": "Conflict resolution UI when reassigning shortcuts", + "problem": "When user attempts to assign a binding already used by another configurable action, UI needed a way to swap or cancel.", + "root_cause": "findConflict logic detects conflicts, but UI needed UX to address them.", + "solution": "Show amber conflict banner with Swap and Cancel options; prevent assigning keys reserved by FIXED_SHORTCUTS; persist changes only after user saves.", + "lesson_learned": "Provide clear UX for conflict resolution (swap or cancel) instead of silently overriding; validate against fixed/reserved shortcuts and show informative errors.", + "commits": [ + "d76f38f" + ], + "constructs": [ + "ShortcutsConfigDialog", + "handleSwap", + "handleCancelConflict" + ] + }, + { + "type": "feature", + "category": "UI", + "title": "Configurable shortcuts dialog with capture mode", + "problem": "Users couldn't rebind keyboard shortcuts from within the app.", + "root_cause": "No UI for capturing and persisting custom key bindings.", + "solution": "Implemented dialog that captures keydown events in capture mode, updates draft config, persists via ShortcutsContext.persistShortcuts and supports Reset to defaults. Capturing ignores pure modifier keys and uses escape to cancel.", + "lesson_learned": "Capture mode should prevent default/propagation, ignore modifier-only events, and provide a clear cancel UX (Esc). Persist changes explicitly rather than instantly to allow preview/reset.", + "commits": [ + "9bc2c78" + ], + "constructs": [ + "ShortcutsConfigDialog" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/lib/exporter/index.ts": { + "short_description": "Re-exports video/gif export pipeline modules and types", + "category": "SOURCE_CODE", + "description": "Entry-point barrel that re-exports exporter-related classes, functions and types (frame renderer, gif exporter, muxer, decoders, and types) for easier imports elsewhere in the app. It centralizes exporter API surface used by the editor and export UI.", + "key_constructs": [ + { + "name": "FrameRenderer", + "type": "function", + "purpose": "Re-exported class/function responsible for rendering frames for export." + }, + { + "name": "GifExporter", + "type": "function", + "purpose": "Re-exported exporter for GIF creation and frame rendering." + }, + { + "name": "VideoExporter", + "type": "function", + "purpose": "Re-exported class responsible for producing MP4 exports." + } + ], + "semantic_tags": [ + "exporter", + "video", + "gif", + "muxing", + "decoding" + ], + "handles_entities": [ + "ExportConfig", + "ExportResult", + "VideoFrameData" + ], + "key_behaviors": [ + "provides exporter APIs for GIF and MP4 pipelines", + "exposes decoder and muxer utilities for export flow" + ], + "insights": [ + { + "type": "refactoring", + "category": "Typing", + "title": "Export new ExportQuality type from exporter index", + "problem": "New ExportQuality type was added in types but not re-exported, making imports inconsistent.", + "root_cause": "Types were extended but index barrel file not updated.", + "solution": "Added ExportQuality to exported types.", + "lesson_learned": "Keep barrel files in sync when type surfaces are added so consumers can import from a single entrypoint.", + "commits": [ + "ed3cdab" + ], + "constructs": [] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/lib/customFonts.ts": { + "short_description": "Load, validate, persist and manage custom Google Fonts in the app", + "category": "SOURCE_CODE", + "description": "Utilities to manage custom fonts: persist font metadata in localStorage, dynamically inject Google Fonts @import rules, wait for the font to load via the Font Loading API, and remove fonts. It provides helpers to validate import URLs, parse family names, and generate IDs for stored fonts.", + "key_constructs": [ + { + "name": "addCustomFont", + "type": "function", + "purpose": "Attempt to load a font and add it to persisted custom fonts if successful.", + "callers": [ + { + "file": "src/components/video-editor/AddCustomFontDialog.tsx", + "line": 17, + "context": "addCustomFont," + }, + { + "file": "src/components/video-editor/AddCustomFontDialog.tsx", + "line": 85, + "context": "await addCustomFont(newFont);" + } + ] + }, + { + "name": "loadFont", + "type": "function", + "purpose": "Inject a style @import for a Google Font and wait for it to be available in document.fonts." + }, + { + "name": "loadAllCustomFonts", + "type": "function", + "purpose": "Load all fonts stored in localStorage on app init and log failures.", + "callers": [ + { + "file": "src/App.tsx", + "line": 9, + "context": "import { loadAllCustomFonts } from \"./lib/customFonts\";" + }, + { + "file": "src/App.tsx", + "line": 25, + "context": "loadAllCustomFonts().catch((error) => {" + } + ] + } + ], + "semantic_tags": [ + "fonts", + "google-fonts", + "localstorage", + "ui-styling", + "dynamic-loading" + ], + "handles_entities": [ + "CustomFont" + ], + "key_behaviors": [ + "load and register Google Fonts into the document", + "persist and remove custom font metadata", + "validate Google Fonts import URLs and parse family names" + ], + "insights": [ + { + "type": "feature", + "category": "I/O / Parsing", + "title": "Custom Google Fonts management with storage and loading", + "problem": "Users wanted to add Google Fonts to use in annotations; fonts must be loaded and stored across sessions.", + "root_cause": "No existing plumbing to import, verify, persist and use remote fonts.", + "solution": "Implemented getCustomFonts/saveCustomFonts, addCustomFont (which calls loadFont and persists), loadFont (injects @import into head), waitForFont using document.fonts.load with timeout, parseFontFamilyFromImport and validation helpers, and loadAllCustomFonts to load on app init.", + "lesson_learned": "When dynamically loading fonts: inject CSS @import, wait for document.fonts API to confirm load (with timeout), and persist metadata separately. Failures should not break the app; surface readable errors to users.", + "commits": [ + "05f4e74", + "05f4e74" + ], + "constructs": [ + "getCustomFonts", + "saveCustomFonts", + "addCustomFont", + "removeCustomFont", + "loadFont", + "waitForFont", + "loadAllCustomFonts", + "parseFontFamilyFromImport", + "isValidGoogleFontsUrl", + "generateFontId" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/lib/frameStep.ts": { + "short_description": "Compute precise single-frame step timing for playback controls", + "category": "SOURCE_CODE", + "description": "Defines a constant frame duration for 60 FPS and a helper to compute the next playhead time when stepping one frame forward or backward, clamped to the media duration. Used to implement frame-accurate step controls in the editor timeline.", + "key_constructs": [ + { + "name": "FRAME_DURATION_SEC", + "type": "constant", + "purpose": "Duration in seconds of one frame at 60 FPS (~1/60)." + }, + { + "name": "computeFrameStepTime", + "type": "function", + "purpose": "Compute the new playhead time after stepping a single frame forward or backward with clamping.", + "callers": [ + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 31, + "context": "import { computeFrameStepTime } from \"@/lib/frameStep\";" + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 1051, + "context": "const newTime = computeFrameStepTime(" + } + ] + } + ], + "semantic_tags": [ + "timeline", + "frame-stepping", + "playback", + "time" + ], + "handles_entities": [], + "key_behaviors": [ + "computes frame-accurate playhead time for single-frame stepping" + ], + "insights": [ + { + "type": "feature", + "category": "Other", + "title": "Provide deterministic frame-step computation", + "problem": "Frame-step math was initially inline in tests; reusability and testing required extraction.", + "root_cause": "Functionality embedded in UI made unit testing harder.", + "solution": "Add computeFrameStepTime and FRAME_DURATION_SEC constants to a library file so time stepping logic is shared between UI and tests.", + "lesson_learned": "Extract pure logic to small modules and add unit tests to avoid regressions in time-based behaviors.", + "commits": [ + "e5430ee" + ], + "constructs": [ + "computeFrameStepTime", + "FRAME_DURATION_SEC" + ] + } + ], + "tests": { + "exercised_by": [ + "src/lib/__tests__/frameStepNavigation.test.ts" + ], + "test_functions": [ + "computeFrameStepTime" + ], + "source_commits": [ + "e5430ee" + ] + }, + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/lib/requestCameraAccess.ts": { + "short_description": "Unified camera access request with Electron fallback and status reporting", + "category": "SOURCE_CODE", + "description": "Requests camera access either through an exposed Electron API (if available) or via navigator.mediaDevices.getUserMedia, returning a structured result describing granted/denied status and any errors. It normalizes DOMException names into a status string and stops acquired streams immediately to avoid leakage.", + "key_constructs": [ + { + "name": "requestCameraAccess", + "type": "function", + "purpose": "Request camera access using electron bridge when available, otherwise use navigator.getUserMedia and return normalized result.", + "callers": [ + { + "file": "src/hooks/useScreenRecorder.ts", + "line": 5, + "context": "import { requestCameraAccess } from \"@/lib/requestCameraAccess\";" + }, + { + "file": "src/hooks/useScreenRecorder.ts", + "line": 180, + "context": "const accessResult = await requestCameraAccess();" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 28, + "context": "import { requestCameraAccess } from \"../../lib/requestCameraAccess\";" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 160, + "context": "void requestCameraAccess().catch((error) => {" + } + ] + }, + { + "name": "CameraAccessResult", + "type": "function", + "purpose": "Type describing the shape of the camera access result (success, granted, status, error)." + } + ], + "semantic_tags": [ + "camera", + "permissions", + "electron-fallback", + "media-devices" + ], + "handles_entities": [ + "CameraAccessResult" + ], + "key_behaviors": [ + "requests camera permission and returns standardized status", + "uses electron API fallback when present", + "stops temporary media tracks after permission check" + ], + "insights": [ + { + "type": "bug_fix", + "category": "Error Handling", + "title": "Guard electron API requestCameraAccess with try/catch", + "problem": "Calling window.electronAPI.requestCameraAccess could throw and bubble up unhandled, causing an unhandled rejection or crash path.", + "root_cause": "The electronAPI implementation may throw exceptions; the JS code assumed success/shape and didn't guard for exceptions.", + "solution": "Wrap electronAPI.requestCameraAccess in try/catch and return a normalized CameraAccessResult with success:false when an exception occurs so callers can handle failure gracefully.", + "commits": [ + "6236d2a" + ], + "constructs": [ + "requestCameraAccess", + "getDeniedStatus" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/lib/utils.ts": { + "short_description": "Utility to merge and normalize Tailwind/clsx class names", + "category": "SOURCE_CODE", + "description": "Small helper that composes className values using clsx and resolves Tailwind conflicts with tailwind-merge. It centralizes className merging so components can avoid repetitive imports and ensure consistent class merging.", + "key_constructs": [ + { + "name": "cn", + "type": "function", + "purpose": "Combine class values with clsx and then merge Tailwind classes to avoid conflicts.", + "callers": [ + { + "file": "src/components/ui/tooltip.tsx", + "line": 4, + "context": "import { cn } from \"@/lib/utils\";" + }, + { + "file": "src/components/ui/tooltip.tsx", + "line": 37, + "context": "className={cn(" + }, + { + "file": "src/components/ui/button.tsx", + "line": 5, + "context": "import { cn } from \"@/lib/utils\";" + }, + { + "file": "src/components/ui/button.tsx", + "line": 44, + "context": "" + }, + { + "file": "src/components/ui/tabs.tsx", + "line": 4, + "context": "import { cn } from \"@/lib/utils\";" + }, + { + "file": "src/components/ui/tabs.tsx", + "line": 14, + "context": "className={cn(" + }, + { + "file": "src/components/ui/tabs.tsx", + "line": 29, + "context": "className={cn(" + }, + { + "file": "src/components/ui/tabs.tsx", + "line": 44, + "context": "className={cn(" + }, + { + "file": "src/components/ui/dialog.tsx", + "line": 5, + "context": "import { cn } from \"@/lib/utils\";" + }, + { + "file": "src/components/ui/dialog.tsx", + "line": 21, + "context": "className={cn(" + }, + { + "file": "src/components/ui/dialog.tsx", + "line": 38, + "context": "className={cn(" + }, + { + "file": "src/components/ui/dialog.tsx", + "line": 55, + "context": "
" + }, + { + "file": "src/components/ui/dialog.tsx", + "line": 61, + "context": "className={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\", className)}" + }, + { + "file": "src/components/ui/dialog.tsx", + "line": 73, + "context": "className={cn(\"text-lg font-semibold leading-none tracking-tight\", className)}" + }, + { + "file": "src/components/ui/dialog.tsx", + "line": 85, + "context": "className={cn(\"text-sm text-muted-foreground\", className)}" + }, + { + "file": "src/components/video-editor/PlaybackControls.tsx", + "line": 3, + "context": "import { cn } from \"@/lib/utils\";" + }, + { + "file": "src/components/video-editor/PlaybackControls.tsx", + "line": 45, + "context": "className={cn(" + }, + { + "file": "src/components/ui/accordion.tsx", + "line": 5, + "context": "import { cn } from \"@/lib/utils\";" + }, + { + "file": "src/components/ui/accordion.tsx", + "line": 15, + "context": "className={cn(\"border-b border-white/5\", className)}" + }, + { + "file": "src/components/ui/accordion.tsx", + "line": 28, + "context": "className={cn(" + } + ] + } + ], + "semantic_tags": [ + "classnames", + "tailwind", + "ui", + "utility" + ], + "handles_entities": [], + "key_behaviors": [ + "merge and normalize CSS className strings for components" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/lib/exporter/types.ts": { + "short_description": "Type definitions, constants, and validators for export pipeline", + "category": "SOURCE_CODE", + "description": "Defines TypeScript interfaces and types used by the export pipeline (ExportConfig, ExportProgress, VideoFrameData, GIF config/types) plus constants for GIF presets and frame rates and a validator helper. This centralizes export-related types and validation logic used by gif/video exporters and the UI.", + "key_constructs": [ + { + "name": "ExportConfig", + "type": "function", + "purpose": "Interface describing low-level export configuration (dimensions, framerate, bitrate, codec)." + }, + { + "name": "ExportProgress", + "type": "function", + "purpose": "Interface representing progress state during an export operation." + }, + { + "name": "GifExportConfig", + "type": "function", + "purpose": "Interface for GIF-specific export settings (frameRate, loop, size preset, width/height)." + }, + { + "name": "GIF_SIZE_PRESETS", + "type": "constant", + "purpose": "Predefined size presets for GIF export (labels and max heights).", + "callers": [ + { + "file": "src/lib/exporter/gifExporter.ts", + "line": 17, + "context": "GIF_SIZE_PRESETS," + }, + { + "file": "src/lib/exporter/gifExporter.ts", + "line": 67, + "context": "sizePresets: typeof GIF_SIZE_PRESETS," + }, + { + "file": "src/lib/exporter/index.ts", + "line": 19, + "context": "GIF_SIZE_PRESETS," + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 40, + "context": "import { GIF_FRAME_RATES, GIF_SIZE_PRESETS } from \"@/lib/exporter\";" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 1366, + "context": "{Object.entries(GIF_SIZE_PRESETS).map(([key, _preset]) => (" + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 25, + "context": "GIF_SIZE_PRESETS," + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 1461, + "context": "GIF_SIZE_PRESETS," + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 1822, + "context": "GIF_SIZE_PRESETS," + }, + { + "file": "src/components/video-editor/GifOptionsPanel.tsx", + "line": 11, + "context": "GIF_SIZE_PRESETS," + }, + { + "file": "src/components/video-editor/GifOptionsPanel.tsx", + "line": 37, + "context": "const sizePresetOptions = Object.entries(GIF_SIZE_PRESETS).map(([key, value]) => ({" + } + ] + }, + { + "name": "isValidGifFrameRate", + "type": "function", + "purpose": "Validator to check if a numeric rate is an allowed GifFrameRate.", + "callers": [ + { + "file": "src/lib/exporter/index.ts", + "line": 20, + "context": "isValidGifFrameRate," + } + ] + } + ], + "semantic_tags": [ + "exporter", + "types", + "gif", + "mp4", + "validation" + ], + "handles_entities": [ + "ExportConfig", + "ExportResult", + "ExportSettings", + "VideoFrameData" + ], + "key_behaviors": [ + "provide type-safe contracts for export operations", + "offer constants and validation for GIF export options" + ], + "insights": [ + { + "type": "breaking_change", + "category": "API", + "title": "GIF export presets changed (removed 10 FPS and 'small' size)", + "problem": "GIF frame rate and size preset types/consts were updated to remove '10 FPS' and 'small' preset, altering the public types and constants used by exporter and UI.", + "root_cause": "Design decision to remove a lower-quality preset and 10 FPS option; this affects type declarations and constant arrays used for validation and UI.", + "solution": "Updated GifFrameRate union, GifSizePreset union, GIF_SIZE_PRESETS, GIF_FRAME_RATES, and VALID_GIF_FRAME_RATES to only include new allowed values (15,20,25,30 and presets medium, large, original).", + "lesson_learned": "Removing allowed enum members is a breaking change; update all consuming code (UI, tests, validators) and reflect in property tests. Keep migration notes for consumers of types or persistent saved export settings.", + "commits": [ + "23ede0f", + "6e6ecba" + ], + "constructs": [ + "GifFrameRate", + "GifSizePreset", + "GIF_SIZE_PRESETS", + "GIF_FRAME_RATES", + "VALID_GIF_FRAME_RATES", + "isValidGifFrameRate" + ] + }, + { + "type": "refactoring", + "category": "Typing", + "title": "Add ExportQuality union type", + "problem": "No formal type for export quality options, making passing quality strings error-prone.", + "root_cause": "Export quality options were previously implicit strings used across UI and exporter.", + "solution": "Added export type ExportQuality = 'medium' | 'good' | 'source'.", + "lesson_learned": "When adding discrete UI options that affect export logic, define shared typed unions to enforce correctness across renderer and exporter.", + "commits": [ + "ed3cdab" + ], + "constructs": [ + "ExportQuality" + ] + } + ], + "tests": { + "exercised_by": [ + "src/lib/exporter/types.test.ts" + ], + "test_functions": [], + "source_commits": [ + "6e6ecba", + "23ede0f" + ] + }, + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/contexts/I18nContext.tsx": { + "short_description": "React i18n provider and hooks to manage locale and translations", + "category": "SOURCE_CODE", + "description": "Provides a React context for localization including current locale state, persistence to localStorage, and translation helpers. Exposes useI18n and useScopedT hooks and a provider that updates document language and notifies Electron when locale changes.", + "key_constructs": [ + { + "name": "I18nProvider", + "type": "function", + "purpose": "React provider component that manages locale state, persists it, and exposes t() translation function.", + "callers": [ + { + "file": "src/main.tsx", + "line": 4, + "context": "import { I18nProvider } from \"./contexts/I18nContext\";" + }, + { + "file": "src/main.tsx", + "line": 9, + "context": "" + }, + { + "file": "src/main.tsx", + "line": 11, + "context": "" + } + ] + }, + { + "name": "useI18n", + "type": "function", + "purpose": "Hook to access locale, setLocale and t() from the I18n context, throwing if used outside provider.", + "callers": [ + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 20, + "context": "import { useI18n, useScopedT } from \"@/contexts/I18nContext\";" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 75, + "context": "const { locale, setLocale } = useI18n();" + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 14, + "context": "import { useI18n, useScopedT } from \"@/contexts/I18nContext\";" + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 154, + "context": "const { locale, setLocale } = useI18n();" + } + ] + }, + { + "name": "useScopedT", + "type": "function", + "purpose": "Hook returning a namespaced translate function bound to the current locale.", + "callers": [ + { + "file": "src/hooks/useScreenRecorder.ts", + "line": 4, + "context": "import { useScopedT } from \"@/contexts/I18nContext\";" + }, + { + "file": "src/hooks/useScreenRecorder.ts", + "line": 90, + "context": "const t = useScopedT(\"editor\");" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 20, + "context": "import { useI18n, useScopedT } from \"@/contexts/I18nContext\";" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 74, + "context": "const t = useScopedT(\"launch\");" + }, + { + "file": "src/components/launch/SourceSelector.tsx", + "line": 3, + "context": "import { useScopedT } from \"@/contexts/I18nContext\";" + }, + { + "file": "src/components/launch/SourceSelector.tsx", + "line": 17, + "context": "const t = useScopedT(\"launch\");" + }, + { + "file": "src/components/launch/SourceSelector.tsx", + "line": 18, + "context": "const tc = useScopedT(\"common\");" + }, + { + "file": "src/components/video-editor/ShortcutsConfigDialog.tsx", + "line": 12, + "context": "import { useScopedT } from \"@/contexts/I18nContext\";" + }, + { + "file": "src/components/video-editor/ShortcutsConfigDialog.tsx", + "line": 31, + "context": "const t = useScopedT(\"shortcuts\");" + }, + { + "file": "src/components/video-editor/ShortcutsConfigDialog.tsx", + "line": 32, + "context": "const tc = useScopedT(\"common\");" + }, + { + "file": "src/components/video-editor/ExportDialog.tsx", + "line": 4, + "context": "import { useScopedT } from \"@/contexts/I18nContext\";" + }, + { + "file": "src/components/video-editor/ExportDialog.tsx", + "line": 30, + "context": "const t = useScopedT(\"dialogs\");" + }, + { + "file": "src/components/video-editor/PlaybackControls.tsx", + "line": 2, + "context": "import { useScopedT } from \"@/contexts/I18nContext\";" + }, + { + "file": "src/components/video-editor/PlaybackControls.tsx", + "line": 25, + "context": "const t = useScopedT(\"common\");" + }, + { + "file": "src/components/video-editor/AddCustomFontDialog.tsx", + "line": 15, + "context": "import { useScopedT } from \"@/contexts/I18nContext\";" + }, + { + "file": "src/components/video-editor/AddCustomFontDialog.tsx", + "line": 29, + "context": "const t = useScopedT(\"settings\");" + }, + { + "file": "src/components/video-editor/AddCustomFontDialog.tsx", + "line": 30, + "context": "const tc = useScopedT(\"common\");" + }, + { + "file": "src/components/video-editor/AnnotationSettingsPanel.tsx", + "line": 30, + "context": "import { useScopedT } from \"@/contexts/I18nContext\";" + }, + { + "file": "src/components/video-editor/AnnotationSettingsPanel.tsx", + "line": 67, + "context": "const t = useScopedT(\"settings\");" + }, + { + "file": "src/components/video-editor/KeyboardShortcutsHelp.tsx", + "line": 2, + "context": "import { useScopedT } from \"@/contexts/I18nContext\";" + } + ] + } + ], + "semantic_tags": [ + "i18n", + "localization", + "react-context", + "persistence", + "electron-integration" + ], + "handles_entities": [ + "Locale", + "I18nNamespace", + "translations" + ], + "key_behaviors": [ + "stores and persists selected locale", + "provides translation function t() for components", + "notifies Electron main process of locale changes" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/i18n/loader.ts": { + "short_description": "Loads locale JSONs and performs message lookup/interpolation", + "category": "SOURCE_CODE", + "description": "Eagerly imports locale JSON files, builds a messages map, and exposes helpers to fetch messages and translate keys with fallback to the default locale. Also provides interpolation for variables and helpers to get localized locale names.", + "key_constructs": [ + { + "name": "translate", + "type": "function", + "purpose": "Resolves a namespaced translation key for a locale, falling back to default locale and returning a fallback key if missing.", + "callers": [ + { + "file": "src/contexts/I18nContext.tsx", + "line": 17, + "context": "import { translate } from \"@/i18n/loader\";" + }, + { + "file": "src/contexts/I18nContext.tsx", + "line": 38, + "context": "(key: string, vars?: TranslateVars): string => translate(locale, namespace, key, vars)," + }, + { + "file": "src/contexts/I18nContext.tsx", + "line": 82, + "context": "return translate(locale, namespace, key, vars);" + } + ] + }, + { + "name": "getMessages", + "type": "function", + "purpose": "Returns the raw messages object for a given locale and namespace." + }, + { + "name": "getLocaleName", + "type": "function", + "purpose": "Fetches a localized display name for a locale from the common messages.", + "callers": [ + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 22, + "context": "import { getLocaleName } from \"@/i18n/loader\";" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 245, + "context": "{getLocaleName(loc)}" + }, + { + "file": "src/components/video-editor/VideoEditor.tsx", + "line": 18, + "context": "import { getLocaleName } from \"@/i18n/loader\";" + } + ] + } + ], + "semantic_tags": [ + "i18n", + "locales", + "message-loading", + "interpolation", + "json-import" + ], + "handles_entities": [ + "translation messages", + "Locale" + ], + "key_behaviors": [ + "loads locale message files into memory", + "looks up translation keys with fallback", + "interpolates variables into message strings" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/lib/recordingSession.ts": { + "short_description": "Types and normalization utilities for recording session data", + "category": "SOURCE_CODE", + "description": "Declares interfaces representing project media and recording sessions and provides normalization/validation helpers to sanitize incoming data shapes. Ensures required paths exist and coerces createdAt to a timestamp when missing or invalid.", + "key_constructs": [ + { + "name": "RecordingSession", + "type": "constant", + "purpose": "Type interface describing persisted recording session metadata (screen/webcam paths + createdAt)." + }, + { + "name": "normalizeProjectMedia", + "type": "function", + "purpose": "Validates and normalizes a candidate object into a ProjectMedia with trimmed string paths or returns null.", + "callers": [ + { + "file": "src/components/video-editor/projectPersistence.ts", + "line": 3, + "context": "import { normalizeProjectMedia } from \"@/lib/recordingSession\";" + }, + { + "file": "src/components/video-editor/projectPersistence.ts", + "line": 168, + "context": "const media = normalizeProjectMedia(candidate.media);" + }, + { + "file": "electron/ipc/handlers.ts", + "line": 15, + "context": "normalizeProjectMedia," + }, + { + "file": "electron/ipc/handlers.ts", + "line": 130, + "context": "normalizeProjectMedia(rawProject.media) ??" + } + ] + }, + { + "name": "normalizeRecordingSession", + "type": "function", + "purpose": "Validates and normalizes a candidate into a RecordingSession, ensuring media exists and createdAt is numeric.", + "callers": [ + { + "file": "electron/ipc/handlers.ts", + "line": 16, + "context": "normalizeRecordingSession," + }, + { + "file": "electron/ipc/handlers.ts", + "line": 229, + "context": "const session = normalizeRecordingSession(JSON.parse(content));" + }, + { + "file": "electron/ipc/handlers.ts", + "line": 883, + "context": "const normalized = normalizeRecordingSession(session);" + } + ] + } + ], + "semantic_tags": [ + "validation", + "normalization", + "recording", + "types", + "persistence" + ], + "handles_entities": [ + "RecordingSession", + "ProjectMedia", + "RecordedVideoAssetInput" + ], + "key_behaviors": [ + "validates recording session payloads", + "normalizes and trims media path strings", + "ensures createdAt timestamp is present" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/lib/webcamMaskShapes.ts": { + "short_description": "Helpers to compute and draw webcam mask shapes for UI and canvas", + "category": "SOURCE_CODE", + "description": "Provides utilities to generate CSS clip-path values and to draw corresponding Canvas2D clip paths for webcam mask shapes (circle, rectangle, rounded). Used by the editor to mask webcam overlays consistently in both DOM and canvas rendering contexts.", + "key_constructs": [ + { + "name": "getCssClipPath", + "type": "function", + "purpose": "Returns a CSS clip-path string for a given WebcamMaskShape or null if not needed.", + "callers": [ + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 29, + "context": "import { getCssClipPath } from \"@/lib/webcamMaskShapes\";" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 1248, + "context": "const clipPath = getCssClipPath(webcamLayout?.maskShape ?? \"rectangle\");" + } + ] + }, + { + "name": "drawCanvasClipPath", + "type": "function", + "purpose": "Draws a matching clip path onto a CanvasRenderingContext2D for the given shape and dimensions.", + "callers": [ + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 48, + "context": "import { drawCanvasClipPath } from \"@/lib/webcamMaskShapes\";" + }, + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 788, + "context": "drawCanvasClipPath(" + } + ] + } + ], + "semantic_tags": [ + "webcam", + "canvas", + "clip-path", + "ui-rendering", + "shapes" + ], + "handles_entities": [ + "WebcamMaskShape" + ], + "key_behaviors": [ + "produces CSS clip-paths for webcam masks", + "draws canvas clip paths to mask webcam content" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/utils/getTestId.ts": { + "short_description": "Utility to generate standardized test id strings", + "category": "SOURCE_CODE", + "description": "Defines a narrow TestId union type used across tests and a helper to prefix test ids consistently. Simplifies stable test selectors in components by centralizing test id generation.", + "key_constructs": [ + { + "name": "TestId", + "type": "constant", + "purpose": "Type union representing allowed test identifier suffixes used in the app." + }, + { + "name": "getTestId", + "type": "function", + "purpose": "Produces a prefixed test id string for a given TestId value.", + "callers": [ + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 43, + "context": "import { getTestId } from \"@/utils/getTestId\";" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 1294, + "context": "data-testid={getTestId(\"gif-format-button\")}" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 1369, + "context": "data-testid={getTestId(`gif-size-button-${key}`)}" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 1411, + "context": "data-testid={getTestId(\"export-button\")}" + } + ] + } + ], + "semantic_tags": [ + "testing", + "test-ids", + "utilities" + ], + "handles_entities": [], + "key_behaviors": [ + "generates consistent test id attributes for components" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/utils/timeUtils.ts": { + "short_description": "Simple time formatting helpers", + "category": "SOURCE_CODE", + "description": "Provides utility to format a seconds count to a zero-padded MM:SS string. Used by UI components to display durations consistently.", + "key_constructs": [ + { + "name": "formatTimePadded", + "type": "function", + "purpose": "Formats seconds into a MM:SS padded string for display.", + "callers": [ + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 29, + "context": "import { formatTimePadded } from \"../../utils/timeUtils\";" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 453, + "context": "{formatTimePadded(elapsedSeconds)}" + } + ] + } + ], + "semantic_tags": [ + "time-formatting", + "ui-utils", + "duration" + ], + "handles_entities": [], + "key_behaviors": [ + "formats numeric seconds into MM:SS strings" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/video-editor/videoPlayback/videoEventHandlers.ts": { + "short_description": "Creates event handlers to manage video playback, seeking, trims, and speed regions", + "category": "SOURCE_CODE", + "description": "Exports createVideoEventHandlers which wires play/pause/seek behaviors for an HTMLVideoElement considering trim and speed regions. It enforces skipping trimmed ranges, applies dynamic playbackRate from speed regions, and coordinates time updates via requestAnimationFrame.", + "key_constructs": [ + { + "name": "createVideoEventHandlers", + "type": "function", + "purpose": "Builds and returns handlers (handlePlay, handlePause, handleSeeked, handleSeeking) that manage playback state, trim skipping and speed region application.", + "callers": [ + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 58, + "context": "import { createVideoEventHandlers } from \"./videoPlayback/videoEventHandlers\";" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 769, + "context": "const { handlePlay, handlePause, handleSeeked, handleSeeking } = createVideoEventHandlers({" + } + ] + } + ], + "semantic_tags": [ + "video-playback", + "seeking", + "trim", + "playback-rate", + "event-handling" + ], + "handles_entities": [ + "TrimRegion", + "SpeedRegion", + "HTMLVideoElement" + ], + "key_behaviors": [ + "skips trimmed regions during playback and seeking", + "applies speed regions to playbackRate", + "emits time updates to sync UI" + ], + "insights": [ + { + "type": "feature", + "category": "State Management", + "title": "Apply playbackRate from active speed region during playback", + "problem": "Playback did not reflect speed regions configured in timeline while previewing in the editor.", + "root_cause": "Event handlers didn't consult speedRegions; video.playbackRate defaulted to 1.", + "solution": "Added speedRegionsRef and a helper to find active region; set video.playbackRate to active region speed or 1 on each update.", + "lesson_learned": "For runtime playback, use refs for frequently-updated region lists to avoid re-rendering playback engine and update playbackRate on time updates.", + "commits": [ + "397a943" + ], + "constructs": [ + "createVideoEventHandlers", + "findActiveSpeedRegion", + "updateTime" + ] + }, + { + "type": "refactoring", + "category": "State Management", + "title": "Centralized video event handlers with trim awareness", + "problem": "Playback event handlers were scattered and didn't consider trimmed regions when updating playback time or seeking.", + "root_cause": "As trimming added, handlers needed to consult timeline state but had no access to trim list; previously handlers only emitted currentTime.", + "solution": "Created createVideoEventHandlers that accepts trimRegionsRef and updated updateTime and seek handling to detect active trim region and skip appropriately.", + "lesson_learned": "When adding features that change playback timeline, centralize event handling so new rules (skip trims) can be implemented in one place and reused.", + "commits": [ + "3998af5" + ], + "constructs": [ + "createVideoEventHandlers", + "updateTime" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/ui/switch.tsx": { + "short_description": "Styled Radix Switch wrapper component", + "category": "SOURCE_CODE", + "description": "A small forwardRef React component wrapping @radix-ui/react-switch with custom styling and theming helpers. Provides a reusable toggle switch used across the UI.", + "key_constructs": [ + { + "name": "Switch", + "type": "function", + "purpose": "ForwardRef React component that renders a styled Radix Switch with custom classes and thumb transitions.", + "callers": [ + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 34, + "context": "import { Switch } from \"@/components/ui/switch\";" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 889, + "context": "" + } + ] + } + ], + "semantic_tags": [ + "ui", + "toggle", + "radix-ui", + "react-component", + "styling" + ], + "handles_entities": [], + "key_behaviors": [ + "renders a themed toggle switch for user settings" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/lib/exporter/audioEncoder.ts": { + "short_description": "Audio processing/encoding pipeline for exports (trim & speed-aware)", + "category": "SOURCE_CODE", + "description": "Implements AudioProcessor which handles audio export paths: a fast trim-only decode/encode pipeline and a pitch-preserved timeline rendering path for speed edits. It decodes via WebDemuxer/WebCodecs or uses WebAudio + MediaRecorder to render speed-adjusted audio, then feeds encoded chunks into a VideoMuxer.", + "key_constructs": [ + { + "name": "AudioProcessor", + "type": "class", + "purpose": "Coordinates audio demuxing, decoding, optional timeline rendering for speed regions, encoding and streaming encoded chunks to a muxer." + }, + { + "name": "process", + "type": "function", + "purpose": "Top-level method that chooses between trim-only or speed-aware audio paths and executes processing pipeline.", + "callers": [ + { + "file": "src/lib/exporter/videoExporter.ts", + "line": 343, + "context": "await this.audioProcessor.process(" + } + ] + }, + { + "name": "renderPitchPreservedTimelineAudio", + "type": "function", + "purpose": "Renders audio through WebAudio + MediaRecorder while applying trims and playbackRate regions to preserve pitch for speed edits." + } + ], + "semantic_tags": [ + "audio-export", + "webcodecs", + "demuxing", + "webaudio", + "encoding" + ], + "handles_entities": [ + "AudioData", + "EncodedAudioChunk", + "WebDemuxer", + "VideoMuxer" + ], + "key_behaviors": [ + "processes audio for exported video files", + "preserves pitch when applying speed regions by rendering timeline", + "encodes and streams audio chunks into the muxer" + ], + "insights": [ + { + "type": "bug_fix", + "category": "Audio/Timing", + "title": "Fix audio desync & preserve pitch under speed regions", + "problem": "Audio would desynchronize or sound incorrect (pitch-shifted) when speedRegions were applied during exports.", + "root_cause": "Audio export pipeline previously demuxed/decoded chunks directly and attempted naive timestamp remapping; it didn't support pitch-preserving speed changes.", + "solution": "Introduce two-mode AudioProcessor: a trimmed-only fast path and a speed-aware timeline rendering path (renderPitchPreservedTimelineAudio) that uses an AudioElement + MediaRecorder via an AudioContext route to render pitch-preserved audio with playbackRate changes, then demux/re-mux the resulting audio blob into final muxer. Also fixed various decoding loops to ensure reader.cancel() is called and variable renaming fixes.", + "lesson_learned": "Pitch-preserving audio speed edits require actual playback/rendering (or sophisticated time-domain algorithms). For fidelity prefer timeline-rendered audio recorded from the platform's media pipeline, then re-demux for muxing.", + "commits": [ + "16dea49", + "b5cc777" + ], + "constructs": [ + "AudioProcessor.process", + "renderPitchPreservedTimelineAudio", + "startAudioRecording", + "muxRenderedAudioBlob" + ], + "test_mappings": {} + }, + { + "type": "feature", + "category": "State Management", + "title": "Add AudioProcessor: decode, trim-adjust, re-encode, and mux audio", + "problem": "Export pipeline previously lacked robust audio processing that respects trim regions and re-encodes to a muxable codec.", + "root_cause": "Audio track handling was missing; exporting video-only left out audio muxing and timestamp adjustments required by trims/speed changes.", + "solution": "Introduce AudioProcessor that decodes source audio (AudioDecoder), skips trimmed regions, computes trim offset timestamps, re-encodes (AudioEncoder -> Opus), and feeds encoded chunks to VideoMuxer; includes isConfigSupported checks and cancellation support.", + "lesson_learned": "Audio processing in a streaming/export pipeline must rebase timestamps for trims, confirm codec support, and avoid leaking memory by closing AudioData. Use encode/decode support checks and a cancel flag for long-running tasks.", + "commits": [ + "64bc261" + ], + "constructs": [ + "AudioProcessor.process", + "AudioProcessor.cloneWithTimestamp", + "AudioProcessor.isInTrimRegion", + "AudioProcessor.computeTrimOffset", + "AudioProcessor.cancel" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/lib/exporter/muxer.ts": { + "short_description": "MP4 muxer wrapper that assembles encoded packets into a Blob", + "category": "SOURCE_CODE", + "description": "Wraps mediabunny types to create an MP4 output in-memory, exposing initialize, addVideoChunk/addAudioChunk and finalize to produce a video/mp4 Blob. Used by the exporter pipeline to collect encoded WebCodecs chunks into a final file.", + "key_constructs": [ + { + "name": "VideoMuxer", + "type": "class", + "purpose": "Manages mediabunny Output, creates video/audio tracks, accepts encoded packets and finalizes output to an MP4 Blob." + }, + { + "name": "initialize", + "type": "function", + "purpose": "Sets up BufferTarget, Output, and Encoded packet sources and starts the output.", + "callers": [ + { + "file": "src/lib/exporter/videoExporter.ts", + "line": 158, + "context": "await muxer.initialize();" + } + ] + }, + { + "name": "finalize", + "type": "function", + "purpose": "Finalizes the output and returns the assembled MP4 Blob.", + "callers": [ + { + "file": "src/lib/exporter/videoExporter.ts", + "line": 354, + "context": "const blob = await muxer.finalize();" + } + ] + } + ], + "semantic_tags": [ + "muxing", + "mp4", + "mediabunny", + "export", + "packaging" + ], + "handles_entities": [ + "EncodedVideoChunk", + "EncodedAudioChunk", + "MP4 Blob" + ], + "key_behaviors": [ + "collects encoded media packets", + "produces a finalized MP4 Blob for saving/export" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/lib/exporter/videoDecoder.ts": { + "short_description": "Simple HTMLVideo-based decoder to read video metadata and provide a seekable element", + "category": "SOURCE_CODE", + "description": "Provides VideoFileDecoder that loads a video URL into a hidden HTMLVideoElement to extract width, height, duration and a guessed codec/frameRate, and returns the element for seeking or previewing. Intended as a lightweight way to gather video metadata and a seek-capable element for export pipelines.", + "key_constructs": [ + { + "name": "VideoFileDecoder", + "type": "class", + "purpose": "Loads a video into an HTMLVideoElement, exposes metadata (DecodedVideoInfo) and returns the element for seeking/preview." + }, + { + "name": "loadVideo", + "type": "function", + "purpose": "Asynchronously waits for loadedmetadata and resolves decoded video info." + } + ], + "semantic_tags": [ + "video", + "metadata", + "html-video", + "decoder", + "export" + ], + "handles_entities": [ + "DecodedVideoInfo", + "HTMLVideoElement" + ], + "key_behaviors": [ + "loads video metadata for export", + "provides a seekable video element for decoding/export pipelines" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/video-editor/timeline/Row.tsx": { + "short_description": "Timeline row wrapper component used by dnd-timeline", + "category": "SOURCE_CODE", + "description": "Renders a styled timeline row using useRow from dnd-timeline, optionally showing labels and empty-state hints. It sets up required refs and styles for drag-and-drop timeline tracks in the editor UI.", + "key_constructs": [ + { + "name": "Row", + "type": "function", + "purpose": "React component that integrates with dnd-timeline to render a labeled/empty timeline row and apply computed row styles." + } + ], + "semantic_tags": [ + "timeline", + "drag-and-drop", + "ui", + "dnd-timeline", + "editor" + ], + "handles_entities": [ + "RowDefinition" + ], + "key_behaviors": [ + "renders timeline rows with labels and hints", + "connects DOM node to dnd-timeline for drag/drop behavior" + ], + "insights": [ + { + "type": "refactoring", + "category": "UI", + "title": "Reduce row min height for denser timeline", + "problem": "Timeline rows were large and consumed vertical space when adding multi-row timeline.", + "root_cause": "Hardcoded minHeight was 88 which didn't match new compact layout.", + "solution": "Lowered minHeight to 60 so multiple rows fit better.", + "lesson_learned": "UI layout constants often need tuning as features increase; prefer variables or tokens for row sizing.", + "commits": [ + "2b5b15f" + ], + "constructs": [ + "Row" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/video-editor/videoPlayback/zoomRegionUtils.ts": { + "short_description": "Compute dominant zoom regions and transitions for playback focus", + "category": "SOURCE_CODE", + "description": "Contains logic to evaluate zoom regions over time, computing strengths, blending scales and connected transitions while optionally following cursor telemetry. Exposes findDominantRegion which returns the active zoom region, its strength, blended scale and any connected pan transition at a given timestamp.", + "key_constructs": [ + { + "name": "findDominantRegion", + "type": "function", + "purpose": "Determines the most influential ZoomRegion at a given time, handling connected transitions, holds and cursor-driven focus adjustments.", + "callers": [ + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 34, + "context": "import { findDominantRegion } from \"@/components/video-editor/videoPlayback/zoomRegionUtils\";" + }, + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 544, + "context": "const { region, strength, blendedScale, transition } = findDominantRegion(" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 59, + "context": "import { findDominantRegion } from \"./videoPlayback/zoomRegionUtils\";" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 877, + "context": "const { region, strength, blendedScale, transition } = findDominantRegion(" + } + ] + }, + { + "name": "computeRegionStrength", + "type": "function", + "purpose": "Computes how active a zoom region is at a given time based on lead-in/out and transition windows." + }, + { + "name": "getConnectedRegionPairs", + "type": "function", + "purpose": "Finds zoom regions that should be connected for smooth pan transitions based on gap thresholds." + } + ], + "semantic_tags": [ + "zoom", + "timeline", + "interpolation", + "cursor-follow", + "transitions" + ], + "handles_entities": [ + "ZoomRegion", + "ZoomFocus", + "CursorTelemetryPoint" + ], + "key_behaviors": [ + "chooses active zoom region for current playback time", + "computes smooth pan/scale transitions between zoom regions", + "resolves cursor-driven auto-focus when applicable" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/ui/content-clamp.tsx": { + "short_description": "Truncates long text and shows full content in a popover on hover", + "category": "SOURCE_CODE", + "description": "A UI component that truncates text to a given length and displays the full content in a Popover when hovered or focused. It manages hover timing to avoid flicker and reuses shared popover primitives for consistent styling.", + "key_constructs": [ + { + "name": "ContentClamp", + "type": "function", + "purpose": "React component that conditionally truncates text and shows a popover with the full content on hover." + } + ], + "semantic_tags": [ + "ui", + "truncation", + "popover", + "tooltip", + "accessibility" + ], + "handles_entities": [], + "key_behaviors": [ + "truncates long text inline", + "reveals full text in a popover on hover with debounce" + ], + "insights": [ + { + "type": "feature", + "category": "UI", + "title": "Add ContentClamp component for truncated text with popover", + "problem": "Truncation / tooltip behavior duplicated across launch UI.", + "root_cause": "No reusable component to show truncated text with hover popover.", + "solution": "Added ContentClamp component that truncates string children and shows full text inside a Popover; includes hover handling and delayed close via timeout (commit 16752a7).", + "lesson_learned": "Make small UI behaviors reusable. Be mindful of timer cleanup in useEffect and mouseenter/mouseleave race handling. When using NodeJS.Timeout types in browser-target codebases, ensure TypeScript libs include DOM/Node definitions to avoid type mismatch.", + "commits": [ + "16752a7" + ], + "constructs": [ + "ContentClamp" + ] + }, + { + "type": "refactoring", + "category": "Performance", + "title": "Remove popover box-shadow for clamped content", + "problem": "Popover box-shadow caused unwanted visual emphasis for clamped text overlays.", + "root_cause": "Design preference to reduce shadow usage.", + "solution": "Removed shadow class from PopoverContent when used in ContentClamp (commit 4bc8a1e).", + "lesson_learned": "Small visual tweaks are common and should be done in a way that's easy to revert; keep design tokens and classes centralized where possible.", + "commits": [ + "4bc8a1e" + ], + "constructs": [ + "ContentClamp" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/ui/dialog.tsx": { + "short_description": "Styled dialog/modal primitives built on Radix UI", + "category": "SOURCE_CODE", + "description": "Exports a set of dialog components (Dialog, DialogContent, DialogOverlay, Title, Description, Header, Footer, etc.) built on @radix-ui/react-dialog with consistent styling and accessibility hooks. Provides a consistent modal API for the app's dialogs.", + "key_constructs": [ + { + "name": "DialogContent", + "type": "function", + "purpose": "Styled Radix Content wrapper that renders modal content inside a portal with overlay and close button." + }, + { + "name": "DialogOverlay", + "type": "function", + "purpose": "Overlay component providing backdrop and animations for dialogs." + }, + { + "name": "DialogTitle", + "type": "function", + "purpose": "Title primitive to be used inside DialogContent with consistent typography." + } + ], + "semantic_tags": [ + "ui", + "dialog", + "modal", + "radix-ui", + "accessibility" + ], + "handles_entities": [], + "key_behaviors": [ + "renders accessible modal dialogs", + "provides consistent dialog layout and close behavior" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/ui/item-content.tsx": { + "short_description": "Reusable styled container for item content", + "category": "SOURCE_CODE", + "description": "A small React component that wraps children in a consistently styled, responsive container used for list or item rows. It centralizes visual styles (rounded border, padding, hover) so other UI pieces can reuse the same look and spacing.", + "key_constructs": [ + { + "name": "ItemContent", + "type": "function", + "purpose": "Functional React component that renders children inside a styled div with configurable classes." + } + ], + "semantic_tags": [ + "ui", + "react", + "component", + "styling" + ], + "handles_entities": [], + "key_behaviors": [ + "renders children inside a consistent item-style container" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/ui/popover.tsx": { + "short_description": "Radix-based popover primitives with project styles", + "category": "SOURCE_CODE", + "description": "Thin wrappers around @radix-ui/react-popover primitives that apply project-specific classNames, animations and data attributes. Exposes Popover, PopoverTrigger, PopoverContent, PopoverAnchor and PopoverArrow for consistent popover usage across the UI.", + "key_constructs": [ + { + "name": "Popover", + "type": "function", + "purpose": "Wrapper for PopoverPrimitive.Root adding a data-slot attribute and forwarding props." + }, + { + "name": "PopoverTrigger", + "type": "function", + "purpose": "Wrapper for PopoverPrimitive.Trigger that forwards props and sets a data-slot attribute." + }, + { + "name": "PopoverContent", + "type": "function", + "purpose": "Renders PopoverPrimitive.Content inside a Portal with default styling, animation classes and configurable alignment/offset." + }, + { + "name": "PopoverAnchor", + "type": "function", + "purpose": "Wrapper for PopoverPrimitive.Anchor for consistent anchoring." + }, + { + "name": "PopoverArrow", + "type": "function", + "purpose": "Styled wrapper around PopoverPrimitive.Arrow to match popover colors." + } + ], + "semantic_tags": [ + "ui", + "popover", + "radix", + "accessibility", + "component" + ], + "handles_entities": [], + "key_behaviors": [ + "shows contextual popover overlays with consistent styling and animation" + ], + "insights": [ + { + "type": "refactoring", + "category": "UI", + "title": "Expose PopoverArrow and allow disabling animations", + "problem": "Some popovers (ContentClamp) require no animations and an arrow element; previous Popover primitive forced animations.", + "root_cause": "Rigid popover wrapper lacked flexibility.", + "solution": "Added animated?: boolean prop to PopoverContent so callers can turn off animations, and exported a PopoverArrow component for use in content (commit 16752a7). Also simplified className composition to conditionally apply animation classes.", + "lesson_learned": "Utility UI components benefit from small flags (animated) and composable exports (Arrow) so different use-cases can use the same primitive.", + "commits": [ + "16752a7" + ], + "constructs": [ + "Popover", + "PopoverTrigger", + "PopoverContent", + "PopoverAnchor", + "PopoverArrow" + ] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/ui/slider.tsx": { + "short_description": "Styled slider input built on Radix Slider", + "category": "SOURCE_CODE", + "description": "A forwardRef React component that composes @radix-ui/react-slider primitives and applies the app's visual styling for track, range and thumb. It provides a reusable slider input used for numeric adjustments in the UI.", + "key_constructs": [ + { + "name": "Slider", + "type": "function", + "purpose": "ForwardRef component wrapping SliderPrimitive.Root and rendering Track, Range and Thumb with themed classes.", + "callers": [ + { + "file": "src/components/video-editor/AnnotationSettingsPanel.tsx", + "line": 27, + "context": "import { Slider } from \"@/components/ui/slider\";" + }, + { + "file": "src/components/video-editor/AnnotationSettingsPanel.tsx", + "line": 543, + "context": " 'biome check .', added lint:fix and format scripts.", + "lesson_learned": "When switching linters/formatters, update CI scripts, local developer docs, and ensure devDependencies match new tooling to avoid broken lint checks in CI.", + "commits": [ + "db9cf96" + ], + "constructs": [] + }, + { + "type": "other", + "category": "Configuration", + "title": "Add dependencies for GIF export and test tooling", + "problem": "New GIF exporter required gif.js and type definitions; tests required fast-check and vitest in devDependencies.", + "root_cause": "Feature addition requires new packages.", + "solution": "Added gif.js, @types/gif.js, fast-check, vitest and adjusted scripts (test: vitest).", + "lesson_learned": "Feature additions often require coordinated package.json updates; ensure version pins are compatible and CI workflows updated to run new tests.", + "commits": [ + "6e6ecba", + "a2ca079" + ], + "constructs": [] + }, + { + "type": "other", + "category": "Configuration", + "title": "Added build:linux script and bumped package version", + "problem": "No npm script for building linux artifact and version changes across commits.", + "root_cause": "Project is adding Linux support and performing releases.", + "solution": "Added build:linux script and bumped versions in separate commits (ebb1d29, 7e0ce53).", + "lesson_learned": "When adding platform-specific scripts, keep scripts consistent for CI and local builds. Version bumps are expected but should be grouped with release notes.", + "commits": [ + "ebb1d29", + "7e0ce53" + ], + "constructs": [] + } + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/video-editor/videoPlayback/focusUtils.ts": { + "short_description": "Helpers to clamp and convert zoom focus between stage and video space", + "category": "SOURCE_CODE", + "description": "Utility functions that constrain or softly clamp zoom focus coordinates to valid bounds for different zoom depths and scales, and convert focus points from stage coordinates to normalized video space. These functions ensure the editor's pan/zoom focus behaves predictably and stays within allowable margins.", + "key_constructs": [ + { + "name": "clampFocusToStage", + "type": "function", + "purpose": "Clamps a ZoomFocus to bounds appropriate for a given ZoomDepth and stage.", + "callers": [ + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 33, + "context": "import { clampFocusToStage as clampFocusToStageUtil } from \"@/components/video-editor/videoPlayback/" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 54, + "context": "import { clampFocusToStage as clampFocusToStageUtil } from \"./videoPlayback/focusUtils\";" + }, + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 33, + "context": "import { clampFocusToStage as clampFocusToStageUtil } from \"@/components/video-editor/videoPlayback/" + }, + { + "file": "src/lib/exporter/frameRenderer.ts", + "line": 532, + "context": "return clampFocusToStageUtil(focus, depth, this.layoutCache.stageSize);" + }, + { + "file": "src/components/video-editor/videoPlayback/overlayUtils.ts", + "line": 2, + "context": "import { clampFocusToStage } from \"./focusUtils\";" + }, + { + "file": "src/components/video-editor/videoPlayback/overlayUtils.ts", + "line": 39, + "context": "const focus = clampFocusToStage(focusOverride ?? region.focus, region.depth, {" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 54, + "context": "import { clampFocusToStage as clampFocusToStageUtil } from \"./videoPlayback/focusUtils\";" + }, + { + "file": "src/components/video-editor/VideoPlayback.tsx", + "line": 217, + "context": "return clampFocusToStageUtil(focus, depth, stageSizeRef.current);" + } + ] + }, + { + "name": "clampFocusToScale", + "type": "function", + "purpose": "Clamps a focus to bounds computed for an arbitrary zoom scale and optional viewport ratio.", + "callers": [ + { + "file": "src/components/video-editor/videoPlayback/zoomRegionUtils.ts", + "line": 5, + "context": "import { clampFocusToScale } from \"./focusUtils\";" + }, + { + "file": "src/components/video-editor/videoPlayback/zoomRegionUtils.ts", + "line": 96, + "context": "return clampFocusToScale(focus, zoomScale, viewportRatio);" + }, + { + "file": "src/components/video-editor/videoPlayback/zoomRegionUtils.ts", + "line": 223, + "context": "const currentFocus = clampFocusToScale(" + }, + { + "file": "src/components/video-editor/videoPlayback/zoomRegionUtils.ts", + "line": 230, + "context": "const nextFocus = clampFocusToScale(" + } + ] + }, + { + "name": "softenFocusToScale", + "type": "function", + "purpose": "Applies a soft clamp so the focus eases into bounds near edges instead of hard-clamping." + }, + { + "name": "stageFocusToVideoSpace", + "type": "function", + "purpose": "Converts normalized stage focus coordinates into normalized video-space coordinates using base scale/offset." + }, + { + "name": "getFocusBoundsForScale", + "type": "function", + "purpose": "Computes allowable normalized focus bounds for a given zoom scale and viewport ratio." + } + ], + "semantic_tags": [ + "zoom", + "focus", + "math", + "video", + "ui" + ], + "handles_entities": [ + "ZoomFocus", + "ZoomDepth" + ], + "key_behaviors": [ + "keeps pan/zoom focus within allowed margins", + "smoothly eases focus near boundaries", + "translates focus coordinates between stage and video spaces" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/ui/accordion.tsx": { + "short_description": "Accordion UI primitives using Radix with styling", + "category": "SOURCE_CODE", + "description": "Wraps @radix-ui/react-accordion primitives with app-specific styling and behavior for triggers, items and content animations. It exports Accordion, AccordionItem, AccordionTrigger and AccordionContent for consistent collapsible sections across the UI.", + "key_constructs": [ + { + "name": "Accordion", + "type": "constant", + "purpose": "Re-export of AccordionPrimitive.Root to use as the accordion container.", + "callers": [ + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 20, + "context": "Accordion," + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 725, + "context": "" + } + ] + }, + { + "name": "AccordionItem", + "type": "function", + "purpose": "ForwardRef wrapper around AccordionPrimitive.Item applying border styling.", + "callers": [ + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 22, + "context": "AccordionItem," + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 731, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 876, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 986, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 988, + "context": "" + } + ] + }, + { + "name": "AccordionTrigger", + "type": "function", + "purpose": "ForwardRef wrapper that renders the header trigger with an icon and open/close animation.", + "callers": [ + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 23, + "context": "AccordionTrigger," + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 735, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 740, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 877, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 882, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 992, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 997, + "context": "" + } + ] + }, + { + "name": "AccordionContent", + "type": "function", + "purpose": "ForwardRef wrapper to render collapsible content with enter/exit animations.", + "callers": [ + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 21, + "context": "AccordionContent," + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 741, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 872, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 883, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 985, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 998, + "context": "" + }, + { + "file": "src/components/video-editor/SettingsPanel.tsx", + "line": 1151, + "context": "" + } + ] + } + ], + "semantic_tags": [ + "ui", + "accordion", + "radix", + "component" + ], + "handles_entities": [], + "key_behaviors": [ + "renders collapsible sections with animated open/close behavior" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/ui/audio-level-meter.tsx": { + "short_description": "Compact audio level meter visual component", + "category": "SOURCE_CODE", + "description": "Renders a small multi-bar audio level meter that maps a 0-100 level to colored bars with thresholds and heights. Colors change from green to yellow to red as level thresholds are crossed to visually indicate input loudness.", + "key_constructs": [ + { + "name": "AudioLevelMeter", + "type": "function", + "purpose": "React component that displays multiple bars representing the current audio level.", + "callers": [ + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 30, + "context": "import { AudioLevelMeter } from \"../ui/audio-level-meter\";" + }, + { + "file": "src/components/launch/LaunchWindow.tsx", + "line": 293, + "context": "" + }, + { + "file": "src/components/launch/SourceSelector.tsx", + "line": 154, + "context": "" + }, + { + "file": "src/components/video-editor/ShortcutsConfigDialog.tsx", + "line": 4, + "context": "import { Button } from \"@/components/ui/button\";" + }, + { + "file": "src/components/video-editor/ShortcutsConfigDialog.tsx", + "line": 218, + "context": "" + }, + { + "file": "src/components/video-editor/ShortcutsConfigDialog.tsx", + "line": 228, + "context": "" + }, + { + "file": "src/components/video-editor/ShortcutsConfigDialog.tsx", + "line": 231, + "context": "" + }, + { + "file": "src/components/video-editor/ExportDialog.tsx", + "line": 3, + "context": "import { Button } from \"@/components/ui/button\";" + }, + { + "file": "src/components/video-editor/ExportDialog.tsx", + "line": 113, + "context": "" + }, + { + "file": "src/components/video-editor/ExportDialog.tsx", + "line": 147, + "context": "" + }, + { + "file": "src/components/video-editor/ExportDialog.tsx", + "line": 251, + "context": "" + }, + { + "file": "src/components/video-editor/timeline/TimelineEditor.tsx", + "line": 1440, + "context": "" + } + ] + }, + { + "name": "DropdownMenuContent", + "type": "function", + "purpose": "Portal-wrapped content element with default styling and animations." + }, + { + "name": "DropdownMenuItem", + "type": "function", + "purpose": "Menu item wrapper with spacing and focus styles." + }, + { + "name": "DropdownMenuCheckboxItem", + "type": "function", + "purpose": "Checkbox-style menu item that shows a check indicator when selected." + }, + { + "name": "DropdownMenuRadioItem", + "type": "function", + "purpose": "Radio-style menu item that shows a circular indicator when selected." + }, + { + "name": "DropdownMenuSubTrigger", + "type": "function", + "purpose": "Trigger for nested submenus that includes a right chevron indicator." + }, + { + "name": "DropdownMenuSubContent", + "type": "function", + "purpose": "Content wrapper for submenu content with styling and animations." + } + ], + "semantic_tags": [ + "ui", + "dropdown", + "radix", + "menu", + "navigation" + ], + "handles_entities": [], + "key_behaviors": [ + "renders styled dropdown menus with items, submenus and selection indicators" + ], + "generated_at_commit": "e7d5f51740dda13b90c5662bcf5ec8c4fa144311" + }, + "src/components/ui/input.tsx": { + "short_description": "Styled input React component with forwarded ref", + "category": "SOURCE_CODE", + "description": "A small reusable TextInput React component that forwards refs and applies consistent Tailwind-based styling. It wraps a native input element and merges custom className props via a utility. Used throughout the UI for consistent input appearance and behavior.", + "key_constructs": [ + { + "name": "Input", + "type": "function", + "purpose": "ForwardRef React component wrapping an with standard styles", + "callers": [ + { + "file": "src/components/video-editor/AddCustomFontDialog.tsx", + "line": 13, + "context": "import { Input } from \"@/components/ui/input\";" + }, + { + "file": "src/components/video-editor/AddCustomFontDialog.tsx", + "line": 136, + "context": " to ensure consistent appearance across the app. Used with form controls to provide accessible labels.", + "key_constructs": [ + { + "name": "Label", + "type": "function", + "purpose": "ForwardRef React component wrapping a