Skip to content
Source
Sign inSign up
Open History to see the last commit

FoldedSettingsSection.tsx

102 lines4.0 KB

1import { SettingsGroup } from "./SettingsGroup";
2import { ChevronRightIcon } from "lucide-react";
3import { type ReactNode, useState } from "react";
4​
5import { cn } from "~/lib/utils";
6import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";
7import { useSettingsSearchTarget, useSettingsSearchTargetId } from "./settingsLayout";
8​
9export function FoldedSettingsSection({
10 id,
11 title,
12 summary,
13 control,
14 headerPlacement = "inside",
15 collapsedContent,
16 children,
17}: {
18 readonly id: string;
19 readonly title: ReactNode;
20 readonly summary?: string | null;
21 readonly control?: ReactNode;
22 readonly headerPlacement?: "inside" | "outside";
23 /** Shown under the header only while folded. */
24 readonly collapsedContent?: ReactNode;
25 readonly children: ReactNode;
26}) {
27 const [open, setOpen] = useState(false);
28 const searchTargetId = useSettingsSearchTargetId();
29 const targetRef = useSettingsSearchTarget<HTMLElement>(id);
30 // A search jump lands inside the fold, so open it before the scroll runs.
31 const [openedForTarget, setOpenedForTarget] = useState<string | null>(null);
32 if (searchTargetId === id && openedForTarget !== id) {
33 setOpenedForTarget(id);
34 if (!open) setOpen(true);
35 }
36​
37 if (headerPlacement === "outside") {
38 return (
39 <section id={id} ref={targetRef} tabIndex={-1} className="outline-none">
40 <Collapsible open={open} onOpenChange={setOpen}>
41 <div className="space-y-2.5">
42 <div
43 data-settings-scroll-target
44 className="flex min-h-7 items-start justify-between gap-4 px-3 sm:px-4"
45 >
46 <h2>
47 <CollapsibleTrigger className="flex min-h-7 items-center gap-2 rounded-md text-sm font-normal text-foreground/70 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
48 {title}
49 <ChevronRightIcon
50 aria-hidden
51 className={cn(
52 "size-4 shrink-0 transition-transform duration-150 motion-reduce:transition-none",
53 open && "rotate-90",
54 )}
55 />
56 </CollapsibleTrigger>
57 </h2>
58 {control}
59 </div>
60 <CollapsiblePanel>
61 <SettingsGroup>{children}</SettingsGroup>
62 </CollapsiblePanel>
63 </div>
64 </Collapsible>
65 </section>
66 );
67 }
68​
69 return (
70 <section id={id} ref={targetRef} tabIndex={-1} className="outline-none">
71 <Collapsible open={open} onOpenChange={setOpen} render={<SettingsGroup divided={false} />}>
72 {/* On a narrow screen the controls drop below the title instead of covering it. */}
73 <div className="flex flex-wrap items-center gap-x-4 px-3 sm:px-4">
74 <CollapsibleTrigger className="flex min-h-11 min-w-0 flex-1 basis-40 items-center gap-2 py-2 text-left outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring rounded-md">
75 <ChevronRightIcon
76 aria-hidden
77 className={cn(
78 "size-4 shrink-0 text-muted-foreground transition-transform duration-150 motion-reduce:transition-none",
79 open && "rotate-90",
80 )}
81 />
82 <span className="shrink-0 text-sm font-medium">{title}</span>
83 {summary ? (
84 <span className="min-w-0 truncate text-xs text-muted-foreground">{summary}</span>
85 ) : null}
86 </CollapsibleTrigger>
87 {control ? (
88 <div className="ms-auto flex max-w-full items-center py-1.5">{control}</div>
89 ) : null}
90 </div>
91 {!open && collapsedContent ? (
92 <div className="border-t border-border/50 empty:hidden">{collapsedContent}</div>
93 ) : null}
94 <CollapsiblePanel>
95 <div className="border-t border-border/50 *:not-first:border-t *:not-first:border-border/50">
96 {children}
97 </div>
98 </CollapsiblePanel>
99 </Collapsible>
100 </section>
101 );