Navigation.jsx

  1import { useRef } from 'react'
  2import Link from 'next/link'
  3import { useRouter } from 'next/router'
  4import clsx from 'clsx'
  5import { AnimatePresence, motion, useIsPresent } from 'framer-motion'
  6
  7import { Button } from '@/components/Button'
  8import { useIsInsideMobileNavigation } from '@/components/MobileNavigation'
  9import { useSectionStore } from '@/components/SectionProvider'
 10import { Tag } from '@/components/Tag'
 11import { remToPx } from '@/lib/remToPx'
 12
 13function useInitialValue(value, condition = true) {
 14  let initialValue = useRef(value).current
 15  return condition ? initialValue : value
 16}
 17
 18function TopLevelNavItem({ href, children }) {
 19  return (
 20    <li className="md:hidden">
 21      <Link
 22        href={href}
 23        className="block py-1 text-sm text-zinc-600 transition hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white"
 24      >
 25        {children}
 26      </Link>
 27    </li>
 28  )
 29}
 30
 31function NavLink({ href, tag, active, isAnchorLink = false, children }) {
 32  return (
 33    <Link
 34      href={href}
 35      aria-current={active ? 'page' : undefined}
 36      className={clsx(
 37        'flex justify-between gap-2 py-1 pr-3 text-sm transition',
 38        isAnchorLink ? 'pl-7' : 'pl-4',
 39        active
 40          ? 'text-zinc-900 dark:text-white'
 41          : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
 42      )}
 43    >
 44      <span className="truncate">{children}</span>
 45      {tag && (
 46        <Tag variant="small" color="zinc">
 47          {tag}
 48        </Tag>
 49      )}
 50    </Link>
 51  )
 52}
 53
 54function VisibleSectionHighlight({ group, pathname }) {
 55  let [sections, visibleSections] = useInitialValue(
 56    [
 57      useSectionStore((s) => s.sections),
 58      useSectionStore((s) => s.visibleSections),
 59    ],
 60    useIsInsideMobileNavigation()
 61  )
 62
 63  let isPresent = useIsPresent()
 64  let firstVisibleSectionIndex = Math.max(
 65    0,
 66    [{ id: '_top' }, ...sections].findIndex(
 67      (section) => section.id === visibleSections[0]
 68    )
 69  )
 70  let itemHeight = remToPx(2)
 71  let height = isPresent
 72    ? Math.max(1, visibleSections.length) * itemHeight
 73    : itemHeight
 74  let top =
 75    group.links.findIndex((link) => link.href === pathname) * itemHeight +
 76    firstVisibleSectionIndex * itemHeight
 77
 78  return (
 79    <motion.div
 80      layout
 81      initial={{ opacity: 0 }}
 82      animate={{ opacity: 1, transition: { delay: 0.2 } }}
 83      exit={{ opacity: 0 }}
 84      className="absolute inset-x-0 top-0 bg-zinc-800/2.5 will-change-transform dark:bg-white/2.5"
 85      style={{ borderRadius: 8, height, top }}
 86    />
 87  )
 88}
 89
 90function ActivePageMarker({ group, pathname }) {
 91  let itemHeight = remToPx(2)
 92  let offset = remToPx(0.25)
 93  let activePageIndex = group.links.findIndex((link) => link.href === pathname)
 94  let top = offset + activePageIndex * itemHeight
 95
 96  return (
 97    <motion.div
 98      layout
 99      className="absolute left-2 h-6 w-px bg-emerald-500"
100      initial={{ opacity: 0 }}
101      animate={{ opacity: 1, transition: { delay: 0.2 } }}
102      exit={{ opacity: 0 }}
103      style={{ top }}
104    />
105  )
106}
107
108function NavigationGroup({ group, className }) {
109  // If this is the mobile navigation then we always render the initial
110  // state, so that the state does not change during the close animation.
111  // The state will still update when we re-open (re-render) the navigation.
112  let isInsideMobileNavigation = useIsInsideMobileNavigation()
113  let [router, sections] = useInitialValue(
114    [useRouter(), useSectionStore((s) => s.sections)],
115    isInsideMobileNavigation
116  )
117
118  let isActiveGroup =
119    group.links.findIndex((link) => link.href === router.pathname) !== -1
120
121  return (
122    <li className={clsx('relative mt-6', className)}>
123      <motion.h2
124        layout="position"
125        className="text-xs font-semibold text-zinc-900 dark:text-white"
126      >
127        {group.title}
128      </motion.h2>
129      <div className="relative mt-3 pl-2">
130        <AnimatePresence initial={!isInsideMobileNavigation}>
131          {isActiveGroup && (
132            <VisibleSectionHighlight group={group} pathname={router.pathname} />
133          )}
134        </AnimatePresence>
135        <motion.div
136          layout
137          className="absolute inset-y-0 left-2 w-px bg-zinc-900/10 dark:bg-white/5"
138        />
139        <AnimatePresence initial={false}>
140          {isActiveGroup && (
141            <ActivePageMarker group={group} pathname={router.pathname} />
142          )}
143        </AnimatePresence>
144        <ul role="list" className="border-l border-transparent">
145          {group.links.map((link) => (
146            <motion.li key={link.href} layout="position" className="relative">
147              <NavLink href={link.href} active={link.href === router.pathname}>
148                {link.title}
149              </NavLink>
150              <AnimatePresence mode="popLayout" initial={false}>
151                {link.href === router.pathname && sections.length > 0 && (
152                  <motion.ul
153                    role="list"
154                    initial={{ opacity: 0 }}
155                    animate={{
156                      opacity: 1,
157                      transition: { delay: 0.1 },
158                    }}
159                    exit={{
160                      opacity: 0,
161                      transition: { duration: 0.15 },
162                    }}
163                  >
164                    {sections.map((section) => (
165                      <li key={section.id}>
166                        <NavLink
167                          href={`${link.href}#${section.id}`}
168                          tag={section.tag}
169                          isAnchorLink
170                        >
171                          {section.title}
172                        </NavLink>
173                      </li>
174                    ))}
175                  </motion.ul>
176                )}
177              </AnimatePresence>
178            </motion.li>
179          ))}
180        </ul>
181      </div>
182    </li>
183  )
184}
185
186export const navigation = [
187  {
188    title: 'Onboarding',
189    links: [
190      { title: 'Tools', href: '/tools' },
191      { title: 'Environment Setup', href: '/setup' },
192      { title: 'Backend', href: '/backend' },
193    ],
194  },
195  {
196    title: 'Zed',
197    links: [{ title: 'Release Process', href: '/release-process' }],
198  },
199]
200
201export function Navigation(props) {
202  return (
203    <nav {...props}>
204      <ul role="list">
205        <TopLevelNavItem href="/">API</TopLevelNavItem>
206        <TopLevelNavItem href="#">Documentation</TopLevelNavItem>
207        <TopLevelNavItem href="#">Support</TopLevelNavItem>
208        {navigation.map((group, groupIndex) => (
209          <NavigationGroup
210            key={group.title}
211            group={group}
212            className={groupIndex === 0 && 'md:mt-0'}
213          />
214        ))}
215        <li className="sticky bottom-0 z-10 mt-6 min-[416px]:hidden">
216          <Button href="#" variant="filled" className="w-full">
217            Sign in
218          </Button>
219        </li>
220      </ul>
221    </nav>
222  )
223}