History

fix(web): stop the settings sidebar shifting when switching pages (#10705)

Theo Browne committed 2 months ago

Commit d6dbe8d1 parent 134b7194 files · +3 −570Browse files

Unified · Split

4 files
  1. apps/web/src/components/settings/SettingsSidebarNav.tsx +2 −135
  2. apps/web/src/components/settings/settingsSectionVisibility.test.ts +0 −245
  3. apps/web/src/components/settings/settingsSectionVisibility.ts +0 −186
  4. apps/web/src/routes/settings.tsx +1 −4
apps/web/src/components/settings/SettingsSidebarNav.tsx+2−135
@@ -8,7 +8,6 @@ import { · 7 unmodified lines
88 useState,
99 type ComponentType,
1010 type KeyboardEvent,
11− type ReactNode,
1211 } from "react";
1312 import {
1413 ArchiveIcon,
@@ -24,13 +23,11 @@ import { · 9 unmodified lines
2423 Settings2Icon,
2524 XIcon,
2625 } from "lucide-react";
27−import { useLocation, useNavigate, useRouterState } from "@tanstack/react-router";
26+import { useLocation, useNavigate } from "@tanstack/react-router";
2827
2928 import { Button } from "../ui/button";
30−import { Collapsible, CollapsiblePanel } from "../ui/collapsible";
3129 import { Input } from "../ui/input";
3230 import { Kbd } from "../ui/kbd";
33−import { cn } from "../../lib/utils";
3431 import {
3532 SidebarContent,
3633 SidebarFooter,
@@ -38,18 +35,10 @@ import { · 1 unmodified line
3835 SidebarMenu,
3936 SidebarMenuButton,
4037 SidebarMenuItem,
41− SidebarMenuSub,
42− SidebarMenuSubButton,
43− SidebarMenuSubItem,
4438 useSidebar,
4539 } from "../ui/sidebar";
4640 import { SidebarUtilityMenu } from "../sidebar/SidebarChrome";
4741 import { scrollToSettingsTarget } from "./settingsLayout";
48−import {
49− getVisibleSettingsSectionIds,
50− observeSettingsSectionVisibility,
51− type SettingsSectionVisibilityState,
52−} from "./settingsSectionVisibility";
5342 import {
5443 searchSettings,
5544 SETTINGS_SECTION_LABELS,
@@ -106,102 +95,22 @@ const SETTINGS_NAV_ITEMS: ReadonlyArray<{ · 50 unmodified lines
10695 icon: SETTINGS_SECTION_ICONS[to],
10796 }));
10897
109−const SETTINGS_PAGE_SECTIONS: Partial<
110− Readonly<Record<SettingsPath, ReadonlyArray<{ label: string; targetId: string }>>>
111−> = {
112− "/settings/general": [
113− { label: "Organization", targetId: "organization" },
114− { label: "Behavior", targetId: "behavior" },
115− { label: "Projects & threads", targetId: "projects-and-threads" },
116− { label: "Confirmations", targetId: "confirmations" },
117− { label: "Text generation", targetId: "text-generation" },
118− { label: "About", targetId: "about" },
119− { label: "Legacy features", targetId: "legacy-features" },
120− ],
121− "/settings/appearance": [
122− { label: "Colors & themes", targetId: "appearance" },
123− { label: "Interface", targetId: "appearance-interface" },
124− { label: "Motion", targetId: "motion" },
125− { label: "Typography", targetId: "typography" },
126− ],
127− "/settings/source-control": [
128− { label: "Version control", targetId: "source-control" },
129− { label: "Text generation", targetId: "source-control-text-generation" },
130− ],
131− "/settings/connections": [
132− { label: "This environment", targetId: "connections-environment" },
133− { label: "Remote environments", targetId: "remote-environments" },
134− ],
135−};
136−
13798 function SettingsSectionIcon({ to }: { to: SettingsPath }) {
13899 const Icon = SETTINGS_SECTION_ICONS[to];
139100 return <Icon className="mt-0.5 size-3.5 shrink-0 text-sidebar-muted-foreground/60" />;
140101 }
141102
142−function SettingsSubmenuCollapse({
143− open,
144− children,
145−}: {
146− readonly open: boolean;
147− readonly children: ReactNode;
148−}) {
149− return (
150− <Collapsible open={open}>
151− <CollapsiblePanel className="duration-150 ease-out motion-reduce:transition-none">
152− {children}
153− </CollapsiblePanel>
154− </Collapsible>
155− );
156−}
157−
158103 export function SettingsSidebarNav({ pathname }: { pathname: string }) {
159104 const navigate = useNavigate();
160105 const currentHash = useLocation({ select: (location) => location.hash });
161− const resolvedPathname = useRouterState({
162− select: (state) => state.resolvedLocation?.pathname,
163− });
164106 const { isMobile, setOpenMobile, open, setOpen } = useSidebar();
165107 const searchInputRef = useRef<HTMLInputElement>(null);
166108 const [query, setQuery] = useState("");
167109 const [activeResultIndex, setActiveResultIndex] = useState(0);
168− const [sectionVisibility, setSectionVisibility] = useState<SettingsSectionVisibilityState | null>(
169− null,
170− );
171110 const searchableItems = useAvailableSettingsSearchItems();
172111 const results = useMemo(() => searchSettings(query, searchableItems), [query, searchableItems]);
173112 const isSearching = query.trim().length > 0;
174113 const hasResults = results.length > 0;
175− const activeSettingsPath = SETTINGS_NAV_ITEMS.find(
176− (item) => pathname === item.to || pathname.startsWith(`${item.to}/`),
177− )?.to;
178− const observedVisibilityScope = useMemo(() => {
179− const path = SETTINGS_NAV_ITEMS.find(
180− (item) =>
181− resolvedPathname === item.to || resolvedPathname?.startsWith(`${item.to}/`) === true,
182− )?.to;
183− const pageSections = path ? SETTINGS_PAGE_SECTIONS[path] : undefined;
184− return path && pageSections ? { path, pageSections } : null;
185− }, [resolvedPathname]);
186− const visiblePageSectionIds = getVisibleSettingsSectionIds({
187− activePath: activeSettingsPath,
188− scope: observedVisibilityScope,
189− visibility: sectionVisibility,
190− });
191−
192− useEffect(() => {
193− if (!observedVisibilityScope) return;
194− const container = document.querySelector<HTMLElement>("[data-settings-page-layout]");
195− if (!container) return;
196−
197− return observeSettingsSectionVisibility({
198− container,
199− targetIds: observedVisibilityScope.pageSections.map((section) => section.targetId),
200− onChange(targetIds) {
201− setSectionVisibility({ scope: observedVisibilityScope, targetIds: new Set(targetIds) });
202− },
203− });
204− }, [observedVisibilityScope]);
205114
206115 useEffect(() => {
207116 setActiveResultIndex((index) => Math.min(index, Math.max(results.length - 1, 0)));
@@ -261,24 +170,6 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { · 53 unmodified lines
261170 },
262171 [isMobile, navigate, setOpenMobile],
263172 );
264− const handlePageSectionClick = useCallback(
265− (to: SettingsPath, targetId: string) => {
266− if (isMobile) {
267− setOpenMobile(false);
268− }
269− if (pathname === to && scrollToSettingsTarget(targetId, { highlight: false })) {
270− return;
271− }
272− void navigate({
273− to,
274− hash: targetId,
275− replace: true,
276− hashScrollIntoView: false,
277− state: { settingsTargetHighlight: false },
278− });
279− },
280− [isMobile, navigate, pathname, setOpenMobile],
281− );
282173 const clearSearch = useCallback(() => {
283174 setQuery("");
284175 setActiveResultIndex(0);
@@ -430,8 +321,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { · 145 unmodified lines
430321 <SidebarMenu className="ps-px">
431322 {SETTINGS_NAV_ITEMS.map((item) => {
432323 const Icon = item.icon;
433− const pageSections = SETTINGS_PAGE_SECTIONS[item.to];
434− const isActive = activeSettingsPath === item.to;
324+ const isActive = pathname === item.to || pathname.startsWith(`${item.to}/`);
435325 return (
436326 <SidebarMenuItem key={item.to}>
437327 <SidebarMenuButton
@@ -441,29 +331,6 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { · 3 unmodified lines
441331 <Icon />
442332 <span className="truncate">{item.label}</span>
443333 </SidebarMenuButton>
444− {pageSections ? (
445− <SettingsSubmenuCollapse open={isActive}>
446− <SidebarMenuSub className="border-l-0">
447− {pageSections.map((section) => (
448− <SidebarMenuSubItem key={section.targetId}>
449− <SidebarMenuSubButton
450− render={<button type="button" />}
451− size="sm"
452− data-visible={visiblePageSectionIds.has(section.targetId)}
453− className={cn(
454− "w-full text-sidebar-muted-foreground/65",
455− visiblePageSectionIds.has(section.targetId) &&
456− "font-medium text-sidebar-foreground",
457− )}
458− onClick={() => handlePageSectionClick(item.to, section.targetId)}
459− >
460− <span className="ms-0.5">{section.label}</span>
461− </SidebarMenuSubButton>
462− </SidebarMenuSubItem>
463− ))}
464− </SidebarMenuSub>
465− </SettingsSubmenuCollapse>
466− ) : null}
467334 </SidebarMenuItem>
468335 );
469336 })}
apps/web/src/components/settings/settingsSectionVisibility.test.ts+0−245
@@ -1,245 +0,0 @@
1−import { describe, expect, it, vi } from "vite-plus/test";
2−
3−import {
4− getVisibleSettingsSectionIds,
5− observeSettingsSectionVisibility,
6− type SettingsSectionVisibilityEnvironment,
7−} from "./settingsSectionVisibility";
8−
9−type VisibilityEntry = Pick<
10− IntersectionObserverEntry,
11− "intersectionRatio" | "isIntersecting" | "target"
12−>;
13−
14−function createHarness(
15− targetIds: ReadonlyArray<string>,
16− initialRoot: Element | null = { name: "initial-root" } as unknown as Element,
17−) {
18− const targets = new Map(
19− targetIds.map((targetId) => [targetId, { targetId } as unknown as Element]),
20− );
21− const observed = new Set<Element>();
22− const unobserve = vi.fn((target: Element) => observed.delete(target));
23− const disconnectIntersections = vi.fn();
24− const disconnectMutations = vi.fn();
25− const intersectionCallbacks: Array<(entries: ReadonlyArray<VisibilityEntry>) => void> = [];
26− const intersectionRoots: Element[] = [];
27− let root = initialRoot;
28− let onMutation = () => {};
29−
30− const environment: SettingsSectionVisibilityEnvironment = {
31− findRoot: () => root,
32− findTarget: (_root, targetId) => targets.get(targetId) ?? null,
33− createIntersectionObserver(callback, observedRoot) {
34− intersectionCallbacks.push(callback);
35− intersectionRoots.push(observedRoot);
36− return {
37− observe: (target) => observed.add(target),
38− unobserve,
39− disconnect: disconnectIntersections,
40− };
41− },
42− createMutationObserver(callback) {
43− onMutation = callback;
44− return { disconnect: disconnectMutations };
45− },
46− };
47−
48− return {
49− environment,
50− targets,
51− observed,
52− unobserve,
53− disconnectIntersections,
54− disconnectMutations,
55− intersectionCallbacks,
56− intersectionRoots,
57− intersect(entries: ReadonlyArray<VisibilityEntry>) {
58− intersectionCallbacks.at(-1)?.(entries);
59− },
60− mutate() {
61− onMutation();
62− },
63− replaceRoot(nextRoot: Element | null) {
64− root = nextRoot;
65− onMutation();
66− },
67− };
68−}
69−
70−function visibleEntry(
71− target: Element,
72− { isIntersecting = true, intersectionRatio = 1 } = {},
73−): VisibilityEntry {
74− return { target, isIntersecting, intersectionRatio };
75−}
76−
77−describe("settings section visibility", () => {
78− it("does not reuse visibility when returning to the same sectioned route", () => {
79− const firstGeneralVisit = { path: "/settings/general" };
80− const firstVisibility = {
81− scope: firstGeneralVisit,
82− targetIds: new Set(["text-generation"]),
83− };
84−
85− expect(
86− getVisibleSettingsSectionIds({
87− activePath: "/settings/general",
88− scope: firstGeneralVisit,
89− visibility: firstVisibility,
90− }),
91− ).toEqual(new Set(["text-generation"]));
92− expect(
93− getVisibleSettingsSectionIds({
94− activePath: "/settings/providers",
95− scope: null,
96− visibility: firstVisibility,
97− }),
98− ).toEqual(new Set());
99−
100− const secondGeneralVisit = { path: "/settings/general" };
101− expect(
102− getVisibleSettingsSectionIds({
103− activePath: "/settings/general",
104− scope: secondGeneralVisit,
105− visibility: firstVisibility,
106− }),
107− ).toEqual(new Set());
108− });
109−
110− it("accumulates visible sections and emits them in sidebar order", () => {
111− const harness = createHarness(["one", "two", "three"]);
112− const emissions: ReadonlyArray<string>[] = [];
113− observeSettingsSectionVisibility({
114− container: {} as Element,
115− targetIds: ["one", "two", "three"],
116− onChange: (visible) => emissions.push(visible),
117− environment: harness.environment,
118− });
119−
120− harness.intersect([visibleEntry(harness.targets.get("two")!)]);
121− harness.intersect([visibleEntry(harness.targets.get("one")!)]);
122−
123− expect(emissions).toEqual([[], ["two"], ["one", "two"]]);
124− });
125−
126− it("treats zero-ratio and non-intersecting entries as hidden", () => {
127− const harness = createHarness(["one", "two"]);
128− const emissions: ReadonlyArray<string>[] = [];
129− observeSettingsSectionVisibility({
130− container: {} as Element,
131− targetIds: ["one", "two"],
132− onChange: (visible) => emissions.push(visible),
133− environment: harness.environment,
134− });
135−
136− const one = harness.targets.get("one")!;
137− const two = harness.targets.get("two")!;
138− harness.intersect([visibleEntry(one), visibleEntry(two)]);
139− harness.intersect([visibleEntry(one, { intersectionRatio: 0 })]);
140− harness.intersect([visibleEntry(two, { isIntersecting: false })]);
141−
142− expect(emissions).toEqual([[], ["one", "two"], ["two"], []]);
143− });
144−
145− it("resyncs replaced and removed targets without retaining stale visibility", () => {
146− const harness = createHarness(["dynamic"]);
147− const emissions: ReadonlyArray<string>[] = [];
148− observeSettingsSectionVisibility({
149− container: {} as Element,
150− targetIds: ["dynamic"],
151− onChange: (visible) => emissions.push(visible),
152− environment: harness.environment,
153− });
154−
155− const firstTarget = harness.targets.get("dynamic")!;
156− harness.intersect([visibleEntry(firstTarget)]);
157− const replacementTarget = { targetId: "dynamic-replacement" } as unknown as Element;
158− harness.targets.set("dynamic", replacementTarget);
159− harness.mutate();
160−
161− expect(harness.unobserve).toHaveBeenCalledWith(firstTarget);
162− expect(harness.observed.has(replacementTarget)).toBe(true);
163− expect(emissions.at(-1)).toEqual([]);
164−
165− harness.intersect([visibleEntry(firstTarget)]);
166− expect(emissions.at(-1)).toEqual([]);
167− harness.intersect([visibleEntry(replacementTarget)]);
168− expect(emissions.at(-1)).toEqual(["dynamic"]);
169−
170− harness.targets.delete("dynamic");
171− harness.mutate();
172− expect(harness.unobserve).toHaveBeenCalledWith(replacementTarget);
173− expect(emissions.at(-1)).toEqual([]);
174− });
175−
176− it("rebinds when navigation replaces the settings scroll root", () => {
177− const harness = createHarness(["section"]);
178− const emissions: ReadonlyArray<string>[] = [];
179− observeSettingsSectionVisibility({
180− container: {} as Element,
181− targetIds: ["section"],
182− onChange: (visible) => emissions.push(visible),
183− environment: harness.environment,
184− });
185−
186− const firstTarget = harness.targets.get("section")!;
187− harness.intersect([visibleEntry(firstTarget)]);
188− const firstObserverCallback = harness.intersectionCallbacks[0]!;
189− const nextRoot = { name: "next-root" } as unknown as Element;
190− const nextTarget = { targetId: "next-section" } as unknown as Element;
191− harness.targets.set("section", nextTarget);
192− harness.replaceRoot(nextRoot);
193−
194− expect(harness.disconnectIntersections).toHaveBeenCalledOnce();
195− expect(harness.intersectionRoots.at(-1)).toBe(nextRoot);
196− expect(harness.observed.has(nextTarget)).toBe(true);
197− expect(emissions.at(-1)).toEqual([]);
198−
199− firstObserverCallback([visibleEntry(firstTarget)]);
200− expect(emissions.at(-1)).toEqual([]);
201− harness.intersect([visibleEntry(nextTarget)]);
202− expect(emissions.at(-1)).toEqual(["section"]);
203− });
204−
205− it("starts observing when the settings scroll root mounts later", () => {
206− const harness = createHarness(["section"], null);
207− const emissions: ReadonlyArray<string>[] = [];
208− observeSettingsSectionVisibility({
209− container: {} as Element,
210− targetIds: ["section"],
211− onChange: (visible) => emissions.push(visible),
212− environment: harness.environment,
213− });
214−
215− expect(emissions).toEqual([[]]);
216− expect(harness.intersectionRoots).toEqual([]);
217−
218− const root = { name: "mounted-root" } as unknown as Element;
219− harness.replaceRoot(root);
220− harness.intersect([visibleEntry(harness.targets.get("section")!)]);
221−
222− expect(harness.intersectionRoots).toEqual([root]);
223− expect(emissions.at(-1)).toEqual(["section"]);
224− });
225−
226− it("disconnects both observers and ignores callbacks after cleanup", () => {
227− const harness = createHarness(["one"]);
228− const onChange = vi.fn();
229− const cleanup = observeSettingsSectionVisibility({
230− container: {} as Element,
231− targetIds: ["one"],
232− onChange,
233− environment: harness.environment,
234− });
235−
236− cleanup();
237− harness.intersect([visibleEntry(harness.targets.get("one")!)]);
238− harness.mutate();
239−
240− expect(harness.disconnectIntersections).toHaveBeenCalledOnce();
241− expect(harness.disconnectMutations).toHaveBeenCalledOnce();
242− expect(onChange).toHaveBeenCalledTimes(1);
243− expect(onChange).toHaveBeenLastCalledWith([]);
244− });
245−});
apps/web/src/components/settings/settingsSectionVisibility.ts+0−186
@@ -1,186 +0,0 @@
1−type VisibilityEntry = Pick<
2− IntersectionObserverEntry,
3− "intersectionRatio" | "isIntersecting" | "target"
4−>;
5−
6−export type SettingsSectionVisibilityScope = {
7− readonly path: string;
8−};
9−
10−export type SettingsSectionVisibilityState = {
11− readonly scope: SettingsSectionVisibilityScope;
12− readonly targetIds: ReadonlySet<string>;
13−};
14−
15−const EMPTY_VISIBLE_SETTINGS_SECTION_IDS: ReadonlySet<string> = new Set();
16−
17−export function getVisibleSettingsSectionIds({
18− activePath,
19− scope,
20− visibility,
21−}: {
22− readonly activePath: string | undefined;
23− readonly scope: SettingsSectionVisibilityScope | null;
24− readonly visibility: SettingsSectionVisibilityState | null;
25−}): ReadonlySet<string> {
26− if (!scope || activePath !== scope.path || visibility?.scope !== scope) {
27− return EMPTY_VISIBLE_SETTINGS_SECTION_IDS;
28− }
29− return visibility.targetIds;
30−}
31−
32−type ElementObserver = {
33− observe(target: Element): void;
34− unobserve(target: Element): void;
35− disconnect(): void;
36−};
37−
38−type MutationSubscription = {
39− disconnect(): void;
40−};
41−
42−export type SettingsSectionVisibilityEnvironment = {
43− findRoot(container: Element): Element | null;
44− findTarget(root: Element, targetId: string): Element | null;
45− createIntersectionObserver(
46− onEntries: (entries: ReadonlyArray<VisibilityEntry>) => void,
47− root: Element,
48− ): ElementObserver;
49− createMutationObserver(onMutation: () => void, container: Element): MutationSubscription;
50−};
51−
52−function createBrowserEnvironment(): SettingsSectionVisibilityEnvironment {
53− return {
54− findRoot(container) {
55− return container.querySelector("[data-settings-page-scroll]");
56− },
57− findTarget(root, targetId) {
58− const target = root.ownerDocument.getElementById(targetId);
59− return target && root.contains(target) ? target : null;
60− },
61− createIntersectionObserver(onEntries, scrollRoot) {
62− const observer = new IntersectionObserver(onEntries, {
63− root: scrollRoot,
64− threshold: 0,
65− });
66− return observer;
67− },
68− createMutationObserver(onMutation, container) {
69− const observer = new MutationObserver(onMutation);
70− observer.observe(container, { childList: true, subtree: true });
71− return observer;
72− },
73− };
74−}
75−
76−export function observeSettingsSectionVisibility({
77− container,
78− targetIds,
79− onChange,
80− environment = createBrowserEnvironment(),
81−}: {
82− readonly container: Element;
83− readonly targetIds: ReadonlyArray<string>;
84− readonly onChange: (visibleTargetIds: ReadonlyArray<string>) => void;
85− readonly environment?: SettingsSectionVisibilityEnvironment;
86−}): () => void {
87− const orderedTargetIds = [...new Set(targetIds)];
88− const targetsById = new Map<string, Element>();
89− const targetIdsByElement = new Map<Element, string>();
90− const visibleTargetIds = new Set<string>();
91− let lastEmission: string | null = null;
92− let stopped = false;
93− let root: Element | null = null;
94− let intersectionObserver: ElementObserver | null = null;
95− let observerGeneration = 0;
96−
97− const emit = () => {
98− const visibleInOrder = orderedTargetIds.filter((targetId) => visibleTargetIds.has(targetId));
99− const emissionKey = visibleInOrder.join("\0");
100− if (emissionKey === lastEmission) return;
101− lastEmission = emissionKey;
102− onChange(visibleInOrder);
103− };
104−
105− const handleEntries = (entries: ReadonlyArray<VisibilityEntry>, generation: number) => {
106− if (stopped || generation !== observerGeneration) return;
107− let changed = false;
108− for (const entry of entries) {
109− const targetId = targetIdsByElement.get(entry.target);
110− if (!targetId || targetsById.get(targetId) !== entry.target) continue;
111− const visible = entry.isIntersecting && entry.intersectionRatio > 0;
112− if (visible === visibleTargetIds.has(targetId)) continue;
113− changed = true;
114− if (visible) {
115− visibleTargetIds.add(targetId);
116− } else {
117− visibleTargetIds.delete(targetId);
118− }
119− }
120− if (changed) emit();
121− };
122−
123− const syncTargets = () => {
124− if (stopped) return;
125− let changed = false;
126− const nextRoot = environment.findRoot(container);
127−
128− if (nextRoot !== root) {
129− observerGeneration += 1;
130− intersectionObserver?.disconnect();
131− intersectionObserver = null;
132− root = nextRoot;
133− targetsById.clear();
134− targetIdsByElement.clear();
135− changed = visibleTargetIds.size > 0;
136− visibleTargetIds.clear();
137−
138− if (root) {
139− const generation = observerGeneration;
140− intersectionObserver = environment.createIntersectionObserver(
141− (entries) => handleEntries(entries, generation),
142− root,
143− );
144− }
145− }
146−
147− if (!root || !intersectionObserver) {
148− if (changed) emit();
149− return;
150− }
151−
152− for (const targetId of orderedTargetIds) {
153− const previousTarget = targetsById.get(targetId) ?? null;
154− const nextTarget = environment.findTarget(root, targetId);
155− if (previousTarget === nextTarget) continue;
156−
157− if (previousTarget) {
158− intersectionObserver.unobserve(previousTarget);
159− targetsById.delete(targetId);
160− targetIdsByElement.delete(previousTarget);
161− changed = visibleTargetIds.delete(targetId) || changed;
162− }
163− if (nextTarget) {
164− targetsById.set(targetId, nextTarget);
165− targetIdsByElement.set(nextTarget, targetId);
166− intersectionObserver.observe(nextTarget);
167− }
168− }
169−
170− if (changed) emit();
171− };
172−
173− const mutationObserver = environment.createMutationObserver(syncTargets, container);
174− syncTargets();
175− emit();
176−
177− return () => {
178− stopped = true;
179− observerGeneration += 1;
180− intersectionObserver?.disconnect();
181− mutationObserver.disconnect();
182− targetsById.clear();
183− targetIdsByElement.clear();
184− visibleTargetIds.clear();
185− };
186−}
apps/web/src/routes/settings.tsx+1−4
@@ -69,10 +69,7 @@ function SettingsContentLayout() { · 68 unmodified lines
6969 }, [navigateBackWithinApp]);
7070
7171 return (
72− <SidebarInset
73− className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate"
74− data-settings-page-layout
75− >
72+ <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate">
7673 <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground">
7774 <WorkspacePageHeader electron={isElectron}>
7875 <div className="flex w-full items-center gap-3">