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 (