diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-25 16:47:22 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-29 15:48:22 +0200 |
| commit | 8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch) | |
| tree | e9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/components/editor/panels/properties/section.tsx | |
| parent | 39ea298a9ccba3104c549c0ca079c231981811f5 (diff) | |
| download | opencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz | |
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'apps/web/src/components/editor/panels/properties/section.tsx')
| -rw-r--r-- | apps/web/src/components/editor/panels/properties/section.tsx | 262 |
1 files changed, 0 insertions, 262 deletions
diff --git a/apps/web/src/components/editor/panels/properties/section.tsx b/apps/web/src/components/editor/panels/properties/section.tsx deleted file mode 100644 index 5989108b..00000000 --- a/apps/web/src/components/editor/panels/properties/section.tsx +++ /dev/null @@ -1,262 +0,0 @@ -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<string, boolean>(); -const mountedSectionKeys = new Set<string>(); - -interface SectionContext { - isOpen: boolean; - toggle: () => void; - collapsible: boolean; -} - -const SectionCtx = createContext<SectionContext | null>(null); - -function useSectionContext() { - return useContext(SectionCtx); -} - -interface SectionProps { - children: React.ReactNode; - collapsible?: boolean; - defaultOpen?: boolean; - sectionKey?: string; - className?: string; - showTopBorder?: boolean; - showBottomBorder?: boolean; -} - -export function Section({ - children, - collapsible = false, - defaultOpen = true, - sectionKey, - className, - 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); - if (sectionKey) sectionExpandedCache.set(sectionKey, next); - }; - - return ( - <SectionCtx.Provider value={{ isOpen, toggle, collapsible }}> - <div - className={cn( - "flex flex-col", - showTopBorder && "border-t", - showBottomBorder && "last:border-b", - className, - )} - > - {children} - </div> - </SectionCtx.Provider> - ); -} - -interface SectionHeaderProps { - children?: React.ReactNode; - trailing?: React.ReactNode; - leading?: React.ReactNode; - actions?: React.ReactNode; - onClick?: () => void; - className?: string; -} - -export function SectionHeader({ - children, - trailing, - leading, - actions, - onClick, - className, -}: SectionHeaderProps) { - const ctx = useSectionContext(); - const isCollapsible = ctx?.collapsible ?? false; - const isOpen = ctx?.isOpen ?? true; - const isInteractive = isCollapsible || !!onClick; - const handleClick = isCollapsible ? ctx?.toggle : onClick; - - const chevronIcon = isCollapsible ? ( - <HugeiconsIcon - icon={ArrowDownIcon} - className={cn( - "size-4 shrink-0 transition-transform duration-200 ease-out", - isOpen ? "rotate-0 text-foreground" : "-rotate-90 text-muted-foreground", - )} - /> - ) : null; - - const headerContent = ( - <> - {leading} - <div className="min-w-0 flex-1">{children}</div> - {(trailing || chevronIcon) && ( - <div className="flex items-center"> - {trailing} - {chevronIcon && ( - <Button - variant="ghost" - size="icon" - aria-label={isOpen ? "Collapse section" : "Expand section"} - onClick={(event) => { - event.stopPropagation(); - handleClick?.(); - }} - > - {chevronIcon} - </Button> - )} - </div> - )} - {actions} - </> - ); - - if (!isInteractive) { - return ( - <div className={cn("flex h-11 w-full items-center gap-2 px-3.5", className)}> - {headerContent} - </div> - ); - } - - return ( - // biome-ignore lint/a11y/useSemanticElements: outer div intentionally wraps a nested <Button> (chevron), making <button> invalid HTML here - <div - role="button" - tabIndex={0} - className={cn( - "flex h-11 w-full cursor-pointer items-center gap-2 px-3.5", - className, - )} - onClick={handleClick} - onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") handleClick?.(); }} - > - {headerContent} - </div> - ); -} - -export function SectionTitle({ - children, - className, - onClick, -}: { - children: React.ReactNode; - className?: string; - onClick?: () => void; -}) { - const ctx = useSectionContext(); - const isOpen = ctx?.isOpen ?? true; - - if (onClick) { - return ( - <button - type="button" - className={cn( - "cursor-pointer text-sm font-medium", - isOpen ? "text-foreground" : "text-muted-foreground", - className, - )} - onClick={onClick} - > - {children} - </button> - ); - } - - return ( - <span - className={cn( - "text-sm font-medium", - isOpen ? "text-foreground" : "text-muted-foreground", - className, - )} - > - {children} - </span> - ); -} - -export function SectionFields({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return ( - <div className={cn("flex flex-col gap-3.5", className)}>{children}</div> - ); -} - -export function SectionField({ - label, - beforeLabel, - children, - className, -}: { - label: string; - beforeLabel?: React.ReactNode; - children: React.ReactNode; - className?: string; -}) { - return ( - <div className={cn("flex flex-col gap-2", className)}> - <div className="flex h-4 items-center gap-1.5"> - {beforeLabel} - <Label>{label}</Label> - </div> - {children} - </div> - ); -} - -export function SectionContent({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - const ctx = useSectionContext(); - const isCollapsible = ctx?.collapsible ?? false; - const isOpen = ctx?.isOpen ?? true; - - if (isCollapsible) { - return ( - <div - className={cn( - "grid transition-[grid-template-rows] duration-100 ease-out", - isOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]", - )} - > - <div className="overflow-hidden"> - <div className={cn("p-4 pt-0", className)}>{children}</div> - </div> - </div> - ); - } - - return <div className={cn("p-4 pt-0", className)}>{children}</div>; -} |
