authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-04 17:06:27-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 17:06:27-07:00
log2e61301b1325e30cb9464e193299a7e469f7ec08
tree5a71e79af31a39f3379f51219169742bdc9b80d6
parent4f1092cec2ebaa6ed0e49821238dfe6f1add362d
signature Signed by PGP key B5690EEEBB952194

fix(web): smooth settings sidebar transitions (#9811)


1 files changed, 39 insertions(+), 16 deletions(-)

apps/web/src/components/settings/SettingsSidebarNav.tsx+39-16
...@@ -8,6 +8,7 @@ import {...@@ -8,6 +8,7 @@ import {
8 useState,8 useState,
9 type ComponentType,9 type ComponentType,
10 type KeyboardEvent,10 type KeyboardEvent,
11 type ReactNode,
11} from "react";12} from "react";
12import {13import {
13 ArchiveIcon,14 ArchiveIcon,
...@@ -24,6 +25,7 @@ import {...@@ -24,6 +25,7 @@ import {
24import { useLocation, useNavigate } from "@tanstack/react-router";25import { useLocation, useNavigate } from "@tanstack/react-router";
2526
26import { Button } from "../ui/button";27import { Button } from "../ui/button";
28import { Collapsible, CollapsiblePanel } from "../ui/collapsible";
27import { Input } from "../ui/input";29import { Input } from "../ui/input";
28import { Kbd } from "../ui/kbd";30import { Kbd } from "../ui/kbd";
29import {31import {
...@@ -114,6 +116,22 @@ function SettingsSectionIcon({ to }: { to: SettingsPath }) {...@@ -114,6 +116,22 @@ function SettingsSectionIcon({ to }: { to: SettingsPath }) {
114 return <Icon className="mt-0.5 size-3.5 shrink-0 text-sidebar-muted-foreground/60" />;116 return <Icon className="mt-0.5 size-3.5 shrink-0 text-sidebar-muted-foreground/60" />;
115}117}
116118
119function SettingsSubmenuCollapse({
120 open,
121 children,
122}: {
123 readonly open: boolean;
124 readonly children: ReactNode;
125}) {
126 return (
127 <Collapsible open={open}>
128 <CollapsiblePanel className="duration-150 ease-out motion-reduce:transition-none">
129 {children}
130 </CollapsiblePanel>
131 </Collapsible>
132 );
133}
134
117export function SettingsSidebarNav({ pathname }: { pathname: string }) {135export function SettingsSidebarNav({ pathname }: { pathname: string }) {
118 const navigate = useNavigate();136 const navigate = useNavigate();
119 const currentHash = useLocation({ select: (location) => location.hash });137 const currentHash = useLocation({ select: (location) => location.hash });
...@@ -125,6 +143,9 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -125,6 +143,9 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
125 const results = useMemo(() => searchSettings(query, searchableItems), [query, searchableItems]);143 const results = useMemo(() => searchSettings(query, searchableItems), [query, searchableItems]);
126 const isSearching = query.trim().length > 0;144 const isSearching = query.trim().length > 0;
127 const hasResults = results.length > 0;145 const hasResults = results.length > 0;
146 const activeSettingsPath = SETTINGS_NAV_ITEMS.find(
147 (item) => pathname === item.to || pathname.startsWith(`${item.to}/`),
148 )?.to;
128149
129 useEffect(() => {150 useEffect(() => {
130 setActiveResultIndex((index) => Math.min(index, Math.max(results.length - 1, 0)));151 setActiveResultIndex((index) => Math.min(index, Math.max(results.length - 1, 0)));
...@@ -347,8 +368,8 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -347,8 +368,8 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
347 <SidebarMenu className="ps-px">368 <SidebarMenu className="ps-px">
348 {SETTINGS_NAV_ITEMS.map((item) => {369 {SETTINGS_NAV_ITEMS.map((item) => {
349 const Icon = item.icon;370 const Icon = item.icon;
350 const isActive = pathname === item.to || pathname.startsWith(`${item.to}/`);
351 const pageSections = SETTINGS_PAGE_SECTIONS[item.to];371 const pageSections = SETTINGS_PAGE_SECTIONS[item.to];
372 const isActive = activeSettingsPath === item.to;
352 return (373 return (
353 <SidebarMenuItem key={item.to}>374 <SidebarMenuItem key={item.to}>
354 <SidebarMenuButton375 <SidebarMenuButton
...@@ -358,21 +379,23 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -358,21 +379,23 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
358 <Icon />379 <Icon />
359 <span className="truncate">{item.label}</span>380 <span className="truncate">{item.label}</span>
360 </SidebarMenuButton>381 </SidebarMenuButton>
361 {isActive && pageSections ? (382 {pageSections ? (
362 <SidebarMenuSub className="border-l-0">383 <SettingsSubmenuCollapse open={isActive}>
363 {pageSections.map((section) => (384 <SidebarMenuSub className="border-l-0">
364 <SidebarMenuSubItem key={section.targetId}>385 {pageSections.map((section) => (
365 <SidebarMenuSubButton386 <SidebarMenuSubItem key={section.targetId}>
366 render={<button type="button" />}387 <SidebarMenuSubButton
367 size="sm"388 render={<button type="button" />}
368 className="w-full text-sidebar-muted-foreground/65"389 size="sm"
369 onClick={() => handlePageSectionClick(item.to, section.targetId)}390 className="w-full text-sidebar-muted-foreground/65"
370 >391 onClick={() => handlePageSectionClick(item.to, section.targetId)}
371 <span className="ms-0.5">{section.label}</span>392 >
372 </SidebarMenuSubButton>393 <span className="ms-0.5">{section.label}</span>
373 </SidebarMenuSubItem>394 </SidebarMenuSubButton>
374 ))}395 </SidebarMenuSubItem>
375 </SidebarMenuSub>396 ))}
397 </SidebarMenuSub>
398 </SettingsSubmenuCollapse>
376 ) : null}399 ) : null}
377 </SidebarMenuItem>400 </SidebarMenuItem>
378 );401 );