From 128dd6edfdf68c2a969e13512c9af37f399b543d Mon Sep 17 00:00:00 2001 From: Yevhenii Zhaivoronok Date: Thu, 5 Feb 2026 23:53:46 +0100 Subject: [PATCH 1/2] feat: Complete visual redesign with aurora/gradient theme Replace the dark-first yellow-accent design with a new deep-space aurora theme featuring indigo/violet/pink/cyan gradients, animated mesh background, and modern glass morphism effects. Redesign all pages including homepage hero section with animated scroll reveals, topic selection with progress tracking, and content pages with updated typography and code block styling. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 5 +- src/app/(main)/home.tsx | 309 ++++++++++++++---- src/app/frontend/junior/frontend-junior.tsx | 215 +++++++++--- src/app/globals.css | 228 ++++++++++--- .../ambient-background/ambient-background.tsx | 115 ++++++- src/components/code-block/code-block.tsx | 15 +- src/components/layout/content-page.tsx | 2 +- src/components/notes-area/notes-area.tsx | 4 +- src/components/page-header/page-header.tsx | 19 +- src/components/section-card/section-card.tsx | 8 +- .../table-of-contents/table-of-contents.tsx | 34 +- src/components/typography/callout.tsx | 2 +- src/components/typography/code-span.tsx | 2 +- src/components/typography/header.tsx | 2 +- src/components/typography/subheader.tsx | 2 +- src/components/typography/text.tsx | 3 +- .../width-switcher/width-switcher.tsx | 7 +- 17 files changed, 729 insertions(+), 243 deletions(-) diff --git a/.gitignore b/.gitignore index 5d1eac7..802c1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,7 @@ yarn-error.log* next-env.d.ts # env files -!.env.local.example \ No newline at end of file +!.env.local.example + +# claude +.claude \ No newline at end of file diff --git a/src/app/(main)/home.tsx b/src/app/(main)/home.tsx index 831aacb..6fe13fa 100644 --- a/src/app/(main)/home.tsx +++ b/src/app/(main)/home.tsx @@ -1,4 +1,14 @@ 'use client'; + +import { motion } from 'framer-motion'; +import { + ArrowRight, + BookOpen, + Code2, + Layers, + Sparkles, + Zap, +} from 'lucide-react'; import { useRouter } from 'next/navigation'; import type { Section } from '@/types'; @@ -13,76 +23,255 @@ const sections: Section[] = [ }, ]; +const features = [ + { + icon: Code2, + title: 'Code Examples', + description: 'Syntax-highlighted snippets with line-by-line annotations', + }, + { + icon: BookOpen, + title: 'Interactive Notes', + description: 'Study materials organized by topic with deep-dive sections', + }, + { + icon: Layers, + title: 'Structured Learning', + description: 'Progressive curriculum from fundamentals to advanced topics', + }, + { + icon: Zap, + title: 'Interview Ready', + description: 'Curated content focused on what interviewers actually ask', + }, +]; + +const stagger = { + hidden: {}, + show: { + transition: { + staggerChildren: 0.1, + }, + }, +}; + +const fadeUp = { + hidden: { opacity: 0, y: 30 }, + show: { + opacity: 1, + y: 0, + transition: { duration: 0.6, ease: 'easeOut' as const }, + }, +}; + export default function HomeComponent() { const router = useRouter(); - const totalItems = sections.length; - const columns = 3; + return ( +
+ {/* Hero Section */} +
+ {/* Decorative grid */} +
+ + + {/* Badge */} + + + + + Interview Preparation Platform + + + - const colSpanClasses: { [key: number]: string } = { - 2: 'md:col-span-2', - 3: 'md:col-span-3', - 6: 'md:col-span-6', - }; + {/* Main Heading */} + + Master your +
+ next interview +
- return ( -
-
-

- Any interview prep -

-
-
- {sections.map((section, index) => { - const rowNumber = Math.floor(index / columns); - const isLastRow = - rowNumber === Math.floor((totalItems - 1) / columns); - - const itemsOnLastRow = totalItems % columns; - const itemsOnThisRow = - isLastRow && itemsOnLastRow > 0 ? itemsOnLastRow : columns; - - const colSpan = Math.round(6 / itemsOnThisRow); - - const className = `rounded-lg border border-zinc-800 bg-zinc-900/90 p-6 shadow-sm transition-colors duration-200 hover:border-zinc-600 ${ - colSpanClasses[colSpan] - }`; - - return ( + {/* Subtitle */} + + A structured study resource with code examples, interactive notes, + and curated interview prep materials for multiple positions and + experience levels. + + + {/* CTA */} + + + + + {/* Scroll indicator */} + +
+ +
+
+
+ + {/* Topics Section */} +
+ +
+

+ Choose your + learning path +

+

+ Start with structured preparation tracks designed for specific + roles and experience levels. +

+
+ +
+ {sections.map((section) => ( + !section.inProgress && router.push(section.href)} + type='button' + whileHover={{ y: -4 }} + whileTap={{ scale: 0.98 }} + > +
+
+
+
+ +
+
+

+ {section.title} +

+ {section.level && ( + + {section.level} + + )} +
+
+

+ {section.description} +

+
+
+ {section.inProgress ? ( + Coming soon + ) : ( + <> + Start learning + + + )} +
-

- {section.description} + + ))} +

+
+
+ + {/* Features Grid */} +
+ +
+

+ Built for + real preparation +

+
+ +
+ {features.map((feature) => ( + +
+ +
+

+ {feature.title} +

+

+ {feature.description}

-
-
- {section.inProgress ? 'Coming soon...' : 'Start learning →'} -
- - ); - })} -
+ + ))} + + + + + {/* Footer */} +
+
+

Prep — Interview preparation, simplified.

+
+
); } diff --git a/src/app/frontend/junior/frontend-junior.tsx b/src/app/frontend/junior/frontend-junior.tsx index 86969bd..39961f0 100644 --- a/src/app/frontend/junior/frontend-junior.tsx +++ b/src/app/frontend/junior/frontend-junior.tsx @@ -1,104 +1,211 @@ 'use client'; + +import { motion } from 'framer-motion'; +import { + ArrowLeft, + ArrowRight, + CheckCircle2, + Clock, + Code2, + Globe, + Paintbrush, + Terminal, + Wrench, +} from 'lucide-react'; import { useRouter } from 'next/navigation'; import type { Section } from '@/types'; -const sections: Section[] = [ +interface TopicSection extends Section { + icon: typeof Code2; + color: string; + gradient: string; +} + +const sections: TopicSection[] = [ { href: '/frontend/junior/html&css', title: 'HTML & CSS', description: 'Semantic HTML, accessibility basics, Flexbox, Grid, responsive design', inProgress: false, + icon: Paintbrush, + color: 'var(--aurora-3)', + gradient: 'from-pink-500/10 to-rose-500/10', }, { href: '#', title: 'JavaScript Fundamentals', description: 'ES6+ syntax, scope, closures, async patterns, DOM APIs', inProgress: true, + icon: Code2, + color: 'var(--aurora-1)', + gradient: 'from-indigo-500/10 to-violet-500/10', }, { href: '#', title: 'API Integration', description: 'fetch/axios, REST, GraphQL, error handling, loading states', inProgress: true, + icon: Globe, + color: 'var(--aurora-4)', + gradient: 'from-cyan-500/10 to-teal-500/10', }, { href: '#', title: 'Framework Basics (React)', description: 'Components, props, state, hooks, Context API, lifecycle', inProgress: true, + icon: Terminal, + color: 'var(--aurora-2)', + gradient: 'from-violet-500/10 to-purple-500/10', }, { href: '#', title: 'Tooling & Debugging', description: 'Chrome DevTools, ESLint/Prettier, npm scripts, build tools', inProgress: true, + icon: Wrench, + color: 'var(--aurora-5)', + gradient: 'from-emerald-500/10 to-green-500/10', }, ]; +const stagger = { + hidden: {}, + show: { + transition: { + staggerChildren: 0.08, + }, + }, +}; + +const fadeUp = { + hidden: { opacity: 0, y: 20 }, + show: { + opacity: 1, + y: 0, + transition: { duration: 0.5, ease: 'easeOut' as const }, + }, +}; + export default function FrontendJunior() { const router = useRouter(); - const totalItems = sections.length; - const columns = 3; - - const colSpanClasses: { [key: number]: string } = { - 2: 'md:col-span-2', - 3: 'md:col-span-3', - 6: 'md:col-span-6', - }; + const completedCount = sections.filter((s) => !s.inProgress).length; + const totalCount = sections.length; return ( -
-
-

- Junior Frontend Developer Preparation -

-

- (in development, 'in progress' parts are not completed) -

-
-
- {sections.map((section, index) => { - const rowNumber = Math.floor(index / columns); - const isLastRow = - rowNumber === Math.floor((totalItems - 1) / columns); +
+ {/* Header */} +
+
+ +
+
+ + {/* Content */} +
+ + {/* Page Title */} + +

+ Junior Frontend +
+ Developer Prep +

+

+ Master the fundamentals of frontend development. Complete each + topic to build a strong foundation for your interviews. +

+ + {/* Progress bar */} +
+
+ +
+ + {completedCount}/{totalCount} topics + +
+
- const itemsOnLastRow = totalItems % columns; - const itemsOnThisRow = - isLastRow && itemsOnLastRow > 0 ? itemsOnLastRow : columns; + {/* Topic Cards */} +
+ {sections.map((section, index) => ( + !section.inProgress && router.push(section.href)} + type='button' + variants={fadeUp} + whileHover={section.inProgress ? {} : { x: 4 }} + > + {/* Number */} +
+ {String(index + 1).padStart(2, '0')} +
- const colSpan = Math.round(6 / itemsOnThisRow); + {/* Icon */} +
+ +
- const className = `rounded-lg border border-zinc-800 bg-zinc-900/90 p-6 shadow-sm transition-colors duration-200 hover:border-zinc-600 ${ - colSpanClasses[colSpan] - }`; + {/* Content */} +
+

+ {section.title} +

+

+ {section.description} +

+
- return ( - - ); - })} -
+
+ + ))} +
+ +
); } diff --git a/src/app/globals.css b/src/app/globals.css index e1274dc..3cfd0a8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,29 +1,40 @@ @import "tailwindcss"; :root { - /* Default to dark theme */ - --background: #0a0a0a; - --foreground: #ededed; - --accent: #eab308; /* yellow-500 */ - --muted: #a1a1aa; /* zinc-400 */ - --muted-foreground: #d4d4d8; /* zinc-300 */ - --border: rgba(255, 255, 255, 0.08); - --glass-bg: rgba(0, 0, 0, 0.1); - --glass-strong-bg: rgba(0, 0, 0, 0.22); + /* New design system: Deep space + aurora */ + --background: #050a18; + --foreground: #e8edf5; + --accent: #6366f1; /* indigo-500 */ + --accent-light: #818cf8; /* indigo-400 */ + --accent-glow: rgba(99, 102, 241, 0.35); + --secondary: #f472b6; /* pink-400 */ + --secondary-glow: rgba(244, 114, 182, 0.3); + --tertiary: #34d399; /* emerald-400 */ + --tertiary-glow: rgba(52, 211, 153, 0.25); + --muted: #94a3b8; /* slate-400 */ + --muted-foreground: #cbd5e1; /* slate-300 */ + --border: rgba(148, 163, 184, 0.1); + --border-hover: rgba(148, 163, 184, 0.2); + --glass-bg: rgba(15, 23, 42, 0.6); + --glass-strong-bg: rgba(15, 23, 42, 0.8); + --card-bg: rgba(15, 23, 42, 0.5); --ring: var(--accent); - /* Current page header height (controlled by PageHeader) */ + --surface-1: #0f172a; + --surface-2: #1e293b; + + /* Page header */ --page-header-height: 128px; - /* Offset built-in anchor navigation and scrollIntoView to account for sticky header */ scroll-padding-top: var(--page-header-height, 128px); - /* Ambient background hues */ - --ambient-1: 255 90 31; /* warm orange */ - --ambient-2: 234 179 8; /* yellow-500 */ - --ambient-3: 168 85 247; /* purple */ - --ambient-4: 14 165 233; /* cyan */ + /* Aurora gradient colors */ + --aurora-1: #6366f1; + --aurora-2: #8b5cf6; + --aurora-3: #ec4899; + --aurora-4: #06b6d4; + --aurora-5: #34d399; } -/* Smooth scrolling for anchor navigation; respect reduced motion */ +/* Smooth scrolling; respect reduced motion */ html { scroll-behavior: smooth; } @@ -31,9 +42,13 @@ html { html { scroll-behavior: auto; } + html .motion-safe-only, + body .motion-safe-only { + animation: none; + transition: none; + } } -/* Ensure headings with anchors don't hide under the sticky header */ h2[id], h3[id], h4[id] { @@ -43,20 +58,31 @@ h4[id] { @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); + --color-accent: var(--accent); + --color-accent-light: var(--accent-light); + --color-secondary: var(--secondary); + --color-tertiary: var(--tertiary); + --color-muted: var(--muted); + --color-surface-1: var(--surface-1); + --color-surface-2: var(--surface-2); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); } -/* Respect system light preference while favoring dark by default */ +/* Light mode */ @media (prefers-color-scheme: light) { :root { - --background: #ffffff; - --foreground: #171717; - --muted: #52525b; /* zinc-600 */ - --muted-foreground: #3f3f46; /* zinc-700 */ - --border: rgba(0, 0, 0, 0.08); - --glass-bg: rgba(255, 255, 255, 0.5); - --glass-strong-bg: rgba(255, 255, 255, 0.6); + --background: #f8fafc; + --foreground: #0f172a; + --muted: #64748b; + --muted-foreground: #475569; + --border: rgba(15, 23, 42, 0.1); + --border-hover: rgba(15, 23, 42, 0.2); + --glass-bg: rgba(248, 250, 252, 0.7); + --glass-strong-bg: rgba(248, 250, 252, 0.85); + --card-bg: rgba(255, 255, 255, 0.7); + --surface-1: #ffffff; + --surface-2: #f1f5f9; } } @@ -76,25 +102,6 @@ body { "Segoe UI Emoji"; } -/* Image + gradient background is rendered via AmbientBackground component */ - -/* subtle vignette to highlight glass layers */ -body::after { - content: ""; - position: fixed; - inset: 0; - z-index: -1; - background: - radial-gradient( - 60% 60% at 50% 0%, - rgba(255, 255, 255, 0.03), - transparent 70% - ), - radial-gradient(80% 50% at 50% 100%, rgba(0, 0, 0, 0.25), transparent 70%); - pointer-events: none; - transition: filter 0.28s ease-in-out; -} - /* Accessible focus styles */ button:focus-visible, a:focus-visible, @@ -107,24 +114,119 @@ a:focus-visible, .glass { background: var(--glass-bg); border: 1px solid var(--border); - backdrop-filter: blur(12px) saturate(140%); + backdrop-filter: blur(16px) saturate(180%); + -webkit-backdrop-filter: blur(16px) saturate(180%); } .glass-strong { background: var(--glass-strong-bg); border-bottom: 1px solid var(--border); - backdrop-filter: blur(14px) saturate(160%); + backdrop-filter: blur(20px) saturate(200%); + -webkit-backdrop-filter: blur(20px) saturate(200%); +} + +/* Aurora card hover glow */ +.card-glow { + position: relative; + overflow: hidden; +} + +.card-glow::before { + content: ""; + position: absolute; + inset: -1px; + border-radius: inherit; + padding: 1px; + background: linear-gradient( + 135deg, + var(--aurora-1), + var(--aurora-3), + var(--aurora-4) + ); + mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + mask-composite: exclude; + -webkit-mask-composite: xor; + opacity: 0; + transition: opacity 0.4s ease; } -/* Mobile: allow full viewport width */ +.card-glow:hover::before { + opacity: 1; +} + +/* Gradient text utility */ +.gradient-text { + background: linear-gradient( + 135deg, + var(--aurora-1), + var(--aurora-2), + var(--aurora-3) + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.gradient-text-cool { + background: linear-gradient( + 135deg, + var(--aurora-4), + var(--aurora-1), + var(--aurora-5) + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Aurora mesh background animation */ +@keyframes aurora-shift { + 0%, + 100% { + background-position: 0% 50%; + } + 25% { + background-position: 100% 0%; + } + 50% { + background-position: 100% 100%; + } + 75% { + background-position: 0% 100%; + } +} + +@keyframes float { + 0%, + 100% { + transform: translateY(0) scale(1); + } + 50% { + transform: translateY(-20px) scale(1.05); + } +} + +@keyframes pulse-glow { + 0%, + 100% { + opacity: 0.4; + transform: scale(1); + } + 50% { + opacity: 0.7; + transform: scale(1.1); + } +} + +/* Content container widths */ @media (max-width: 640px) { .content-container { max-width: 100vw; } } -/* SSR-safe initial width via data attribute (desktop only to avoid descending specificity with mobile override) */ -/* Runtime inline style from PageContainer drives width; these are fallback for SSR/first paint */ @media (min-width: 641px) { html[data-content-width="narrow"] .content-container { max-width: 50vw; @@ -140,7 +242,7 @@ a:focus-visible, } } -/* Hide scrollbars */ +/* Scrollbar */ .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; @@ -149,3 +251,27 @@ a:focus-visible, .scrollbar-hide::-webkit-scrollbar { display: none; } + +/* Custom styled scrollbar for content */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(99, 102, 241, 0.3); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(99, 102, 241, 0.5); +} + +/* Selection */ +::selection { + background: rgba(99, 102, 241, 0.3); + color: #e8edf5; +} diff --git a/src/components/ambient-background/ambient-background.tsx b/src/components/ambient-background/ambient-background.tsx index bef1f56..1303f66 100644 --- a/src/components/ambient-background/ambient-background.tsx +++ b/src/components/ambient-background/ambient-background.tsx @@ -1,29 +1,114 @@ 'use client'; -import Image from 'next/image'; +import { motion } from 'framer-motion'; export function AmbientBackground() { return (
- + {/* Base gradient */}
+ + {/* Aurora orbs */} + + + + + + + + + {/* Noise texture overlay for depth */} +
+ + {/* Top edge glow */} +
diff --git a/src/components/code-block/code-block.tsx b/src/components/code-block/code-block.tsx index c83e946..1a71292 100644 --- a/src/components/code-block/code-block.tsx +++ b/src/components/code-block/code-block.tsx @@ -3,7 +3,6 @@ import { coldarkDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { parseHighlightLines } from '@/helpers/parse-highlight-lines'; import type { CodeBlockProps } from '@/types'; -// Move regex patterns to top level for better performance const COMMENT_REGEX = /^\/\*\s*\w+\s*\*\/\s*\n?/; const LEADING_SPACES_REGEX = /^\s+/; const TRAILING_SPACES_REGEX = /\s+$/; @@ -45,7 +44,6 @@ export const CodeBlock = ({ const highlightedLines = parseHighlightLines(highlightLines); const highlightedLinesEnd = parseHighlightLines(highlightLinesEnd); - // Line props function for highlighting const getLineProps = (lineNumber: number) => { const isHighlighted = highlightedLines.includes(lineNumber); const isHighlightedEnd = highlightedLinesEnd.includes(lineNumber); @@ -54,12 +52,11 @@ export const CodeBlock = ({ let borderLeft = 'none'; if (isHighlightedEnd) { - backgroundColor = 'rgba(34, 197, 94, 0.15)'; // Green for end lines - borderLeft = '3px solid rgb(34, 197, 94)'; + backgroundColor = 'rgba(52, 211, 153, 0.12)'; + borderLeft = '3px solid rgb(52, 211, 153)'; } else if (isHighlighted) { - // Yellow accent for highlighted lines - backgroundColor = 'rgba(234, 179, 8, 0.18)'; - borderLeft = '3px solid rgb(234, 179, 8)'; + backgroundColor = 'rgba(99, 102, 241, 0.15)'; + borderLeft = '3px solid rgb(99, 102, 241)'; } return { @@ -76,9 +73,9 @@ export const CodeBlock = ({ }; return ( -
+
{comment && ( -
+
{`/* ${comment} */`}
)} diff --git a/src/components/layout/content-page.tsx b/src/components/layout/content-page.tsx index 92cf0dd..53f1f93 100644 --- a/src/components/layout/content-page.tsx +++ b/src/components/layout/content-page.tsx @@ -14,7 +14,7 @@ export function ContentPage({ allowWidthToggle = true, }: ContentPageProps) { return ( -
+
{ return (
-

{placeholder}

+

{placeholder}

); }; diff --git a/src/components/page-header/page-header.tsx b/src/components/page-header/page-header.tsx index 219d2b2..d2b9113 100644 --- a/src/components/page-header/page-header.tsx +++ b/src/components/page-header/page-header.tsx @@ -28,7 +28,6 @@ export const PageHeader = ({ const isHiddenOnMobileRef = useRef(false); const lastCssHeaderHeight = useRef(null); - // Threshold constants for mobile hysteresis const JITTER_PX = 5; const HIDE_THRESHOLD_PX = 24; const SHOW_THRESHOLD_PX = 64; @@ -111,7 +110,6 @@ export const PageHeader = ({ ] ); - // Track mobile breakpoint to enable full hide behavior on small screens useEffect(() => { if (typeof window === 'undefined') return; const mq: MediaQueryList = window.matchMedia('(max-width: 639px)'); @@ -135,7 +133,6 @@ export const PageHeader = ({ }; }, []); - // Scroll listener with rAF batching useEffect(() => { if (typeof window === 'undefined') return; let rafId: number | null = null; @@ -145,7 +142,6 @@ export const PageHeader = ({ rafId = null; const currentScrollY = Math.max(0, window.scrollY); const delta = currentScrollY - lastScrollY.current; - // Avoid toggling collapse state on mobile to reduce flicker; rely on full-hide if (!isMobile.current) { setIsScrolled(currentScrollY > 20 && delta > 0); } @@ -163,7 +159,6 @@ export const PageHeader = ({ }; }, [handleMobileScroll]); - // Expose current header height as a CSS variable for other components useEffect(() => { if (typeof document === 'undefined') { return; @@ -174,7 +169,6 @@ export const PageHeader = ({ if (isHiddenOnMobile) { current = '0px'; } else if (isMobileScreen) { - // On mobile, when visible, keep the header at full height for clear tap targets current = expandedHeight; } else if (isInitialLoad || !isScrolled) { current = expandedHeight; @@ -206,7 +200,6 @@ export const PageHeader = ({ style={{ willChange: 'height', overflow: 'hidden', - // Remove bottom border when fully hidden on mobile to avoid a 1px line borderBottomWidth: isHiddenOnMobile ? 0 : 1, transform: 'translateZ(0)', backfaceVisibility: 'hidden', @@ -221,7 +214,7 @@ export const PageHeader = ({
@@ -230,7 +223,7 @@ export const PageHeader = ({ {(isInitialLoad || !isScrolled) && ( router.push(topicHome)} type='button' > - + )}
diff --git a/src/components/section-card/section-card.tsx b/src/components/section-card/section-card.tsx index 26e0617..db263bc 100644 --- a/src/components/section-card/section-card.tsx +++ b/src/components/section-card/section-card.tsx @@ -5,16 +5,18 @@ export const SectionCard = ({ title, children }: SectionCardProps) => { const id = slugify(title); return (

{title}

-
{children}
+
+ {children} +
); }; diff --git a/src/components/table-of-contents/table-of-contents.tsx b/src/components/table-of-contents/table-of-contents.tsx index 8bde15c..3150315 100644 --- a/src/components/table-of-contents/table-of-contents.tsx +++ b/src/components/table-of-contents/table-of-contents.tsx @@ -57,7 +57,6 @@ export const TableOfContents = () => { setIsLoaded(true); }; - // Initial update with delay to ensure content is rendered const timeoutId = setTimeout(updateTOC, 0); const header = document.getElementById('page-header'); @@ -103,7 +102,6 @@ export const TableOfContents = () => { }; }, []); - // Also open on single tap anywhere near the left edge on mobile useEffect(() => { const onTouchTap = (e: TouchEvent) => { if (window.innerWidth >= 768) return; @@ -130,7 +128,6 @@ export const TableOfContents = () => { setOpen(false); } - // Prevent default navigation and use CSS scroll-margin/scroll-padding e.preventDefault(); const href = e.currentTarget.getAttribute('href'); if (href?.startsWith('#')) { @@ -138,7 +135,6 @@ export const TableOfContents = () => { const targetElement = document.getElementById(targetId); if (targetElement) { targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); - // Update the address bar without triggering a jump if ( typeof history !== 'undefined' && typeof history.replaceState === 'function' @@ -153,7 +149,6 @@ export const TableOfContents = () => { setOpen(!open); }; - // Handle click outside on mobile and always unpin when mobile useEffect(() => { openRef.current = open; pinnedRef.current = pinned; @@ -162,7 +157,6 @@ export const TableOfContents = () => { useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (window.innerWidth >= 768) return; - // On mobile ensure menu is unpinned if (pinnedRef.current) { setPinned(false); } @@ -184,7 +178,6 @@ export const TableOfContents = () => { }; }, []); - // Don't render until content is loaded to prevent flickering if (!isLoaded) { return null; } @@ -198,7 +191,6 @@ export const TableOfContents = () => { }} >
- {/* Hover/edge affordance – no explicit button */}