summaryrefslogtreecommitdiff
path: root/apps/web/src/components/section.tsx
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-27 00:55:15 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-29 15:56:48 +0200
commit4d127a038bf768eded68aae3cfa5c1b12241bdac (patch)
tree16bbb801caf6770546cff4fdb37b6408ec9ead98 /apps/web/src/components/section.tsx
parent82817cb50cb0487350573d6129130aec705f924b (diff)
downloadopencut-4d127a038bf768eded68aae3cfa5c1b12241bdac.tar.gz
chore: normalize line endings
Diffstat (limited to 'apps/web/src/components/section.tsx')
-rw-r--r--apps/web/src/components/section.tsx514
1 files changed, 257 insertions, 257 deletions
diff --git a/apps/web/src/components/section.tsx b/apps/web/src/components/section.tsx
index e3fc96df..86f2f71f 100644
--- a/apps/web/src/components/section.tsx
+++ b/apps/web/src/components/section.tsx
@@ -1,257 +1,257 @@
-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 = false,
- 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 first:border-t-0",
- showBottomBorder && "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 = (
- <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",
- )}
- />
- );
-
- const trailingArea =
- trailing || isCollapsible ? (
- <div className="flex items-center">
- {trailing}
- {isCollapsible && (
- <Button
- variant="ghost"
- size="icon"
- aria-label={isOpen ? "Collapse section" : "Expand section"}
- onClick={handleClick}
- >
- {chevronIcon}
- </Button>
- )}
- </div>
- ) : null;
-
- const innerContent = isInteractive ? (
- <button
- type="button"
- className="min-w-0 flex-1 flex items-center gap-2 h-full cursor-pointer text-left"
- onClick={handleClick}
- >
- {leading}
- <div className="min-w-0 flex-1 flex items-center">{children}</div>
- </button>
- ) : (
- <>
- {leading}
- <div className="min-w-0 flex-1 flex items-center">{children}</div>
- </>
- );
-
- return (
- <div
- className={cn("flex h-11 w-full items-center gap-2 px-3.5", className)}
- >
- {innerContent}
- {trailingArea}
- {actions}
- </div>
- );
-}
-
-export function SectionTitle({
- children,
- className,
- onClick,
-}: {
- children: React.ReactNode;
- className?: string;
- onClick?: () => void;
-}) {
- const ctx = useSectionContext();
- const isCollapsible = ctx?.collapsible ?? false;
- const isOpen = ctx?.isOpen ?? true;
-
- const titleClass = cn(
- "text-sm font-medium",
- isCollapsible && isOpen ? "text-foreground" : "text-muted-foreground",
- className,
- );
-
- if (onClick) {
- return (
- <button
- type="button"
- className={cn("cursor-pointer", titleClass)}
- onClick={onClick}
- >
- {children}
- </button>
- );
- }
-
- return <span className={titleClass}>{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;
-
- const content = <div className={cn("p-4 pt-0", className)}>{children}</div>;
-
- 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">{content}</div>
- </div>
- );
- }
-
- return content;
-}
+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 = false,
+ 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 first:border-t-0",
+ showBottomBorder && "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 = (
+ <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",
+ )}
+ />
+ );
+
+ const trailingArea =
+ trailing || isCollapsible ? (
+ <div className="flex items-center">
+ {trailing}
+ {isCollapsible && (
+ <Button
+ variant="ghost"
+ size="icon"
+ aria-label={isOpen ? "Collapse section" : "Expand section"}
+ onClick={handleClick}
+ >
+ {chevronIcon}
+ </Button>
+ )}
+ </div>
+ ) : null;
+
+ const innerContent = isInteractive ? (
+ <button
+ type="button"
+ className="min-w-0 flex-1 flex items-center gap-2 h-full cursor-pointer text-left"
+ onClick={handleClick}
+ >
+ {leading}
+ <div className="min-w-0 flex-1 flex items-center">{children}</div>
+ </button>
+ ) : (
+ <>
+ {leading}
+ <div className="min-w-0 flex-1 flex items-center">{children}</div>
+ </>
+ );
+
+ return (
+ <div
+ className={cn("flex h-11 w-full items-center gap-2 px-3.5", className)}
+ >
+ {innerContent}
+ {trailingArea}
+ {actions}
+ </div>
+ );
+}
+
+export function SectionTitle({
+ children,
+ className,
+ onClick,
+}: {
+ children: React.ReactNode;
+ className?: string;
+ onClick?: () => void;
+}) {
+ const ctx = useSectionContext();
+ const isCollapsible = ctx?.collapsible ?? false;
+ const isOpen = ctx?.isOpen ?? true;
+
+ const titleClass = cn(
+ "text-sm font-medium",
+ isCollapsible && isOpen ? "text-foreground" : "text-muted-foreground",
+ className,
+ );
+
+ if (onClick) {
+ return (
+ <button
+ type="button"
+ className={cn("cursor-pointer", titleClass)}
+ onClick={onClick}
+ >
+ {children}
+ </button>
+ );
+ }
+
+ return <span className={titleClass}>{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;
+
+ const content = <div className={cn("p-4 pt-0", className)}>{children}</div>;
+
+ 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">{content}</div>
+ </div>
+ );
+ }
+
+ return content;
+}