diff --git a/components/nav.tsx b/components/nav.tsx index 12e9639..cb6027a 100644 --- a/components/nav.tsx +++ b/components/nav.tsx @@ -2,18 +2,141 @@ import Image from 'next/image' import { useEffect, useRef, useState } from 'react' +import { + RiBookOpenLine, + RiGithubFill, + RiHome5Line, + RiImageLine, + RiLinksLine, + RiLinkedinBoxFill, + RiMusic2Line, + RiPulseLine, + RiServerLine, + RiTwitterXFill, + type RemixiconComponentType, +} from '@remixicon/react' +import { + BASE_URL, + GALLERY_URL, + GITHUB, + INFO_URL, + INSIGHT_URL, + LINKEDIN, + MUSIC_URL, + STATUS_URL, + TWITTER, +} from '@/constants' const CURRENT = 'gallery' -const APPS = [ - { label: 'home', href: 'https://hovanhoa.net' }, - { label: 'insight', href: 'https://insight.hovanhoa.net' }, - { label: 'gallery', href: 'https://gallery.hovanhoa.net' }, - { label: 'music', href: 'https://music.hovanhoa.net' }, - { label: 'status', href: 'https://status.hovanhoa.net' }, +type Item = { + label: string + desc: string + href: string + icon: RemixiconComponentType + external?: boolean +} +type Column = { title: string; items: Item[] } + +// Every destination here is real — sourced from src/constants (site config). +const COLUMNS: Column[] = [ + { + title: 'this site', + items: [ + { + label: 'home', + desc: 'profile & writing', + href: BASE_URL, + icon: RiHome5Line, + }, + { + label: 'blog', + desc: 'notes & posts', + href: `${BASE_URL}/#blog`, + icon: RiBookOpenLine, + }, + ], + }, + { + title: 'the family', + items: [ + { + label: 'insight', + desc: 'coding activity', + href: INSIGHT_URL, + icon: RiPulseLine, + external: true, + }, + { + label: 'gallery', + desc: 'photography', + href: GALLERY_URL, + icon: RiImageLine, + external: true, + }, + { + label: 'music', + desc: "what's on repeat", + href: MUSIC_URL, + icon: RiMusic2Line, + external: true, + }, + { + label: 'status', + desc: 'service uptime', + href: STATUS_URL, + icon: RiServerLine, + external: true, + }, + { + label: 'info', + desc: 'links & bio', + href: INFO_URL, + icon: RiLinksLine, + external: true, + }, + ], + }, + { + title: 'connect', + items: [ + { + label: 'github', + desc: `@${GITHUB}`, + href: `https://github.com/${GITHUB}`, + icon: RiGithubFill, + external: true, + }, + { + label: 'twitter', + desc: `@${TWITTER}`, + href: `https://twitter.com/${TWITTER}`, + icon: RiTwitterXFill, + external: true, + }, + { + label: 'linkedin', + desc: `in/${LINKEDIN}`, + href: `https://linkedin.com/in/${LINKEDIN}`, + icon: RiLinkedinBoxFill, + external: true, + }, + ], + }, ] -function Caret() { +// dev-native extras pinned to the footer (both routes exist) +const FOOTER_LINKS = [ + { label: 'llms.txt', href: `${BASE_URL}/llms.txt` }, + { label: 'sitemap', href: `${BASE_URL}/sitemap.xml` }, +] + +// the trigger icon mirrors the page you're on +const CurrentIcon = + COLUMNS.flatMap((c) => c.items).find((it) => it.label === CURRENT)?.icon ?? + RiHome5Line + +function Caret({ open }: { open?: boolean }) { return ( @@ -54,7 +180,8 @@ function ThemeToggle() { type="button" onClick={toggle} aria-label="Toggle dark mode" - className="inline-flex h-9 w-9 items-center justify-center rounded-lg text-[var(--rd-text-3)] transition-colors hover:bg-[var(--rd-surface-2)] hover:text-[var(--rd-text)]" + aria-pressed={dark} + className="-m-1 inline-flex h-11 w-11 items-center justify-center rounded-lg text-[var(--rd-text-3)] transition-colors hover:bg-[var(--rd-surface-2)] hover:text-[var(--rd-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--rd-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--rd-bg-sub)]" > {dark ? ( (null) + const ref = useRef(null) + const btnRef = useRef(null) useEffect(() => { const onClick = (e: MouseEvent) => { @@ -98,21 +226,34 @@ export default function Navbar() { setOpen(false) } } + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setOpen(false) + btnRef.current?.focus() + } + } document.addEventListener('mousedown', onClick) - return () => document.removeEventListener('mousedown', onClick) + document.addEventListener('keydown', onKey) + return () => { + document.removeEventListener('mousedown', onClick) + document.removeEventListener('keydown', onKey) + } }, []) const linkClass = - 'font-[family-name:var(--font-mono)] text-[13px] text-[var(--rd-text-3)] transition-colors duration-200 hover:text-[var(--rd-accent-ink)]' + 'font-[family-name:var(--font-mono)] text-[13px] text-[var(--rd-text-2)] transition-colors duration-200 hover:text-[var(--rd-accent-ink)]' return ( -
{/* logo */} - {/* app switcher — orange-tinted, mono */} -
- + + {/* inline quick links */} +
+ + blog + + - / - {CURRENT} - - - {open && ( -
- {APPS.map((a) => ( - - {a.label} - - ))} -
- )} + insight + + + gallery +
+
- {/* inline links */} -
- {APPS.map((a) => ( + + + {/* mega-menu panel — content-aligned, spans the header width */} + {open && ( +
+
+ {COLUMNS.map((col, ci) => ( + + ))} +
+ + {/* footer row */} +
setOpen(false)} + className="inline-flex items-center gap-1.5 font-[family-name:var(--font-mono)] text-[13px] font-semibold text-[var(--rd-accent-ink)] transition-opacity hover:opacity-80" > - {a.label} + browse everything + - ))} + +
-
- - + )} ) } diff --git a/constants/index.tsx b/constants/index.tsx index 8a3271e..a83d850 100644 --- a/constants/index.tsx +++ b/constants/index.tsx @@ -5,6 +5,8 @@ const BASE_URL = 'https://hovanhoa.net' const INSIGHT_URL = 'https://insight.hovanhoa.net' const GALLERY_URL = 'https://gallery.hovanhoa.net' const MUSIC_URL = 'https://music.hovanhoa.net' +const STATUS_URL = 'https://status.hovanhoa.net' +const INFO_URL = 'https://info.hovanhoa.net' const TWITTER = '_hovanhoa_' const LINKEDIN = 'hovanhoa' const GITHUB = 'hovanhoa' @@ -81,5 +83,7 @@ export { BASE_URL, INSIGHT_URL, MUSIC_URL, + STATUS_URL, + INFO_URL, ROLE, } diff --git a/package-lock.json b/package-lock.json index 018a854..e465531 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "dependencies": { "@headlessui/react": "^1.7.4", "@heroicons/react": "^2.0.13", + "@remixicon/react": "^4.9.0", "@tailwindcss/typography": "^0.5.10", "@vercel/analytics": "^1.5.0", "cloudinary": "^1.32.0", @@ -1086,6 +1087,15 @@ "node": ">=14" } }, + "node_modules/@remixicon/react": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@remixicon/react/-/react-4.9.0.tgz", + "integrity": "sha512-5/jLDD4DtKxH2B4QVXTobvV1C2uL8ab9D5yAYNtFt+w80O0Ys1xFOrspqROL3fjrZi+7ElFUWE37hBfaAl6U+Q==", + "license": "Remix Icon License 1.0", + "peerDependencies": { + "react": ">=18.2.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", diff --git a/package.json b/package.json index 94081f3..8fc2019 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dependencies": { "@headlessui/react": "^1.7.4", "@heroicons/react": "^2.0.13", + "@remixicon/react": "^4.9.0", "@tailwindcss/typography": "^0.5.10", "@vercel/analytics": "^1.5.0", "cloudinary": "^1.32.0", diff --git a/pages/global.css b/pages/global.css index d7a54b5..3bacb4a 100644 --- a/pages/global.css +++ b/pages/global.css @@ -229,3 +229,23 @@ body { grid-template-columns: 1fr; } } +/* nav app-switcher overlay entrance — the one floating surface in the header */ +@keyframes rd-menu-in { + from { + opacity: 0; + transform: translateY(-4px) scale(0.985); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} +.rd-menu-in { + transform-origin: top left; + animation: rd-menu-in 150ms cubic-bezier(0.22, 1, 0.36, 1); +} +@media (prefers-reduced-motion: reduce) { + .rd-menu-in { + animation: none; + } +}