Skip to content

Commit 26fd5f3

Browse files
authored
fix(gastown): fix settings page sub-nav sticky behavior and center layout (#1418)
Resolve three layout issues on the gastown town settings page: 1. Move mx-auto and max-w centering to the shared flex container so both content and sidebar share the same centered context, fixing the off-center content appearance. 2. Change sticky nav from top-6 to top-0 so it properly sticks at the scroll container edge (pt-6 on the inner nav handles visual offset). 3. Add whitespace-nowrap, overflow-hidden, text-ellipsis to nav buttons and truncate to label spans to prevent line-wrapping in the sidebar. Closes #1417
1 parent 81dfac6 commit 26fd5f3

1 file changed

Lines changed: 43 additions & 21 deletions

File tree

src/app/(app)/gastown/[townId]/settings/TownSettingsPageClient.tsx

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState, useEffect } from 'react';
3+
import { useState, useEffect, useRef } from 'react';
44
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
55
import { useGastownTRPC } from '@/lib/gastown/trpc';
66
import { useUser } from '@/hooks/useUser';
@@ -49,10 +49,12 @@ const SECTIONS = [
4949

5050
function useScrollSpy(sectionIds: readonly string[]) {
5151
const [activeId, setActiveId] = useState<string>(sectionIds[0]);
52+
const suppressRef = useRef(false);
5253

5354
useEffect(() => {
5455
const observer = new IntersectionObserver(
5556
entries => {
57+
if (suppressRef.current) return;
5658
// Find the topmost visible section
5759
const visible = entries
5860
.filter(e => e.isIntersecting)
@@ -61,7 +63,7 @@ function useScrollSpy(sectionIds: readonly string[]) {
6163
setActiveId(visible[0].target.id);
6264
}
6365
},
64-
{ rootMargin: '-80px 0px -60% 0px', threshold: 0 }
66+
{ rootMargin: '-56px 0px -60% 0px', threshold: 0 }
6567
);
6668

6769
for (const id of sectionIds) {
@@ -72,14 +74,26 @@ function useScrollSpy(sectionIds: readonly string[]) {
7274
return () => observer.disconnect();
7375
}, [sectionIds]);
7476

75-
return activeId;
76-
}
77+
function scrollTo(id: string) {
78+
const el = document.getElementById(id);
79+
const header = document.getElementById('settings-sticky-header');
80+
if (!el) return;
81+
82+
// Immediately highlight the target and suppress observer during scroll
83+
setActiveId(id);
84+
suppressRef.current = true;
7785

78-
function scrollToSection(id: string) {
79-
const el = document.getElementById(id);
80-
if (el) {
81-
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
86+
const headerHeight = header?.getBoundingClientRect().height ?? 0;
87+
const top = el.getBoundingClientRect().top + window.scrollY - headerHeight - 24;
88+
window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
89+
90+
// Re-enable observer after scroll settles
91+
setTimeout(() => {
92+
suppressRef.current = false;
93+
}, 1000);
8294
}
95+
96+
return { activeId, scrollTo };
8397
}
8498

8599
export function TownSettingsPageClient({ townId, readOnly = false }: Props) {
@@ -149,7 +163,9 @@ export function TownSettingsPageClient({ townId, readOnly = false }: Props) {
149163
setInitialized(true);
150164
}
151165

152-
const activeSection = useScrollSpy(SECTIONS.map(s => s.id));
166+
const { activeId: activeSection, scrollTo: scrollToSection } = useScrollSpy(
167+
SECTIONS.map(s => s.id)
168+
);
153169

154170
function handleSave() {
155171
const envVarObj: Record<string, string> = {};
@@ -228,9 +244,12 @@ export function TownSettingsPageClient({ townId, readOnly = false }: Props) {
228244
}
229245

230246
return (
231-
<div className="flex h-full flex-col">
247+
<div>
232248
{/* Top bar */}
233-
<div className="flex items-center justify-between border-b border-white/[0.06] px-6 py-3">
249+
<div
250+
id="settings-sticky-header"
251+
className="sticky top-0 z-10 flex items-center justify-between border-b border-white/[0.06] bg-[oklch(0.1_0_0)] px-6 py-3"
252+
>
234253
<div className="flex items-center gap-2.5">
235254
<Settings className="size-4 text-white/40" />
236255
<h1 className="text-lg font-semibold tracking-tight text-white/90">Settings</h1>
@@ -255,12 +274,12 @@ export function TownSettingsPageClient({ townId, readOnly = false }: Props) {
255274
)}
256275
</div>
257276

258-
{/* Two-column body — single scroll container so sticky works */}
259-
<div className="flex-1 overflow-y-auto scroll-smooth">
260-
<div className="flex">
277+
{/* Two-column body — viewport scrolls so sticky works */}
278+
<div className="scroll-smooth">
279+
<div className="mx-auto flex max-w-4xl px-6">
261280
{/* Main content */}
262281
<div className="min-w-0 flex-1">
263-
<div className="mx-auto max-w-2xl space-y-8 px-6 pt-6 pb-24">
282+
<div className="space-y-8 pt-6" style={{ paddingBottom: '75vh' }}>
264283
{/* ── Git Authentication ──────────────────────────────── */}
265284
<SettingsSection
266285
id="git-auth"
@@ -617,8 +636,8 @@ export function TownSettingsPageClient({ townId, readOnly = false }: Props) {
617636
</div>
618637

619638
{/* Right sidebar — sticky scrollspy nav */}
620-
<div className="hidden w-52 shrink-0 lg:block">
621-
<nav className="sticky top-6 px-4 pt-6">
639+
<div className="hidden w-52 shrink-0 lg:sticky lg:top-[53px] lg:self-start lg:block">
640+
<nav className="px-4 pt-6">
622641
<div className="mb-3 text-[10px] font-medium tracking-wide text-white/25 uppercase">
623642
On this page
624643
</div>
@@ -631,19 +650,23 @@ export function TownSettingsPageClient({ townId, readOnly = false }: Props) {
631650
<li key={section.id}>
632651
<button
633652
onClick={() => scrollToSection(section.id)}
634-
className={`flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-xs transition-colors ${
653+
className={`flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-xs whitespace-nowrap overflow-hidden text-ellipsis transition-colors ${
635654
isActive
636655
? 'bg-white/[0.06] text-white/80'
637656
: 'text-white/35 hover:bg-white/[0.03] hover:text-white/55'
638657
}`}
639658
>
640659
<SectionIcon className="size-3 shrink-0" />
641-
<span>{section.label}</span>
660+
<span className="truncate">{section.label}</span>
642661
{isActive && (
643662
<motion.div
644663
layoutId="settings-nav-indicator"
645664
className="ml-auto size-1 rounded-full bg-[color:oklch(95%_0.15_108)]"
646-
transition={{ type: 'spring', stiffness: 350, damping: 30 }}
665+
transition={{
666+
type: 'spring',
667+
stiffness: 350,
668+
damping: 30,
669+
}}
647670
/>
648671
)}
649672
</button>
@@ -700,7 +723,6 @@ function SettingsSection({
700723
initial={{ opacity: 0, y: 16 }}
701724
animate={{ opacity: 1, y: 0 }}
702725
transition={{ delay: index * 0.06, duration: 0.35 }}
703-
className="scroll-mt-6"
704726
>
705727
<div className="mb-4 flex items-start justify-between">
706728
<div className="flex items-start gap-3">

0 commit comments

Comments
 (0)