From 3931f23e05c38d2cfbe9e664dcb8242c75a7b7f7 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:56:29 +1000 Subject: [PATCH] Refine HeroUI editor spacing, cards and responsive alignment --- docs/HEROUI_MIGRATION.md | 17 +- electron/electron-env.d.ts | 4 + electron/ipc/register/settings.ts | 13 +- electron/preload.ts | 9 + electron/windows.ts | 13 +- src/components/ui/choice-group.tsx | 66 ++ src/components/ui/color-picker.tsx | 47 +- .../video-editor/AnnotationSettingsPanel.tsx | 196 ++--- .../video-editor/ExportSettingsMenu.tsx | 53 +- src/components/video-editor/SettingsPanel.tsx | 793 +++++++++--------- src/components/video-editor/SliderControl.tsx | 2 +- src/components/video-editor/TutorialHelp.tsx | 2 +- src/components/video-editor/VideoEditor.tsx | 25 +- .../video-editor/layout/EditorExportMenu.tsx | 2 +- .../video-editor/layout/EditorHeader.tsx | 43 +- .../video-editor/layout/EditorPresetMenu.tsx | 6 +- .../layout/EditorPreviewPanel.tsx | 36 +- .../video-editor/layout/EditorShell.tsx | 2 +- .../video-editor/layout/EditorSidebar.tsx | 71 +- .../layout/EditorTimelinePanel.tsx | 2 +- .../video-editor/timeline/TimelineEditor.tsx | 4 +- src/index.css | 38 + tests/ui/bridge.ts | 8 + tests/ui/editor-layout.spec.ts | 119 +++ tests/ui/editor.spec.ts | 15 +- 25 files changed, 928 insertions(+), 658 deletions(-) create mode 100644 src/components/ui/choice-group.tsx create mode 100644 tests/ui/editor-layout.spec.ts diff --git a/docs/HEROUI_MIGRATION.md b/docs/HEROUI_MIGRATION.md index fab0c125..a84272eb 100644 --- a/docs/HEROUI_MIGRATION.md +++ b/docs/HEROUI_MIGRATION.md @@ -19,20 +19,24 @@ helper installation. The dependency lockfile belongs to this branch. To go back, close the development app and run `npm run dev` from the original `recordly` directory. No reset, stash, or file restoration is needed. You can -keep both checkouts while comparing them. If this commit is merged later, -`git revert ` reverses the migration without rewriting history. +keep both checkouts while comparing them. To undo just the UI refinement pass, +use `git revert `. To undo the entire migration after merging, +revert the branch commits newest-first, including the original `93be193` migration. ## UI approach The controls use HeroUI React 3.2.6 and its default light/dark theme, following [the official component demos](https://heroui.com/en/docs/react/components). -This includes buttons, fields, switches, sliders, tabs, toggle groups, radios, +This includes buttons, fields, switches, sliders, tabs, tag groups, toggle groups, radios, selects, modals, popovers, menus, tooltips, color pickers, progress indicators, skeletons, and toasts. React 19 and Tailwind 4 satisfy HeroUI v3 requirements. The former Radix, Sonner, and third-party color picker dependencies are removed. -The editor uses docked surfaces, a fixed-width inspector, aligned toolbars, -consistent spacing, and restrained selection colors. Floating layers keep one +The editor uses a floating inspector card, an open canvas and timeline, aligned +toolbars, consistent spacing, and restrained selection colors. Advanced controls +live behind a per-section switch; changing views preserves project values. +Background types and other exclusive choices use TagGroup. The header follows +native fullscreen state and keeps project titles centered at narrow widths. Floating layers keep one surface instead of nesting cards and shadows. Timeline colors follow the theme and retain the distinction between clip types. The recorder keeps its compact desktop layout. @@ -60,7 +64,8 @@ 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 callbacks and keyboard behavior, modal focus, export settings, presets, cropping, annotation formatting/undo, project menus, recorder popovers, countdown and update -windows, theme switching, and a smaller desktop layout. Screenshots and failure +windows, theme switching, Advanced state, color editing, and header/playback +alignment from 800–1440px with and without macOS window controls. Screenshots and failure traces go to the ignored `test-results/` directory. `npm run dev:ui` starts only Vite for browser inspection; the component fixture diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 1a926718..61bf1fd2 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -864,6 +864,10 @@ interface Window { onMenuLoadProject: (callback: () => void) => () => void; onMenuSaveProject: (callback: () => void) => () => void; onMenuSaveProjectAs: (callback: () => void) => () => void; + getWindowChrome: () => Promise<{ trafficLightsVisible: boolean }>; + onWindowChromeChanged: ( + callback: (chrome: { trafficLightsVisible: boolean }) => void, + ) => () => void; getPlatform: () => Promise; getLinuxWindowSystem: () => Promise<"wayland" | "x11" | null>; revealInFolder: ( diff --git a/electron/ipc/register/settings.ts b/electron/ipc/register/settings.ts index 93aee878..7fea305e 100644 --- a/electron/ipc/register/settings.ts +++ b/electron/ipc/register/settings.ts @@ -1,5 +1,5 @@ import fs from "node:fs/promises"; -import { app, ipcMain } from "electron"; +import { app, BrowserWindow, ipcMain } from "electron"; import { hasAppSetting, readAppSettingsStore, writeAppSettingsStore } from "../../appSettingsStore"; import { hideCursor } from "../../cursorHider"; import { closeCountdownWindow, createCountdownWindow, getCountdownWindow } from "../../windows"; @@ -47,6 +47,17 @@ export function registerSettingsHandlers() { return app.getVersion(); }); + ipcMain.handle("get-window-chrome", (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + return { + trafficLightsVisible: + process.platform === "darwin" && + !!win && + !win.isFullScreen() && + !win.isSimpleFullScreen(), + }; + }); + ipcMain.handle("get-platform", () => { return process.platform; }); diff --git a/electron/preload.ts b/electron/preload.ts index 990ee7a8..f854fe07 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -940,6 +940,15 @@ contextBridge.exposeInMainWorld("electronAPI", { ipcRenderer.on("menu-save-project-as", listener); return () => ipcRenderer.removeListener("menu-save-project-as", listener); }, + getWindowChrome: () => ipcRenderer.invoke("get-window-chrome"), + onWindowChromeChanged: (callback: (chrome: { trafficLightsVisible: boolean }) => void) => { + const listener = ( + _event: Electron.IpcRendererEvent, + chrome: { trafficLightsVisible: boolean }, + ) => callback(chrome); + ipcRenderer.on("window-chrome-changed", listener); + return () => ipcRenderer.removeListener("window-chrome-changed", listener); + }, getPlatform: () => { return ipcRenderer.invoke("get-platform"); }, diff --git a/electron/windows.ts b/electron/windows.ts index 23e874fa..de948903 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -919,7 +919,7 @@ export function createEditorWindow(): BrowserWindow { }), ...(isMac && { titleBarStyle: "hiddenInset", - trafficLightPosition: { x: 12, y: 12 }, + trafficLightPosition: { x: 16, y: 20 }, }), autoHideMenuBar: !isMac, transparent: false, @@ -938,6 +938,17 @@ export function createEditorWindow(): BrowserWindow { }, }); + const publishWindowChrome = () => { + if (!win.isDestroyed()) + win.webContents.send("window-chrome-changed", { + trafficLightsVisible: isMac && !win.isFullScreen() && !win.isSimpleFullScreen(), + }); + }; + win.on("enter-full-screen", publishWindowChrome); + win.on("leave-full-screen", publishWindowChrome); + win.on("resize", publishWindowChrome); + win.webContents.on("did-finish-load", publishWindowChrome); + win.once("ready-to-show", () => { console.log(`[PERF:MAIN] Editor Window: ready-to-show in ${Date.now() - perfStart}ms`); win.show(); diff --git a/src/components/ui/choice-group.tsx b/src/components/ui/choice-group.tsx new file mode 100644 index 00000000..e6edc166 --- /dev/null +++ b/src/components/ui/choice-group.tsx @@ -0,0 +1,66 @@ +import { Tag, TagGroup } from "@heroui/react"; +import type { ComponentProps, ReactNode } from "react"; + +/** Mutually exclusive settings, with HeroUI's keyboard-accessible tag selection. */ +export function ChoiceGroup({ + value, + onValueChange, + children, + className, + ...props +}: { + value?: string; + onValueChange?: (value: string) => void; + children: ReactNode; + className?: string; + type?: "single"; + size?: "sm" | "md" | "lg"; + fullWidth?: boolean; + "aria-label"?: string; +}) { + return ( + { + if (keys !== "all") { + const key = Array.from(keys)[0]; + if (key !== undefined) onValueChange?.(String(key)); + } + }} + > + + {children} + + + ); +} +export function ChoiceItem({ + value, + title, + children, + className, + ...props +}: Omit, "id"> & { + value: string; + title?: string; + "aria-label"?: string; +}) { + return ( + + {children} + + ); +} diff --git a/src/components/ui/color-picker.tsx b/src/components/ui/color-picker.tsx index 49357596..5f8d90d5 100644 --- a/src/components/ui/color-picker.tsx +++ b/src/components/ui/color-picker.tsx @@ -32,13 +32,6 @@ export function ColorPalette({ color = "#000000", colors, onChange }: PalettePro ))} - value && onChange({ hex: value.toString("hex") })} - > - - ); } @@ -46,19 +39,34 @@ export function ColorControl({ value, onChange, label, + colors, + compact = false, + onClear, }: { value: string; onChange: (value: string) => void; label: string; + colors?: readonly string[]; + compact?: boolean; + onClear?: () => void; }) { return ( - onChange(color.toString("hex"))}> - - + @@ -67,10 +75,25 @@ export function ColorControl({ + {colors && ( + + {colors.map((color) => ( + + + + + ))} + + )} - + + {onClear && ( + + )} diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index 9d50c04b..e2c72704 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -6,7 +6,6 @@ import { AlignLeft, AlignRight, TextB as Bold, - CaretDown as ChevronDown, ImageSquare as ImageIcon, Info, TextItalic as Italic, @@ -20,7 +19,7 @@ import { ColorControl, ColorPalette } from "@/components/ui/color-picker"; import { useEffect, useMemo, useRef, useState } from "react"; import { toast } from "@/components/ui/toast"; import { Button } from "@/components/ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; + import { Select, SelectContent, @@ -143,7 +142,7 @@ export function AnnotationSettingsPanel({ return ( -
+
{/* Type Selector */} {/* Text Content */} - +
-