Skip to content

Commit e96dc96

Browse files
author
Marvin Zhang
committed
feat: add auto-hiding scrollbar styles and improve scroll handling in TableOfContents component
1 parent 0dee8eb commit e96dc96

File tree

3 files changed

+70
-16
lines changed

3 files changed

+70
-16
lines changed

packages/ui/src/app/globals.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,4 +538,31 @@ html.changing-theme * {
538538
writing-mode: vertical-rl;
539539
transform: rotate(180deg);
540540
}
541+
542+
/* Auto-hiding scrollbar - visible only on hover */
543+
.scrollbar-auto-hide {
544+
scrollbar-width: thin;
545+
scrollbar-color: transparent transparent;
546+
}
547+
548+
.scrollbar-auto-hide:hover {
549+
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
550+
}
551+
552+
.scrollbar-auto-hide::-webkit-scrollbar {
553+
width: 4px;
554+
}
555+
556+
.scrollbar-auto-hide::-webkit-scrollbar-track {
557+
background: transparent;
558+
}
559+
560+
.scrollbar-auto-hide::-webkit-scrollbar-thumb {
561+
background-color: transparent;
562+
border-radius: 20px;
563+
}
564+
565+
.scrollbar-auto-hide:hover::-webkit-scrollbar-thumb {
566+
background-color: hsl(var(--muted-foreground) / 0.3);
567+
}
541568
}

packages/ui/src/components/spec-detail-client.tsx

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,10 @@ export function SpecDetailClient({ initialSpec, initialSubSpec }: SpecDetailClie
104104

105105
// Fetch complete dependency graph when dialog opens
106106
const { data: dependencyGraphData } = useSWR<{
107-
current: any;
108-
dependsOn: any[];
109-
requiredBy: any[];
110-
related: any[];
107+
current: { specName: string; specNumber?: number };
108+
dependsOn: { specName: string; specNumber?: number }[];
109+
requiredBy: { specName: string; specNumber?: number }[];
110+
related: { specName: string; specNumber?: number }[];
111111
}>(
112112
dependenciesDialogOpen ? `/api/specs/${initialSpec.specNumber || initialSpec.id}/dependency-graph` : null,
113113
fetcher,
@@ -118,7 +118,7 @@ export function SpecDetailClient({ initialSpec, initialSubSpec }: SpecDetailClie
118118
);
119119

120120
const spec = specData?.spec || initialSpec;
121-
const tags = spec.tags || [];
121+
const tags = React.useMemo(() => spec.tags || [], [spec.tags]);
122122
const updatedRelative = spec.updatedAt ? formatRelativeTime(spec.updatedAt) : 'N/A';
123123
const relationships = spec.relationships;
124124

@@ -175,10 +175,45 @@ export function SpecDetailClient({ initialSpec, initialSubSpec }: SpecDetailClie
175175
router.push(newUrl, { scroll: false });
176176
};
177177

178+
const headerRef = React.useRef<HTMLElement>(null);
179+
180+
// Handle scroll padding for sticky header
181+
React.useEffect(() => {
182+
const updateScrollPadding = () => {
183+
const navbarHeight = 56; // 3.5rem / top-14
184+
let offset = navbarHeight;
185+
186+
// On large screens, the spec header is also sticky
187+
if (window.innerWidth >= 1024 && headerRef.current) {
188+
offset += headerRef.current.offsetHeight - navbarHeight;
189+
}
190+
191+
document.documentElement.style.scrollPaddingTop = `${offset}px`;
192+
};
193+
194+
// Initial update
195+
updateScrollPadding();
196+
197+
// Update on resize
198+
window.addEventListener('resize', updateScrollPadding);
199+
200+
// Update when content changes (might affect header height if tags wrap)
201+
const observer = new ResizeObserver(updateScrollPadding);
202+
if (headerRef.current) {
203+
observer.observe(headerRef.current);
204+
}
205+
206+
return () => {
207+
window.removeEventListener('resize', updateScrollPadding);
208+
observer.disconnect();
209+
document.documentElement.style.scrollPaddingTop = '';
210+
};
211+
}, [spec, tags]); // Re-run if spec metadata changes
212+
178213
return (
179214
<>
180215
{/* Compact Header - sticky on desktop, static on mobile */}
181-
<header className="lg:sticky lg:top-14 lg:z-20 border-b bg-card">
216+
<header ref={headerRef} className="lg:sticky lg:top-14 lg:z-20 border-b bg-card">
182217
<div className="px-3 sm:px-6 py-3 sm:py-4">
183218
{/* Line 1: Spec number + H1 Title */}
184219
<h1 className="text-xl sm:text-2xl font-bold tracking-tight mb-2 sm:mb-3">
@@ -367,7 +402,7 @@ export function SpecDetailClient({ initialSpec, initialSubSpec }: SpecDetailClie
367402
</main>
368403

369404
{/* Right Sidebar for TOC (Desktop only) */}
370-
<aside className="hidden xl:block w-72 shrink-0 px-6 py-8 sticky top-40 h-[calc(100vh-10rem)] overflow-y-auto">
405+
<aside className="hidden xl:block w-72 shrink-0 px-6 py-8 sticky top-40 h-[calc(100vh-10rem)] overflow-y-auto scrollbar-auto-hide">
371406
<TableOfContentsSidebar content={displayContent} />
372407
</aside>
373408
</div>

packages/ui/src/components/table-of-contents.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,7 @@ function TOCList({ headings, onHeadingClick }: TOCListProps) {
8888
function scrollToHeading(id: string) {
8989
const element = document.getElementById(id);
9090
if (element) {
91-
// Scroll with offset for sticky header (top navbar + spec header)
92-
const headerOffset = 180; // Adjust based on your header height
93-
const elementPosition = element.getBoundingClientRect().top;
94-
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
95-
96-
window.scrollTo({
97-
top: offsetPosition,
98-
behavior: 'smooth'
99-
});
91+
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
10092

10193
if (window.history.replaceState) {
10294
window.history.replaceState(null, '', `#${id}`);

0 commit comments

Comments
 (0)