From 7dba6e30719bdf23d6d558b35cdaeefcb6df2c6d Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:55:27 +1000 Subject: [PATCH] Refine editor panels and add source-frame timeline filmstrips --- docs/HEROUI_MIGRATION.md | 16 +- src/components/launch/SourceSelector.tsx | 2 +- .../launch/popovers/PopoverScaffold.tsx | 2 +- src/components/ui/dialog.tsx | 2 +- src/components/ui/popover.tsx | 46 +- .../video-editor/AnnotationSettingsPanel.tsx | 882 ++++++++++-------- .../video-editor/ProjectBrowserDialog.tsx | 45 +- src/components/video-editor/SettingsPanel.tsx | 265 +++--- src/components/video-editor/WallpaperGrid.tsx | 13 +- .../video-editor/layout/EditorExportMenu.tsx | 2 +- .../video-editor/layout/EditorPresetMenu.tsx | 31 +- .../layout/EditorPreviewPanel.tsx | 4 +- .../layout/EditorTimelinePanel.tsx | 2 +- src/components/video-editor/timeline/Item.tsx | 141 ++- src/components/video-editor/timeline/Row.tsx | 9 +- .../video-editor/timeline/TimelineEditor.tsx | 1 + .../timeline/components/axis/TimelineAxis.tsx | 24 +- .../components/filmstrip/ClipFilmstrip.tsx | 74 ++ .../components/filmstrip/frameCache.ts | 101 ++ .../components/playhead/PlaybackCursor.tsx | 11 +- .../components/viewport/TimelineCanvas.tsx | 11 +- .../timeline/core/filmstrip.test.ts | 43 + .../video-editor/timeline/core/filmstrip.ts | 15 + .../video-editor/timeline/core/time.test.ts | 13 + .../video-editor/timeline/core/time.ts | 46 +- .../timeline/timelineLayout.test.ts | 8 +- .../video-editor/timeline/timelineLayout.ts | 2 +- src/components/video-editor/types.ts | 2 +- src/index.css | 29 + tests/ui/bridge.ts | 8 +- tests/ui/editor-refinements.spec.ts | 158 ++++ tests/ui/editor.spec.ts | 5 + tests/ui/fixtures/filmstrip.mp4 | Bin 0 -> 104495 bytes 33 files changed, 1291 insertions(+), 722 deletions(-) create mode 100644 src/components/video-editor/timeline/components/filmstrip/ClipFilmstrip.tsx create mode 100644 src/components/video-editor/timeline/components/filmstrip/frameCache.ts create mode 100644 src/components/video-editor/timeline/core/filmstrip.test.ts create mode 100644 src/components/video-editor/timeline/core/filmstrip.ts create mode 100644 tests/ui/editor-refinements.spec.ts create mode 100644 tests/ui/fixtures/filmstrip.mp4 diff --git a/docs/HEROUI_MIGRATION.md b/docs/HEROUI_MIGRATION.md index 0a1cca7c..f65106cf 100644 --- a/docs/HEROUI_MIGRATION.md +++ b/docs/HEROUI_MIGRATION.md @@ -41,8 +41,17 @@ surface instead of nesting cards and shadows. Timeline blocks retain the origina Recordly palette in both themes. Inspector controls use compact 12–13px text and 32–36px controls, with 14px section titles. Image and video wallpaper grids share a plus tile for importing and a small remove control on custom tiles, revealed -on hover or keyboard focus. The recorder keeps its compact -desktop layout. +on hover or keyboard focus. The background gallery slides left/right with tab order. + +The clip lane samples real source frames with a bounded cache and one background +video decoder. Sampling follows clip source offsets, speed, and the visible timeline +range; it never seeks the playback element. Zooms use a compact lane with labels +that adapt to block width. The ruler chooses tick density from available width, +and the red playhead has a white centre line. Webcam roundness defaults to 69%; +existing saved values remain intact. Three compact timeline tracks fit before +vertical scrolling. Popovers support outside-click and Escape dismissal and +share one padding layer. Project and preset names truncate inside their rows. +The recorder keeps its compact desktop layout. The adapters in `src/components/ui` translate existing Recordly state/callback props to HeroUI APIs. The timeline geometry, crop handles, video canvas, caption @@ -64,7 +73,8 @@ The production build includes both the renderer and Electron main/preload bundles. It is not an installer packaging run. Browser tests use an explicit mocked Electron bridge and a generated six-second -video fixture; they never start a real screen recording. They cover control +video fixture; they never start a real screen recording. A second changing-frame +fixture verifies that timeline thumbnails decode distinct source frames. They cover control callbacks and keyboard behavior, modal focus, export settings, presets, cropping, annotation formatting/undo, project menus, recorder popovers, countdown and update windows, theme switching, Advanced state, color editing, wallpaper uploads and diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx index 0d2dfca6..8e96b231 100644 --- a/src/components/launch/SourceSelector.tsx +++ b/src/components/launch/SourceSelector.tsx @@ -319,7 +319,7 @@ export const SourceSelector = React.memo(function SourceSelector({ const { onMouseEnter } = useHudInteraction(); return ( - + {trigger} + {trigger} + diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index c34e20ea..adad21b4 100644 --- a/src/components/ui/popover.tsx +++ b/src/components/ui/popover.tsx @@ -1,18 +1,28 @@ import { Popover as HeroPopover } from "@heroui/react"; -import { createContext, useContext, type ComponentProps, type ReactNode } from "react"; +import { createContext, useContext, useState, type ComponentProps, type ReactNode } from "react"; import { cn } from "@/lib/utils"; const ModalContext = createContext(true); +const CloseContext = createContext<(() => void) | undefined>(undefined); export function Popover({ open, + defaultOpen, + onOpenChange, modal = true, children, ...props }: Omit, "isOpen"> & { open?: boolean; modal?: boolean }) { + const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false); + const changeOpen = (value: boolean) => { + setInternalOpen(value); + onOpenChange?.(value); + }; return ( - - {children} - + changeOpen(false)}> + + {children} + + ); } @@ -46,6 +56,7 @@ export function PopoverContent({ ...props }: ContentProps) { const modal = useContext(ModalContext); + const close = useContext(CloseContext); const placement = (align === "center" ? side : `${side} ${align}`) as ComponentProps< typeof HeroPopover.Content >["placement"]; @@ -61,10 +72,31 @@ export function PopoverContent({ UNSTABLE_portalContainer={ usePortal ? undefined : (document.getElementById("root") ?? undefined) } - className={cn("max-w-[calc(100vw-24px)]", className)} + className="max-w-[calc(100vw-24px)]" > - - {children} + +
{ + // Choice groups can consume Escape; dismiss before their keyboard handler. + // A nested portalled menu handles its own Escape first. + if ( + event.key === "Escape" && + !event.defaultPrevented && + !props.isKeyboardDismissDisabled && + event.currentTarget.contains(event.target as Node) + ) { + event.preventDefault(); + event.stopPropagation(); + close?.(); + } + }} + > + {children} +
); diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index e2c72704..6736630c 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -28,7 +28,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ChoiceGroup, ChoiceItem } from "@/components/ui/choice-group"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { type CustomFont, getCustomFonts } from "@/lib/customFonts"; import { cn } from "@/lib/utils"; @@ -145,21 +145,34 @@ export function AnnotationSettingsPanel({
{/* Type Selector */} - onTypeChange(value as AnnotationType)} - className="mb-6" - > - - +
+ onTypeChange(value as AnnotationType)} + className="grid grid-cols-4 gap-2" + > + {t("annotations.text")} - - + + {t("annotations.image")} - - + + {t("annotations.arrow")} - - + + {t("annotations.blur")} - - + + {/* Text Content */} - -
- -