feat: redesign website layout with right-side TOC and cleaner UI#18
feat: redesign website layout with right-side TOC and cleaner UI#18
Conversation
Overhaul the website layout and design system: - Table of Contents: moved from hidden left drawer to always-visible right sidebar on desktop (xl+), with IntersectionObserver-based active section highlighting. Mobile uses a FAB + slide-in drawer. - Page Header: simplified from animated collapsing header (128px/72px) to a clean 64px sticky bar, removing framer-motion dependency. - Width Switcher: compact segmented control (S/M/L/XL) placed inline above content instead of floating pill bar between header and content. - Content Layout: two-column flex layout (main + TOC sidebar) inside a max-w-7xl container, replacing the old stacked layout. - Section Cards: softer styling with yellow accent pill on h2 headings, lighter backgrounds, and reduced border contrast. - Typography: refined text sizes, lighter default text colors (zinc-200), updated callout to rounded card style, cleaner code-span borders. - Homepage & Hub pages: polished card grid with hover effects, badge labels, and better visual hierarchy. - CSS: updated design tokens (header height 64px, TOC width 260px), added thin scrollbar utility, cleaner glass effects. https://claude.ai/code/session_01YQ82VmvfdGFZCTpNoLKvNq
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This pull request implements a comprehensive redesign of the website layout and design system, moving from an animated, collapsing header design to a cleaner, simpler fixed-height layout with an always-visible table of contents on desktop.
Changes:
- Replaced animated collapsing header (128px/72px) with a clean 64px sticky header, removing framer-motion dependency from components
- Relocated table of contents from a left-drawer with hover/pin mechanism to an always-visible right sidebar on desktop (xl+) with IntersectionObserver-based active section highlighting; mobile uses a FAB + slide-in drawer
- Redesigned width switcher from floating pill bar to inline segmented control, and updated typography components with refined sizing and lighter color palette
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| bun.lock | Updates Next.js from 16.0.1 to 16.0.10 and associated @next/swc platform binaries |
| src/app/globals.css | Updates design tokens (64px header, 260px TOC width), adds scrollbar utilities, refines color values and glass effects |
| src/app/(main)/home.tsx | Modernizes homepage card grid with improved hover states, badge labels, and cleaner visual hierarchy |
| src/app/frontend/junior/frontend-junior.tsx | Updates hub page cards with refined styling, improved in-progress state handling, and better visual feedback |
| src/components/layout/content-page.tsx | Restructures layout to use two-column flex container (main + TOC sidebar) inside max-w-7xl wrapper |
| src/components/layout/page-container.tsx | Removes header height tracking logic, wraps content in semantic main element with flex-1 for proper layout |
| src/components/page-header/page-header.tsx | Simplifies to static 64px header, removes framer-motion animations and complex scroll behavior |
| src/components/table-of-contents/table-of-contents.tsx | Complete rewrite: removes framer-motion, implements IntersectionObserver for active tracking, adds desktop sidebar and mobile FAB+drawer pattern |
| src/components/width-switcher/width-switcher.tsx | Converts from floating pill bar to inline segmented control with compact S/M/L/XL labels |
| src/components/section-card/section-card.tsx | Updates styling with yellow accent pill, softer borders, lighter backgrounds, and refined spacing |
| src/components/notes-area/notes-area.tsx | Refines styling with dashed border, lighter background, and improved text colors |
| src/components/typography/text.tsx | Adds explicit font size (15px) and line-height, lightens text color from white to zinc-200 |
| src/components/typography/subheader.tsx | Reduces font weight and size, updates color to zinc-200 for consistency |
| src/components/typography/header.tsx | Replaces underline decoration with border-b on child span, updates to semibold weight |
| src/components/typography/code-span.tsx | Adds border, refines padding and sizing, updates to use specific pixel values |
| src/components/typography/callout.tsx | Changes from p to div element, updates to rounded card style with yellow accent border |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| setActiveId(el.id); | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
The IntersectionObserver callback doesn't handle the case when all headings become invisible (e.g., when scrolling past all content). In this scenario, the visibleSet becomes empty but activeId retains the last visible heading ID. Consider clearing activeId when no headings are visible, or setting it to an empty string after the loop completes without finding any visible headings.
| } | |
| } | |
| // If no headings are visible, clear the activeId | |
| setActiveId(''); |
| {mobileOpen && ( | ||
| <> | ||
| <div | ||
| className='h-full w-full opacity-80' | ||
| style={{ | ||
| background: | ||
| 'linear-gradient(to right, color-mix(in srgb, rgba(255, 255, 255, 0.3) 100%, var(--glass-strong-bg) 20%), transparent)', | ||
| }} | ||
| aria-label='Close table of contents' | ||
| className='fixed inset-0 z-40 bg-black/50' | ||
| onClick={() => setMobileOpen(false)} | ||
| onKeyDown={(e) => e.key === 'Escape' && setMobileOpen(false)} | ||
| role='button' | ||
| tabIndex={-1} | ||
| /> | ||
| <div className='absolute inset-y-0 right-0 w-px md:hidden' /> | ||
| </motion.div> | ||
| <nav className='glass fixed top-0 right-0 z-50 h-full w-[280px] overflow-y-auto p-6 pt-16'> | ||
| <p className='mb-3 font-medium text-[11px] text-zinc-500 uppercase tracking-widest'> | ||
| On this page | ||
| </p> | ||
| {tocContent} | ||
| </nav> | ||
| </> |
There was a problem hiding this comment.
The mobile drawer for the table of contents lacks focus management. When the drawer opens, focus should be moved inside the drawer, and when it closes, focus should return to the toggle button. Additionally, focus should be trapped within the drawer while it's open to ensure keyboard users don't accidentally tab to elements behind the overlay. Consider using the useEffect hook to manage focus when mobileOpen changes, or use a focus trap library.
| id={id} | ||
| > | ||
| <h2 | ||
| className='mb-4 border-zinc-700 border-b pb-2 font-bold text-2xl text-white' | ||
| className='mb-5 flex items-center gap-3 font-semibold text-white text-xl' | ||
| id={id} |
There was a problem hiding this comment.
Both the section element and the h2 element have the same id attribute. This creates duplicate IDs on the page, which violates HTML standards and can cause issues with anchor navigation, accessibility tools, and DOM queries. Consider removing the id from the section element and keeping it only on the h2, or use different IDs for each element.
| return ( | ||
| <p | ||
| className={`mb-3 rounded-md border-yellow-500 border-l-4 bg-zinc-800/60 p-3 text-zinc-100 ${className}`} | ||
| <div |
There was a problem hiding this comment.
The Callout component has been changed from a p element to a div element, but the existing test at tests/components/callout.test.tsx:12 expects to query for a p element. This test will fail with the current changes. The test should be updated to query for a div instead, or use a more generic query method.
| aria-label='Close table of contents' | ||
| className='fixed inset-0 z-40 bg-black/50' | ||
| onClick={() => setMobileOpen(false)} | ||
| onKeyDown={(e) => e.key === 'Escape' && setMobileOpen(false)} | ||
| role='button' | ||
| tabIndex={-1} |
There was a problem hiding this comment.
The backdrop overlay uses role='button' with tabIndex={-1}, which is not keyboard accessible. While the overlay can be closed with the Escape key (via onKeyDown), users cannot tab to it to activate the onClick handler. Consider using a proper button element for the close functionality or removing the role and tabIndex attributes if the overlay is purely decorative and relies only on the Escape key for dismissal.
| aria-label='Close table of contents' | |
| className='fixed inset-0 z-40 bg-black/50' | |
| onClick={() => setMobileOpen(false)} | |
| onKeyDown={(e) => e.key === 'Escape' && setMobileOpen(false)} | |
| role='button' | |
| tabIndex={-1} | |
| className='fixed inset-0 z-40 bg-black/50' | |
| onClick={() => setMobileOpen(false)} |
Overhaul the website layout and design system:
https://claude.ai/code/session_01YQ82VmvfdGFZCTpNoLKvNq