From e7dcb586c0408255a0bf788e355fdb6d50d9b923 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 2 Mar 2026 13:13:07 +0100 Subject: feat: Clip effects, asset sorting, and timeline improvements Major features and improvements: * **Clip Effects**: * Added UI in Properties Panel to manage effects on video/image clips (add, remove, toggle, reorder). * Implemented dynamic parameter fields for effects. * Added support for keyframing effect parameters. * **Assets Panel**: * Added sorting options: Name, Type, Duration, and File Size. * Persisted view preferences (grid/list mode, sort order) to local storage. * Refactored media item rendering and drag interactions. * **Timeline & Interaction**: * **Keyframe Dragging**: Added ability to drag keyframes directly on the timeline element. * **Resizing**: Improved resize logic to respect neighboring clips (prevents overlaps). * **Visuals**: Implemented tiled background rendering for video/image clips on the timeline. * **Shortcuts**: Added "Deselect All" action bound to the `Escape` key. * **Fixes**: Corrected drag-and-drop coordinate calculations when the timeline track area is scrolled. * **Text Elements**: * Refactored text background storage to use an explicit `enabled` flag. * Added `V8toV9` storage migration to update existing projects. * **Architecture**: * Moved export state management to `ProjectManager` for better lifecycle handling. * Refactored `PropertiesPanel` sections to be more composable (custom headers, borders). --- .../editor/panels/properties/section.tsx | 154 +++++++++++++++------ 1 file changed, 110 insertions(+), 44 deletions(-) (limited to 'apps/web/src/components/editor/panels/properties/section.tsx') diff --git a/apps/web/src/components/editor/panels/properties/section.tsx b/apps/web/src/components/editor/panels/properties/section.tsx index 3edd17b5..4428b59c 100644 --- a/apps/web/src/components/editor/panels/properties/section.tsx +++ b/apps/web/src/components/editor/panels/properties/section.tsx @@ -1,10 +1,12 @@ -import { createContext, useContext, useState } from "react"; +import { createContext, useContext, useEffect, useState } from "react"; import { cn } from "@/utils/ui"; import { HugeiconsIcon } from "@hugeicons/react"; import { ArrowDownIcon } from "@hugeicons/core-free-icons"; import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; const sectionExpandedCache = new Map(); +const mountedSectionKeys = new Set(); interface SectionContext { isOpen: boolean; @@ -24,8 +26,8 @@ interface SectionProps { defaultOpen?: boolean; sectionKey?: string; className?: string; - hasBorderTop?: boolean; - hasBorderBottom?: boolean; + showTopBorder?: boolean; + showBottomBorder?: boolean; } export function Section({ @@ -34,12 +36,21 @@ export function Section({ defaultOpen = true, sectionKey, className, - hasBorderTop = true, - hasBorderBottom = true, + showTopBorder = true, + showBottomBorder = true, }: SectionProps) { const cached = sectionKey ? sectionExpandedCache.get(sectionKey) : undefined; const [isOpen, setIsOpen] = useState(cached ?? defaultOpen); + useEffect(() => { + if (!sectionKey) return; + if (process.env.NODE_ENV !== "production" && mountedSectionKeys.has(sectionKey)) { + console.error(`[Section] duplicate sectionKey mounted simultaneously: "${sectionKey}"`); + } + mountedSectionKeys.add(sectionKey); + return () => { mountedSectionKeys.delete(sectionKey); }; + }, [sectionKey]); + const toggle = () => { const next = !isOpen; setIsOpen(next); @@ -51,8 +62,8 @@ export function Section({
@@ -63,15 +74,19 @@ export function Section({ } interface SectionHeaderProps { - title: string; children?: React.ReactNode; + trailing?: React.ReactNode; + leading?: React.ReactNode; + actions?: React.ReactNode; onClick?: () => void; className?: string; } export function SectionHeader({ - title, children, + trailing, + leading, + actions, onClick, className, }: SectionHeaderProps) { @@ -79,57 +94,108 @@ export function SectionHeader({ const isCollapsible = ctx?.collapsible ?? false; const isOpen = ctx?.isOpen ?? true; const isInteractive = isCollapsible || !!onClick; - const handleClick = isCollapsible ? ctx?.toggle : onClick; - const content = ( + const chevronIcon = isCollapsible ? ( + + ) : null; + + const headerContent = ( <> - - {title} - -
- {children} - {isCollapsible && ( - - )} -
+ {leading} +
{children}
+ {(trailing || chevronIcon) && ( +
+ {trailing} + {chevronIcon && ( + + )} +
+ )} + {actions} ); - const baseClassName = cn( - "flex w-full items-center justify-between h-11 px-3.5", - className, + if (!isInteractive) { + return ( +
+ {headerContent} +
+ ); + } + + return ( + // biome-ignore lint/a11y/useSemanticElements: outer div intentionally wraps a nested ); } - return
{content}
; + return ( + + {children} + + ); } export function SectionFields({ -- cgit v1.3.1