add collapsible main nav items in settings

This commit is contained in:
Josh Hawkins 2026-05-24 07:17:33 -05:00
parent d47ac788c0
commit 0cb030420e

View File

@ -16,6 +16,11 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
useCallback, useCallback,
@ -589,7 +594,7 @@ function MobileMenuItem({
return ( return (
<div <div
className={cn( className={cn(
"inline-flex h-10 w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md px-4 py-2 pr-2 text-sm font-medium text-primary-variant disabled:pointer-events-none disabled:opacity-50", "inline-flex h-10 w-full cursor-pointer items-center whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium text-primary-variant disabled:pointer-events-none disabled:opacity-50",
className, className,
)} )}
onClick={() => { onClick={() => {
@ -600,7 +605,6 @@ function MobileMenuItem({
<div className="w-full"> <div className="w-full">
{label ?? <div>{t("menu." + item.key)}</div>} {label ?? <div>{t("menu." + item.key)}</div>}
</div> </div>
<LuChevronRight className="size-4" />
</div> </div>
); );
} }
@ -613,6 +617,39 @@ export default function Settings() {
const [sectionStatusByKey, setSectionStatusByKey] = useState< const [sectionStatusByKey, setSectionStatusByKey] = useState<
Partial<Record<SettingsType, SectionStatus>> Partial<Record<SettingsType, SectionStatus>>
>({}); >({});
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(
() =>
// all collapsed by default
new Set(
settingsGroups.filter((g) => g.items.length > 1).map((g) => g.label),
),
);
const toggleGroupCollapsed = useCallback((label: string) => {
setCollapsedGroups((prev) => {
const next = new Set(prev);
if (next.has(label)) {
next.delete(label);
} else {
next.add(label);
}
return next;
});
}, []);
// Auto-expand the group containing the active page whenever pageToggle changes
useEffect(() => {
const containingGroup = settingsGroups.find((group) =>
group.items.some((item) => item.key === pageToggle),
);
if (!containingGroup) return;
setCollapsedGroups((prev) => {
if (!prev.has(containingGroup.label)) return prev;
const next = new Set(prev);
next.delete(containingGroup.label);
return next;
});
}, [pageToggle]);
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const { data: profilesData } = useSWR<ProfilesApiResponse>("profiles"); const { data: profilesData } = useSWR<ProfilesApiResponse>("profiles");
@ -1611,14 +1648,10 @@ export default function Settings() {
visibleSettingsViews.includes(item.key as SettingsType), visibleSettingsViews.includes(item.key as SettingsType),
); );
if (filteredItems.length === 0) return null; if (filteredItems.length === 0) return null;
return ( const isMultiItem = filteredItems.length > 1;
<div key={group.label} className="mb-3"> const renderedExpanded =
{filteredItems.length > 1 && ( !isMultiItem || !collapsedGroups.has(group.label);
<h3 className="mb-2 ml-2 text-sm font-medium text-secondary-foreground"> const items = filteredItems.map((item) => (
<div>{t("menu." + group.label)}</div>
</h3>
)}
{filteredItems.map((item) => (
<MobileMenuItem <MobileMenuItem
key={item.key} key={item.key}
item={item} item={item}
@ -1627,9 +1660,7 @@ export default function Settings() {
onSelect={(key) => { onSelect={(key) => {
if ( if (
!isAdmin && !isAdmin &&
!ALLOWED_VIEWS_FOR_VIEWER.includes( !ALLOWED_VIEWS_FOR_VIEWER.includes(key as SettingsType)
key as SettingsType,
)
) { ) {
setPageToggle("uiSettings"); setPageToggle("uiSettings");
} else { } else {
@ -1638,7 +1669,28 @@ export default function Settings() {
setContentMobileOpen(true); setContentMobileOpen(true);
}} }}
/> />
))} ));
return (
<div key={group.label} className="mb-3">
{isMultiItem ? (
<Collapsible
open={renderedExpanded}
onOpenChange={() => toggleGroupCollapsed(group.label)}
>
<CollapsibleTrigger className="flex min-h-10 w-full items-center justify-between rounded-md py-2 pl-2 pr-2 text-sm font-medium text-secondary-foreground">
<div>{t("menu." + group.label)}</div>
<LuChevronRight
className={cn(
"size-4 shrink-0 transition-transform duration-200",
renderedExpanded && "rotate-90",
)}
/>
</CollapsibleTrigger>
<CollapsibleContent>{items}</CollapsibleContent>
</Collapsible>
) : (
items
)}
</div> </div>
); );
})} })}
@ -1940,20 +1992,41 @@ export default function Settings() {
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
) : ( ) : (
<> (() => {
<SidebarGroupLabel const hasActiveItem = filteredItems.some(
className={cn(
"ml-2 cursor-default pl-0 text-sm",
filteredItems.some(
(item) => pageToggle === item.key, (item) => pageToggle === item.key,
) );
const renderedExpanded = !collapsedGroups.has(
group.label,
);
return (
<Collapsible
open={renderedExpanded}
onOpenChange={() =>
toggleGroupCollapsed(group.label)
}
>
<SidebarGroupLabel
asChild
className={cn(
"ml-2 pl-0 text-sm",
hasActiveItem
? "text-primary" ? "text-primary"
: "text-sidebar-foreground/80", : "text-sidebar-foreground/80",
)} )}
> >
<CollapsibleTrigger className="flex w-full items-center justify-between">
<div>{t("menu." + group.label)}</div> <div>{t("menu." + group.label)}</div>
<LuChevronRight
className={cn(
"size-4 shrink-0 transition-transform duration-200",
renderedExpanded && "rotate-90",
)}
/>
</CollapsibleTrigger>
</SidebarGroupLabel> </SidebarGroupLabel>
<SidebarMenuSub className="mx-2 border-0"> <CollapsibleContent>
<SidebarMenuSub className="mx-2 border-0 md:mx-0">
{filteredItems.map((item) => ( {filteredItems.map((item) => (
<SidebarMenuSubItem key={item.key}> <SidebarMenuSubItem key={item.key}>
<SidebarMenuSubButton <SidebarMenuSubButton
@ -1968,7 +2041,9 @@ export default function Settings() {
) { ) {
setPageToggle("uiSettings"); setPageToggle("uiSettings");
} else { } else {
setPageToggle(item.key as SettingsType); setPageToggle(
item.key as SettingsType,
);
} }
}} }}
> >
@ -1981,7 +2056,10 @@ export default function Settings() {
</SidebarMenuSubItem> </SidebarMenuSubItem>
))} ))}
</SidebarMenuSub> </SidebarMenuSub>
</> </CollapsibleContent>
</Collapsible>
);
})()
)} )}
</SidebarGroup> </SidebarGroup>
); );