diff --git a/app/styles/toc.css b/app/styles/toc.css index 438635f..b910776 100644 --- a/app/styles/toc.css +++ b/app/styles/toc.css @@ -297,7 +297,8 @@ Shared sidebar positioning for new variants (staffline + vim) ────────────────────────────────────────────────────────────── */ .toc-variant-staffline, -.toc-variant-vim { +.toc-variant-vim, +.toc-variant-constellation { position: fixed; left: 20px; top: 120px; @@ -309,13 +310,15 @@ } .toc-variant-staffline.mounted, -.toc-variant-vim.mounted { +.toc-variant-vim.mounted, +.toc-variant-constellation.mounted { animation: tocFadeIn 0.7s cubic-bezier(0.25, 0.1, 0.25, 1) forwards; } @media (min-width: 1024px) { .toc-variant-staffline, - .toc-variant-vim { + .toc-variant-vim, + .toc-variant-constellation { display: flex; flex-direction: column; width: 17ch; @@ -324,13 +327,15 @@ @media (min-width: 1280px) { .toc-variant-staffline, - .toc-variant-vim { + .toc-variant-vim, + .toc-variant-constellation { width: 25ch; } } .toc-variant-staffline .toc-title, -.toc-variant-vim .toc-title { +.toc-variant-vim .toc-title, +.toc-variant-constellation .toc-title { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.16em; @@ -544,3 +549,93 @@ color: var(--amber); font-weight: 400; } + +/* ────────────────────────────────────────────────────────────── + Constellation variant — star map nav + ────────────────────────────────────────────────────────────── */ +.toc-variant-constellation { + font-size: 11.5px; + position: fixed; +} + +.toc-variant-constellation .map { + position: relative; + height: 240px; + width: 100%; +} + +.toc-variant-constellation svg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.toc-variant-constellation ul { + position: absolute; + inset: 0; + list-style: none; + margin: 0; + padding: 0; +} + +.toc-variant-constellation .node { + position: absolute; + width: 0; + height: 0; +} + +.toc-variant-constellation .node > a { + position: absolute; + top: 0; + left: 0; + cursor: pointer; + text-decoration: none; + border: none; + color: inherit; +} + +.toc-variant-constellation .node .dot { + position: absolute; + top: 0; + left: 0; + width: 6px; + height: 6px; + border-radius: 50%; + transform: translate(-50%, -50%); + background: var(--fg-light); + opacity: 0.5; + transition: all 0.3s; +} + +.toc-variant-constellation .node .lbl { + position: absolute; + top: 6px; + left: 6px; + font-family: var(--font-serif); + font-size: 12px; + color: var(--fg-light); + opacity: 0.55; + white-space: nowrap; + transition: opacity 0.2s, color 0.2s; +} + +.toc-variant-constellation .node > a:hover .dot, +.toc-variant-constellation .node > a:focus-visible .dot, +.toc-variant-constellation .node.active .dot { + opacity: 1; + background: var(--amber); + box-shadow: 0 0 12px 2px rgba(186, 149, 94, 0.25); +} + +.toc-variant-constellation .node > a:hover .lbl, +.toc-variant-constellation .node > a:focus-visible .lbl, +.toc-variant-constellation .node.active .lbl { + opacity: 1; + color: var(--fg); +} + +.toc-variant-constellation .node.active .lbl { + color: var(--amber); +} diff --git a/app/writing/exploring-a-recurrent-neural-network/page.mdx b/app/writing/exploring-a-recurrent-neural-network/page.mdx index 13534ff..9ce4768 100644 --- a/app/writing/exploring-a-recurrent-neural-network/page.mdx +++ b/app/writing/exploring-a-recurrent-neural-network/page.mdx @@ -6,7 +6,7 @@ readingTime: '14 min' tags: ['Machine Learning', 'RNNs'] --- - + These notes walk through Andrej Karpathy's minimal character-level **Recurrent Neural Network (RNN)** implementation. [^1] diff --git a/components/ui/toc.tsx b/components/ui/toc.tsx index 71de50d..84b19c7 100644 --- a/components/ui/toc.tsx +++ b/components/ui/toc.tsx @@ -1,10 +1,11 @@ 'use client' import GithubSlugger from 'github-slugger' import { usePathname } from 'next/navigation' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { BLOG_POSTS } from '@/app/data' import '@/app/styles/toc.css' +import { layoutStars } from '@/lib/toc-starmap' interface TocItem { level: number @@ -12,7 +13,9 @@ interface TocItem { slug: string } -export type TocVariant = 'classic' | 'staffline' | 'vim' +export type TocVariant = 'classic' | 'staffline' | 'vim' | 'constellation' + +const CONSTELLATION_MAX_NODES = 12 const variantByTag: Record = { jazz: 'staffline', @@ -47,11 +50,20 @@ export function TableOfContents({ const [minLevel, setMinLevel] = useState(1) const [activeId, setActiveId] = useState('') const [scrollProgress, setScrollProgress] = useState(0) + const [prefersReducedMotion, setPrefersReducedMotion] = useState(false) const post = BLOG_POSTS.find((p) => p.link === pathname) - const resolvedVariant: TocVariant = + const requestedVariant: TocVariant = variant ?? resolveVariantFromTags(post?.tags ?? []) + const shouldFallbackFromConstellation = + requestedVariant === 'constellation' && + (headings.length > CONSTELLATION_MAX_NODES || prefersReducedMotion) + + const resolvedVariant: TocVariant = shouldFallbackFromConstellation + ? 'staffline' + : requestedVariant + const totalMinutes = post?.readingTime ? parseInt(post.readingTime, 10) || 0 : 0 @@ -119,6 +131,16 @@ export function TableOfContents({ return () => observer.disconnect() }, [headings]) + // Detect prefers-reduced-motion so constellation can fall back + useEffect(() => { + if (typeof window === 'undefined' || !window.matchMedia) return + const mq = window.matchMedia('(prefers-reduced-motion: reduce)') + const update = () => setPrefersReducedMotion(mq.matches) + update() + mq.addEventListener('change', update) + return () => mq.removeEventListener('change', update) + }, []) + // Track scroll progress for the vim status bar useEffect(() => { if (resolvedVariant !== 'vim') return @@ -195,7 +217,22 @@ export function TableOfContents({ ? ':TOC' : resolvedVariant === 'staffline' ? '§ contents' - : title + : resolvedVariant === 'constellation' + ? 'index' + : title + + if (resolvedVariant === 'constellation') { + return ( + + ) + } return (