+
+
+ {showLogo && (
+
+ )}
+
+
+
+
+ {(config.topNav?.length || 0) > 0 && (
+ <>
+
+
+
+
3 ? styles.hideCompact : null,
+ )}
+ />
+ >
+ )}
+
+ {config.socials && config.socials?.length > 0 && (
+ <>
+
3 ? styles.hideCompact : null,
+ )}
+ style={{ marginLeft: '-8px', marginRight: '-8px' }}
+ >
+ {config.socials.map((social, i) => (
+
+
+
+ ))}
+
+ {!config.theme?.colorScheme && (
+
+ )}
+ >
+ )}
+
+ {!config.theme?.colorScheme && (
+
+ )}
+
+
+ )
+}
+
+export function Curtain() {
+ return
+}
+
+function Navigation() {
+ const { topNav } = useConfig()
+ if (!topNav) return null
+
+ const { pathname } = useLocation()
+ const activeIds = useActiveNavIds({ pathname, items: topNav })
+
+ return (
+
+
+ {topNav.map((item, i) =>
+ item.link ? (
+
+ {item.text}
+
+ ) : item.items ? (
+
+
+ {item.text}
+
+
+
+
+
+ ) : null,
+ )}
+
+
+ )
+}
+
+function NavigationMenuContent({ items }: { items: ParsedTopNavItem[] }) {
+ const { pathname } = useLocation()
+ const activeIds = useActiveNavIds({ pathname, items })
+ return (
+
+ {items?.map((item, i) => (
+
+ {item.text}
+
+ ))}
+
+ )
+}
+
+function ThemeToggleButton() {
+ const { toggle } = useTheme()
+ return (
+
+
+
+
+ )
+}
+
+const iconsForIcon = {
+ discord: Discord,
+ github: GitHub,
+ telegram: Telegram,
+ warpcast: Warpcast,
+ x: X,
+} satisfies Record
+
+const sizesForType = {
+ discord: '23px',
+ github: '20px',
+ telegram: '21px',
+ warpcast: '20px',
+ x: '18px',
+} satisfies Record
+
+function SocialButton({ icon, label, link }: ParsedSocialItem) {
+ return (
+
+
+
+ )
+}
diff --git a/app/components/ExternalLink.css.ts b/app/components/ExternalLink.css.ts
new file mode 100644
index 00000000..4d94a8b1
--- /dev/null
+++ b/app/components/ExternalLink.css.ts
@@ -0,0 +1,20 @@
+import { createVar, style } from '@vanilla-extract/css'
+
+export const arrowColor = createVar('arrowColor')
+export const iconUrl = createVar('iconUrl')
+
+export const root = style({
+ selectors: {
+ '&::after': {
+ backgroundColor: 'currentColor',
+ content: '',
+ color: arrowColor,
+ display: 'inline-block',
+ height: '0.5em',
+ marginLeft: '0.325em',
+ marginRight: '0.25em',
+ width: '0.5em',
+ mask: `${iconUrl} no-repeat center / contain`,
+ },
+ },
+})
diff --git a/app/components/ExternalLink.tsx b/app/components/ExternalLink.tsx
new file mode 100644
index 00000000..d2e0113b
--- /dev/null
+++ b/app/components/ExternalLink.tsx
@@ -0,0 +1,36 @@
+import { clsx } from 'clsx'
+
+import { assignInlineVars } from '@vanilla-extract/dynamic'
+import { forwardRef } from 'react'
+import { useConfig } from '../hooks/useConfig.js'
+import * as styles from './ExternalLink.css.js'
+
+export type ExternalLinkProps = React.DetailedHTMLProps<
+ React.AnchorHTMLAttributes,
+ HTMLAnchorElement
+> & { hideExternalIcon?: boolean }
+
+export const ExternalLink = forwardRef(
+ ({ className, children, hideExternalIcon, href, ...props }: ExternalLinkProps, ref) => {
+ const { basePath } = useConfig()
+ const assetBasePath = import.meta.env.PROD ? basePath : ''
+ return (
+
+ {children}
+
+ )
+ },
+)
diff --git a/app/components/Footer.css.ts b/app/components/Footer.css.ts
new file mode 100644
index 00000000..0ab2e889
--- /dev/null
+++ b/app/components/Footer.css.ts
@@ -0,0 +1,141 @@
+import { createVar, style } from '@vanilla-extract/css'
+
+import {
+ contentVars,
+ fontSizeVars,
+ fontWeightVars,
+ primitiveColorVars,
+ spaceVars,
+ viewportVars,
+} from '../styles/vars.css.js'
+
+const iconWidthVar = createVar('iconWidth')
+
+export const root = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spaceVars['32'],
+ maxWidth: contentVars.width,
+ overflowX: 'hidden',
+ padding: `${spaceVars['28']} ${contentVars.horizontalPadding} ${spaceVars['48']}`,
+ vars: {
+ [iconWidthVar]: '24px',
+ },
+})
+
+export const container = style(
+ {
+ borderBottom: `1px solid ${primitiveColorVars.border}`,
+ display: 'flex',
+ justifyContent: 'space-between',
+ paddingBottom: spaceVars['16'],
+ },
+ 'container',
+)
+
+export const editLink = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ fontSize: fontSizeVars['14'],
+ gap: spaceVars['8'],
+ textDecoration: 'none',
+ },
+ 'editLink',
+)
+
+export const lastUpdated = style(
+ {
+ color: primitiveColorVars.text3,
+ fontSize: fontSizeVars['14'],
+ },
+ 'lastUpdated',
+)
+
+export const navigation = style(
+ {
+ display: 'flex',
+ justifyContent: 'space-between',
+ },
+ 'navigation',
+)
+
+export const navigationIcon = style(
+ {
+ width: iconWidthVar,
+ },
+ 'navigationIcon',
+)
+
+export const navigationIcon_left = style(
+ {
+ display: 'flex',
+ '@media': {
+ [viewportVars['max-720px']]: {
+ justifyContent: 'center',
+ },
+ },
+ },
+ 'navigationIcon_left',
+)
+
+export const navigationIcon_right = style(
+ {
+ display: 'flex',
+ justifyContent: 'flex-end',
+ '@media': {
+ [viewportVars['max-720px']]: {
+ justifyContent: 'center',
+ },
+ },
+ },
+ 'navigationIcon_right',
+)
+
+export const navigationItem = style(
+ {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spaceVars['4'],
+ },
+ 'navigationItem',
+)
+
+export const navigationItem_left = style({}, 'navigationItem_left')
+
+export const navigationItem_right = style(
+ {
+ alignItems: 'flex-end',
+ },
+ 'navigationItem_right',
+)
+
+export const navigationText = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ fontSize: fontSizeVars['18'],
+ fontWeight: fontWeightVars.medium,
+ '@media': {
+ [viewportVars['max-720px']]: {
+ fontSize: fontSizeVars['12'],
+ },
+ },
+ },
+ 'navigationText',
+)
+
+export const navigationTextInner = style(
+ {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ width: '26ch',
+ whiteSpace: 'pre',
+ '@media': {
+ [viewportVars['max-480px']]: {
+ width: '20ch',
+ },
+ },
+ },
+ 'navigationTextInner',
+)
diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx
new file mode 100644
index 00000000..f22192a6
--- /dev/null
+++ b/app/components/Footer.tsx
@@ -0,0 +1,193 @@
+import { Pencil2Icon } from '@radix-ui/react-icons'
+import { assignInlineVars } from '@vanilla-extract/dynamic'
+import clsx from 'clsx'
+import { useEffect, useMemo } from 'react'
+import { useLocation, useNavigate } from 'react-router-dom'
+import { Footer as ConsumerFooter } from 'virtual:consumer-components'
+
+import type { SidebarItem } from '../../config.js'
+import { useEditLink } from '../hooks/useEditLink.js'
+import { useLayout } from '../hooks/useLayout.js'
+import { useMounted } from '../hooks/useMounted.js'
+import { usePageData } from '../hooks/usePageData.js'
+import { useSidebar } from '../hooks/useSidebar.js'
+import * as styles from './Footer.css.js'
+import { sizeVar } from './Icon.css.js'
+import { Icon } from './Icon.js'
+import { KeyboardShortcut } from './KeyboardShortcut.js'
+import { Link } from './Link.js'
+import { ArrowLeft } from './icons/ArrowLeft.js'
+import { ArrowRight } from './icons/ArrowRight.js'
+
+export function Footer() {
+ const { layout } = useLayout()
+ const mounted = useMounted()
+ const pageData = usePageData()
+
+ const lastUpdatedAtDate = useMemo(
+ () => (pageData.lastUpdatedAt ? new Date(pageData.lastUpdatedAt) : undefined),
+ [pageData.lastUpdatedAt],
+ )
+ const lastUpdatedAtISOString = useMemo(
+ () => lastUpdatedAtDate?.toISOString(),
+ [lastUpdatedAtDate],
+ )
+
+ return (
+
+ )
+}
+
+function EditLink() {
+ const editLink = useEditLink()
+
+ if (!editLink.url) return null
+ return (
+
+ )
+}
+
+function Navigation() {
+ const mounted = useMounted()
+ const sidebar = useSidebar()
+
+ const { pathname } = useLocation()
+ const flattenedSidebar = useMemo(
+ () => flattenSidebar(sidebar.items || []).filter((item) => item.link),
+ [sidebar],
+ )
+ const currentPageIndex = useMemo(
+ () => flattenedSidebar.findIndex((item) => item.link === pathname),
+ [flattenedSidebar, pathname],
+ )
+
+ const [prevPage, nextPage] = useMemo(() => {
+ if (currentPageIndex < 0) return []
+ if (currentPageIndex === 0) return [null, flattenedSidebar[currentPageIndex + 1]]
+ if (currentPageIndex === flattenedSidebar.length - 1)
+ return [flattenedSidebar[currentPageIndex - 1], null]
+ return [flattenedSidebar[currentPageIndex - 1], flattenedSidebar[currentPageIndex + 1]]
+ }, [currentPageIndex, flattenedSidebar])
+
+ const navigate = useNavigate()
+ // biome-ignore lint/correctness/useExhaustiveDependencies:
+ useEffect(() => {
+ let index = currentPageIndex
+ let isListening = false
+ const keydown = (event: KeyboardEvent) => {
+ if (event.code === 'ShiftLeft') isListening = true
+ if (isListening) {
+ const nextPage = flattenedSidebar[index + 1]
+ const prevPage = flattenedSidebar[index - 1]
+ if (event.code === 'ArrowRight' && nextPage?.link) {
+ navigate(nextPage.link)
+ index++
+ }
+ if (event.code === 'ArrowLeft' && prevPage?.link) {
+ navigate(prevPage.link)
+ index--
+ }
+ }
+ }
+ const keyup = (event: KeyboardEvent) => {
+ if (event.code === 'ShiftLeft') isListening = false
+ }
+
+ window.addEventListener('keydown', keydown)
+ window.addEventListener('keyup', keyup)
+ return () => {
+ window.removeEventListener('keydown', keydown)
+ window.removeEventListener('keyup', keyup)
+ }
+ }, [])
+
+ if (!mounted) return null
+ return (
+
+ {prevPage ? (
+
+
+
+
+
+
{prevPage.text}
+
+ {/* TODO: Place in hover card */}
+
+
+ ) : (
+
+ )}
+ {nextPage ? (
+
+
+
+ {nextPage.text}
+
+
+
+
+
+ {/* TODO: Place in hover card */}
+
+
+ ) : (
+
+ )}
+
+ )
+}
+
+function flattenSidebar(sidebar: SidebarItem[]) {
+ const items: SidebarItem[] = []
+
+ for (const item of sidebar) {
+ if (item.link) {
+ items.push(item)
+ }
+ if (item.items) {
+ items.push(...flattenSidebar(item.items))
+ }
+ }
+
+ return items
+}
diff --git a/app/components/HomePage.css.ts b/app/components/HomePage.css.ts
new file mode 100644
index 00000000..44c6da37
--- /dev/null
+++ b/app/components/HomePage.css.ts
@@ -0,0 +1,117 @@
+import { style } from '@vanilla-extract/css'
+import {
+ fontFamilyVars,
+ fontSizeVars,
+ fontWeightVars,
+ lineHeightVars,
+ primitiveColorVars,
+ spaceVars,
+ viewportVars,
+} from '../styles/vars.css.js'
+
+export const root = style({
+ alignItems: 'center',
+ display: 'flex',
+ flexDirection: 'column',
+ paddingTop: spaceVars['64'],
+ textAlign: 'center',
+ gap: spaceVars['32'],
+ '@media': {
+ [viewportVars['max-720px']]: {
+ paddingTop: spaceVars['32'],
+ },
+ },
+})
+
+export const logo = style(
+ {
+ display: 'flex',
+ justifyContent: 'center',
+ height: '48px',
+ '@media': {
+ [viewportVars['max-720px']]: {
+ height: '36px',
+ },
+ },
+ },
+ 'logo',
+)
+
+export const title = style(
+ {
+ fontSize: fontSizeVars['64'],
+ fontWeight: fontWeightVars.semibold,
+ lineHeight: '1em',
+ },
+ 'title',
+)
+
+export const tagline = style(
+ {
+ color: primitiveColorVars.text2,
+ fontSize: fontSizeVars['20'],
+ fontWeight: fontWeightVars.medium,
+ lineHeight: '1.5em',
+ selectors: {
+ [`${title} + &`]: {
+ marginTop: `calc(-1 * ${spaceVars['8']})`,
+ },
+ },
+ },
+ 'tagline',
+)
+
+export const description = style(
+ {
+ color: primitiveColorVars.text,
+ fontSize: fontSizeVars['16'],
+ fontWeight: fontWeightVars.regular,
+ lineHeight: lineHeightVars.paragraph,
+ selectors: {
+ [`${tagline} + &`]: {
+ marginTop: `calc(-1 * ${spaceVars['8']})`,
+ },
+ },
+ },
+ 'description',
+)
+
+export const buttons = style(
+ {
+ display: 'flex',
+ gap: spaceVars['16'],
+ },
+ 'buttons',
+)
+
+export const button = style({}, 'button')
+
+export const tabs = style(
+ {
+ minWidth: '300px',
+ },
+ 'tabs',
+)
+
+export const tabsList = style(
+ {
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ 'tabsList',
+)
+
+export const tabsContent = style(
+ {
+ color: primitiveColorVars.text2,
+ fontFamily: fontFamilyVars.mono,
+ },
+ 'tabsContent',
+)
+
+export const packageManager = style(
+ {
+ color: primitiveColorVars.textAccent,
+ },
+ 'packageManager',
+)
diff --git a/app/components/HomePage.tsx b/app/components/HomePage.tsx
new file mode 100644
index 00000000..98789e20
--- /dev/null
+++ b/app/components/HomePage.tsx
@@ -0,0 +1,71 @@
+import clsx from 'clsx'
+import type { ReactNode } from 'react'
+
+import { useConfig } from '../hooks/useConfig.js'
+import { Button as Button_, type ButtonProps } from './Button.js'
+import * as styles from './HomePage.css.js'
+import { Logo as Logo_ } from './Logo.js'
+import * as Tabs from './Tabs.js'
+
+export type HomePageProps = {
+ description?: ReactNode
+ tagline?: ReactNode
+}
+
+export function Root({ children, className }: { children: ReactNode; className?: string }) {
+ return {children}
+}
+
+export function Logo({ className }: { className?: string }) {
+ const { logoUrl, title } = useConfig()
+ return logoUrl ? (
+
+
+
+ ) : (
+ {title}
+ )
+}
+
+export function Tagline({ children, className }: { children: ReactNode; className?: string }) {
+ return {children}
+}
+
+export function Description({ children, className }: { children: ReactNode; className?: string }) {
+ return {children}
+}
+
+export function Buttons({ children, className }: { children: ReactNode; className?: string }) {
+ return {children}
+}
+
+export function Button(props: ButtonProps) {
+ return
+}
+
+export function InstallPackage({
+ name,
+ type = 'install',
+}: { children: ReactNode; className?: string; name: string; type?: 'install' | 'init' }) {
+ return (
+
+
+ npm
+ pnpm
+ yarn
+
+
+ npm {type === 'init' ? 'init' : 'install'}{' '}
+ {name}
+
+
+ pnpm {type === 'init' ? 'create' : 'add'}{' '}
+ {name}
+
+
+ yarn {type === 'init' ? 'create' : 'add'}{' '}
+ {name}
+
+
+ )
+}
diff --git a/app/components/Icon.css.ts b/app/components/Icon.css.ts
new file mode 100644
index 00000000..9df2655e
--- /dev/null
+++ b/app/components/Icon.css.ts
@@ -0,0 +1,11 @@
+import { createVar, style } from '@vanilla-extract/css'
+
+export const sizeVar = createVar('size')
+export const srcVar = createVar('src')
+
+export const root = style({
+ alignItems: 'center',
+ display: 'flex',
+ height: sizeVar,
+ width: sizeVar,
+})
diff --git a/app/components/Icon.tsx b/app/components/Icon.tsx
new file mode 100644
index 00000000..6273536d
--- /dev/null
+++ b/app/components/Icon.tsx
@@ -0,0 +1,28 @@
+import { assignInlineVars } from '@vanilla-extract/dynamic'
+
+import clsx from 'clsx'
+import * as styles from './Icon.css.js'
+
+export type IconProps = {
+ className?: string
+ label: string
+ icon: React.ElementType
+ size?: string
+ style?: React.CSSProperties
+}
+
+export function Icon({ className, label, icon: Icon, size, style }: IconProps) {
+ return (
+
+
+
+ )
+}
diff --git a/app/components/KeyboardShortcut.css.ts b/app/components/KeyboardShortcut.css.ts
new file mode 100644
index 00000000..1cb93f4b
--- /dev/null
+++ b/app/components/KeyboardShortcut.css.ts
@@ -0,0 +1,24 @@
+import { style } from '@vanilla-extract/css'
+
+import { fontSizeVars, spaceVars, viewportVars } from '../styles/vars.css.js'
+
+export const root = style({
+ alignItems: 'center',
+ display: 'inline-flex',
+ gap: spaceVars[6],
+ fontSize: fontSizeVars[12],
+ '@media': {
+ [viewportVars['max-720px']]: {
+ display: 'none',
+ },
+ },
+})
+
+export const kbdGroup = style(
+ {
+ alignItems: 'center',
+ display: 'inline-flex',
+ gap: spaceVars[3],
+ },
+ 'kbdGroup',
+)
diff --git a/app/components/KeyboardShortcut.tsx b/app/components/KeyboardShortcut.tsx
new file mode 100644
index 00000000..ce79aac5
--- /dev/null
+++ b/app/components/KeyboardShortcut.tsx
@@ -0,0 +1,21 @@
+import { Kbd } from './mdx/Kbd.js'
+
+import * as styles from './KeyboardShortcut.css.js'
+
+export function KeyboardShortcut(props: {
+ description: string
+ keys: string[]
+}) {
+ const { description, keys } = props
+ return (
+
+ {description}
+
+
+ {keys.map((key) => (
+ {key}
+ ))}
+
+
+ )
+}
diff --git a/app/components/Link.css.ts b/app/components/Link.css.ts
new file mode 100644
index 00000000..0b103a7b
--- /dev/null
+++ b/app/components/Link.css.ts
@@ -0,0 +1,36 @@
+import { style } from '@vanilla-extract/css'
+
+import {
+ fontWeightVars,
+ primitiveColorVars,
+ semanticColorVars,
+ spaceVars,
+} from '../styles/vars.css.js'
+import { arrowColor } from './ExternalLink.css.js'
+
+export const root = style({})
+
+export const accent_underlined = style(
+ {
+ color: semanticColorVars.link,
+ fontWeight: fontWeightVars.medium,
+ textUnderlineOffset: spaceVars['2'],
+ textDecoration: 'underline',
+ transition: 'color 0.1s',
+ selectors: {
+ '&:hover': {
+ color: semanticColorVars.linkHover,
+ },
+ },
+ },
+ 'accent_underlined',
+)
+
+export const styleless = style(
+ {
+ vars: {
+ [arrowColor]: primitiveColorVars.text3,
+ },
+ },
+ 'styleless',
+)
diff --git a/app/components/Link.tsx b/app/components/Link.tsx
new file mode 100644
index 00000000..254def65
--- /dev/null
+++ b/app/components/Link.tsx
@@ -0,0 +1,55 @@
+import { clsx } from 'clsx'
+import { forwardRef } from 'react'
+
+import { useLocation } from 'react-router-dom'
+import { ExternalLink } from './ExternalLink.js'
+import * as styles from './Link.css.js'
+import { RouterLink, type RouterLinkProps } from './RouterLink.js'
+
+type LinkProps = {
+ children: React.ReactNode
+ className?: string
+ hideExternalIcon?: boolean
+ onClick?: () => void
+ href?: string
+ variant?: 'accent underlined' | 'styleless'
+}
+
+export const Link = forwardRef((props: LinkProps, ref) => {
+ const { href, variant = 'accent underlined' } = props
+
+ const { pathname } = useLocation()
+
+ // External links
+ if (href?.match(/^(www|https?)/))
+ return (
+
+ )
+
+ // Internal links
+ const [before, after] = (href || '').split('#')
+ const to = `${before ? before : pathname}${after ? `#${after}` : ''}`
+ return (
+
+ )
+})
diff --git a/app/components/Logo.css.ts b/app/components/Logo.css.ts
new file mode 100644
index 00000000..15cb213d
--- /dev/null
+++ b/app/components/Logo.css.ts
@@ -0,0 +1,13 @@
+import { globalStyle, style } from '@vanilla-extract/css'
+
+export const root = style({})
+
+export const logoDark = style({}, 'logoDark')
+globalStyle(`:root:not(.dark) ${logoDark}`, {
+ display: 'none',
+})
+
+export const logoLight = style({}, 'logoLight')
+globalStyle(`:root.dark ${logoLight}`, {
+ display: 'none',
+})
diff --git a/app/components/Logo.tsx b/app/components/Logo.tsx
new file mode 100644
index 00000000..b9d7d642
--- /dev/null
+++ b/app/components/Logo.tsx
@@ -0,0 +1,30 @@
+import clsx from 'clsx'
+
+import { useConfig } from '../hooks/useConfig.js'
+import * as styles from './Logo.css.js'
+
+export function Logo({ className }: { className?: string }) {
+ const { logoUrl } = useConfig()
+
+ if (!logoUrl) return null
+ return (
+ <>
+ {typeof logoUrl === 'string' ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+ >
+ )
+}
diff --git a/app/components/MobileSearch.css.ts b/app/components/MobileSearch.css.ts
new file mode 100644
index 00000000..63bc1f79
--- /dev/null
+++ b/app/components/MobileSearch.css.ts
@@ -0,0 +1,15 @@
+import { style } from '@vanilla-extract/css'
+
+import { primitiveColorVars, spaceVars } from '../styles/vars.css.js'
+
+export const searchButton = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ color: primitiveColorVars.text,
+ height: spaceVars[28],
+ justifyContent: 'center',
+ width: spaceVars[28],
+ },
+ 'searchButton',
+)
diff --git a/app/components/MobileSearch.tsx b/app/components/MobileSearch.tsx
new file mode 100644
index 00000000..a14950d2
--- /dev/null
+++ b/app/components/MobileSearch.tsx
@@ -0,0 +1,22 @@
+import { useState } from 'react'
+import { MagnifyingGlassIcon } from '@radix-ui/react-icons'
+import * as Dialog from '@radix-ui/react-dialog'
+
+import * as styles from './MobileSearch.css.js'
+import { SearchDialog } from './SearchDialog.js'
+
+export function MobileSearch() {
+ const [open, setOpen] = useState(false)
+
+ return (
+
+
+
+
+
+
+
+ setOpen(false)} />
+
+ )
+}
diff --git a/app/components/MobileTopNav.css.ts b/app/components/MobileTopNav.css.ts
new file mode 100644
index 00000000..68eb19b1
--- /dev/null
+++ b/app/components/MobileTopNav.css.ts
@@ -0,0 +1,322 @@
+import { createVar, keyframes, style } from '@vanilla-extract/css'
+import {
+ borderRadiusVars,
+ contentVars,
+ fontSizeVars,
+ fontWeightVars,
+ lineHeightVars,
+ primitiveColorVars,
+ sidebarVars,
+ spaceVars,
+ topNavVars,
+ viewportVars,
+} from '../styles/vars.css.js'
+
+const fadeIn = keyframes(
+ {
+ from: {
+ opacity: 0,
+ },
+ to: {
+ opacity: 1,
+ },
+ },
+ 'fadeIn',
+)
+
+export const root = style({
+ alignItems: 'center',
+ backgroundColor: primitiveColorVars.backgroundDark,
+ borderBottom: `1px solid ${primitiveColorVars.border}`,
+ display: 'none',
+ height: '100%',
+ justifyContent: 'space-between',
+ padding: `${spaceVars['0']} ${contentVars.horizontalPadding}`,
+ width: '100%',
+ '@media': {
+ [viewportVars['max-1080px']]: {
+ display: 'flex',
+ },
+ },
+})
+
+export const button = style(
+ {
+ borderRadius: borderRadiusVars[4],
+ padding: spaceVars[8],
+ },
+ 'button',
+)
+
+export const content = style(
+ {
+ left: `calc(-1 * ${spaceVars['24']})`,
+ },
+ 'content',
+)
+
+export const curtain = style(
+ {
+ alignItems: 'center',
+ backgroundColor: primitiveColorVars.backgroundDark,
+ borderBottom: `1px solid ${primitiveColorVars.border}`,
+ display: 'none',
+ justifyContent: 'space-between',
+ fontSize: fontSizeVars['13'],
+ fontWeight: fontWeightVars.medium,
+ height: '100%',
+ padding: `${spaceVars['0']} ${contentVars.horizontalPadding}`,
+ width: '100%',
+ '@media': {
+ [viewportVars['max-1080px']]: {
+ display: 'flex',
+ },
+ },
+ },
+ 'curtain',
+)
+
+export const curtainGroup = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ gap: spaceVars['12'],
+ },
+ 'curtainGroup',
+)
+
+export const curtainItem = style({}, 'curtainItem')
+
+export const divider = style(
+ {
+ backgroundColor: primitiveColorVars.border,
+ height: '35%',
+ width: '1px',
+ },
+ 'divider',
+)
+
+export const group = style({ alignItems: 'center', display: 'flex', height: '100%' }, 'group')
+
+export const icon = style(
+ {
+ color: primitiveColorVars.text2,
+ transition: 'color 0.1s',
+ selectors: {
+ [`${button}:hover &`]: {
+ color: primitiveColorVars.text,
+ },
+ },
+ },
+ 'icon',
+)
+
+export const item = style(
+ {
+ position: 'relative',
+ },
+ 'item',
+)
+
+export const logo = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ height: topNavVars.height,
+ },
+ 'logo',
+)
+
+export const logoImage = style(
+ {
+ height: '30%',
+ },
+ 'logoImage',
+)
+
+export const menuTrigger = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ gap: spaceVars['8'],
+ },
+ 'menuTrigger',
+)
+
+export const menuTitle = style(
+ {
+ maxWidth: '22ch',
+ overflow: 'hidden',
+ textAlign: 'left',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'pre',
+ },
+ 'menuTitle',
+)
+
+export const navigation_compact = style({}, 'navigation_compact')
+
+export const navigation = style(
+ {
+ marginLeft: spaceVars[8],
+ selectors: {
+ [`&:not(${navigation_compact})`]: {
+ '@media': {
+ [viewportVars['max-720px']]: {
+ display: 'none',
+ },
+ },
+ },
+ [`&${navigation_compact}`]: {
+ '@media': {
+ [viewportVars['min-720px']]: {
+ display: 'none',
+ },
+ },
+ },
+ },
+ },
+ 'navigation',
+)
+
+export const navigationContent = style(
+ {
+ display: 'flex',
+ flexDirection: 'column',
+ marginLeft: spaceVars[8],
+ },
+ 'navigationContent',
+)
+
+export const navigationItem = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ justifyContent: 'flex-start',
+ fontSize: fontSizeVars[14],
+ fontWeight: fontWeightVars.medium,
+ width: '100%',
+ selectors: {
+ '&:hover': { color: primitiveColorVars.textAccent },
+ '&[data-active="true"]': { color: primitiveColorVars.textAccent },
+ '&[data-state="open"]': { color: primitiveColorVars.textAccent },
+ },
+ },
+ 'navigationItem',
+)
+
+export const chevronDownIcon = createVar('chevronDownIcon')
+export const chevronUpIcon = createVar('chevronUpIcon')
+
+export const navigationTrigger = style(
+ {
+ selectors: {
+ '&::after': {
+ backgroundColor: 'currentColor',
+ content: '',
+ display: 'inline-block',
+ height: '0.625em',
+ marginLeft: '0.325em',
+ width: '0.625em',
+ mask: `${chevronDownIcon} no-repeat center / contain`,
+ },
+ '&[data-state="open"]::after': {
+ mask: `${chevronUpIcon} no-repeat center / contain`,
+ },
+ },
+ },
+ 'trigger',
+)
+
+export const outlineTrigger = style(
+ {
+ animation: `${fadeIn} 500ms cubic-bezier(0.16, 1, 0.3, 1)`,
+ alignItems: 'center',
+ color: primitiveColorVars.text2,
+ display: 'flex',
+ gap: spaceVars['6'],
+ selectors: {
+ '&[data-state="open"]': {
+ color: primitiveColorVars.textAccent,
+ },
+ },
+ },
+ 'outlineTrigger',
+)
+
+export const outlinePopover = style(
+ {
+ display: 'none',
+ overflowY: 'scroll',
+ padding: spaceVars['16'],
+ maxHeight: '80vh',
+ '@media': {
+ [viewportVars['max-1080px']]: {
+ display: 'block',
+ maxWidth: '300px',
+ },
+ },
+ },
+ 'outlinePopover',
+)
+
+export const section = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ height: '100%',
+ gap: spaceVars[16],
+ },
+ 'section',
+)
+
+export const separator = style(
+ {
+ backgroundColor: primitiveColorVars.border,
+ height: '1.75em',
+ width: '1px',
+ },
+ 'separator',
+)
+
+export const sidebarPopover = style(
+ {
+ display: 'none',
+ overflowY: 'scroll',
+ padding: `0 ${sidebarVars.horizontalPadding}`,
+ maxHeight: '80vh',
+ width: sidebarVars.width,
+ '@media': {
+ [viewportVars['max-1080px']]: {
+ display: 'block',
+ },
+ },
+ },
+ 'sidebarPopover',
+)
+
+export const title = style(
+ {
+ fontSize: fontSizeVars['18'],
+ fontWeight: fontWeightVars.semibold,
+ lineHeight: lineHeightVars.heading,
+ },
+ 'title',
+)
+
+export const topNavPopover = style(
+ {
+ display: 'none',
+ overflowY: 'scroll',
+ padding: `${sidebarVars.verticalPadding} ${sidebarVars.horizontalPadding}`,
+ maxHeight: '80vh',
+ width: sidebarVars.width,
+ '@media': {
+ [viewportVars['max-1080px']]: {
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ },
+ },
+ 'topNavPopover',
+)
diff --git a/app/components/MobileTopNav.tsx b/app/components/MobileTopNav.tsx
new file mode 100644
index 00000000..963899a1
--- /dev/null
+++ b/app/components/MobileTopNav.tsx
@@ -0,0 +1,321 @@
+import * as Accordion from '@radix-ui/react-accordion'
+import { assignInlineVars } from '@vanilla-extract/dynamic'
+import clsx from 'clsx'
+import { type ComponentType, useMemo, useState } from 'react'
+import { useLocation } from 'react-router-dom'
+
+import type * as Config from '../../config.js'
+import { useActiveNavIds } from '../hooks/useActiveNavIds.js'
+import { useConfig } from '../hooks/useConfig.js'
+import { useLayout } from '../hooks/useLayout.js'
+import { usePageData } from '../hooks/usePageData.js'
+import { useSidebar } from '../hooks/useSidebar.js'
+import { Icon } from './Icon.js'
+import { Link } from './Link.js'
+import { MobileSearch } from './MobileSearch.js'
+import * as styles from './MobileTopNav.css.js'
+import { NavLogo } from './NavLogo.js'
+import * as NavigationMenu from './NavigationMenu.js'
+import { Outline } from './Outline.js'
+import { Popover } from './Popover.js'
+import { RouterLink } from './RouterLink.js'
+import { Sidebar } from './Sidebar.js'
+import { ChevronDown } from './icons/ChevronDown.js'
+import { ChevronRight } from './icons/ChevronRight.js'
+import { ChevronUp } from './icons/ChevronUp.js'
+import { Discord } from './icons/Discord.js'
+import { GitHub } from './icons/GitHub.js'
+import { Menu } from './icons/Menu.js'
+import { Telegram } from './icons/Telegram.js'
+import { Warpcast } from './icons/Warpcast.js'
+import { X } from './icons/X.js'
+
+MobileTopNav.Curtain = Curtain
+
+export function MobileTopNav() {
+ const config = useConfig()
+ const { showLogo } = useLayout()
+
+ return (
+
+
+ {showLogo && (
+
+ )}
+ {config.topNav && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+ {config.socials && config.socials?.length > 0 && (
+ <>
+
+
+ {config.socials?.map((social, i) => (
+
+ ))}
+
+ >
+ )}
+
+
+ )
+}
+
+function Navigation({ items }: { items: Config.ParsedTopNavItem[] }) {
+ const { pathname } = useLocation()
+ const activeIds = useActiveNavIds({ pathname, items })
+ return (
+
+
+ {items.map((item, i) =>
+ item?.link ? (
+
+ {item.text}
+
+ ) : (
+
+
+ {item.text}
+
+
+
+
+
+ ),
+ )}
+
+
+ )
+}
+
+function NavigationMenuContent({ items }: { items: Config.ParsedTopNavItem[] }) {
+ const { pathname } = useLocation()
+ const activeIds = useActiveNavIds({ pathname, items })
+ return (
+
+ {items?.map((item, i) => (
+
+ {item.text}
+
+ ))}
+
+ )
+}
+
+function CompactNavigation({ items }: { items: Config.ParsedTopNavItem[] }) {
+ const [showPopover, setShowPopover] = useState(false)
+
+ const { pathname } = useLocation()
+ const activeIds = useActiveNavIds({ pathname, items })
+ const activeItem = items.filter((item) => item.id === activeIds[0])[0]
+
+ const { basePath } = useConfig()
+ const assetBasePath = import.meta.env.PROD ? basePath : ''
+
+ return (
+
+ {activeItem ? (
+
+
+ {activeItem.text}
+
+
+
+
+ {items.map((item, i) =>
+ item?.link ? (
+ setShowPopover(false)}
+ variant="styleless"
+ >
+ {item.text}
+
+ ) : (
+
+
+ {item.text}
+
+
+ {item.items?.map((item, i) => (
+ setShowPopover(false)}
+ variant="styleless"
+ >
+ {item.text}
+
+ ))}
+
+
+ ),
+ )}
+
+
+
+ ) : items[0]?.link ? (
+
+ {items[0].text}
+
+ ) : null}
+
+ )
+}
+
+const iconsForIcon = {
+ discord: Discord,
+ github: GitHub,
+ telegram: Telegram,
+ warpcast: Warpcast,
+ x: X,
+} satisfies Record
+
+const sizesForTypes = {
+ discord: '21px',
+ github: '18px',
+ telegram: '21px',
+ warpcast: '18px',
+ x: '16px',
+} satisfies Record
+
+function SocialButton({ icon, label, link, type }: Config.ParsedSocialItem) {
+ return (
+
+
+
+ )
+}
+
+export function Curtain({
+ enableScrollToTop,
+}: {
+ enableScrollToTop?: boolean
+}) {
+ const { pathname } = useLocation()
+ const { layout, showSidebar } = useLayout()
+ const { frontmatter = {} } = usePageData()
+ const sidebar = useSidebar()
+
+ const [isOutlineOpen, setOutlineOpen] = useState(false)
+ const [isSidebarOpen, setSidebarOpen] = useState(false)
+
+ const sidebarItemTitle = useMemo(() => {
+ if (!sidebar || layout === 'minimal') return
+ const sidebarItem = getSidebarItemFromPathname({
+ sidebarItems: sidebar.items,
+ pathname,
+ })
+ return sidebarItem?.text
+ }, [layout, pathname, sidebar])
+
+ const contentTitle = useMemo(() => {
+ if (typeof window === 'undefined') return
+ return document.querySelector('.vocs_Content h1')?.textContent
+ }, [])
+
+ const title = sidebarItemTitle || frontmatter.title || contentTitle
+
+ return (
+
+
+
+ {showSidebar ? (
+
+
+
+ {title}
+
+
+ setSidebarOpen(false)} />
+
+
+ ) : (
+ title
+ )}
+
+
+
+ {enableScrollToTop && (
+ <>
+
+ window.scrollTo({ behavior: 'smooth', top: 0 })}
+ type="button"
+ >
+ Top
+
+
+
+
+ >
+ )}
+ {layout === 'docs' && (
+
+
+
+ On this page
+
+
+
+ setOutlineOpen(false)} showTitle={false} />
+
+
+
+ )}
+
+
+ )
+}
+
+function getSidebarItemFromPathname({
+ sidebarItems,
+ pathname: pathname_,
+}: { sidebarItems: Config.SidebarItem[]; pathname: string }): Config.SidebarItem | undefined {
+ const pathname = pathname_.replace(/(.+)\/$/, '$1')
+ for (const item of sidebarItems) {
+ if (item?.link === pathname) return item
+ if (item.items) {
+ const childItem = getSidebarItemFromPathname({ sidebarItems: item.items, pathname })
+ if (childItem) return childItem
+ }
+ }
+ return undefined
+}
diff --git a/app/components/NavLogo.css.ts b/app/components/NavLogo.css.ts
new file mode 100644
index 00000000..446cb342
--- /dev/null
+++ b/app/components/NavLogo.css.ts
@@ -0,0 +1,19 @@
+import { style } from '@vanilla-extract/css'
+import { fontSizeVars, fontWeightVars, lineHeightVars } from '../styles/vars.css.js'
+
+export const logoImage = style(
+ {
+ height: '50%',
+ width: 'auto',
+ },
+ 'logoImage',
+)
+
+export const title = style(
+ {
+ fontSize: fontSizeVars['18'],
+ fontWeight: fontWeightVars.semibold,
+ lineHeight: lineHeightVars.heading,
+ },
+ 'title',
+)
diff --git a/app/components/NavLogo.tsx b/app/components/NavLogo.tsx
new file mode 100644
index 00000000..a63e0118
--- /dev/null
+++ b/app/components/NavLogo.tsx
@@ -0,0 +1,10 @@
+import { useConfig } from '../hooks/useConfig.js'
+import { Logo } from './Logo.js'
+import * as styles from './NavLogo.css.js'
+
+export function NavLogo() {
+ const config = useConfig()
+
+ if (config.logoUrl) return
+ return {config.title}
+}
diff --git a/app/components/NavigationMenu.css.ts b/app/components/NavigationMenu.css.ts
new file mode 100644
index 00000000..c2f3ec37
--- /dev/null
+++ b/app/components/NavigationMenu.css.ts
@@ -0,0 +1,91 @@
+import { createVar, keyframes, style } from '@vanilla-extract/css'
+import {
+ borderRadiusVars,
+ fontSizeVars,
+ fontWeightVars,
+ primitiveColorVars,
+ spaceVars,
+ zIndexVars,
+} from '../styles/vars.css.js'
+
+const fadeIn = keyframes(
+ {
+ from: {
+ opacity: 0,
+ transform: 'translateY(-6px)',
+ },
+ to: {
+ opacity: 1,
+ transform: 'translateY(0px)',
+ },
+ },
+ 'fadeIn',
+)
+
+export const root = style({})
+
+export const list = style(
+ {
+ display: 'flex',
+ gap: spaceVars[20],
+ },
+ 'list',
+)
+
+export const link = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ fontSize: fontSizeVars[14],
+ fontWeight: fontWeightVars.medium,
+ height: '100%',
+ selectors: {
+ '&:hover': { color: primitiveColorVars.textAccent },
+ '&[data-active="true"]': { color: primitiveColorVars.textAccent },
+ },
+ },
+ 'link',
+)
+
+export const item = style({}, 'item')
+
+export const chevronDownIcon = createVar('chevronDownIcon')
+
+export const trigger = style(
+ [
+ link,
+ {
+ selectors: {
+ '&::after': {
+ backgroundColor: 'currentColor',
+ content: '',
+ color: primitiveColorVars.text3,
+ display: 'inline-block',
+ height: '0.625em',
+ marginLeft: '0.325em',
+ width: '0.625em',
+ mask: `${chevronDownIcon} no-repeat center / contain`,
+ },
+ },
+ },
+ ],
+ 'trigger',
+)
+
+export const content = style(
+ {
+ backgroundColor: primitiveColorVars.background2,
+ border: `1px solid ${primitiveColorVars.border}`,
+ borderRadius: borderRadiusVars[4],
+ boxShadow: `0 3px 10px ${primitiveColorVars.shadow}`,
+ display: 'flex',
+ flexDirection: 'column',
+ padding: `${spaceVars['12']} ${spaceVars['16']}`,
+ position: 'absolute',
+ top: `calc(100% + ${spaceVars['8']})`,
+ minWidth: '200px',
+ zIndex: zIndexVars.popover,
+ animation: `${fadeIn} 500ms cubic-bezier(0.16, 1, 0.3, 1)`,
+ },
+ 'content',
+)
diff --git a/app/components/NavigationMenu.tsx b/app/components/NavigationMenu.tsx
new file mode 100644
index 00000000..40f776d6
--- /dev/null
+++ b/app/components/NavigationMenu.tsx
@@ -0,0 +1,68 @@
+import * as NavigationMenu from '@radix-ui/react-navigation-menu'
+import { assignInlineVars } from '@vanilla-extract/dynamic'
+import clsx from 'clsx'
+import type { ReactNode } from 'react'
+
+import { useConfig } from '../hooks/useConfig.js'
+import { Link as Link_ } from './Link.js'
+import * as styles from './NavigationMenu.css.js'
+
+export const Root = (props: NavigationMenu.NavigationMenuProps) => (
+
+)
+
+export const List = (props: NavigationMenu.NavigationMenuListProps) => (
+
+)
+
+export const Link = ({
+ active,
+ children,
+ className,
+ href,
+}: {
+ active?: boolean
+ children: ReactNode
+ className?: string
+ href?: string
+}) => (
+
+
+ {children}
+
+
+)
+
+export const Item = (props: NavigationMenu.NavigationMenuItemProps) => (
+
+)
+
+export const Trigger = ({
+ active,
+ className,
+ ...props
+}: NavigationMenu.NavigationMenuTriggerProps & {
+ active?: boolean
+}) => {
+ const { basePath } = useConfig()
+ const assetBasePath = import.meta.env.PROD ? basePath : ''
+ return (
+
+ )
+}
+
+export const Content = (props: NavigationMenu.NavigationMenuContentProps) => (
+
+)
diff --git a/app/components/NotFound.css.ts b/app/components/NotFound.css.ts
new file mode 100644
index 00000000..ce4ca1ba
--- /dev/null
+++ b/app/components/NotFound.css.ts
@@ -0,0 +1,19 @@
+import { style } from '@vanilla-extract/css'
+import { primitiveColorVars, spaceVars } from '../styles/vars.css.js'
+
+export const root = style({
+ alignItems: 'center',
+ display: 'flex',
+ flexDirection: 'column',
+ maxWidth: '400px',
+ margin: '0 auto',
+ paddingTop: spaceVars['64'],
+})
+
+export const divider = style(
+ {
+ borderColor: primitiveColorVars.border,
+ width: '50%',
+ },
+ 'divider',
+)
diff --git a/app/components/NotFound.tsx b/app/components/NotFound.tsx
new file mode 100644
index 00000000..5463eae0
--- /dev/null
+++ b/app/components/NotFound.tsx
@@ -0,0 +1,19 @@
+import { spaceVars } from '../styles/vars.css.js'
+import { Link } from './Link.js'
+import * as styles from './NotFound.css.js'
+import { H1 } from './mdx/H1.js'
+import { Paragraph } from './mdx/Paragraph.js'
+
+export function NotFound() {
+ return (
+
+
Page Not Found
+
+
+
+
The page you were looking for could not be found.
+
+
Go to Home Page
+
+ )
+}
diff --git a/app/components/Outline.css.ts b/app/components/Outline.css.ts
new file mode 100644
index 00000000..be2e841c
--- /dev/null
+++ b/app/components/Outline.css.ts
@@ -0,0 +1,85 @@
+import { style } from '@vanilla-extract/css'
+
+import { gutterRight as DocsLayout_gutterRight } from '../layouts/DocsLayout.css.js'
+
+import {
+ fontSizeVars,
+ fontWeightVars,
+ lineHeightVars,
+ primitiveColorVars,
+ spaceVars,
+} from '../styles/vars.css.js'
+
+export const root = style({
+ width: '100%',
+})
+
+export const nav = style(
+ {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spaceVars[8],
+ selectors: {
+ [`${DocsLayout_gutterRight} &`]: {
+ borderLeft: `1px solid ${primitiveColorVars.border}`,
+ paddingLeft: spaceVars[16],
+ },
+ },
+ },
+ 'nav',
+)
+
+export const heading = style(
+ {
+ color: primitiveColorVars.title,
+ fontSize: fontSizeVars[13],
+ fontWeight: fontWeightVars.semibold,
+ lineHeight: lineHeightVars.heading,
+ letterSpacing: '0.025em',
+ },
+ 'heading',
+)
+
+export const items = style(
+ {
+ selectors: {
+ '& &': {
+ paddingLeft: spaceVars[12],
+ },
+ },
+ },
+ 'items',
+)
+
+export const item = style(
+ {
+ lineHeight: lineHeightVars.outlineItem,
+ marginBottom: spaceVars[8],
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ // @ts-expect-error
+ textWrap: 'nowrap',
+ },
+ 'item',
+)
+
+export const link = style(
+ {
+ color: primitiveColorVars.text2,
+ fontWeight: fontWeightVars.medium,
+ fontSize: fontSizeVars[13],
+ transition: 'color 0.1s',
+ selectors: {
+ '&[data-active="true"]': {
+ color: primitiveColorVars.textAccent,
+ },
+ '&[data-active="true"]:hover': {
+ color: primitiveColorVars.textAccentHover,
+ },
+ '&:hover': {
+ color: primitiveColorVars.text,
+ },
+ },
+ },
+ 'link',
+)
diff --git a/app/components/Outline.tsx b/app/components/Outline.tsx
new file mode 100644
index 00000000..cf9024a5
--- /dev/null
+++ b/app/components/Outline.tsx
@@ -0,0 +1,254 @@
+import { Fragment, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
+import { Link, useLocation } from 'react-router-dom'
+
+import { useConfig } from '../hooks/useConfig.js'
+import { useLayout } from '../hooks/useLayout.js'
+import { debounce } from '../utils/debounce.js'
+import { deserializeElement } from '../utils/deserializeElement.js'
+import * as styles from './Outline.css.js'
+import { root as Heading, slugTarget } from './mdx/Heading.css.js'
+
+type OutlineItems = {
+ id: string
+ level: number
+ slugTargetElement: Element
+ topOffset: number
+ text: string | null
+}[]
+
+export function Outline({
+ minLevel = 2,
+ maxLevel: maxLevel_ = 3,
+ highlightActive = true,
+ onClickItem,
+ showTitle = true,
+}: {
+ minLevel?: number
+ maxLevel?: number
+ highlightActive?: boolean
+ onClickItem?: () => void
+ showTitle?: boolean
+} = {}) {
+ const { outlineFooter } = useConfig()
+
+ const { showOutline } = useLayout()
+ const maxLevel = (() => {
+ if (typeof showOutline === 'number') return minLevel + showOutline - 1
+ return maxLevel_
+ })()
+
+ const active = useRef(true)
+
+ const { pathname, hash } = useLocation()
+
+ const [headingElements, setHeadingElements] = useState([])
+ // biome-ignore lint/correctness/useExhaustiveDependencies:
+ useEffect(() => {
+ if (typeof window === 'undefined') return
+ const elements = Array.from(document.querySelectorAll(`.${Heading}`))
+ setHeadingElements(elements)
+ }, [pathname])
+
+ const items = useMemo(() => {
+ if (!headingElements) return []
+
+ return headingElements
+ .map((element) => {
+ const slugTargetElement = element.querySelector(`.${slugTarget}`)
+ if (!slugTargetElement) return null
+
+ const box = slugTargetElement.getBoundingClientRect()
+
+ const id = slugTargetElement.id
+ const level = Number(element.tagName[1])
+ const text = element.textContent
+ const topOffset = window.scrollY + box.top
+
+ if (level < minLevel || level > maxLevel) return null
+
+ return {
+ id,
+ level,
+ slugTargetElement,
+ text,
+ topOffset,
+ }
+ })
+ .filter(Boolean) as OutlineItems
+ }, [headingElements, maxLevel, minLevel])
+
+ const [activeId, setActiveId] = useState(hash.replace('#', ''))
+
+ // As the user scrolls the page, we want to make the corresponding outline item active.
+ useEffect(() => {
+ if (typeof window === 'undefined') return
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (!active.current) return
+
+ const id = entry.target.id
+
+ if (entry.isIntersecting) setActiveId(id)
+ else {
+ const box = entry.target.getBoundingClientRect()
+ const isVisible = box.top > 0
+ if (!isVisible) return
+
+ const activeIndex = items.findIndex((item) => item.id === activeId)
+ const previousId = items[activeIndex - 1]?.id
+ setActiveId(previousId)
+ }
+ },
+ {
+ rootMargin: '0px 0px -95% 0px',
+ },
+ )
+
+ for (const item of items) observer.observe(item.slugTargetElement)
+
+ return () => observer.disconnect()
+ }, [activeId, items])
+
+ // When the user hits the bottom of the page, we want to make the last outline item active.
+ useEffect(() => {
+ if (typeof window === 'undefined') return
+
+ const observer = new IntersectionObserver(([entry]) => {
+ if (!active.current) return
+
+ const lastItemId = items[items.length - 1]?.id
+
+ if (entry.isIntersecting) setActiveId(lastItemId)
+ else if (activeId === lastItemId) setActiveId(items[items.length - 2].id)
+ })
+
+ observer.observe(document.querySelector('[data-bottom-observer]')!)
+
+ return () => observer.disconnect()
+ }, [activeId, items])
+
+ // Intersection observers are a bit unreliable for fast scrolling,
+ // use scroll event listener to sync active item.
+ useEffect(() => {
+ if (typeof window === 'undefined') return
+
+ const callback = debounce(() => {
+ if (!active.current) return
+
+ if (window.scrollY === 0) {
+ setActiveId(items[0]?.id)
+ return
+ }
+
+ if (
+ window.scrollY + document.documentElement.clientHeight >=
+ document.documentElement.scrollHeight
+ ) {
+ setActiveId(items[items.length - 1]?.id)
+ return
+ }
+
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i]
+ if (window.scrollY < item.topOffset) {
+ setActiveId(items[i - 1]?.id)
+ break
+ }
+ }
+ }, 100)
+
+ window.addEventListener('scroll', callback)
+ return () => window.removeEventListener('scroll', callback)
+ }, [items])
+
+ if (items.length === 0) return null
+
+ const levelItems = items.filter((item) => item.level === minLevel)
+ return (
+
+
+ {showTitle && On this page }
+ {
+ onClickItem?.()
+ active.current = false
+ setTimeout(() => {
+ active.current = true
+ }, 500)
+ }}
+ levelItems={levelItems}
+ setActiveId={setActiveId}
+ />
+
+ {deserializeElement(outlineFooter as ReactElement)}
+
+ )
+}
+
+function Items({
+ activeId,
+ items,
+ levelItems,
+ onClickItem,
+ setActiveId,
+}: {
+ activeId: string | null
+ items: OutlineItems
+ levelItems: OutlineItems
+ onClickItem?: () => void
+ setActiveId: (id: string) => void
+}) {
+ return (
+
+ {levelItems.map(({ id, level, text }) => {
+ const hash = `#${id}`
+ const isActive = activeId === id
+
+ const nextLevelItems = (() => {
+ const itemIndex = items.findIndex((item) => item.id === id)
+ const nextIndex = itemIndex + 1
+ const nextItemLevel = items[nextIndex]?.level
+ if (nextItemLevel <= level) return null
+
+ const nextItems = []
+ for (let i = nextIndex; i < items.length; i++) {
+ const item = items[i]
+ if (item.level !== nextItemLevel) break
+ nextItems.push(item)
+ }
+ return nextItems
+ })()
+
+ return (
+
+
+ {
+ onClickItem?.()
+ setActiveId(id)
+ }}
+ className={styles.link}
+ >
+ {text}
+
+
+ {nextLevelItems && (
+
+ )}
+
+ )
+ })}
+
+ )
+}
diff --git a/app/components/Popover.css.ts b/app/components/Popover.css.ts
new file mode 100644
index 00000000..dee5ec5a
--- /dev/null
+++ b/app/components/Popover.css.ts
@@ -0,0 +1,10 @@
+import { style } from '@vanilla-extract/css'
+import { borderRadiusVars, primitiveColorVars, spaceVars, zIndexVars } from '../styles/vars.css.js'
+
+export const root = style({
+ backgroundColor: primitiveColorVars.background2,
+ border: `1px solid ${primitiveColorVars.border}`,
+ borderRadius: borderRadiusVars[4],
+ margin: `0 ${spaceVars[6]}`,
+ zIndex: zIndexVars.popover,
+})
diff --git a/app/components/Popover.tsx b/app/components/Popover.tsx
new file mode 100644
index 00000000..6fb21dda
--- /dev/null
+++ b/app/components/Popover.tsx
@@ -0,0 +1,18 @@
+import * as Popover_ from '@radix-ui/react-popover'
+import type { ReactNode } from 'react'
+
+import clsx from 'clsx'
+import * as styles from './Popover.css.js'
+
+Popover.Root = Popover_.Root
+Popover.Trigger = Popover_.Trigger
+
+export function Popover({ children, className }: { children: ReactNode; className?: string }) {
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/app/components/Raw.tsx b/app/components/Raw.tsx
new file mode 100644
index 00000000..b6d5dda8
--- /dev/null
+++ b/app/components/Raw.tsx
@@ -0,0 +1,6 @@
+import { MDXProvider } from '@mdx-js/react'
+import type { ReactNode } from 'react'
+
+export function Raw({ children }: { children: ReactNode }) {
+ return {children}
+}
diff --git a/app/components/RouterLink.tsx b/app/components/RouterLink.tsx
new file mode 100644
index 00000000..50e56df5
--- /dev/null
+++ b/app/components/RouterLink.tsx
@@ -0,0 +1,19 @@
+import { forwardRef, useEffect } from 'react'
+import { useInView } from 'react-intersection-observer'
+import { Link, type LinkProps } from 'react-router-dom'
+import { routes } from 'virtual:routes'
+
+import { mergeRefs } from '../utils/mergeRefs.js'
+
+export type RouterLinkProps = LinkProps
+
+export const RouterLink = forwardRef((props: RouterLinkProps, ref) => {
+ const loadRoute = () => routes.find((route) => route.path === props.to)?.lazy()
+
+ const { ref: intersectionRef, inView } = useInView()
+ useEffect(() => {
+ if (inView) loadRoute()
+ }, [inView, loadRoute])
+
+ return
+})
diff --git a/app/components/SearchDialog.css.ts b/app/components/SearchDialog.css.ts
new file mode 100644
index 00000000..47f09c8a
--- /dev/null
+++ b/app/components/SearchDialog.css.ts
@@ -0,0 +1,317 @@
+import { globalStyle, keyframes, style } from '@vanilla-extract/css'
+
+import {
+ borderRadiusVars,
+ fontSizeVars,
+ fontWeightVars,
+ primitiveColorVars,
+ semanticColorVars,
+ spaceVars,
+ viewportVars,
+ zIndexVars,
+} from '../styles/vars.css.js'
+
+const fadeIn = keyframes(
+ {
+ from: {
+ opacity: 0,
+ },
+ to: {
+ opacity: 1,
+ },
+ },
+ 'fadeIn',
+)
+
+const fadeAndSlideIn = keyframes(
+ {
+ from: {
+ opacity: 0,
+ transform: 'translate(-50%, -5%) scale(0.96)',
+ },
+ to: {
+ opacity: 1,
+ transform: 'translate(-50%, 0%) scale(1)',
+ },
+ },
+ 'fadeAndSlideIn',
+)
+
+export const root = style({
+ animation: `${fadeAndSlideIn} 0.1s ease-in-out`,
+ background: primitiveColorVars.background,
+ borderRadius: borderRadiusVars[6],
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spaceVars[8],
+ height: 'min-content',
+ left: '50%',
+ margin: '64px auto',
+ maxHeight: 'min(100vh - 128px, 900px)',
+ padding: spaceVars[12],
+ paddingBottom: spaceVars[8],
+ position: 'fixed',
+ top: 0,
+ transform: 'translate(-50%, 0%)',
+ width: 'min(100vw - 60px, 775px)',
+ zIndex: zIndexVars.backdrop,
+
+ '@media': {
+ [viewportVars['max-720px']]: {
+ borderRadius: 0,
+ // TODO: Not working
+ height: 'calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom))',
+ margin: 0,
+ maxHeight: 'unset',
+ width: '100vw',
+ },
+ },
+})
+
+export const overlay = style(
+ {
+ animation: `${fadeIn} 0.1s ease-in-out`,
+ // TODO: Refactor to variable
+ background: 'rgba(0, 0, 0, .6)',
+ position: 'fixed',
+ inset: 0,
+ zIndex: zIndexVars.backdrop,
+ },
+ 'overlay',
+)
+
+export const searchBox = style(
+ {
+ alignItems: 'center',
+ border: `1px solid ${primitiveColorVars.border}`,
+ borderRadius: borderRadiusVars[4],
+ display: 'flex',
+ gap: spaceVars[8],
+ paddingLeft: spaceVars[8],
+ paddingRight: spaceVars[8],
+ marginBottom: spaceVars[8],
+ width: '100%',
+ selectors: {
+ '&:focus-within': {
+ borderColor: primitiveColorVars.borderAccent,
+ },
+ },
+ },
+ 'searchBox',
+)
+
+export const searchInput = style(
+ {
+ background: 'transparent',
+ display: 'flex',
+ fontSize: fontSizeVars[16],
+ height: spaceVars[40],
+ width: '100%',
+ selectors: {
+ '&:focus': {
+ outline: 'none',
+ },
+ '&::placeholder': {
+ color: primitiveColorVars.text4,
+ },
+ },
+ },
+ 'searchInput',
+)
+
+export const searchInputIcon = style(
+ {
+ color: primitiveColorVars.text3,
+ },
+ 'searchInputIcon',
+)
+
+export const searchInputIconDesktop = style(
+ {
+ '@media': {
+ [viewportVars['max-720px']]: {
+ display: 'none',
+ },
+ },
+ },
+ 'searchInputIconDesktop',
+)
+
+export const searchInputIconMobile = style(
+ {
+ display: 'none',
+ '@media': {
+ [viewportVars['max-720px']]: {
+ display: 'block',
+ },
+ },
+ },
+ 'searchInputIconMobile',
+)
+
+export const results = style(
+ {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spaceVars[8],
+ overflowX: 'hidden',
+ overflowY: 'auto',
+ overscrollBehavior: 'contain',
+ width: '100%',
+ },
+ 'results',
+)
+
+export const result = style(
+ {
+ border: `1.5px solid ${primitiveColorVars.border}`,
+ borderRadius: borderRadiusVars[4],
+ width: '100%',
+ selectors: {
+ '&:focus-within': {
+ borderColor: primitiveColorVars.borderAccent,
+ },
+ },
+ },
+ 'result',
+)
+
+globalStyle(`${result} > a`, {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spaceVars[8],
+ minHeight: spaceVars[36],
+ outline: 'none',
+ justifyContent: 'center',
+ padding: spaceVars[12],
+ width: '100%',
+})
+
+export const resultSelected = style(
+ {
+ borderColor: primitiveColorVars.borderAccent,
+ },
+ 'resultSelected',
+)
+
+export const resultIcon = style(
+ {
+ color: primitiveColorVars.textAccent,
+ marginRight: 1,
+ width: 15,
+ },
+ 'resultIcon',
+)
+
+export const titles = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ flexWrap: 'wrap',
+ fontWeight: fontWeightVars.medium,
+ gap: spaceVars[4],
+ lineHeight: '22px',
+ },
+ 'titles',
+)
+
+export const title = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ gap: spaceVars[4],
+ whiteSpace: 'nowrap',
+ },
+ 'title',
+)
+
+export const titleIcon = style(
+ {
+ color: primitiveColorVars.text,
+ display: 'inline-block',
+ opacity: 0.5,
+ },
+ 'titleIcon',
+)
+
+globalStyle(`${resultSelected} ${title}, ${resultSelected} ${titleIcon}`, {
+ color: primitiveColorVars.textAccent,
+})
+
+export const content = style({ padding: 0 }, 'content')
+
+export const excerpt = style(
+ {
+ maxHeight: '8.75rem',
+ overflow: 'hidden',
+ opacity: 0.5,
+ position: 'relative',
+ '::before': {
+ content: '',
+ position: 'absolute',
+ top: '-1px',
+ left: 0,
+ width: '100%',
+ height: '8px',
+ background: `linear-gradient(${primitiveColorVars.background}, transparent)`,
+ zIndex: '1000',
+ },
+ '::after': {
+ content: '',
+ position: 'absolute',
+ bottom: '-1px',
+ left: 0,
+ width: '100%',
+ height: '12px',
+ background: `linear-gradient(transparent, ${primitiveColorVars.background})`,
+ zIndex: '1000',
+ },
+ '@media': {
+ [viewportVars['max-720px']]: {
+ opacity: 1,
+ },
+ },
+ },
+ 'excerpt',
+)
+
+globalStyle(`${title} mark, ${excerpt} mark`, {
+ backgroundColor: semanticColorVars.searchHighlightBackground,
+ color: semanticColorVars.searchHighlightText,
+ borderRadius: borderRadiusVars[2],
+ paddingBottom: 0,
+ paddingLeft: spaceVars[2],
+ paddingRight: spaceVars[2],
+ paddingTop: 0,
+})
+
+globalStyle(`${resultSelected} ${excerpt}`, {
+ opacity: 1,
+})
+
+export const searchShortcuts = style(
+ {
+ alignItems: 'center',
+ color: primitiveColorVars.text2,
+ display: 'flex',
+ gap: spaceVars[20],
+ fontSize: fontSizeVars[14],
+
+ '@media': {
+ [viewportVars['max-720px']]: {
+ display: 'none',
+ },
+ },
+ },
+ 'searchShortcuts',
+)
+
+export const searchShortcutsGroup = style(
+ {
+ alignItems: 'center',
+ display: 'inline-flex',
+ gap: spaceVars[3],
+ marginRight: spaceVars[6],
+ },
+ 'searchShortcutsGroup',
+)
diff --git a/app/components/SearchDialog.tsx b/app/components/SearchDialog.tsx
new file mode 100644
index 00000000..be9858a4
--- /dev/null
+++ b/app/components/SearchDialog.tsx
@@ -0,0 +1,299 @@
+import * as Dialog from '@radix-ui/react-dialog'
+import {
+ ArrowLeftIcon,
+ ChevronRightIcon,
+ FileIcon,
+ ListBulletIcon,
+ MagnifyingGlassIcon,
+} from '@radix-ui/react-icons'
+import * as Label from '@radix-ui/react-label'
+import clsx from 'clsx'
+import { default as Mark } from 'mark.js'
+import { type SearchResult } from 'minisearch'
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { Link, useNavigate } from 'react-router-dom'
+
+import { useConfig } from '../hooks/useConfig.js'
+import { useDebounce } from '../hooks/useDebounce.js'
+import { useLocalStorage } from '../hooks/useLocalStorage.js'
+import { type Result, useSearchIndex } from '../hooks/useSearchIndex.js'
+import { visuallyHidden } from '../styles/utils.css.js'
+import { Content } from './Content.js'
+import { KeyboardShortcut } from './KeyboardShortcut.js'
+import * as styles from './SearchDialog.css.js'
+
+export function SearchDialog(props: { open: boolean; onClose(): void }) {
+ const { search: searchOptions } = useConfig()
+ const navigate = useNavigate()
+ const inputRef = useRef(null)
+ const listRef = useRef(null)
+
+ const [filterText, setFilterText] = useLocalStorage('filterText', '')
+ const searchTerm = useDebounce(filterText, 200)
+ const searchIndex = useSearchIndex()
+
+ const [selectedIndex, setSelectedIndex] = useState(-1)
+ const [disableMouseOver, setDisableMouseOver] = useState(false)
+ const [showDetailView, setShowDetailView] = useLocalStorage('showDetailView', true)
+
+ const results: (SearchResult & Result)[] = useMemo(() => {
+ if (!searchIndex) return []
+ if (!searchTerm) {
+ setSelectedIndex(-1)
+ return []
+ }
+ setSelectedIndex(0)
+ return searchIndex.search(searchTerm, searchOptions).slice(0, 16) as (SearchResult & Result)[]
+ }, [searchIndex, searchOptions, searchTerm])
+
+ const resultsCount = results.length
+ const selectedResult = results[selectedIndex]
+
+ const highlight = useCallback(() => {
+ if (!listRef.current) return
+
+ const terms = new Set()
+ for (const result of results) {
+ for (const term in result.match) {
+ terms.add(term)
+ }
+ }
+
+ const mark = new Mark(listRef.current)
+ mark.unmark({
+ done() {
+ mark?.markRegExp(formMarkRegex(terms))
+ },
+ })
+
+ const excerptElements = listRef.current.querySelectorAll(`.${styles.excerpt}`)
+ for (const element of excerptElements) {
+ element.querySelector('mark[data-markjs="true"]')?.scrollIntoView({ block: 'center' })
+ }
+ listRef.current?.firstElementChild?.scrollIntoView({ block: 'start' })
+ }, [results])
+
+ useEffect(() => {
+ if (!props.open) return
+
+ function keyDownHandler(event: KeyboardEvent) {
+ switch (event.key) {
+ case 'ArrowDown': {
+ event.preventDefault()
+ setSelectedIndex((index) => {
+ let nextIndex = index + 1
+ if (nextIndex >= resultsCount) nextIndex = 0
+ const element = listRef.current?.children[nextIndex]
+ element?.scrollIntoView({ block: 'nearest' })
+ return nextIndex
+ })
+ setDisableMouseOver(true)
+ break
+ }
+ case 'ArrowUp': {
+ event.preventDefault()
+ setSelectedIndex((index) => {
+ let nextIndex = index - 1
+ if (nextIndex < 0) nextIndex = resultsCount - 1
+ const element = listRef.current?.children[nextIndex]
+ element?.scrollIntoView({ block: 'nearest' })
+ return nextIndex
+ })
+ setDisableMouseOver(true)
+ break
+ }
+ case 'Backspace': {
+ if (!event.metaKey) return
+ event.preventDefault()
+ setFilterText('')
+ inputRef.current?.focus()
+ break
+ }
+ case 'Enter': {
+ if (event.target instanceof HTMLButtonElement && event.target.type !== 'submit') return
+ if (!selectedResult) return
+ event.preventDefault()
+ navigate(selectedResult.href)
+ props.onClose()
+ break
+ }
+ }
+ }
+
+ window.addEventListener('keydown', keyDownHandler)
+ return () => {
+ window.removeEventListener('keydown', keyDownHandler)
+ }
+ }, [navigate, resultsCount, setFilterText, selectedResult, props.open, props.onClose])
+
+ useEffect(() => {
+ if (searchTerm === '') return
+ if (!listRef.current) return
+ highlight()
+ }, [highlight, searchTerm])
+
+ return (
+
+
+
+ {
+ if (inputRef.current) {
+ event.preventDefault()
+ inputRef.current.focus()
+ }
+ highlight()
+ }}
+ onCloseAutoFocus={() => {
+ setSelectedIndex(0)
+ }}
+ className={styles.root}
+ aria-describedby={undefined}
+ >
+ Search
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function formMarkRegex(terms: Set) {
+ return new RegExp(
+ [...terms]
+ .sort((a, b) => b.length - a.length)
+ .map((term) => {
+ return `(${term.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d')})`
+ })
+ .join('|'),
+ 'gi',
+ )
+}
diff --git a/app/components/Sidebar.css.ts b/app/components/Sidebar.css.ts
new file mode 100644
index 00000000..828026de
--- /dev/null
+++ b/app/components/Sidebar.css.ts
@@ -0,0 +1,237 @@
+import { style } from '@vanilla-extract/css'
+import {
+ fontSizeVars,
+ fontWeightVars,
+ lineHeightVars,
+ primitiveColorVars,
+ sidebarVars,
+ spaceVars,
+ topNavVars,
+ zIndexVars,
+} from '../styles/vars.css.js'
+
+export const root = style({
+ display: 'flex',
+ flexDirection: 'column',
+ fontSize: fontSizeVars['14'],
+ overflowY: 'auto',
+ width: sidebarVars.width,
+ '@media': {
+ 'screen and (max-width: 1080px)': {
+ width: '100%',
+ },
+ },
+})
+
+export const backLink = style(
+ {
+ textAlign: 'left',
+ },
+ 'backLink',
+)
+
+export const divider = style(
+ {
+ backgroundColor: primitiveColorVars.border,
+ width: '100%',
+ height: '1px',
+ },
+ 'divider',
+)
+
+export const navigation = style(
+ {
+ outline: 0,
+ selectors: {
+ '&:first-child': {
+ paddingTop: spaceVars['16'],
+ },
+ },
+ },
+ 'navigation',
+)
+
+export const group = style(
+ {
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ 'group',
+)
+
+export const logo = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ height: topNavVars.height,
+ paddingTop: spaceVars[4],
+ },
+ 'logo',
+)
+
+export const logoWrapper = style(
+ {
+ backgroundColor: primitiveColorVars.backgroundDark,
+ position: 'sticky',
+ top: 0,
+ zIndex: zIndexVars.gutterTopCurtain,
+ '@media': {
+ 'screen and (max-width: 1080px)': {
+ display: 'none',
+ },
+ },
+ },
+ 'logoWrapper',
+)
+
+export const section = style(
+ {
+ display: 'flex',
+ flexDirection: 'column',
+ fontSize: '1em',
+ get selectors() {
+ return {
+ [`${navigation} > ${group} > ${section} + ${section}`]: {
+ borderTop: `1px solid ${primitiveColorVars.border}`,
+ },
+ }
+ },
+ },
+ 'section',
+)
+
+export const level = style({}, 'level')
+
+export const levelCollapsed = style(
+ {
+ gap: spaceVars['4'],
+ paddingBottom: spaceVars['12'],
+ },
+ 'levelCollapsed',
+)
+
+export const levelInset = style(
+ {
+ borderLeft: `1px solid ${primitiveColorVars.border}`,
+ fontSize: fontSizeVars['13'],
+ marginTop: spaceVars['8'],
+ paddingLeft: spaceVars['12'],
+ selectors: {
+ '&&&': {
+ fontWeight: fontWeightVars.regular,
+ paddingTop: 0,
+ paddingBottom: 0,
+ },
+ },
+ },
+ 'levelInset',
+)
+
+export const items = style(
+ {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '0.625em',
+ paddingTop: spaceVars['16'],
+ paddingBottom: spaceVars['16'],
+ fontWeight: fontWeightVars.medium,
+ selectors: {
+ [`${level} &`]: {
+ paddingTop: spaceVars['6'],
+ },
+ },
+ },
+ 'items',
+)
+
+export const item = style(
+ {
+ color: primitiveColorVars.text3,
+ letterSpacing: '0.25px',
+ lineHeight: lineHeightVars.sidebarItem,
+ width: '100%',
+ transition: 'color 0.1s',
+ selectors: {
+ '&:hover': {
+ color: primitiveColorVars.text,
+ },
+ '&[data-active="true"]': {
+ color: primitiveColorVars.textAccent,
+ },
+ },
+ },
+ 'item',
+)
+
+export const sectionHeader = style(
+ {
+ alignItems: 'center',
+ display: 'flex',
+ justifyContent: 'space-between',
+ selectors: {
+ [`${level} > &`]: {
+ paddingTop: spaceVars['12'],
+ },
+ },
+ },
+ 'sectionHeader',
+)
+
+export const sectionHeaderActive = style(
+ {
+ color: primitiveColorVars.text,
+ },
+ 'sectionHeaderActive',
+)
+
+export const sectionTitle = style(
+ {
+ color: primitiveColorVars.title,
+ fontSize: fontSizeVars['14'],
+ fontWeight: fontWeightVars.semibold,
+ letterSpacing: '0.25px',
+ width: '100%',
+ },
+ 'sectionTitle',
+)
+
+export const sectionTitleActive = style(
+ {
+ color: primitiveColorVars.textAccent,
+ fontSize: fontSizeVars['14'],
+ fontWeight: fontWeightVars.semibold,
+ letterSpacing: '0.25px',
+ width: '100%',
+ },
+ 'sectionTitleActive',
+)
+
+export const sectionTitleLink = style(
+ {
+ selectors: {
+ '&:hover': {
+ color: primitiveColorVars.text,
+ },
+ '&[data-active="true"]': {
+ color: primitiveColorVars.textAccent,
+ },
+ },
+ },
+ 'sectionTitleLink',
+)
+
+export const sectionCollapse = style(
+ {
+ color: primitiveColorVars.text3,
+ transform: 'rotate(90deg)',
+ transition: 'transform 0.25s',
+ },
+ 'sectionCollapse',
+)
+
+export const sectionCollapseActive = style(
+ {
+ transform: 'rotate(0)',
+ },
+ 'sectionCollapseActive',
+)
diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx
new file mode 100644
index 00000000..a4172a5f
--- /dev/null
+++ b/app/components/Sidebar.tsx
@@ -0,0 +1,298 @@
+import clsx from 'clsx'
+import {
+ type KeyboardEvent,
+ type MouseEvent,
+ type MouseEventHandler,
+ type RefObject,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
+import { matchPath, useLocation, useMatch } from 'react-router-dom'
+
+import { type SidebarItem as SidebarItemType } from '../../config.js'
+import { usePageData } from '../hooks/usePageData.js'
+import { useSidebar } from '../hooks/useSidebar.js'
+import { Icon } from './Icon.js'
+import { NavLogo } from './NavLogo.js'
+import { RouterLink } from './RouterLink.js'
+import * as styles from './Sidebar.css.js'
+import { ChevronRight } from './icons/ChevronRight.js'
+
+function checkSectionTitleActive(items: any[], pathname: string) {
+ const result = Boolean(
+ items.find((item) => {
+ if (item.link) {
+ return item.link === pathname
+ }
+ return false
+ }),
+ )
+
+ return !!result
+}
+
+export function Sidebar(props: {
+ className?: string
+ onClickItem?: MouseEventHandler
+}) {
+ const { className, onClickItem } = props
+
+ const { previousPath } = usePageData()
+ const sidebarRef = useRef(null)
+ const sidebar = useSidebar()
+ const [backPath, setBackPath] = useState('/')
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies:
+ useEffect(() => {
+ if (typeof window === 'undefined') return
+ if (!previousPath) return
+ setBackPath(previousPath)
+ }, [sidebar.key, sidebar.backLink])
+
+ if (!sidebar) return null
+
+ const groups = getSidebarGroups(sidebar.items)
+
+ return (
+
+
+
+
+
+ {sidebar.backLink && (
+
+
+
+ ←{' '}
+ {typeof history !== 'undefined' && history.state?.key && backPath !== '/'
+ ? 'Back'
+ : 'Home'}
+
+
+
+ )}
+ {groups.map((group, i) => (
+
+ ))}
+
+
+
+ )
+}
+
+function getSidebarGroups(sidebar: SidebarItemType[]): SidebarItemType[] {
+ const groups: SidebarItemType[] = []
+
+ let lastGroupIndex = 0
+ for (const item of sidebar) {
+ if (item.items) {
+ lastGroupIndex = groups.push(item)
+ continue
+ }
+
+ if (!groups[lastGroupIndex]) groups.push({ text: '', items: [item] })
+ else groups[lastGroupIndex].items!.push(item)
+ }
+
+ return groups
+}
+
+function getActiveChildItem(
+ items: SidebarItemType[],
+ pathname: string,
+): SidebarItemType | undefined {
+ return items.find((item) => {
+ if (matchPath(pathname, item.link ?? '')) return true
+ if (item.link === pathname) return true
+ if (!item.items) return false
+ return getActiveChildItem(item.items, pathname)
+ })
+}
+
+function SidebarItem(props: {
+ depth: number
+ item: SidebarItemType
+ onClick?: MouseEventHandler
+ sidebarRef?: RefObject
+}) {
+ const { depth, item, onClick, sidebarRef } = props
+
+ const itemRef = useRef(null)
+
+ const { pathname } = useLocation()
+ const match = useMatch(item.link ?? '')
+
+ const hasActiveChildItem = useMemo(
+ () => (item.items ? Boolean(getActiveChildItem(item.items, pathname)) : false),
+ [item.items, pathname],
+ )
+
+ const [collapsed, setCollapsed] = useState(() => {
+ if (match) return false
+ if (!item.items) return false
+ if (hasActiveChildItem) return false
+ return Boolean(item.collapsed)
+ })
+ const isCollapsable = item.collapsed !== undefined && item.items !== undefined
+ const onCollapseInteraction = useCallback(
+ (event: KeyboardEvent | MouseEvent) => {
+ if ('key' in event && event.key !== 'Enter') return
+ if (item.link) return
+ setCollapsed((x) => !x)
+ },
+ [item.link],
+ )
+ const onCollapseTriggerInteraction = useCallback(
+ (event: KeyboardEvent | MouseEvent) => {
+ if ('key' in event && event.key !== 'Enter') return
+ if (!item.link) return
+ setCollapsed((x) => !x)
+ },
+ [item.link],
+ )
+
+ const active = useRef(true)
+ useEffect(() => {
+ if (!active.current) return
+ active.current = false
+
+ const match = matchPath(pathname, item.link ?? '')
+ if (!match) return
+
+ requestAnimationFrame(() => {
+ const offsetTop = itemRef.current?.offsetTop ?? 0
+ const sidebarHeight = sidebarRef?.current?.clientHeight ?? 0
+ if (offsetTop < sidebarHeight) return
+ sidebarRef?.current?.scrollTo({ top: offsetTop - 100 })
+ })
+ }, [item, pathname, sidebarRef])
+
+ if (item.items)
+ return (
+
+ {item.text && (
+
+ {item.text &&
+ (item.link ? (
+
+ {item.text}
+
+ ) : (
+
+ {item.items && !checkSectionTitleActive(item.items, pathname) && collapsed ? (
+
+ {item.text}
+
+ ) : (
+ item.text
+ )}
+
+ ))}
+
+ {isCollapsable && (
+
+
+
+ )}
+
+ )}
+
+ {!collapsed && (
+
+ {item.items &&
+ item.items.length > 0 &&
+ depth < 5 &&
+ item.items.map((item, i) => (
+
+ ))}
+
+ )}
+
+ )
+
+ return (
+ <>
+ {item.link ? (
+
+ {item.text}
+
+ ) : (
+ {item.text}
+ )}
+ >
+ )
+}
diff --git a/app/components/SkipLink.css.ts b/app/components/SkipLink.css.ts
new file mode 100644
index 00000000..f7666ea0
--- /dev/null
+++ b/app/components/SkipLink.css.ts
@@ -0,0 +1,29 @@
+import { style } from '@vanilla-extract/css'
+import {
+ borderRadiusVars,
+ fontSizeVars,
+ fontWeightVars,
+ primitiveColorVars,
+ semanticColorVars,
+ spaceVars,
+} from '../styles/vars.css.js'
+
+export const root = style({
+ background: primitiveColorVars.background,
+ borderRadius: borderRadiusVars['4'],
+ color: semanticColorVars.link,
+ fontSize: fontSizeVars['14'],
+ fontWeight: fontWeightVars.semibold,
+ left: spaceVars[8],
+ padding: `${spaceVars['8']} ${spaceVars['16']}`,
+ position: 'fixed',
+ textDecoration: 'none',
+ top: spaceVars[8],
+ zIndex: 999,
+ ':focus': {
+ clip: 'auto',
+ clipPath: 'none',
+ height: 'auto',
+ width: 'auto',
+ },
+})
diff --git a/app/components/SkipLink.tsx b/app/components/SkipLink.tsx
new file mode 100644
index 00000000..27547e99
--- /dev/null
+++ b/app/components/SkipLink.tsx
@@ -0,0 +1,15 @@
+import clsx from 'clsx'
+import { useLocation } from 'react-router-dom'
+import { visuallyHidden } from '../styles/utils.css.js'
+import * as styles from './SkipLink.css.js'
+
+export const skipLinkId = 'vocs-content'
+
+export function SkipLink() {
+ const { pathname } = useLocation()
+ return (
+
+ Skip to content
+
+ )
+}
diff --git a/app/components/Sponsors.css.ts b/app/components/Sponsors.css.ts
new file mode 100644
index 00000000..05e04292
--- /dev/null
+++ b/app/components/Sponsors.css.ts
@@ -0,0 +1,84 @@
+import { createVar, style } from '@vanilla-extract/css'
+import {
+ borderRadiusVars,
+ fontSizeVars,
+ fontWeightVars,
+ primitiveColorVars,
+ spaceVars,
+} from '../styles/vars.css.js'
+
+export const columnsVar = createVar('columns')
+export const heightVar = createVar('height')
+
+export const root = style({
+ borderRadius: borderRadiusVars['8'],
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spaceVars['4'],
+ overflow: 'hidden',
+})
+
+export const title = style(
+ {
+ backgroundColor: primitiveColorVars.background3,
+ color: primitiveColorVars.text3,
+ fontSize: fontSizeVars['13'],
+ fontWeight: fontWeightVars.medium,
+ padding: `${spaceVars['4']} 0`,
+ textAlign: 'center',
+ },
+ 'title',
+)
+
+export const row = style(
+ {
+ display: 'flex',
+ flexDirection: 'row',
+ gap: spaceVars['4'],
+ },
+ 'row',
+)
+
+export const column = style(
+ {
+ alignItems: 'center',
+ backgroundColor: primitiveColorVars.background3,
+ display: 'flex',
+ justifyContent: 'center',
+ padding: spaceVars['32'],
+ width: `calc(${columnsVar} * 100%)`,
+ },
+ 'column',
+)
+
+export const sponsor = style(
+ {
+ transition: 'background-color 0.1s',
+ selectors: {
+ '&:hover': {
+ backgroundColor: primitiveColorVars.background5,
+ },
+ '.dark &:hover': {
+ backgroundColor: primitiveColorVars.white,
+ },
+ },
+ },
+ 'sponsor',
+)
+
+export const image = style(
+ {
+ filter: 'grayscale(1)',
+ height: heightVar,
+ transition: 'filter 0.1s',
+ selectors: {
+ '.dark &': {
+ filter: 'grayscale(1) invert(1)',
+ },
+ [`${column}:hover &`]: {
+ filter: 'none',
+ },
+ },
+ },
+ 'image',
+)
diff --git a/app/components/Sponsors.tsx b/app/components/Sponsors.tsx
new file mode 100644
index 00000000..256e44c2
--- /dev/null
+++ b/app/components/Sponsors.tsx
@@ -0,0 +1,41 @@
+import { assignInlineVars } from '@vanilla-extract/dynamic'
+import clsx from 'clsx'
+import { Fragment } from 'react'
+import { useConfig } from '../hooks/useConfig.js'
+import { Link } from './Link.js'
+import * as styles from './Sponsors.css.js'
+
+export function Sponsors() {
+ const { sponsors } = useConfig()
+ return (
+
+ {sponsors?.map((sponsorSet, i) => (
+
+ {sponsorSet.name}
+ {sponsorSet.items.map((sponsorRow, i) => (
+
+ {sponsorRow.map((sponsor, i) => (
+
+
+
+ ))}
+
+ ))}
+
+ ))}
+
+ )
+}
diff --git a/app/components/Step.css.ts b/app/components/Step.css.ts
new file mode 100644
index 00000000..f894819e
--- /dev/null
+++ b/app/components/Step.css.ts
@@ -0,0 +1,82 @@
+import { globalStyle, style } from '@vanilla-extract/css'
+
+import { fontWeightVars, primitiveColorVars, spaceVars, viewportVars } from '../styles/vars.css.js'
+import { root as Tabs } from './Tabs.css.js'
+import { root as CodeBlock } from './mdx/CodeBlock.css.js'
+import { root as H2 } from './mdx/H2.css.js'
+import { root as H3 } from './mdx/H3.css.js'
+import { root as H4 } from './mdx/H4.css.js'
+import { root as H5 } from './mdx/H5.css.js'
+import { root as H6 } from './mdx/H6.css.js'
+
+export const root = style({
+ selectors: {
+ '&:not(:last-child)': {
+ marginBottom: spaceVars['24'],
+ },
+ },
+})
+
+export const title = style(
+ {
+ marginBottom: spaceVars['8'],
+ position: 'relative',
+ '::before': {
+ alignItems: 'center',
+ backgroundColor: primitiveColorVars.background5,
+ borderRadius: '100%',
+ border: `0.5em solid ${primitiveColorVars.background}`,
+ boxSizing: 'content-box',
+ color: primitiveColorVars.text2,
+ content: 'counter(step)',
+ counterIncrement: 'step',
+ display: 'flex',
+ fontSize: '0.625em',
+ fontWeight: fontWeightVars.regular,
+ height: '2em',
+ justifyContent: 'center',
+ left: 'calc(-25.125px - 1.45em)',
+ position: 'absolute',
+ top: '-0.25em',
+ width: '2em',
+ },
+ },
+ 'title',
+)
+
+export const content = style(
+ {
+ selectors: {
+ [`${H2}+&,${H3}+&,${H4}+&,${H5}+&,${H6}+&`]: {
+ marginTop: `calc(${spaceVars['8']} * -1)`,
+ },
+ },
+ },
+ 'content',
+)
+
+globalStyle(`${content} > *:not(:last-child)`, {
+ marginBottom: spaceVars['16'],
+})
+
+globalStyle(`${content} > *:last-child`, {
+ marginBottom: spaceVars['0'],
+})
+
+globalStyle(`${content} > ${Tabs}, ${content} > ${CodeBlock}`, {
+ '@media': {
+ [viewportVars['max-720px']]: {
+ outline: `6px solid ${primitiveColorVars.background}`,
+ marginLeft: `calc(-1 * ${spaceVars['44']} - 2px)`,
+ marginRight: `calc(-1 * ${spaceVars['16']})`,
+ },
+ },
+})
+
+globalStyle(`${content} ${Tabs} pre.shiki`, {
+ '@media': {
+ [viewportVars['max-720px']]: {
+ borderTop: 'none',
+ },
+ },
+})
diff --git a/app/components/Step.tsx b/app/components/Step.tsx
new file mode 100644
index 00000000..8dda0602
--- /dev/null
+++ b/app/components/Step.tsx
@@ -0,0 +1,34 @@
+import { type ClassValue, clsx } from 'clsx'
+import type { ReactNode } from 'react'
+
+import * as styles from './Step.css.js'
+import { H2 } from './mdx/H2.js'
+import { H3 } from './mdx/H3.js'
+import { H4 } from './mdx/H4.js'
+import { H5 } from './mdx/H5.js'
+import { H6 } from './mdx/H6.js'
+
+export type StepProps = {
+ children: ReactNode
+ className?: ClassValue
+ title: ReactNode | string
+ titleLevel?: 2 | 3 | 4 | 5 | 6
+}
+
+export function Step({ children, className, title, titleLevel = 2 }: StepProps) {
+ const Element = (() => {
+ if (titleLevel === 2) return H2
+ if (titleLevel === 3) return H3
+ if (titleLevel === 4) return H4
+ if (titleLevel === 5) return H5
+ if (titleLevel === 6) return H6
+ throw new Error('Invalid.')
+ })()
+
+ return (
+
+ {typeof title === 'string' ?
{title} : title}
+
{children}
+
+ )
+}
diff --git a/app/components/Steps.css.ts b/app/components/Steps.css.ts
new file mode 100644
index 00000000..9b5994a7
--- /dev/null
+++ b/app/components/Steps.css.ts
@@ -0,0 +1,16 @@
+import { style } from '@vanilla-extract/css'
+
+import { primitiveColorVars, spaceVars, viewportVars } from '../styles/vars.css.js'
+
+export const root = style({
+ borderLeft: `1.5px solid ${primitiveColorVars.border}`,
+ counterReset: 'step',
+ paddingLeft: spaceVars['24'],
+ marginLeft: spaceVars['12'],
+ marginTop: spaceVars['24'],
+ '@media': {
+ [viewportVars['max-720px']]: {
+ marginLeft: spaceVars['4'],
+ },
+ },
+})
diff --git a/app/components/Steps.tsx b/app/components/Steps.tsx
new file mode 100644
index 00000000..8fd8d92c
--- /dev/null
+++ b/app/components/Steps.tsx
@@ -0,0 +1,13 @@
+import { type ClassValue, clsx } from 'clsx'
+import type { ReactNode } from 'react'
+
+import * as styles from './Steps.css.js'
+
+export type StepsProps = {
+ children: ReactNode
+ className?: ClassValue
+}
+
+export function Steps({ children, className }: StepsProps) {
+ return {children}
+}
diff --git a/app/components/Tabs.css.ts b/app/components/Tabs.css.ts
new file mode 100644
index 00000000..2768ca04
--- /dev/null
+++ b/app/components/Tabs.css.ts
@@ -0,0 +1,81 @@
+import { globalStyle, style } from '@vanilla-extract/css'
+import {
+ borderRadiusVars,
+ fontSizeVars,
+ fontWeightVars,
+ primitiveColorVars,
+ semanticColorVars,
+ spaceVars,
+ viewportVars,
+} from '../styles/vars.css.js'
+
+export const root = style({
+ backgroundColor: semanticColorVars.codeBlockBackground,
+ border: `1px solid ${semanticColorVars.codeInlineBorder}`,
+ borderRadius: borderRadiusVars['4'],
+})
+
+export const list = style(
+ {
+ backgroundColor: semanticColorVars.codeTitleBackground,
+ borderBottom: `1px solid ${primitiveColorVars.border}`,
+ borderTopLeftRadius: borderRadiusVars['4'],
+ borderTopRightRadius: borderRadiusVars['4'],
+ display: 'flex',
+ padding: `${spaceVars['0']} ${spaceVars['14']}`,
+ '@media': {
+ [viewportVars['max-720px']]: {
+ borderRadius: 0,
+ padding: `${spaceVars['0']} ${spaceVars['8']}`,
+ },
+ },
+ },
+ 'list',
+)
+
+export const trigger = style(
+ {
+ borderBottom: '2px solid transparent',
+ color: primitiveColorVars.text3,
+ fontSize: fontSizeVars['14'],
+ fontWeight: fontWeightVars.medium,
+ padding: `${spaceVars['8']} ${spaceVars['8']} ${spaceVars['6']} ${spaceVars['8']}`,
+ transition: 'color 0.1s',
+ selectors: {
+ '&:hover': {
+ color: primitiveColorVars.text,
+ },
+ '&[data-state="active"]': {
+ borderBottom: `2px solid ${primitiveColorVars.borderAccent}`,
+ color: primitiveColorVars.text,
+ },
+ },
+ },
+ 'trigger',
+)
+
+export const content = style(
+ {
+ backgroundColor: semanticColorVars.codeBlockBackground,
+ selectors: {
+ '&:not([data-shiki="true"])': {
+ padding: `${spaceVars['20']} ${spaceVars['22']}`,
+ '@media': {
+ [viewportVars['max-720px']]: {
+ padding: `${spaceVars['20']} ${spaceVars['16']}`,
+ },
+ },
+ },
+ },
+ },
+ 'content',
+)
+
+globalStyle(`${root} pre`, {
+ marginBottom: spaceVars['0'],
+ '@media': {
+ [viewportVars['max-720px']]: {
+ margin: 'unset',
+ },
+ },
+})
diff --git a/app/components/Tabs.tsx b/app/components/Tabs.tsx
new file mode 100644
index 00000000..8bc9faab
--- /dev/null
+++ b/app/components/Tabs.tsx
@@ -0,0 +1,20 @@
+import * as Tabs from '@radix-ui/react-tabs'
+import clsx from 'clsx'
+
+import * as styles from './Tabs.css.js'
+
+export function Root(props: Tabs.TabsProps) {
+ return
+}
+
+export function List(props: Tabs.TabsListProps) {
+ return
+}
+
+export function Trigger(props: Tabs.TabsTriggerProps) {
+ return
+}
+
+export function Content(props: Tabs.TabsContentProps) {
+ return
+}
diff --git a/app/components/icons/ArrowDiagonal.tsx b/app/components/icons/ArrowDiagonal.tsx
new file mode 100644
index 00000000..3dd5f17e
--- /dev/null
+++ b/app/components/icons/ArrowDiagonal.tsx
@@ -0,0 +1,17 @@
+export function ArrowDiagonal() {
+ return (
+
+ Arrow Diagonal
+
+
+ )
+}
diff --git a/app/components/icons/ArrowLeft.tsx b/app/components/icons/ArrowLeft.tsx
new file mode 100644
index 00000000..2b9e072a
--- /dev/null
+++ b/app/components/icons/ArrowLeft.tsx
@@ -0,0 +1,17 @@
+export function ArrowLeft() {
+ return (
+
+ Arrow Left
+
+
+ )
+}
diff --git a/app/components/icons/ArrowRight.tsx b/app/components/icons/ArrowRight.tsx
new file mode 100644
index 00000000..4e0f33c1
--- /dev/null
+++ b/app/components/icons/ArrowRight.tsx
@@ -0,0 +1,17 @@
+export function ArrowRight() {
+ return (
+
+ Arrow Right
+
+
+ )
+}
diff --git a/app/components/icons/Checkmark.tsx b/app/components/icons/Checkmark.tsx
new file mode 100644
index 00000000..abd9ab41
--- /dev/null
+++ b/app/components/icons/Checkmark.tsx
@@ -0,0 +1,17 @@
+export function Checkmark() {
+ return (
+
+ Checkmark
+
+
+ )
+}
diff --git a/app/components/icons/ChevronDown.tsx b/app/components/icons/ChevronDown.tsx
new file mode 100644
index 00000000..f4290f62
--- /dev/null
+++ b/app/components/icons/ChevronDown.tsx
@@ -0,0 +1,17 @@
+export function ChevronDown() {
+ return (
+
+ Chevron Down
+
+
+ )
+}
diff --git a/app/components/icons/ChevronRight.tsx b/app/components/icons/ChevronRight.tsx
new file mode 100644
index 00000000..cc4154f9
--- /dev/null
+++ b/app/components/icons/ChevronRight.tsx
@@ -0,0 +1,17 @@
+export function ChevronRight() {
+ return (
+
+ Chevron Right
+
+
+ )
+}
diff --git a/app/components/icons/ChevronUp.tsx b/app/components/icons/ChevronUp.tsx
new file mode 100644
index 00000000..558af4ab
--- /dev/null
+++ b/app/components/icons/ChevronUp.tsx
@@ -0,0 +1,17 @@
+export function ChevronUp() {
+ return (
+
+ Chevron Up
+
+
+ )
+}
diff --git a/app/components/icons/Copy.tsx b/app/components/icons/Copy.tsx
new file mode 100644
index 00000000..2e53751f
--- /dev/null
+++ b/app/components/icons/Copy.tsx
@@ -0,0 +1,22 @@
+export function Copy() {
+ return (
+
+ Copy
+
+
+
+ )
+}
diff --git a/app/components/icons/Discord.tsx b/app/components/icons/Discord.tsx
new file mode 100644
index 00000000..fb910929
--- /dev/null
+++ b/app/components/icons/Discord.tsx
@@ -0,0 +1,17 @@
+export function Discord() {
+ return (
+
+ Discord
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/components/icons/File.tsx b/app/components/icons/File.tsx
new file mode 100644
index 00000000..41ab0ab2
--- /dev/null
+++ b/app/components/icons/File.tsx
@@ -0,0 +1,28 @@
+export function File() {
+ return (
+
+ File
+
+
+
+ )
+}
diff --git a/app/components/icons/GitHub.tsx b/app/components/icons/GitHub.tsx
new file mode 100644
index 00000000..084195e1
--- /dev/null
+++ b/app/components/icons/GitHub.tsx
@@ -0,0 +1,13 @@
+export function GitHub() {
+ return (
+
+ GitHub
+
+
+ )
+}
diff --git a/app/components/icons/Link.tsx b/app/components/icons/Link.tsx
new file mode 100644
index 00000000..de783a29
--- /dev/null
+++ b/app/components/icons/Link.tsx
@@ -0,0 +1,17 @@
+export function Link() {
+ return (
+
+ Link
+
+
+ )
+}
diff --git a/app/components/icons/Menu.tsx b/app/components/icons/Menu.tsx
new file mode 100644
index 00000000..96419a22
--- /dev/null
+++ b/app/components/icons/Menu.tsx
@@ -0,0 +1,17 @@
+export function Menu() {
+ return (
+
+ Menu
+
+
+ )
+}
diff --git a/app/components/icons/Moon.tsx b/app/components/icons/Moon.tsx
new file mode 100644
index 00000000..4f22100d
--- /dev/null
+++ b/app/components/icons/Moon.tsx
@@ -0,0 +1,21 @@
+export function Moon() {
+ return (
+
+ Moon
+
+
+
+ )
+}
diff --git a/app/components/icons/Sun.tsx b/app/components/icons/Sun.tsx
new file mode 100644
index 00000000..61ada4af
--- /dev/null
+++ b/app/components/icons/Sun.tsx
@@ -0,0 +1,21 @@
+export function Sun() {
+ return (
+
+ Sun
+
+
+
+ )
+}
diff --git a/app/components/icons/Telegram.tsx b/app/components/icons/Telegram.tsx
new file mode 100644
index 00000000..476fec56
--- /dev/null
+++ b/app/components/icons/Telegram.tsx
@@ -0,0 +1,11 @@
+export function Telegram() {
+ return (
+
+ Telegram
+
+
+ )
+}
diff --git a/app/components/icons/Terminal.tsx b/app/components/icons/Terminal.tsx
new file mode 100644
index 00000000..4ee4acb3
--- /dev/null
+++ b/app/components/icons/Terminal.tsx
@@ -0,0 +1,18 @@
+export function Terminal() {
+ return (
+
+ Terminal
+
+
+
+ )
+}
diff --git a/app/components/icons/Warpcast.tsx b/app/components/icons/Warpcast.tsx
new file mode 100644
index 00000000..ca0bc771
--- /dev/null
+++ b/app/components/icons/Warpcast.tsx
@@ -0,0 +1,13 @@
+export function Warpcast() {
+ return (
+
+ Warpcast
+
+
+ )
+}
diff --git a/app/components/icons/X.tsx b/app/components/icons/X.tsx
new file mode 100644
index 00000000..f8edba15
--- /dev/null
+++ b/app/components/icons/X.tsx
@@ -0,0 +1,17 @@
+export function X() {
+ return (
+
+ X
+
+
+ )
+}
diff --git a/app/components/mdx/Anchor.css.ts b/app/components/mdx/Anchor.css.ts
new file mode 100644
index 00000000..28254908
--- /dev/null
+++ b/app/components/mdx/Anchor.css.ts
@@ -0,0 +1,59 @@
+import { globalStyle, style } from '@vanilla-extract/css'
+
+import { fontWeightVars, semanticColorVars, spaceVars } from '../../styles/vars.css.js'
+import { danger, info, success, tip, warning } from '../Callout.css.js'
+import { root as Section } from './Section.css.js'
+
+export const root = style({
+ color: semanticColorVars.link,
+ fontWeight: fontWeightVars.medium,
+ textUnderlineOffset: spaceVars['2'],
+ textDecoration: 'underline',
+ transition: 'color 0.1s',
+ selectors: {
+ [`${danger} &`]: {
+ color: semanticColorVars.dangerText,
+ },
+ [`${danger} &:hover`]: {
+ color: semanticColorVars.dangerTextHover,
+ },
+ [`${info} &`]: {
+ color: semanticColorVars.infoText,
+ },
+ [`${info} &:hover`]: {
+ color: semanticColorVars.infoTextHover,
+ },
+ [`${success} &`]: {
+ color: semanticColorVars.successText,
+ },
+ [`${success} &:hover`]: {
+ color: semanticColorVars.successTextHover,
+ },
+ [`${tip} &`]: {
+ color: semanticColorVars.tipText,
+ },
+ [`${tip} &:hover`]: {
+ color: semanticColorVars.tipTextHover,
+ },
+ [`${warning} &`]: {
+ color: semanticColorVars.warningText,
+ },
+ [`${warning} &:hover`]: {
+ color: semanticColorVars.warningTextHover,
+ },
+ '&:hover': {
+ color: semanticColorVars.linkHover,
+ },
+ },
+})
+
+globalStyle(`${Section} a.data-footnote-backref`, {
+ color: semanticColorVars.link,
+ fontWeight: fontWeightVars.medium,
+ textUnderlineOffset: spaceVars['2'],
+ textDecoration: 'underline',
+})
+
+globalStyle(`${Section} a.data-footnote-backref:hover`, {
+ color: semanticColorVars.linkHover,
+})
diff --git a/app/components/mdx/Anchor.tsx b/app/components/mdx/Anchor.tsx
new file mode 100644
index 00000000..03bec575
--- /dev/null
+++ b/app/components/mdx/Anchor.tsx
@@ -0,0 +1,35 @@
+import { clsx } from 'clsx'
+import type { ReactNode } from 'react'
+import { useLocation } from 'react-router-dom'
+
+import { Link } from '../Link.js'
+import * as styles from './Anchor.css.js'
+import { Autolink } from './Autolink.js'
+
+type AnchorProps = {
+ children: ReactNode
+ className?: string
+ href?: string
+}
+
+export function Anchor(props: AnchorProps) {
+ const { children, href } = props
+ const { pathname } = useLocation()
+
+ // Heading slug links
+ if (
+ children &&
+ typeof children === 'object' &&
+ 'props' in children &&
+ children.props['data-autolink-icon']
+ )
+ return
+
+ // ID links
+ if (href?.match(/^#/))
+ return (
+
+ )
+
+ return
+}
diff --git a/app/components/mdx/Aside.css.ts b/app/components/mdx/Aside.css.ts
new file mode 100644
index 00000000..3c03259e
--- /dev/null
+++ b/app/components/mdx/Aside.css.ts
@@ -0,0 +1,3 @@
+import { style } from '@vanilla-extract/css'
+
+export const root = style({})
diff --git a/app/components/mdx/Aside.tsx b/app/components/mdx/Aside.tsx
new file mode 100644
index 00000000..c37ca15a
--- /dev/null
+++ b/app/components/mdx/Aside.tsx
@@ -0,0 +1,16 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import { Callout, type CalloutProps } from '../Callout.js'
+import * as styles from './Aside.css.js'
+
+export function Aside(props: DetailedHTMLProps, HTMLElement>) {
+ const className = clsx(props.className, styles.root)
+ if ('data-callout' in props)
+ return (
+
+ {props.children}
+
+ )
+ return
+}
diff --git a/app/components/mdx/Autolink.css.ts b/app/components/mdx/Autolink.css.ts
new file mode 100644
index 00000000..d54d249f
--- /dev/null
+++ b/app/components/mdx/Autolink.css.ts
@@ -0,0 +1,17 @@
+import { style } from '@vanilla-extract/css'
+
+import { root as Heading } from './Heading.css.js'
+
+export const root = style({
+ opacity: 0,
+ marginTop: '0.1em',
+ position: 'absolute',
+ transition: 'opacity 0.1s, transform 0.1s,',
+ transform: 'translateX(-2px) scale(0.98)',
+ selectors: {
+ [`${Heading}:hover &`]: {
+ opacity: 1,
+ transform: 'translateX(0) scale(1)',
+ },
+ },
+})
diff --git a/app/components/mdx/Autolink.tsx b/app/components/mdx/Autolink.tsx
new file mode 100644
index 00000000..cbc41d86
--- /dev/null
+++ b/app/components/mdx/Autolink.tsx
@@ -0,0 +1,12 @@
+import { clsx } from 'clsx'
+import { type AnchorHTMLAttributes, type DetailedHTMLProps } from 'react'
+import { Link } from 'react-router-dom'
+
+import * as styles from './Autolink.css.js'
+
+export function Autolink(
+ props: Omit, HTMLAnchorElement>, 'ref'>,
+) {
+ if (!props.href) return null
+ return
+}
diff --git a/app/components/mdx/AutolinkIcon.css.ts b/app/components/mdx/AutolinkIcon.css.ts
new file mode 100644
index 00000000..6c33580b
--- /dev/null
+++ b/app/components/mdx/AutolinkIcon.css.ts
@@ -0,0 +1,21 @@
+import { createVar, style } from '@vanilla-extract/css'
+
+import { primitiveColorVars } from '../../styles/vars.css.js'
+import { root as Autolink } from './Autolink.css.js'
+
+export const iconUrl = createVar('iconUrl')
+
+export const root = style({
+ backgroundColor: primitiveColorVars.textAccent,
+ display: 'inline-block',
+ marginLeft: '0.25em',
+ height: '0.8em',
+ width: '0.8em',
+ mask: `${iconUrl} no-repeat center / contain`,
+ transition: 'background-color 0.1s',
+ selectors: {
+ [`${Autolink}:hover &`]: {
+ backgroundColor: primitiveColorVars.textAccentHover,
+ },
+ },
+})
diff --git a/app/components/mdx/AutolinkIcon.tsx b/app/components/mdx/AutolinkIcon.tsx
new file mode 100644
index 00000000..4adb8057
--- /dev/null
+++ b/app/components/mdx/AutolinkIcon.tsx
@@ -0,0 +1,22 @@
+import { assignInlineVars } from '@vanilla-extract/dynamic'
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type ImgHTMLAttributes } from 'react'
+
+import { useConfig } from '../../hooks/useConfig.js'
+import * as styles from './AutolinkIcon.css.js'
+
+export function AutolinkIcon(
+ props: DetailedHTMLProps, HTMLImageElement>,
+) {
+ const { basePath } = useConfig()
+ const assetBasePath = import.meta.env.PROD ? basePath : ''
+ return (
+
+ )
+}
diff --git a/app/components/mdx/Blockquote.css.ts b/app/components/mdx/Blockquote.css.ts
new file mode 100644
index 00000000..900e5679
--- /dev/null
+++ b/app/components/mdx/Blockquote.css.ts
@@ -0,0 +1,9 @@
+import { style } from '@vanilla-extract/css'
+
+import { semanticColorVars, spaceVars } from '../../styles/vars.css.js'
+
+export const root = style({
+ borderLeft: `2px solid ${semanticColorVars.blockquoteBorder}`,
+ paddingLeft: spaceVars['16'],
+ marginBottom: spaceVars['16'],
+})
diff --git a/app/components/mdx/Blockquote.tsx b/app/components/mdx/Blockquote.tsx
new file mode 100644
index 00000000..7a0980ad
--- /dev/null
+++ b/app/components/mdx/Blockquote.tsx
@@ -0,0 +1,10 @@
+import { clsx } from 'clsx'
+import { type BlockquoteHTMLAttributes, type DetailedHTMLProps } from 'react'
+
+import * as styles from './Blockquote.css.js'
+
+export function Blockquote(
+ props: DetailedHTMLProps, HTMLQuoteElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/Code.css.ts b/app/components/mdx/Code.css.ts
new file mode 100644
index 00000000..d8f1e6ca
--- /dev/null
+++ b/app/components/mdx/Code.css.ts
@@ -0,0 +1,61 @@
+import { style } from '@vanilla-extract/css'
+
+import {
+ borderRadiusVars,
+ fontSizeVars,
+ semanticColorVars,
+ spaceVars,
+} from '../../styles/vars.css.js'
+import { danger, info, success, tip, warning } from '../Callout.css.js'
+import { root as Anchor } from './Anchor.css.js'
+import { root as Heading } from './Heading.css.js'
+import { root as Pre } from './Pre.css.js'
+
+export const root = style({
+ transition: 'color 0.1s',
+ selectors: {
+ [`:not(${Pre})>&`]: {
+ backgroundColor: semanticColorVars.codeInlineBackground,
+ border: `1px solid ${semanticColorVars.codeInlineBorder}`,
+ borderRadius: borderRadiusVars['4'],
+ color: semanticColorVars.codeInlineText,
+ fontSize: fontSizeVars.code,
+ padding: `${spaceVars['3']} ${spaceVars['6']}`,
+ },
+ [`${Anchor}>&`]: {
+ color: semanticColorVars.link,
+ textDecoration: 'underline',
+ textUnderlineOffset: spaceVars['2'],
+ },
+ [`${Anchor}:hover>&`]: {
+ color: semanticColorVars.linkHover,
+ },
+ [`${danger} &`]: {
+ color: semanticColorVars.dangerText,
+ },
+ [`${info} &`]: {
+ color: semanticColorVars.infoText,
+ },
+ [`${success} &`]: {
+ color: semanticColorVars.successText,
+ },
+ [`${tip} &`]: {
+ color: semanticColorVars.tipText,
+ },
+ [`${warning} &`]: {
+ color: semanticColorVars.warningText,
+ },
+ [`${Heading} &`]: {
+ color: 'inherit',
+ },
+ '.twoslash-popup-info-hover>&': {
+ backgroundColor: 'inherit',
+ padding: 0,
+ // @ts-expect-error
+ textWrap: 'wrap',
+ },
+ '.twoslash-popup-jsdoc &': {
+ display: 'inline',
+ },
+ },
+})
diff --git a/app/components/mdx/Code.tsx b/app/components/mdx/Code.tsx
new file mode 100644
index 00000000..ca284e65
--- /dev/null
+++ b/app/components/mdx/Code.tsx
@@ -0,0 +1,28 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './Code.css.js'
+
+export function Code(props: DetailedHTMLProps, HTMLElement>) {
+ const children = filterEmptyLines(props.children)
+ return (
+
+ {children}
+
+ )
+}
+
+function filterEmptyLines(nodes: React.ReactNode) {
+ if (!Array.isArray(nodes)) return nodes
+ return nodes
+ .map((child, index) =>
+ child.props &&
+ 'data-line' in child.props &&
+ typeof child.props.children === 'string' &&
+ child.props.children.trim() === '' &&
+ nodes[index + 1]?.props?.className?.includes('twoslash-tag-line')
+ ? null
+ : child,
+ )
+ .filter(Boolean)
+}
diff --git a/app/components/mdx/CodeBlock.css.ts b/app/components/mdx/CodeBlock.css.ts
new file mode 100644
index 00000000..7a075f4f
--- /dev/null
+++ b/app/components/mdx/CodeBlock.css.ts
@@ -0,0 +1,182 @@
+import { globalStyle, style } from '@vanilla-extract/css'
+import {
+ borderRadiusVars,
+ fontSizeVars,
+ lineHeightVars,
+ primitiveColorVars,
+ semanticColorVars,
+ spaceVars,
+ viewportVars,
+} from '../../styles/vars.css.js'
+import { root as Callout } from '../Callout.css.js'
+import { root as Tabs } from '../Tabs.css.js'
+
+export const root = style({
+ border: `1px solid ${semanticColorVars.codeInlineBorder}`,
+ borderRadius: borderRadiusVars['4'],
+ '@media': {
+ [viewportVars['max-720px']]: {
+ borderRadius: 0,
+ borderRight: 'none',
+ borderLeft: 'none',
+ marginLeft: `calc(-1 * ${spaceVars['16']})`,
+ marginRight: `calc(-1 * ${spaceVars['16']})`,
+ },
+ },
+ selectors: {
+ [`${Tabs} &, ${Callout} &`]: {
+ border: 'none',
+ marginLeft: 'unset',
+ marginRight: 'unset',
+ },
+ },
+})
+
+globalStyle(`${root} code`, {
+ display: 'grid',
+ fontSize: fontSizeVars.codeBlock,
+})
+
+globalStyle(`${Callout} ${root} code`, {
+ fontSize: fontSizeVars.calloutCodeBlock,
+})
+
+globalStyle(`${root} pre`, {
+ backgroundColor: semanticColorVars.codeBlockBackground,
+ borderRadius: borderRadiusVars['4'],
+ overflowX: 'auto',
+ padding: `${spaceVars['20']} ${spaceVars['0']}`,
+ '@media': {
+ [viewportVars['max-720px']]: {
+ borderRadius: 0,
+ },
+ },
+})
+
+globalStyle(`${Callout} ${root} pre`, {
+ backgroundColor: `color-mix(in srgb, ${semanticColorVars.codeBlockBackground} 65%, transparent) !important`,
+ border: `1px solid ${semanticColorVars.codeInlineBorder}`,
+ borderRadius: borderRadiusVars['4'],
+ padding: `${spaceVars['12']} ${spaceVars['0']}`,
+})
+
+globalStyle(`${root} .line`, {
+ borderLeft: '2px solid transparent',
+ padding: `${spaceVars['0']} ${spaceVars['22']}`,
+ lineHeight: lineHeightVars.code,
+})
+globalStyle(`${Callout} ${root} .line`, {
+ padding: `${spaceVars['0']} ${spaceVars['12']}`,
+})
+globalStyle(`${root} .twoslash-popup-info .line`, {
+ padding: `${spaceVars['0']} ${spaceVars['4']}`,
+})
+globalStyle(`${root} .twoslash-popup-info-hover .line`, {
+ display: 'inline-block',
+ padding: `${spaceVars['0']} ${spaceVars['8']}`,
+})
+globalStyle(`${root} .twoslash-error-line, ${root} .twoslash-tag-line`, {
+ padding: `${spaceVars['0']} ${spaceVars['22']}`,
+})
+
+globalStyle(`${root} [data-line-numbers]`, {
+ counterReset: 'line',
+})
+
+globalStyle(`${root} [data-line-numbers] > .line`, {
+ padding: `${spaceVars['0']} ${spaceVars['16']}`,
+})
+
+globalStyle(`${root} [data-line-numbers] > .line::before`, {
+ color: semanticColorVars.lineNumber,
+ content: 'counter(line)',
+ display: 'inline-block',
+ fontSize: fontSizeVars.lineNumber,
+ marginRight: spaceVars['16'],
+ textAlign: 'right',
+ width: '1rem',
+})
+
+globalStyle(`${root} [data-line-numbers] > .line:not(.diff.remove + .diff.add)::before`, {
+ counterIncrement: 'line',
+})
+
+globalStyle(`${root} [data-line-numbers] > .line.diff::after`, {
+ marginLeft: `calc(-1 * ${spaceVars['4']})`,
+})
+
+globalStyle(`${root} .highlighted`, {
+ backgroundColor: semanticColorVars.codeHighlightBackground,
+ borderLeft: `2px solid ${semanticColorVars.codeHighlightBorder}`,
+ boxSizing: 'content-box',
+})
+
+globalStyle(`${root} .highlighted-word`, {
+ borderRadius: borderRadiusVars['2'],
+ backgroundColor: `${semanticColorVars.codeCharacterHighlightBackground} !important`,
+ boxShadow: `0 0 0 4px ${semanticColorVars.codeCharacterHighlightBackground}`,
+})
+
+globalStyle(`${root} .has-diff`, {
+ position: 'relative',
+})
+
+globalStyle(`${root} .line.diff::after`, {
+ position: 'absolute',
+ left: spaceVars['8'],
+})
+
+globalStyle(`${root} .line.diff.add`, {
+ backgroundColor: primitiveColorVars.backgroundGreenTint2,
+})
+
+globalStyle(`${root} .line.diff.add::after`, {
+ content: '+',
+ color: primitiveColorVars.textGreen,
+})
+
+globalStyle(`${root} .line.diff.remove`, {
+ backgroundColor: primitiveColorVars.backgroundRedTint2,
+ opacity: '0.6',
+})
+
+globalStyle(`${root} .line.diff.remove > span`, {
+ filter: 'grayscale(1)',
+})
+
+globalStyle(`${root} .line.diff.remove::after`, {
+ content: '-',
+ color: primitiveColorVars.textRed,
+})
+
+globalStyle(
+ `${root} .has-focused > code > .line:not(.focused), ${root} .has-focused > code > .twoslash-meta-line:not(.focused)`,
+ {
+ opacity: '0.3',
+ transition: 'opacity 0.2s',
+ },
+)
+
+globalStyle(
+ `${root}:hover .has-focused .line:not(.focused), ${root}:hover .has-focused .twoslash-meta-line:not(.focused)`,
+ {
+ opacity: '1',
+ transition: 'opacity 0.2s',
+ },
+)
+
+globalStyle(`${root} .line, ${root} .twoslash-error-line, ${root} .twoslash-tag-line`, {
+ '@media': {
+ [viewportVars['max-720px']]: {
+ padding: `0 ${spaceVars['16']}`,
+ },
+ },
+})
+
+globalStyle(`${root} .line.diff::after`, {
+ '@media': {
+ [viewportVars['max-720px']]: {
+ left: spaceVars['6'],
+ },
+ },
+})
diff --git a/app/components/mdx/CodeBlock.tsx b/app/components/mdx/CodeBlock.tsx
new file mode 100644
index 00000000..c3d9243a
--- /dev/null
+++ b/app/components/mdx/CodeBlock.tsx
@@ -0,0 +1,10 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './CodeBlock.css.js'
+
+export function CodeBlock(
+ props: DetailedHTMLProps, HTMLDivElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/CodeGroup.css.ts b/app/components/mdx/CodeGroup.css.ts
new file mode 100644
index 00000000..55c657e9
--- /dev/null
+++ b/app/components/mdx/CodeGroup.css.ts
@@ -0,0 +1,15 @@
+import { style } from '@vanilla-extract/css'
+
+import { spaceVars, viewportVars } from '../../styles/vars.css.js'
+
+export const root = style({
+ '@media': {
+ [viewportVars['max-720px']]: {
+ borderRadius: 0,
+ borderRight: 'none',
+ borderLeft: 'none',
+ marginLeft: `calc(-1 * ${spaceVars['16']})`,
+ marginRight: `calc(-1 * ${spaceVars['16']})`,
+ },
+ },
+})
diff --git a/app/components/mdx/CodeGroup.tsx b/app/components/mdx/CodeGroup.tsx
new file mode 100644
index 00000000..8703b80d
--- /dev/null
+++ b/app/components/mdx/CodeGroup.tsx
@@ -0,0 +1,38 @@
+import type { ReactElement } from 'react'
+
+import * as Tabs from '../Tabs.js'
+import * as styles from './CodeGroup.css.js'
+
+export function CodeGroup({ children }: { children: ReactElement[] }) {
+ if (!Array.isArray(children)) return null
+ const tabs = children.map((child_) => {
+ const child = child_.props['data-title'] ? child_ : child_.props.children
+ const { props } = child
+ const title = props['data-title'] as string
+ const content = props.children as ReactElement
+ return { title, content }
+ })
+ return (
+
+
+ {tabs.map(({ title }, i) => (
+
+ {title}
+
+ ))}
+
+ {tabs.map(({ title, content }, i) => {
+ const isShiki = content.props?.className?.includes('shiki')
+ return (
+
+ {content}
+
+ )
+ })}
+
+ )
+}
diff --git a/app/components/mdx/CodeTitle.css.ts b/app/components/mdx/CodeTitle.css.ts
new file mode 100644
index 00000000..c2eb1ff1
--- /dev/null
+++ b/app/components/mdx/CodeTitle.css.ts
@@ -0,0 +1,34 @@
+import { style } from '@vanilla-extract/css'
+import {
+ fontSizeVars,
+ fontWeightVars,
+ primitiveColorVars,
+ semanticColorVars,
+ spaceVars,
+ viewportVars,
+} from '../../styles/vars.css.js'
+import { root as CodeGroup } from './CodeGroup.css.js'
+
+export const root = style({
+ alignItems: 'center',
+ backgroundColor: semanticColorVars.codeTitleBackground,
+ borderBottom: `1px solid ${primitiveColorVars.border}`,
+ color: primitiveColorVars.text3,
+ display: 'flex',
+ fontSize: fontSizeVars['14'],
+ fontWeight: fontWeightVars.medium,
+ gap: spaceVars['6'],
+ padding: `${spaceVars['8']} ${spaceVars['24']}`,
+ '@media': {
+ [viewportVars['max-720px']]: {
+ borderRadius: 0,
+ paddingLeft: spaceVars['16'],
+ paddingRight: spaceVars['16'],
+ },
+ },
+ selectors: {
+ [`${CodeGroup} &`]: {
+ display: 'none',
+ },
+ },
+})
diff --git a/app/components/mdx/CodeTitle.tsx b/app/components/mdx/CodeTitle.tsx
new file mode 100644
index 00000000..c89dc259
--- /dev/null
+++ b/app/components/mdx/CodeTitle.tsx
@@ -0,0 +1,24 @@
+import { clsx } from 'clsx'
+
+import { Icon } from '../Icon.js'
+import { File } from '../icons/File.js'
+import { Terminal } from '../icons/Terminal.js'
+import * as styles from './CodeTitle.css.js'
+
+export function CodeTitle({
+ children,
+ className,
+ language,
+ ...props
+}: { children: string; className?: string; language?: string }) {
+ return (
+
+ {language === 'bash' ? (
+
+ ) : children.match(/\.(.*)$/) ? (
+
+ ) : null}
+ {children}
+
+ )
+}
diff --git a/app/components/mdx/Details.css.ts b/app/components/mdx/Details.css.ts
new file mode 100644
index 00000000..699d339d
--- /dev/null
+++ b/app/components/mdx/Details.css.ts
@@ -0,0 +1,11 @@
+import { style } from '@vanilla-extract/css'
+
+import { root as Callout } from '../Callout.css.js'
+
+export const root = style({
+ selectors: {
+ [`${Callout} > * + &`]: {
+ marginTop: '-8px',
+ },
+ },
+})
diff --git a/app/components/mdx/Details.tsx b/app/components/mdx/Details.tsx
new file mode 100644
index 00000000..93f7142f
--- /dev/null
+++ b/app/components/mdx/Details.tsx
@@ -0,0 +1,10 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type DetailsHTMLAttributes } from 'react'
+
+import * as styles from './Details.css.js'
+
+export function Details(
+ props: DetailedHTMLProps, HTMLDetailsElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/Div.css.ts b/app/components/mdx/Div.css.ts
new file mode 100644
index 00000000..3c03259e
--- /dev/null
+++ b/app/components/mdx/Div.css.ts
@@ -0,0 +1,3 @@
+import { style } from '@vanilla-extract/css'
+
+export const root = style({})
diff --git a/app/components/mdx/Div.tsx b/app/components/mdx/Div.tsx
new file mode 100644
index 00000000..76908c87
--- /dev/null
+++ b/app/components/mdx/Div.tsx
@@ -0,0 +1,29 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import { useLayout } from '../../hooks/useLayout.js'
+import { Authors } from '../Authors.js'
+import { BlogPosts } from '../BlogPosts.js'
+import { Sponsors } from '../Sponsors.js'
+import { AutolinkIcon } from './AutolinkIcon.js'
+import { CodeGroup } from './CodeGroup.js'
+import * as styles from './Div.css.js'
+import { Steps } from './Steps.js'
+import { Subtitle } from './Subtitle.js'
+
+export function Div(props: DetailedHTMLProps, HTMLDivElement>) {
+ const { layout } = useLayout()
+
+ const className = clsx(props.className, styles.root)
+
+ if (props.className === 'code-group')
+ return
+ if ('data-authors' in props) return
+ if ('data-blog-posts' in props) return
+ if ('data-sponsors' in props) return
+ if ('data-autolink-icon' in props && layout === 'docs')
+ return
+ if ('data-vocs-steps' in props) return
+ if (props.role === 'doc-subtitle') return
+ return
+}
diff --git a/app/components/mdx/Figcaption.css.ts b/app/components/mdx/Figcaption.css.ts
new file mode 100644
index 00000000..3c03259e
--- /dev/null
+++ b/app/components/mdx/Figcaption.css.ts
@@ -0,0 +1,3 @@
+import { style } from '@vanilla-extract/css'
+
+export const root = style({})
diff --git a/app/components/mdx/Figcaption.tsx b/app/components/mdx/Figcaption.tsx
new file mode 100644
index 00000000..fdd6f3ed
--- /dev/null
+++ b/app/components/mdx/Figcaption.tsx
@@ -0,0 +1,10 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './Figcaption.css.js'
+
+export function Figcaption(props: DetailedHTMLProps, HTMLElement>) {
+ const className = clsx(props.className, styles.root)
+
+ return
+}
diff --git a/app/components/mdx/Figure.css.ts b/app/components/mdx/Figure.css.ts
new file mode 100644
index 00000000..3c03259e
--- /dev/null
+++ b/app/components/mdx/Figure.css.ts
@@ -0,0 +1,3 @@
+import { style } from '@vanilla-extract/css'
+
+export const root = style({})
diff --git a/app/components/mdx/Figure.tsx b/app/components/mdx/Figure.tsx
new file mode 100644
index 00000000..3be4ba94
--- /dev/null
+++ b/app/components/mdx/Figure.tsx
@@ -0,0 +1,10 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './Figure.css.js'
+
+export function Figure(props: DetailedHTMLProps, HTMLElement>) {
+ const className = clsx(props.className, styles.root)
+
+ return
+}
diff --git a/app/components/mdx/Footnotes.css.ts b/app/components/mdx/Footnotes.css.ts
new file mode 100644
index 00000000..3c03259e
--- /dev/null
+++ b/app/components/mdx/Footnotes.css.ts
@@ -0,0 +1,3 @@
+import { style } from '@vanilla-extract/css'
+
+export const root = style({})
diff --git a/app/components/mdx/Footnotes.tsx b/app/components/mdx/Footnotes.tsx
new file mode 100644
index 00000000..29c206a4
--- /dev/null
+++ b/app/components/mdx/Footnotes.tsx
@@ -0,0 +1,8 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './Footnotes.css.js'
+
+export function Footnotes(props: DetailedHTMLProps, HTMLElement>) {
+ return
+}
diff --git a/app/components/mdx/H1.css.ts b/app/components/mdx/H1.css.ts
new file mode 100644
index 00000000..7f07ed03
--- /dev/null
+++ b/app/components/mdx/H1.css.ts
@@ -0,0 +1,8 @@
+import { style } from '@vanilla-extract/css'
+
+import { fontSizeVars } from '../../styles/vars.css.js'
+
+export const root = style({
+ fontSize: fontSizeVars.h1,
+ letterSpacing: '-0.02em',
+})
diff --git a/app/components/mdx/H1.tsx b/app/components/mdx/H1.tsx
new file mode 100644
index 00000000..403c53d9
--- /dev/null
+++ b/app/components/mdx/H1.tsx
@@ -0,0 +1,11 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './H1.css.js'
+import { Heading } from './Heading.js'
+
+export function H1(
+ props: DetailedHTMLProps, HTMLHeadingElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/H2.css.ts b/app/components/mdx/H2.css.ts
new file mode 100644
index 00000000..97c12161
--- /dev/null
+++ b/app/components/mdx/H2.css.ts
@@ -0,0 +1,23 @@
+import { style } from '@vanilla-extract/css'
+import { fontSizeVars, primitiveColorVars, spaceVars } from '../../styles/vars.css.js'
+import { root as header } from './Header.css.js'
+
+export const root = style({
+ fontSize: fontSizeVars.h2,
+ letterSpacing: '-0.02em',
+ selectors: {
+ '&&:not(:last-child)': {
+ marginBottom: spaceVars['24'],
+ },
+ [`:not(${header}) + &:not(:only-child)`]: {
+ borderTop: `1px solid ${primitiveColorVars.border}`,
+ marginTop: spaceVars['56'],
+ paddingTop: spaceVars['24'],
+ },
+ '[data-layout="landing"] &&': {
+ borderTop: 'none',
+ marginTop: spaceVars['24'],
+ paddingTop: 0,
+ },
+ },
+})
diff --git a/app/components/mdx/H2.tsx b/app/components/mdx/H2.tsx
new file mode 100644
index 00000000..cb242edd
--- /dev/null
+++ b/app/components/mdx/H2.tsx
@@ -0,0 +1,11 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './H2.css.js'
+import { Heading } from './Heading.js'
+
+export function H2(
+ props: DetailedHTMLProps, HTMLHeadingElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/H3.css.ts b/app/components/mdx/H3.css.ts
new file mode 100644
index 00000000..d5f9532a
--- /dev/null
+++ b/app/components/mdx/H3.css.ts
@@ -0,0 +1,19 @@
+import { style } from '@vanilla-extract/css'
+import { fontSizeVars, spaceVars } from '../../styles/vars.css.js'
+import { root as H2 } from './H2.css.js'
+
+export const root = style({
+ fontSize: fontSizeVars.h3,
+ selectors: {
+ '&:not(:first-child)': {
+ marginTop: spaceVars['18'],
+ paddingTop: spaceVars['18'],
+ },
+ '&&:not(:last-child)': {
+ marginBottom: spaceVars['24'],
+ },
+ [`${H2}+&`]: {
+ paddingTop: spaceVars['0'],
+ },
+ },
+})
diff --git a/app/components/mdx/H3.tsx b/app/components/mdx/H3.tsx
new file mode 100644
index 00000000..55347ed4
--- /dev/null
+++ b/app/components/mdx/H3.tsx
@@ -0,0 +1,11 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './H3.css.js'
+import { Heading } from './Heading.js'
+
+export function H3(
+ props: DetailedHTMLProps, HTMLHeadingElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/H4.css.ts b/app/components/mdx/H4.css.ts
new file mode 100644
index 00000000..a1f79042
--- /dev/null
+++ b/app/components/mdx/H4.css.ts
@@ -0,0 +1,19 @@
+import { style } from '@vanilla-extract/css'
+import { fontSizeVars, spaceVars } from '../../styles/vars.css.js'
+import { root as H3 } from './H3.css.js'
+
+export const root = style({
+ fontSize: fontSizeVars.h4,
+ selectors: {
+ '&:not(:first-child)': {
+ marginTop: spaceVars['18'],
+ paddingTop: spaceVars['12'],
+ },
+ '&&:not(:last-child)': {
+ marginBottom: spaceVars['24'],
+ },
+ [`${H3}+&`]: {
+ paddingTop: spaceVars['0'],
+ },
+ },
+})
diff --git a/app/components/mdx/H4.tsx b/app/components/mdx/H4.tsx
new file mode 100644
index 00000000..2da241f4
--- /dev/null
+++ b/app/components/mdx/H4.tsx
@@ -0,0 +1,11 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './H4.css.js'
+import { Heading } from './Heading.js'
+
+export function H4(
+ props: DetailedHTMLProps, HTMLHeadingElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/H5.css.ts b/app/components/mdx/H5.css.ts
new file mode 100644
index 00000000..4dd7664b
--- /dev/null
+++ b/app/components/mdx/H5.css.ts
@@ -0,0 +1,18 @@
+import { style } from '@vanilla-extract/css'
+import { fontSizeVars, spaceVars } from '../../styles/vars.css.js'
+import { root as H4 } from './H4.css.js'
+
+export const root = style({
+ fontSize: fontSizeVars.h5,
+ selectors: {
+ '&:not(:first-child)': {
+ marginTop: spaceVars['16'],
+ },
+ '&&:not(:last-child)': {
+ marginBottom: spaceVars['24'],
+ },
+ [`${H4}+&`]: {
+ paddingTop: spaceVars['0'],
+ },
+ },
+})
diff --git a/app/components/mdx/H5.tsx b/app/components/mdx/H5.tsx
new file mode 100644
index 00000000..2e842eb8
--- /dev/null
+++ b/app/components/mdx/H5.tsx
@@ -0,0 +1,11 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './H5.css.js'
+import { Heading } from './Heading.js'
+
+export function H5(
+ props: DetailedHTMLProps, HTMLHeadingElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/H6.css.ts b/app/components/mdx/H6.css.ts
new file mode 100644
index 00000000..f5465bcc
--- /dev/null
+++ b/app/components/mdx/H6.css.ts
@@ -0,0 +1,18 @@
+import { style } from '@vanilla-extract/css'
+import { fontSizeVars, spaceVars } from '../../styles/vars.css.js'
+import { root as H5 } from './H5.css.js'
+
+export const root = style({
+ fontSize: fontSizeVars.h6,
+ selectors: {
+ '&:not(:first-child)': {
+ marginTop: spaceVars['16'],
+ },
+ '&&:not(:last-child)': {
+ marginBottom: spaceVars['24'],
+ },
+ [`${H5}+&`]: {
+ paddingTop: spaceVars['0'],
+ },
+ },
+})
diff --git a/app/components/mdx/H6.tsx b/app/components/mdx/H6.tsx
new file mode 100644
index 00000000..c1819c10
--- /dev/null
+++ b/app/components/mdx/H6.tsx
@@ -0,0 +1,11 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './H6.css.js'
+import { Heading } from './Heading.js'
+
+export function H6(
+ props: DetailedHTMLProps, HTMLHeadingElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/Header.css.ts b/app/components/mdx/Header.css.ts
new file mode 100644
index 00000000..03a06b7a
--- /dev/null
+++ b/app/components/mdx/Header.css.ts
@@ -0,0 +1,18 @@
+import { style } from '@vanilla-extract/css'
+import { primitiveColorVars, spaceVars } from '../../styles/vars.css.js'
+
+export const root = style({
+ borderBottom: `1px solid ${primitiveColorVars.border}`,
+ selectors: {
+ '&:not(:last-child)': {
+ marginBottom: spaceVars['28'],
+ paddingBottom: spaceVars['28'],
+ },
+ '[data-layout="landing"] &': {
+ paddingBottom: spaceVars['16'],
+ },
+ '[data-layout="landing"] &:not(:first-child)': {
+ paddingTop: spaceVars['36'],
+ },
+ },
+})
diff --git a/app/components/mdx/Header.tsx b/app/components/mdx/Header.tsx
new file mode 100644
index 00000000..491edf0a
--- /dev/null
+++ b/app/components/mdx/Header.tsx
@@ -0,0 +1,8 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './Header.css.js'
+
+export function Header(props: DetailedHTMLProps, HTMLElement>) {
+ return
+}
diff --git a/app/components/mdx/Heading.css.ts b/app/components/mdx/Heading.css.ts
new file mode 100644
index 00000000..e6d529f1
--- /dev/null
+++ b/app/components/mdx/Heading.css.ts
@@ -0,0 +1,48 @@
+import { style } from '@vanilla-extract/css'
+import {
+ fontWeightVars,
+ lineHeightVars,
+ primitiveColorVars,
+ spaceVars,
+ topNavVars,
+ viewportVars,
+} from '../../styles/vars.css.js'
+
+import { title as Step_title } from '../Step.css.js'
+import { root as Header } from './Header.css.js'
+
+export const root = style({
+ alignItems: 'center',
+ color: primitiveColorVars.heading,
+ fontWeight: fontWeightVars.semibold,
+ gap: '0.25em',
+ lineHeight: lineHeightVars.heading,
+ position: 'relative',
+})
+
+export const slugTarget = style(
+ {
+ position: 'absolute',
+ top: '0px',
+ visibility: 'hidden',
+ '@media': {
+ [viewportVars['min-1080px']]: {
+ top: `calc(-1 * (${topNavVars.height}))`,
+ selectors: {
+ [`${Header} &, ${Step_title} &, ${Header} + ${root} &`]: {
+ top: `calc(-1 * (${topNavVars.height} + ${spaceVars['24']}))`,
+ },
+ },
+ },
+ [viewportVars['max-1080px']]: {
+ top: `calc(-1 * ${topNavVars.curtainHeight})`,
+ selectors: {
+ [`${Header} &, ${Header} + ${root} &`]: {
+ top: `calc(-1 * calc(${topNavVars.curtainHeight} + ${spaceVars['24']}))`,
+ },
+ },
+ },
+ },
+ },
+ 'slugTarget',
+)
diff --git a/app/components/mdx/Heading.tsx b/app/components/mdx/Heading.tsx
new file mode 100644
index 00000000..d999e25d
--- /dev/null
+++ b/app/components/mdx/Heading.tsx
@@ -0,0 +1,19 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import { root, slugTarget } from './Heading.css.js'
+
+export function Heading({
+ level,
+ ...props
+}: DetailedHTMLProps, HTMLHeadingElement> & {
+ level: 1 | 2 | 3 | 4 | 5 | 6
+}) {
+ const Component = `h${level}` as any
+ return (
+
+
+ {props.children}
+
+ )
+}
diff --git a/app/components/mdx/HorizontalRule.css.ts b/app/components/mdx/HorizontalRule.css.ts
new file mode 100644
index 00000000..2638665e
--- /dev/null
+++ b/app/components/mdx/HorizontalRule.css.ts
@@ -0,0 +1,7 @@
+import { style } from '@vanilla-extract/css'
+import { semanticColorVars, spaceVars } from '../../styles/vars.css.js'
+
+export const root = style({
+ borderTop: `1px solid ${semanticColorVars.hr}`,
+ marginBottom: spaceVars['16'],
+})
diff --git a/app/components/mdx/HorizontalRule.tsx b/app/components/mdx/HorizontalRule.tsx
new file mode 100644
index 00000000..75a8d1b9
--- /dev/null
+++ b/app/components/mdx/HorizontalRule.tsx
@@ -0,0 +1,10 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './HorizontalRule.css.js'
+
+export function HorizontalRule(
+ props: DetailedHTMLProps, HTMLHRElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/Kbd.css.ts b/app/components/mdx/Kbd.css.ts
new file mode 100644
index 00000000..b4247435
--- /dev/null
+++ b/app/components/mdx/Kbd.css.ts
@@ -0,0 +1,31 @@
+import { style } from '@vanilla-extract/css'
+
+import {
+ borderRadiusVars,
+ fontFamilyVars,
+ fontSizeVars,
+ primitiveColorVars,
+ spaceVars,
+} from '../../styles/vars.css.js'
+
+export const root = style({
+ color: primitiveColorVars.text2,
+ display: 'inline-block',
+ borderRadius: borderRadiusVars['3'],
+ fontSize: fontSizeVars['11'],
+ fontFamily: fontFamilyVars.default,
+ fontFeatureSettings: 'cv08',
+ lineHeight: '105%',
+ minWidth: '20px',
+ padding: spaceVars['3'],
+ paddingLeft: spaceVars['4'],
+ paddingRight: spaceVars['4'],
+ paddingTop: spaceVars['3'],
+ textAlign: 'center',
+ textTransform: 'capitalize',
+ verticalAlign: 'baseline',
+
+ border: `0.5px solid ${primitiveColorVars.border}`,
+ backgroundColor: primitiveColorVars.background3,
+ boxShadow: `${primitiveColorVars.shadow2} 0px 2px 0px 0px`,
+})
diff --git a/app/components/mdx/Kbd.tsx b/app/components/mdx/Kbd.tsx
new file mode 100644
index 00000000..3c43af6b
--- /dev/null
+++ b/app/components/mdx/Kbd.tsx
@@ -0,0 +1,8 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './Kbd.css.js'
+
+export function Kbd(props: DetailedHTMLProps, HTMLElement>) {
+ return
+}
diff --git a/app/components/mdx/List.css.ts b/app/components/mdx/List.css.ts
new file mode 100644
index 00000000..506ab42f
--- /dev/null
+++ b/app/components/mdx/List.css.ts
@@ -0,0 +1,70 @@
+import { globalStyle, style } from '@vanilla-extract/css'
+
+import { spaceVars } from '../../styles/vars.css.js'
+import { root as H2 } from './H2.css.js'
+import { root as H3 } from './H3.css.js'
+import { root as H4 } from './H4.css.js'
+import { root as H5 } from './H5.css.js'
+import { root as H6 } from './H6.css.js'
+
+export const root = style({
+ selectors: {
+ [`${H2}+&,${H3}+&,${H4}+&,${H5}+&,${H6}+&`]: {
+ marginTop: `calc(${spaceVars['8']} * -1)`,
+ },
+ '.vocs_Paragraph + &': {
+ marginTop: `calc(-1 * ${spaceVars['8']})`,
+ },
+ },
+})
+
+export const ordered = style(
+ {
+ listStyle: 'decimal',
+ paddingLeft: spaceVars['20'],
+ marginBottom: spaceVars['16'],
+ selectors: {
+ '& &': {
+ listStyle: 'lower-alpha',
+ },
+ '& & &': {
+ listStyle: 'lower-roman',
+ },
+ },
+ },
+ 'ordered',
+)
+
+export const unordered = style(
+ {
+ listStyle: 'disc',
+ paddingLeft: spaceVars['24'],
+ marginBottom: spaceVars['16'],
+ selectors: {
+ '& &': {
+ listStyle: 'circle',
+ },
+ },
+ },
+ 'unordered',
+)
+
+globalStyle(
+ [
+ `${ordered} ${ordered}`,
+ `${unordered} ${unordered}`,
+ `${ordered} ${unordered}`,
+ `${unordered} ${ordered}`,
+ ].join(','),
+ {
+ marginBottom: spaceVars['0'],
+ paddingTop: spaceVars['8'],
+ paddingLeft: spaceVars['16'],
+ paddingBottom: spaceVars['0'],
+ },
+)
+
+globalStyle(`${unordered}.contains-task-list`, {
+ listStyle: 'none',
+ paddingLeft: spaceVars['12'],
+})
diff --git a/app/components/mdx/List.tsx b/app/components/mdx/List.tsx
new file mode 100644
index 00000000..09712486
--- /dev/null
+++ b/app/components/mdx/List.tsx
@@ -0,0 +1,17 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './List.css.js'
+
+export function List({
+ ordered,
+ ...props
+}: DetailedHTMLProps, any> & { ordered?: boolean }) {
+ const Element = ordered ? 'ol' : 'ul'
+ return (
+
+ )
+}
diff --git a/app/components/mdx/ListItem.css.ts b/app/components/mdx/ListItem.css.ts
new file mode 100644
index 00000000..5a560be8
--- /dev/null
+++ b/app/components/mdx/ListItem.css.ts
@@ -0,0 +1,11 @@
+import { style } from '@vanilla-extract/css'
+import { lineHeightVars } from '../../styles/vars.css.js'
+
+export const root = style({
+ lineHeight: lineHeightVars.listItem,
+ selectors: {
+ '&:not(:last-child)': {
+ marginBottom: '0.5em',
+ },
+ },
+})
diff --git a/app/components/mdx/ListItem.tsx b/app/components/mdx/ListItem.tsx
new file mode 100644
index 00000000..d4879023
--- /dev/null
+++ b/app/components/mdx/ListItem.tsx
@@ -0,0 +1,8 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './ListItem.css.js'
+
+export function ListItem(props: DetailedHTMLProps, HTMLLIElement>) {
+ return
+}
diff --git a/app/components/mdx/Paragraph.css.ts b/app/components/mdx/Paragraph.css.ts
new file mode 100644
index 00000000..be1a80e4
--- /dev/null
+++ b/app/components/mdx/Paragraph.css.ts
@@ -0,0 +1,28 @@
+import { style } from '@vanilla-extract/css'
+
+import { lineHeightVars, semanticColorVars, spaceVars } from '../../styles/vars.css.js'
+import { root as Blockquote } from './Blockquote.css.js'
+import { root as H2 } from './H2.css.js'
+import { root as H3 } from './H3.css.js'
+import { root as H4 } from './H4.css.js'
+import { root as H5 } from './H5.css.js'
+import { root as H6 } from './H6.css.js'
+import { root as List } from './List.css.js'
+
+export const root = style({
+ lineHeight: lineHeightVars.paragraph,
+ get selectors() {
+ return {
+ [`${Blockquote}>&`]: {
+ color: semanticColorVars.blockquoteText,
+ marginBottom: spaceVars['8'],
+ },
+ [`${H2}+&,${H3}+&,${H4}+&,${H5}+&,${H6}+&,${List}+&`]: {
+ marginTop: `calc(${spaceVars['8']} * -1)`,
+ },
+ [`${root} + ${root}`]: {
+ marginTop: `calc(-1 * ${spaceVars['8']})`,
+ },
+ }
+ },
+})
diff --git a/app/components/mdx/Paragraph.tsx b/app/components/mdx/Paragraph.tsx
new file mode 100644
index 00000000..40c312d0
--- /dev/null
+++ b/app/components/mdx/Paragraph.tsx
@@ -0,0 +1,10 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './Paragraph.css.js'
+
+export function Paragraph(
+ props: DetailedHTMLProps, HTMLParagraphElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/Pre.css.ts b/app/components/mdx/Pre.css.ts
new file mode 100644
index 00000000..d54048f8
--- /dev/null
+++ b/app/components/mdx/Pre.css.ts
@@ -0,0 +1,10 @@
+import { style } from '@vanilla-extract/css'
+
+export const root = style({})
+
+export const wrapper = style(
+ {
+ position: 'relative',
+ },
+ 'wrapper',
+)
diff --git a/app/components/mdx/Pre.tsx b/app/components/mdx/Pre.tsx
new file mode 100644
index 00000000..ca305548
--- /dev/null
+++ b/app/components/mdx/Pre.tsx
@@ -0,0 +1,58 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes, type ReactNode, useMemo } from 'react'
+
+import { useCopyCode } from '../../hooks/useCopyCode.js'
+import { CopyButton } from '../CopyButton.js'
+import { CodeBlock } from './CodeBlock.js'
+import { CodeTitle } from './CodeTitle.js'
+import * as styles from './Pre.css.js'
+
+export function Pre({
+ children,
+ className,
+ ...props
+}: DetailedHTMLProps, HTMLPreElement> & {
+ 'data-lang'?: string
+ 'data-title'?: string
+}) {
+ const { copied, copy, ref } = useCopyCode()
+
+ function recurseChildren(children: ReactNode): ReactNode {
+ if (!children) return children
+ if (typeof children !== 'object') return children
+ if ('props' in children)
+ return {
+ ...children,
+ props: {
+ ...children.props,
+ children: Array.isArray(children.props.children)
+ ? children.props.children.map(recurseChildren)
+ : recurseChildren(children.props.children),
+ },
+ }
+ return children
+ }
+ const children_ = useMemo(() => recurseChildren(children), [children])
+
+ const wrap = (children: ReactNode) => {
+ if (className?.includes('shiki'))
+ return (
+
+ {props['data-title'] && (
+ {props['data-title']}
+ )}
+ {children}
+
+ )
+ return children
+ }
+
+ return wrap(
+
+
+ {'data-language' in props && }
+ {children_}
+
+
,
+ )
+}
diff --git a/app/components/mdx/Section.css.ts b/app/components/mdx/Section.css.ts
new file mode 100644
index 00000000..061f31df
--- /dev/null
+++ b/app/components/mdx/Section.css.ts
@@ -0,0 +1,8 @@
+import { style } from '@vanilla-extract/css'
+import { primitiveColorVars, spaceVars } from '../../styles/vars.css.js'
+
+export const root = style({
+ borderTop: `1px solid ${primitiveColorVars.border}`,
+ marginTop: spaceVars['56'],
+ paddingTop: spaceVars['24'],
+})
diff --git a/app/components/mdx/Section.tsx b/app/components/mdx/Section.tsx
new file mode 100644
index 00000000..57231c81
--- /dev/null
+++ b/app/components/mdx/Section.tsx
@@ -0,0 +1,11 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import { Footnotes } from './Footnotes.js'
+import * as styles from './Section.css.js'
+
+export function Section(props: DetailedHTMLProps, HTMLElement>) {
+ if ('data-footnotes' in props)
+ return
+ return
+}
diff --git a/app/components/mdx/Span.css.ts b/app/components/mdx/Span.css.ts
new file mode 100644
index 00000000..3c03259e
--- /dev/null
+++ b/app/components/mdx/Span.css.ts
@@ -0,0 +1,3 @@
+import { style } from '@vanilla-extract/css'
+
+export const root = style({})
diff --git a/app/components/mdx/Span.tsx b/app/components/mdx/Span.tsx
new file mode 100644
index 00000000..88bf3c1d
--- /dev/null
+++ b/app/components/mdx/Span.tsx
@@ -0,0 +1,13 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './Span.css.js'
+import { TwoslashPopover } from './TwoslashPopover.js'
+
+export function Span(props: DetailedHTMLProps, HTMLSpanElement>) {
+ const className = clsx(props.className, styles.root)
+
+ if (props.className?.includes('twoslash-hover'))
+ return
+ return
+}
diff --git a/app/components/mdx/Steps.tsx b/app/components/mdx/Steps.tsx
new file mode 100644
index 00000000..b46d283b
--- /dev/null
+++ b/app/components/mdx/Steps.tsx
@@ -0,0 +1,23 @@
+import { type ReactNode, cloneElement } from 'react'
+
+import * as stepStyles from '../Step.css.js'
+import { Step } from '../Step.js'
+import { Steps as Steps_ } from '../Steps.js'
+
+export function Steps({ children }: { children: ReactNode }) {
+ if (!Array.isArray(children)) return null
+ return (
+
+ {children.map(({ props }, i) => {
+ const [title, ...children] = Array.isArray(props.children)
+ ? props.children
+ : [props.children]
+ return (
+
+ {children}
+
+ )
+ })}
+
+ )
+}
diff --git a/app/components/mdx/Strong.css.ts b/app/components/mdx/Strong.css.ts
new file mode 100644
index 00000000..6354bb80
--- /dev/null
+++ b/app/components/mdx/Strong.css.ts
@@ -0,0 +1,18 @@
+import { style } from '@vanilla-extract/css'
+
+import { fontWeightVars, spaceVars } from '../../styles/vars.css.js'
+import { root as Callout } from '../Callout.css.js'
+import { root as Content } from '../Content.css.js'
+
+export const root = style({
+ fontWeight: fontWeightVars.semibold,
+ selectors: {
+ [`${Content} > &`]: {
+ display: 'block',
+ },
+ [`${Callout} > &`]: {
+ display: 'block',
+ marginBottom: spaceVars['4'],
+ },
+ },
+})
diff --git a/app/components/mdx/Strong.tsx b/app/components/mdx/Strong.tsx
new file mode 100644
index 00000000..0e7c15bc
--- /dev/null
+++ b/app/components/mdx/Strong.tsx
@@ -0,0 +1,17 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import { CalloutTitle } from '../CalloutTitle.js'
+import * as styles from './Strong.css.js'
+
+export function Strong(props: DetailedHTMLProps, HTMLElement>) {
+ if ('data-callout-title' in props && typeof props.children === 'string')
+ return (
+
+ )
+ return
+}
diff --git a/app/components/mdx/Subtitle.css.ts b/app/components/mdx/Subtitle.css.ts
new file mode 100644
index 00000000..93976802
--- /dev/null
+++ b/app/components/mdx/Subtitle.css.ts
@@ -0,0 +1,19 @@
+import { style } from '@vanilla-extract/css'
+import {
+ fontSizeVars,
+ fontWeightVars,
+ lineHeightVars,
+ primitiveColorVars,
+ spaceVars,
+} from '../../styles/vars.css.js'
+
+export const root = style({
+ color: primitiveColorVars.text2,
+ fontSize: fontSizeVars.subtitle,
+ fontWeight: fontWeightVars.regular,
+ letterSpacing: '-0.02em',
+ lineHeight: lineHeightVars.heading,
+ marginTop: spaceVars['4'],
+ // @ts-expect-error
+ textWrap: 'balance',
+})
diff --git a/app/components/mdx/Subtitle.tsx b/app/components/mdx/Subtitle.tsx
new file mode 100644
index 00000000..742ea211
--- /dev/null
+++ b/app/components/mdx/Subtitle.tsx
@@ -0,0 +1,11 @@
+import type { ReactNode } from 'react'
+
+import * as styles from './Subtitle.css.js'
+
+export function Subtitle({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/app/components/mdx/Summary.css.ts b/app/components/mdx/Summary.css.ts
new file mode 100644
index 00000000..45bfc720
--- /dev/null
+++ b/app/components/mdx/Summary.css.ts
@@ -0,0 +1,23 @@
+import { style } from '@vanilla-extract/css'
+
+import { fontWeightVars, spaceVars } from '../../styles/vars.css.js'
+import { root as Callout } from '../Callout.css.js'
+import { root as Details } from './Details.css.js'
+
+export const root = style({
+ cursor: 'pointer',
+ selectors: {
+ '&&:hover': {
+ textDecoration: 'underline',
+ },
+ [`${Details}[open] &`]: {
+ marginBottom: spaceVars['4'],
+ },
+ [`${Callout} &`]: {
+ fontWeight: fontWeightVars.medium,
+ },
+ [`${Details} &&`]: {
+ marginBottom: 0,
+ },
+ },
+})
diff --git a/app/components/mdx/Summary.tsx b/app/components/mdx/Summary.tsx
new file mode 100644
index 00000000..2c49349b
--- /dev/null
+++ b/app/components/mdx/Summary.tsx
@@ -0,0 +1,8 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './Summary.css.js'
+
+export function Summary(props: DetailedHTMLProps, HTMLElement>) {
+ return
+}
diff --git a/app/components/mdx/Table.css.ts b/app/components/mdx/Table.css.ts
new file mode 100644
index 00000000..149b23bd
--- /dev/null
+++ b/app/components/mdx/Table.css.ts
@@ -0,0 +1,9 @@
+import { style } from '@vanilla-extract/css'
+import { spaceVars } from '../../styles/vars.css.js'
+
+export const root = style({
+ display: 'block',
+ borderCollapse: 'collapse',
+ overflowX: 'auto',
+ marginBottom: spaceVars['24'],
+})
diff --git a/app/components/mdx/Table.tsx b/app/components/mdx/Table.tsx
new file mode 100644
index 00000000..6c6328fc
--- /dev/null
+++ b/app/components/mdx/Table.tsx
@@ -0,0 +1,10 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './Table.css.js'
+
+export function Table(
+ props: DetailedHTMLProps, HTMLTableElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/TableCell.css.ts b/app/components/mdx/TableCell.css.ts
new file mode 100644
index 00000000..fa25f5c0
--- /dev/null
+++ b/app/components/mdx/TableCell.css.ts
@@ -0,0 +1,8 @@
+import { style } from '@vanilla-extract/css'
+import { fontSizeVars, semanticColorVars, spaceVars } from '../../styles/vars.css.js'
+
+export const root = style({
+ border: `1px solid ${semanticColorVars.tableBorder}`,
+ fontSize: fontSizeVars.td,
+ padding: `${spaceVars['8']} ${spaceVars['12']}`,
+})
diff --git a/app/components/mdx/TableCell.tsx b/app/components/mdx/TableCell.tsx
new file mode 100644
index 00000000..a641ef85
--- /dev/null
+++ b/app/components/mdx/TableCell.tsx
@@ -0,0 +1,10 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './TableCell.css.js'
+
+export function TableCell(
+ props: DetailedHTMLProps, HTMLTableCellElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/TableHeader.css.ts b/app/components/mdx/TableHeader.css.ts
new file mode 100644
index 00000000..0b961314
--- /dev/null
+++ b/app/components/mdx/TableHeader.css.ts
@@ -0,0 +1,25 @@
+import { style } from '@vanilla-extract/css'
+import {
+ fontSizeVars,
+ fontWeightVars,
+ semanticColorVars,
+ spaceVars,
+} from '../../styles/vars.css.js'
+
+export const root = style({
+ border: `1px solid ${semanticColorVars.tableBorder}`,
+ backgroundColor: semanticColorVars.tableHeaderBackground,
+ color: semanticColorVars.tableHeaderText,
+ fontSize: fontSizeVars.th,
+ fontWeight: fontWeightVars.medium,
+ padding: `${spaceVars['8']} ${spaceVars['12']}`,
+ textAlign: 'left',
+ selectors: {
+ '&[align="center"]': {
+ textAlign: 'center',
+ },
+ '&[align="right"]': {
+ textAlign: 'right',
+ },
+ },
+})
diff --git a/app/components/mdx/TableHeader.tsx b/app/components/mdx/TableHeader.tsx
new file mode 100644
index 00000000..4995d01d
--- /dev/null
+++ b/app/components/mdx/TableHeader.tsx
@@ -0,0 +1,10 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './TableHeader.css.js'
+
+export function TableHeader(
+ props: DetailedHTMLProps, HTMLTableCellElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/TableRow.css.ts b/app/components/mdx/TableRow.css.ts
new file mode 100644
index 00000000..d005d051
--- /dev/null
+++ b/app/components/mdx/TableRow.css.ts
@@ -0,0 +1,11 @@
+import { style } from '@vanilla-extract/css'
+import { primitiveColorVars, semanticColorVars } from '../../styles/vars.css.js'
+
+export const root = style({
+ borderTop: `1px solid ${semanticColorVars.tableBorder}`,
+ selectors: {
+ '&:nth-child(2n)': {
+ backgroundColor: primitiveColorVars.background2,
+ },
+ },
+})
diff --git a/app/components/mdx/TableRow.tsx b/app/components/mdx/TableRow.tsx
new file mode 100644
index 00000000..22012fd5
--- /dev/null
+++ b/app/components/mdx/TableRow.tsx
@@ -0,0 +1,10 @@
+import { clsx } from 'clsx'
+import { type DetailedHTMLProps, type HTMLAttributes } from 'react'
+
+import * as styles from './TableRow.css.js'
+
+export function TableRow(
+ props: DetailedHTMLProps, HTMLTableRowElement>,
+) {
+ return
+}
diff --git a/app/components/mdx/TwoslashPopover.tsx b/app/components/mdx/TwoslashPopover.tsx
new file mode 100644
index 00000000..726ad7d7
--- /dev/null
+++ b/app/components/mdx/TwoslashPopover.tsx
@@ -0,0 +1,64 @@
+import {
+ FloatingArrow,
+ arrow,
+ offset,
+ safePolygon,
+ shift,
+ useFloating,
+ useHover,
+ useInteractions,
+} from '@floating-ui/react'
+import { type ReactElement, useRef, useState } from 'react'
+import { primitiveColorVars } from '../../styles/vars.css.js'
+
+export function TwoslashPopover({ children, ...props }: { children: ReactElement[] }) {
+ const [popover, target] = children
+
+ const arrowRef = useRef(null)
+ const [isOpen, setIsOpen] = useState(false)
+ const { context, refs, floatingStyles } = useFloating({
+ middleware: [
+ arrow({
+ element: arrowRef,
+ }),
+ offset(8),
+ shift(),
+ ],
+ open: isOpen,
+ onOpenChange: setIsOpen,
+ placement: 'bottom-start',
+ })
+ const hover = useHover(context, { handleClose: safePolygon() })
+
+ const { getReferenceProps, getFloatingProps } = useInteractions([hover])
+
+ const targetChildren = target.props.children
+ const popoverChildren = popover.props.children
+
+ return (
+
+
+ {targetChildren}
+
+ {isOpen && (
+
+ )}
+
+ )
+}
diff --git a/app/components/mdx/index.tsx b/app/components/mdx/index.tsx
new file mode 100644
index 00000000..a50c8fec
--- /dev/null
+++ b/app/components/mdx/index.tsx
@@ -0,0 +1,64 @@
+import type { MDXComponents } from 'mdx/types.js'
+
+import { Anchor } from './Anchor.js'
+import { Aside } from './Aside.js'
+import { Blockquote } from './Blockquote.js'
+import { Code } from './Code.js'
+import { Details } from './Details.js'
+import { Div } from './Div.js'
+import { Figcaption } from './Figcaption.js'
+import { Figure } from './Figure.js'
+import { H1 } from './H1.js'
+import { H2 } from './H2.js'
+import { H3 } from './H3.js'
+import { H4 } from './H4.js'
+import { H5 } from './H5.js'
+import { H6 } from './H6.js'
+import { Header } from './Header.js'
+import { HorizontalRule } from './HorizontalRule.js'
+import { Kbd } from './Kbd.js'
+import { List } from './List.js'
+import { ListItem } from './ListItem.js'
+import { Paragraph } from './Paragraph.js'
+import { Pre } from './Pre.js'
+import { Section } from './Section.js'
+import { Span } from './Span.js'
+import { Strong } from './Strong.js'
+import { Summary } from './Summary.js'
+import { Table } from './Table.js'
+import { TableCell } from './TableCell.js'
+import { TableHeader } from './TableHeader.js'
+import { TableRow } from './TableRow.js'
+
+export const components: MDXComponents = {
+ a: Anchor as any,
+ aside: Aside,
+ blockquote: Blockquote,
+ code: Code,
+ details: Details,
+ div: Div,
+ pre: Pre,
+ header: Header,
+ figcaption: Figcaption,
+ figure: Figure,
+ h1: H1,
+ h2: H2,
+ h3: H3,
+ h4: H4,
+ h5: H5,
+ h6: H6,
+ hr: HorizontalRule,
+ kd: Kbd,
+ li: ListItem,
+ ol: (props) =>
,
+ p: Paragraph,
+ section: Section,
+ span: Span,
+ strong: Strong,
+ summary: Summary,
+ table: Table,
+ td: TableCell,
+ th: TableHeader,
+ tr: TableRow,
+ ul: (props) =>
,
+}
diff --git a/app/dom.d.ts b/app/dom.d.ts
new file mode 100644
index 00000000..7d17aa09
--- /dev/null
+++ b/app/dom.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/app/hooks/useActiveNavIds.ts b/app/hooks/useActiveNavIds.ts
new file mode 100644
index 00000000..7343007c
--- /dev/null
+++ b/app/hooks/useActiveNavIds.ts
@@ -0,0 +1,25 @@
+import { useMemo } from 'react'
+import type { ParsedTopNavItem } from '../../config.js'
+
+function getActiveNavIds({
+ items,
+ pathname,
+}: { items: ParsedTopNavItem[]; pathname: string }): number[] {
+ const path = pathname.replace(/\.html$/, '')
+ const activeIds = []
+ for (const item of items) {
+ if (item.link && path.startsWith(item.match || item.link)) activeIds.push(item.id)
+ else if (item.items) {
+ const activeChildItems = getActiveNavIds({ items: item.items, pathname })
+ if (activeChildItems.length > 0) activeIds.push(item.id)
+ }
+ }
+ return activeIds
+}
+
+export function useActiveNavIds({
+ items,
+ pathname,
+}: { items: ParsedTopNavItem[]; pathname: string }): number[] {
+ return useMemo(() => getActiveNavIds({ items, pathname }), [items, pathname])
+}
diff --git a/app/hooks/useConfig.tsx b/app/hooks/useConfig.tsx
new file mode 100644
index 00000000..46c53647
--- /dev/null
+++ b/app/hooks/useConfig.tsx
@@ -0,0 +1,44 @@
+import { sha256 } from '@noble/hashes/sha256'
+import { bytesToHex } from '@noble/hashes/utils'
+import { type ReactNode, createContext, useContext, useEffect, useState } from 'react'
+import { type ParsedConfig, deserializeConfig, serializeConfig } from '../../config.js'
+import { config as virtualConfig } from 'virtual:config'
+
+const ConfigContext = createContext(virtualConfig)
+
+export const configHash = import.meta.env.DEV
+ ? bytesToHex(sha256(serializeConfig(virtualConfig))).slice(0, 8)
+ : ''
+
+export function getConfig(): ParsedConfig {
+ if (typeof window !== 'undefined' && import.meta.env.DEV) {
+ const storedConfig = window.localStorage.getItem(`vocs.config.${configHash}`)
+ if (storedConfig) return deserializeConfig(storedConfig)
+ }
+ return virtualConfig
+}
+
+export function ConfigProvider({
+ children,
+ config: initialConfig,
+}: { children: ReactNode; config?: ParsedConfig }) {
+ const [config, setConfig] = useState(() => {
+ if (initialConfig) return initialConfig
+ return getConfig()
+ })
+
+ useEffect(() => {
+ if (import.meta.hot) import.meta.hot.on('vocs:config', setConfig)
+ }, [])
+
+ useEffect(() => {
+ if (typeof window !== 'undefined' && import.meta.env.DEV)
+ window.localStorage.setItem(`vocs.config.${configHash}`, serializeConfig(config))
+ }, [config])
+
+ return {children}
+}
+
+export function useConfig() {
+ return useContext(ConfigContext)
+}
diff --git a/app/hooks/useCopyCode.ts b/app/hooks/useCopyCode.ts
new file mode 100644
index 00000000..e45baa26
--- /dev/null
+++ b/app/hooks/useCopyCode.ts
@@ -0,0 +1,30 @@
+import { useEffect, useRef, useState } from 'react'
+
+export function useCopyCode() {
+ const ref = useRef(null)
+
+ const [copied, setCopied] = useState(false)
+
+ useEffect(() => {
+ if (!copied) return
+ const timeout = setTimeout(() => setCopied(false), 1000)
+ return () => clearTimeout(timeout)
+ }, [copied])
+
+ function copy() {
+ setCopied(true)
+
+ const node = ref.current?.cloneNode(true) as HTMLPreElement
+ const nodesToRemove = node?.querySelectorAll(
+ 'button,.line.diff.remove,.twoslash-popup-info-hover,.twoslash-popup-info,.twoslash-meta-line,.twoslash-tag-line',
+ )
+ for (const node of nodesToRemove ?? []) node.remove()
+ navigator.clipboard.writeText(node?.textContent as string)
+ }
+
+ return {
+ copied,
+ copy,
+ ref,
+ }
+}
diff --git a/app/hooks/useDebounce.ts b/app/hooks/useDebounce.ts
new file mode 100644
index 00000000..9855c55d
--- /dev/null
+++ b/app/hooks/useDebounce.ts
@@ -0,0 +1,15 @@
+import { useEffect, useState } from 'react'
+
+export function useDebounce(value: T, delay?: number): T {
+ const [debouncedValue, setDebouncedValue] = useState(value)
+
+ useEffect(() => {
+ const timer = setTimeout(() => setDebouncedValue(value), delay || 500)
+
+ return () => {
+ clearTimeout(timer)
+ }
+ }, [value, delay])
+
+ return debouncedValue
+}
diff --git a/app/hooks/useEditLink.ts b/app/hooks/useEditLink.ts
new file mode 100644
index 00000000..8f42306c
--- /dev/null
+++ b/app/hooks/useEditLink.ts
@@ -0,0 +1,20 @@
+import { useMemo } from 'react'
+
+import { useConfig } from './useConfig.js'
+import { usePageData } from './usePageData.js'
+
+export function useEditLink() {
+ const pageData = usePageData()
+ const config = useConfig()
+
+ return useMemo(() => {
+ const { pattern = '', text = 'Edit page' } = config.editLink ?? {}
+
+ let url = ''
+ // TODO: pattern as function
+ if (typeof pattern === 'function') url = ''
+ else if (pageData.filePath) url = pattern.replace(/:path/g, pageData.filePath)
+
+ return { url, text }
+ }, [config.editLink, pageData.filePath])
+}
diff --git a/app/hooks/useLayout.tsx b/app/hooks/useLayout.tsx
new file mode 100644
index 00000000..2b7a568c
--- /dev/null
+++ b/app/hooks/useLayout.tsx
@@ -0,0 +1,34 @@
+import type { Layout } from '../types.js'
+import { usePageData } from './usePageData.js'
+import { useSidebar } from './useSidebar.js'
+
+export function useLayout(): Layout {
+ const sidebar = useSidebar()
+ const { frontmatter } = usePageData()
+ const { layout: layout_, showLogo, showOutline, showSidebar, showTopNav } = frontmatter || {}
+
+ const layout = layout_ ?? 'docs'
+
+ return {
+ layout,
+ get showLogo() {
+ if (typeof showLogo !== 'undefined') return showLogo
+ return true
+ },
+ get showOutline() {
+ if (typeof showOutline !== 'undefined') return showOutline
+ return layout === 'docs'
+ },
+ get showSidebar() {
+ if (sidebar.items.length === 0) return false
+ if (typeof showSidebar !== 'undefined') return showSidebar
+ if (layout === 'minimal') return false
+ if (layout === 'landing') return false
+ return true
+ },
+ get showTopNav() {
+ if (typeof showTopNav !== 'undefined') return showTopNav
+ return true
+ },
+ }
+}
diff --git a/app/hooks/useLocalStorage.ts b/app/hooks/useLocalStorage.ts
new file mode 100644
index 00000000..ca79ff43
--- /dev/null
+++ b/app/hooks/useLocalStorage.ts
@@ -0,0 +1,51 @@
+import { useState, useEffect, useCallback } from 'react'
+
+type SetValue = (newVal: type | ((prevVal: type) => type)) => void
+
+export function useLocalStorage(
+ key: string,
+ defaultValue: type | undefined,
+): [type | undefined, SetValue] {
+ const [value, setValue] = useState()
+
+ useEffect(() => {
+ const initialValue = getItem(key) as type | undefined
+
+ if (typeof initialValue === 'undefined' || initialValue === null) {
+ setValue(typeof defaultValue === 'function' ? defaultValue() : defaultValue)
+ } else {
+ setValue(initialValue)
+ }
+ }, [defaultValue, key])
+
+ const setter = useCallback(
+ (updater: type | ((prevVal: type) => type)) => {
+ setValue((old) => {
+ let newVal: type
+ if (typeof updater === 'function') newVal = (updater as any)(old)
+ else newVal = updater
+
+ try {
+ localStorage.setItem(key, JSON.stringify(newVal))
+ } catch {}
+
+ return newVal
+ })
+ },
+ [key],
+ )
+
+ return [value, setter]
+}
+
+function getItem(key: string): unknown {
+ try {
+ const itemValue = localStorage.getItem(key)
+ if (typeof itemValue === 'string') {
+ return JSON.parse(itemValue)
+ }
+ return undefined
+ } catch {
+ return undefined
+ }
+}
diff --git a/app/hooks/useMounted.ts b/app/hooks/useMounted.ts
new file mode 100644
index 00000000..a1b6efce
--- /dev/null
+++ b/app/hooks/useMounted.ts
@@ -0,0 +1,9 @@
+import { useEffect, useState } from 'react'
+
+export function useMounted() {
+ const [mounted, setMounted] = useState(false)
+ useEffect(() => {
+ setMounted(true)
+ }, [])
+ return mounted
+}
diff --git a/app/hooks/useOgImageUrl.ts b/app/hooks/useOgImageUrl.ts
new file mode 100644
index 00000000..180ff4d9
--- /dev/null
+++ b/app/hooks/useOgImageUrl.ts
@@ -0,0 +1,21 @@
+import { useMemo } from 'react'
+import { useLocation } from 'react-router-dom'
+
+import { useConfig } from './useConfig.js'
+
+export function useOgImageUrl(): string | undefined {
+ const { pathname } = useLocation()
+ const config = useConfig()
+ const { ogImageUrl } = config
+
+ if (!ogImageUrl) return undefined
+ if (typeof ogImageUrl === 'string') return ogImageUrl
+
+ const pathKey = useMemo(() => {
+ const keys = Object.keys(ogImageUrl).filter((key) => pathname.startsWith(key))
+ return keys[keys.length - 1]
+ }, [ogImageUrl, pathname])
+ if (!pathKey) return undefined
+
+ return ogImageUrl[pathKey]
+}
diff --git a/app/hooks/usePageData.ts b/app/hooks/usePageData.ts
new file mode 100644
index 00000000..3afaa5c2
--- /dev/null
+++ b/app/hooks/usePageData.ts
@@ -0,0 +1,19 @@
+import { createContext, useContext } from 'react'
+
+import { type Module } from '../types.js'
+
+export function usePageData() {
+ const pageData = useContext(PageDataContext)
+ if (!pageData) throw new Error('`usePageData` must be used within `PageDataContext.Provider`.')
+ return pageData
+}
+
+export const PageDataContext = createContext<
+ | {
+ filePath?: string
+ frontmatter: Module['frontmatter']
+ lastUpdatedAt?: number
+ previousPath?: string
+ }
+ | undefined
+>(undefined)
diff --git a/app/hooks/useSearchIndex.ts b/app/hooks/useSearchIndex.ts
new file mode 100644
index 00000000..e17fca9a
--- /dev/null
+++ b/app/hooks/useSearchIndex.ts
@@ -0,0 +1,53 @@
+import MiniSearch from 'minisearch'
+import { useEffect, useState } from 'react'
+import { getSearchIndex } from 'virtual:searchIndex'
+
+export type Result = {
+ href: string
+ html: string
+ isPage: boolean
+ text?: string
+ title: string
+ titles: string[]
+}
+
+let promise: Promise
+
+export function useSearchIndex(): MiniSearch | undefined {
+ const [searchIndex, setSearchIndex] = useState>()
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies:
+ useEffect(() => {
+ ;(async () => {
+ if (!promise) promise = getSearchIndex()
+ const json = await promise
+ const searchIndex = MiniSearch.loadJSON(json, {
+ fields: ['title', 'titles', 'text'],
+ searchOptions: {
+ boost: { title: 4, text: 2, titles: 1 },
+ fuzzy: 0.2,
+ prefix: true,
+ // ...(theme.value.search?.provider === 'local' &&
+ // theme.value.search.options?.miniSearch?.searchOptions),
+ },
+ storeFields: ['href', 'html', 'isPage', 'text', 'title', 'titles'],
+ // ...(theme.value.search?.provider === 'local' &&
+ // theme.value.search.options?.miniSearch?.options),
+ })
+ setSearchIndex(searchIndex)
+ })()
+ }, [])
+
+ useEffect(() => {
+ if (!import.meta.hot) return
+
+ // TODO: Update index
+ import.meta.hot.accept('virtual:searchIndex', (m) => {
+ if (m) {
+ console.log('update', m)
+ }
+ })
+ }, [])
+
+ return searchIndex
+}
diff --git a/app/hooks/useSidebar.ts b/app/hooks/useSidebar.ts
new file mode 100644
index 00000000..76ddfac3
--- /dev/null
+++ b/app/hooks/useSidebar.ts
@@ -0,0 +1,26 @@
+import { useMemo } from 'react'
+import { useLocation } from 'react-router-dom'
+
+import type { SidebarItem } from '../../config.js'
+import { useConfig } from './useConfig.js'
+
+type UseSidebarReturnType = { backLink?: boolean; items: SidebarItem[]; key?: string }
+
+export function useSidebar(): UseSidebarReturnType {
+ const { pathname } = useLocation()
+ const config = useConfig()
+ const { sidebar } = config
+
+ if (!sidebar) return { items: [] }
+ if (Array.isArray(sidebar)) return { items: sidebar }
+
+ const sidebarKey = useMemo(() => {
+ const keys = Object.keys(sidebar).filter((key) => pathname.startsWith(key))
+ return keys[keys.length - 1]
+ }, [sidebar, pathname])
+ if (!sidebarKey) return { items: [] }
+
+ if (Array.isArray(sidebar[sidebarKey]))
+ return { key: sidebarKey, items: sidebar[sidebarKey] } as UseSidebarReturnType
+ return { ...sidebar[sidebarKey], key: sidebarKey } as UseSidebarReturnType
+}
diff --git a/app/hooks/useTheme.ts b/app/hooks/useTheme.ts
new file mode 100644
index 00000000..044db1ed
--- /dev/null
+++ b/app/hooks/useTheme.ts
@@ -0,0 +1,26 @@
+import { useEffect, useState } from 'react'
+
+export function useTheme() {
+ const [theme, setTheme] = useState(() => {
+ if (typeof window === 'undefined') return undefined
+ if (localStorage.getItem('vocs.theme')) {
+ const storedTheme = localStorage.getItem('vocs.theme')
+ if (storedTheme) return storedTheme
+ }
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
+ })
+
+ useEffect(() => {
+ if (theme) localStorage.setItem('vocs.theme', theme)
+
+ if (theme === 'dark') document.documentElement.classList.add('dark')
+ else document.documentElement.classList.remove('dark')
+ }, [theme])
+
+ return {
+ theme,
+ toggle() {
+ setTheme((theme) => (theme === 'light' ? 'dark' : 'light'))
+ },
+ }
+}
diff --git a/app/index.client.tsx b/app/index.client.tsx
new file mode 100644
index 00000000..960264fa
--- /dev/null
+++ b/app/index.client.tsx
@@ -0,0 +1,25 @@
+import './styles/index.css.js'
+
+import { hydrateRoot } from 'react-dom/client'
+import { RouterProvider, createBrowserRouter } from 'react-router-dom'
+import { ConfigProvider, getConfig } from './hooks/useConfig.js'
+import { routes } from './routes.js'
+import { hydrateLazyRoutes } from './utils/hydrateLazyRoutes.js'
+import { removeTempStyles } from './utils/removeTempStyles.js'
+
+hydrate()
+
+async function hydrate() {
+ const basePath = getConfig().basePath
+
+ await hydrateLazyRoutes(routes, basePath)
+ removeTempStyles()
+
+ const router = createBrowserRouter(routes, { basename: basePath })
+ hydrateRoot(
+ document.getElementById('app')!,
+
+
+ ,
+ )
+}
diff --git a/app/index.server.tsx b/app/index.server.tsx
new file mode 100644
index 00000000..75e67da1
--- /dev/null
+++ b/app/index.server.tsx
@@ -0,0 +1,108 @@
+import './styles/index.css.js'
+
+import type { ReactElement } from 'react'
+import { renderToString } from 'react-dom/server'
+import { Helmet } from 'react-helmet'
+import { Route, type RouteObject, Routes } from 'react-router-dom'
+import {
+ type StaticHandlerContext,
+ StaticRouter,
+ StaticRouterProvider,
+ createStaticHandler,
+ createStaticRouter,
+} from 'react-router-dom/server.js'
+
+import { resolveVocsConfig } from '../vite/utils/resolveVocsConfig.js'
+import { ConfigProvider } from './hooks/useConfig.js'
+import { routes } from './routes.js'
+import { createFetchRequest } from './utils/createFetchRequest.js'
+
+export async function prerender(location: string) {
+ const unwrappedRoutes = (
+ await Promise.all(
+ routes.map(async (route) => {
+ const location_ = location === '/' ? '/' : location.replace(/\/$/, '')
+ const path = route.path.replace(/\.html$/, '')
+ if (path !== location_ && path !== '*') return null
+ const element = route.lazy ? (await route.lazy()).element : route.element
+ return {
+ path: route.path,
+ element,
+ }
+ }),
+ )
+ ).filter(Boolean) as RouteObject[]
+
+ const { config } = await resolveVocsConfig()
+ const { basePath } = config
+
+ const body = renderToString(
+
+
+
+ {unwrappedRoutes.map((route) => (
+
+ ))}
+
+
+ ,
+ )
+
+ return { head: await head({ path: location }), body }
+}
+
+export async function render(req: Request) {
+ const { config } = await resolveVocsConfig()
+ const { basePath } = config
+
+ const { query, dataRoutes } = createStaticHandler(routes, { basename: basePath })
+ const fetchRequest = createFetchRequest(req)
+ const context = (await query(fetchRequest)) as StaticHandlerContext
+
+ if (context instanceof Response) throw context
+
+ const router = createStaticRouter(dataRoutes, context)
+
+ const body = renderToString(
+
+
+ ,
+ )
+
+ return { head: await head({ path: context.location.pathname }), body }
+}
+
+async function head({ path }: { path: string }) {
+ const { config } = await resolveVocsConfig()
+
+ const head = await (async () => {
+ if (typeof config.head === 'function') return await config.head({ path })
+ if (typeof config.head === 'object') {
+ const entry = Object.entries(config.head)
+ .reverse()
+ .find(([key]) => path.startsWith(key))
+ return entry?.[1]
+ }
+ return config.head
+ })()
+
+ const helmet = Helmet.renderStatic()
+
+ let meta = helmet.meta.toString()
+ const match = helmet.meta.toString().match(/property="og:image" content="(.*)"/)
+ if (match?.[1]) {
+ meta = meta.replace(
+ /property="og:image" content="(.*)"/,
+ `property="og:image" content="${match[1].replace(/&/g, '&')}"`,
+ )
+ }
+
+ return `
+ ${helmet.title.toString()}
+ ${meta}
+ ${helmet.link.toString()}
+ ${helmet.style.toString()}
+ ${helmet.script.toString()}
+ ${renderToString(head as ReactElement)}
+ `
+}
diff --git a/app/layouts/DocsLayout.css.ts b/app/layouts/DocsLayout.css.ts
new file mode 100644
index 00000000..53829061
--- /dev/null
+++ b/app/layouts/DocsLayout.css.ts
@@ -0,0 +1,224 @@
+import { createVar, fallbackVar, style } from '@vanilla-extract/css'
+import { bannerHeight } from '../components/Banner.css.js'
+import {
+ contentVars,
+ primitiveColorVars,
+ sidebarVars,
+ spaceVars,
+ topNavVars,
+ viewportVars,
+ zIndexVars,
+} from '../styles/vars.css.js'
+
+export const leftGutterWidthVar = createVar('leftGutterWidth')
+
+export const root = style({
+ vars: {
+ [leftGutterWidthVar]: `max(calc((100vw - ${contentVars.width}) / 2), ${sidebarVars.width})`,
+ },
+})
+
+export const content = style(
+ {
+ backgroundColor: primitiveColorVars.background,
+ marginLeft: 'auto',
+ marginRight: 'auto',
+ maxWidth: contentVars.width,
+ minHeight: '100vh',
+ '@media': {
+ [viewportVars['max-720px']]: {
+ overflowX: 'hidden',
+ },
+ [viewportVars['max-1080px']]: {
+ marginLeft: 'auto',
+ marginRight: 'auto',
+ },
+ },
+ },
+ 'content',
+)
+
+export const content_withTopNav = style(
+ {
+ '@media': {
+ [viewportVars['min-1080px']]: {
+ paddingTop: `calc(${topNavVars.height} + ${fallbackVar(bannerHeight, '0px')})`,
+ },
+ },
+ },
+ 'content_withTopNav',
+)
+
+export const content_withSidebar = style(
+ {
+ marginLeft: leftGutterWidthVar,
+ marginRight: 'unset',
+ },
+ 'content_withSidebar',
+)
+
+export const gutterLeft = style(
+ {
+ backgroundColor: primitiveColorVars.backgroundDark,
+ justifyContent: 'flex-end',
+ display: 'flex',
+ height: '100vh',
+ position: 'fixed',
+ top: fallbackVar(bannerHeight, '0px'),
+ width: leftGutterWidthVar,
+ zIndex: zIndexVars.gutterLeft,
+ '@media': {
+ [viewportVars['max-1080px']]: {
+ display: 'none',
+ },
+ },
+ },
+ 'gutterLeft',
+)
+
+export const gutterTop = style(
+ {
+ alignItems: 'center',
+ backgroundColor: `color-mix(in srgb, ${primitiveColorVars.background} 98%, transparent)`,
+ height: topNavVars.height,
+ width: '100vw',
+ zIndex: zIndexVars.gutterTop,
+ '@media': {
+ [viewportVars['min-1080px']]: {
+ paddingLeft: `calc(${leftGutterWidthVar} - ${sidebarVars.width})`,
+ paddingRight: `calc(${leftGutterWidthVar} - ${sidebarVars.width})`,
+ position: 'fixed',
+ top: fallbackVar(bannerHeight, '0px'),
+ },
+ [viewportVars['max-1080px']]: {
+ position: 'initial',
+ },
+ },
+ },
+ 'gutterTop',
+)
+
+export const gutterTop_offsetLeftGutter = style(
+ {
+ '@media': {
+ [viewportVars['min-1080px']]: {
+ paddingLeft: leftGutterWidthVar,
+ },
+ },
+ },
+ 'gutterTop_offsetLeftGutter',
+)
+
+export const gutterTop_sticky = style(
+ {
+ '@media': {
+ [viewportVars['max-1080px']]: {
+ position: 'sticky',
+ top: 0,
+ },
+ },
+ },
+ 'gutterTop_sticky',
+)
+
+export const gutterTopCurtain = style(
+ {
+ display: 'flex',
+ height: topNavVars.curtainHeight,
+ width: '100vw',
+ zIndex: zIndexVars.gutterTopCurtain,
+ '@media': {
+ [viewportVars['min-1080px']]: {
+ position: 'fixed',
+ top: `calc(${topNavVars.height} + ${fallbackVar(bannerHeight, '0px')})`,
+ },
+ [viewportVars['max-1080px']]: {
+ position: 'sticky',
+ top: 0,
+ },
+ },
+ },
+ 'gutterTopCurtain',
+)
+
+export const gutterTopCurtain_hidden = style(
+ {
+ background: 'unset',
+ display: 'none',
+ },
+ 'gutterTopCurtain_hidden',
+)
+
+export const gutterTopCurtain_withSidebar = style(
+ {
+ '@media': {
+ [viewportVars['min-1080px']]: {
+ marginLeft: leftGutterWidthVar,
+ },
+ },
+ },
+ 'gutterTopCurtain_withSidebar',
+)
+
+export const gutterRight = style(
+ {
+ display: 'flex',
+ height: '100vh',
+ overflowY: 'auto',
+ padding: `calc(${contentVars.verticalPadding} + ${topNavVars.height} + ${spaceVars['8']}) ${spaceVars['24']} 0 0`,
+ position: 'fixed',
+ top: fallbackVar(bannerHeight, '0px'),
+ right: '0',
+ width: `calc((100vw - ${contentVars.width}) / 2)`,
+ zIndex: zIndexVars.gutterRight,
+ '@media': {
+ [viewportVars['max-1280px']]: {
+ display: 'none',
+ },
+ },
+ '::-webkit-scrollbar': {
+ display: 'none',
+ },
+ },
+ 'gutterRight',
+)
+
+export const gutterRight_withSidebar = style(
+ {
+ width: `calc(100vw - ${contentVars.width} - ${leftGutterWidthVar})`,
+ },
+ 'gutterRight_withSidebar',
+)
+
+export const outlinePopover = style(
+ {
+ display: 'none',
+ overflowY: 'auto',
+ height: `calc(100vh - ${topNavVars.height} - ${topNavVars.curtainHeight})`,
+ '@media': {
+ [viewportVars['max-1080px']]: {
+ display: 'block',
+ },
+ },
+ },
+ 'outlinePopover',
+)
+
+export const sidebar = style(
+ {
+ padding: `${spaceVars['0']} ${sidebarVars.horizontalPadding} ${spaceVars['24']} ${sidebarVars.horizontalPadding}`,
+ },
+ 'sidebar',
+)
+
+export const sidebarDrawer = style(
+ {
+ display: 'none',
+ '@media': {
+ [viewportVars['max-1080px']]: {
+ display: 'block',
+ },
+ },
+ },
+ 'sidebarDrawer',
+)
diff --git a/app/layouts/DocsLayout.tsx b/app/layouts/DocsLayout.tsx
new file mode 100644
index 00000000..aa564124
--- /dev/null
+++ b/app/layouts/DocsLayout.tsx
@@ -0,0 +1,116 @@
+import clsx from 'clsx'
+import type { ReactNode } from 'react'
+import { useInView } from 'react-intersection-observer'
+
+import { assignInlineVars } from '@vanilla-extract/dynamic'
+import { bannerHeight } from '../components/Banner.css.js'
+import { Banner } from '../components/Banner.js'
+import { Content } from '../components/Content.js'
+import { DesktopTopNav } from '../components/DesktopTopNav.js'
+import { Footer } from '../components/Footer.js'
+import { MobileTopNav } from '../components/MobileTopNav.js'
+import { Outline } from '../components/Outline.js'
+import { Sidebar } from '../components/Sidebar.js'
+import { SkipLink, skipLinkId } from '../components/SkipLink.js'
+import { useConfig } from '../hooks/useConfig.js'
+import { useLayout } from '../hooks/useLayout.js'
+import { useLocalStorage } from '../hooks/useLocalStorage.js'
+import { usePageData } from '../hooks/usePageData.js'
+import { contentVars, defaultFontFamily, fontFamilyVars } from '../styles/vars.css.js'
+import * as styles from './DocsLayout.css.js'
+
+export function DocsLayout({
+ children,
+}: {
+ children: ReactNode
+}) {
+ const { banner, font } = useConfig()
+ const { frontmatter = {} } = usePageData()
+ const { content } = frontmatter
+
+ const { layout, showOutline, showSidebar, showTopNav } = useLayout()
+
+ const { ref, inView } = useInView({
+ initialInView: true,
+ rootMargin: '100px 0px 0px 0px',
+ })
+
+ const [showBanner, setShowBanner] = useLocalStorage('banner', true)
+
+ return (
+
+
+
+ {showBanner &&
setShowBanner(false)} />}
+
+ {showSidebar && (
+
+
+
+ )}
+
+ {showTopNav && (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ {showOutline && (
+
+
+
+ )}
+
+
+ {children}
+
+
+
+
+ )
+}
diff --git a/app/public/.vocs/icons/arrow-diagonal.svg b/app/public/.vocs/icons/arrow-diagonal.svg
new file mode 100644
index 00000000..09d0d0ad
--- /dev/null
+++ b/app/public/.vocs/icons/arrow-diagonal.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/public/.vocs/icons/chevron-down.svg b/app/public/.vocs/icons/chevron-down.svg
new file mode 100644
index 00000000..3fb4a526
--- /dev/null
+++ b/app/public/.vocs/icons/chevron-down.svg
@@ -0,0 +1,13 @@
+
+ Chevron Down
+
+
\ No newline at end of file
diff --git a/app/public/.vocs/icons/chevron-up.svg b/app/public/.vocs/icons/chevron-up.svg
new file mode 100644
index 00000000..cc18dae4
--- /dev/null
+++ b/app/public/.vocs/icons/chevron-up.svg
@@ -0,0 +1,13 @@
+
+ Chevron Up
+
+
\ No newline at end of file
diff --git a/app/public/.vocs/icons/link.svg b/app/public/.vocs/icons/link.svg
new file mode 100644
index 00000000..4e64236b
--- /dev/null
+++ b/app/public/.vocs/icons/link.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/root.tsx b/app/root.tsx
new file mode 100644
index 00000000..5d1971d9
--- /dev/null
+++ b/app/root.tsx
@@ -0,0 +1,157 @@
+import { MDXProvider } from '@mdx-js/react'
+import { type ReactNode, useEffect, useRef } from 'react'
+import { Helmet } from 'react-helmet'
+import { ScrollRestoration, useLocation } from 'react-router-dom'
+import { Layout } from 'virtual:consumer-components'
+import 'virtual:styles'
+
+import { components } from './components/mdx/index.js'
+import { useConfig } from './hooks/useConfig.js'
+import { useOgImageUrl } from './hooks/useOgImageUrl.js'
+import { PageDataContext } from './hooks/usePageData.js'
+import { type Module } from './types.js'
+
+export function Root(props: {
+ children: ReactNode
+ filePath?: string
+ frontmatter: Module['frontmatter']
+ lastUpdatedAt?: number
+ path: string
+}) {
+ const { children, filePath, frontmatter, lastUpdatedAt, path } = props
+ const { pathname } = useLocation()
+
+ const previousPathRef = useRef()
+ // biome-ignore lint/correctness/useExhaustiveDependencies:
+ useEffect(() => {
+ previousPathRef.current = pathname
+ })
+
+ return (
+ <>
+
+ {typeof window !== 'undefined' && }
+
+
+
+ {children}
+
+
+
+ >
+ )
+}
+
+function Head({ frontmatter }: { frontmatter: Module['frontmatter'] }) {
+ const config = useConfig()
+ const ogImageUrl = useOgImageUrl()
+
+ const { baseUrl, font, iconUrl, logoUrl } = config
+
+ const title = frontmatter?.title ?? config.title
+ const description = frontmatter?.description ?? config.description
+
+ const enableTitleTemplate = config.title && !title.includes(config.title)
+
+ const isLocalhost = typeof window !== 'undefined' && window.location.hostname === 'localhost'
+
+ return (
+
+ {/* Title */}
+ {title && {title} }
+
+ {/* Base URL */}
+ {baseUrl && import.meta.env.PROD && !isLocalhost && }
+
+ {/* Description */}
+ {description !== 'undefined' && }
+
+ {/* Icons */}
+ {iconUrl && typeof iconUrl === 'string' && (
+
+ )}
+ {iconUrl && typeof iconUrl !== 'string' && (
+
+ )}
+ {iconUrl && typeof iconUrl !== 'string' && (
+
+ )}
+
+ {/* Open Graph */}
+
+
+ {baseUrl && }
+ {description !== 'undefined' && }
+ {ogImageUrl && (
+
+ )}
+
+ {/* Fonts */}
+ {(font?.default?.google || font?.mono?.google) && (
+
+ )}
+ {(font?.default?.google || font?.mono?.google) && (
+
+ )}
+ {font?.default?.google && (
+
+ )}
+ {font?.mono?.google && (
+
+ )}
+
+ {/* Twitter */}
+
+ {ogImageUrl && (
+
+ )}
+
+ )
+}
+
+function getIconType(iconUrl: string) {
+ if (iconUrl.endsWith('.svg')) return 'image/svg+xml'
+ if (iconUrl.endsWith('.png')) return 'image/png'
+ if (iconUrl.endsWith('.jpg')) return 'image/jpeg'
+ if (iconUrl.endsWith('.ico')) return 'image/x-icon'
+ if (iconUrl.endsWith('.webp')) return 'image/webp'
+ return undefined
+}
diff --git a/app/routes.tsx b/app/routes.tsx
new file mode 100644
index 00000000..6bb58670
--- /dev/null
+++ b/app/routes.tsx
@@ -0,0 +1,68 @@
+import { type RouteObject } from 'react-router-dom'
+import { routes as routes_virtual } from 'virtual:routes'
+
+import { NotFound } from './components/NotFound.js'
+import { DocsLayout } from './layouts/DocsLayout.js'
+import { Root } from './root.js'
+
+const notFoundRoute = (() => {
+ const virtualRoute = routes_virtual.find(({ path }) => path === '*')
+ if (virtualRoute)
+ return {
+ path: virtualRoute.path,
+ lazy: async () => {
+ const { frontmatter, ...route } = await virtualRoute.lazy()
+
+ return {
+ ...route,
+ element: (
+
+
+
+
+
+ ),
+ } satisfies RouteObject
+ },
+ }
+
+ return {
+ path: '*', // 404
+ lazy: undefined,
+ element: (
+
+
+
+
+
+ ),
+ }
+})()
+
+export const routes = [
+ ...routes_virtual
+ .filter(({ path }) => path !== '*')
+ .map((route_virtual) => ({
+ path: route_virtual.path,
+ lazy: async () => {
+ const { frontmatter, ...route } = await route_virtual.lazy()
+
+ return {
+ ...route,
+ element: (
+
+
+
+
+
+ ),
+ } satisfies RouteObject
+ },
+ })),
+ notFoundRoute,
+] satisfies RouteObject[]
diff --git a/app/styles/global.css.ts b/app/styles/global.css.ts
new file mode 100644
index 00000000..85d4f237
--- /dev/null
+++ b/app/styles/global.css.ts
@@ -0,0 +1,71 @@
+import { globalStyle, layer } from '@vanilla-extract/css'
+
+import { root as Callout } from '../components/Callout.css.js'
+import { root as Content } from '../components/Content.css.js'
+import { root as Details } from '../components/mdx/Details.css.js'
+import {
+ fontSizeVars,
+ fontWeightVars,
+ lineHeightVars,
+ primitiveColorVars,
+ semanticColorVars,
+ spaceVars,
+ viewportVars,
+} from './vars.css.js'
+
+////////////////////////////////////////////////////////////////////////
+// Root
+
+const globalLayer = layer('global')
+
+globalStyle(':root', {
+ '@layer': {
+ [globalLayer]: {
+ backgroundColor: primitiveColorVars.background,
+ color: primitiveColorVars.text,
+ lineHeight: lineHeightVars.paragraph,
+ fontSize: fontSizeVars.root,
+ fontWeight: fontWeightVars.regular,
+ },
+ },
+ '@media': {
+ [viewportVars['max-720px']]: {
+ backgroundColor: primitiveColorVars.backgroundDark,
+ },
+ },
+})
+
+globalStyle(':root.dark', {
+ colorScheme: 'dark',
+})
+
+////////////////////////////////////////////////////////////////////////
+// Shiki
+
+globalStyle(':root.dark pre.shiki span:not(.line), :root.dark :not(pre.shiki) .line span', {
+ color: 'var(--shiki-dark) !important',
+})
+globalStyle('pre.shiki', {
+ backgroundColor: `${semanticColorVars.codeBlockBackground} !important`,
+})
+
+////////////////////////////////////////////////////////////////////////
+// Misc.
+
+globalStyle(`${Content} > *:not(:last-child), ${Details} > *:not(:last-child)`, {
+ marginBottom: spaceVars['24'],
+})
+
+globalStyle(`${Callout} > *:not(:last-child), ${Callout} > ${Details} > *:not(:last-child)`, {
+ marginBottom: spaceVars['16'],
+})
+
+globalStyle(`${Content} > *:last-child, ${Callout} > *:last-child, ${Details} > *:last-child`, {
+ marginBottom: spaceVars['0'],
+})
+
+globalStyle('#app[aria-hidden="true"]', {
+ background: primitiveColorVars.background,
+ // TODO: Do we need this? Breaks layout for dialogs
+ // filter: 'brightness(0.5)',
+})
diff --git a/app/styles/index.css.ts b/app/styles/index.css.ts
new file mode 100644
index 00000000..22fa6d69
--- /dev/null
+++ b/app/styles/index.css.ts
@@ -0,0 +1,9 @@
+import './preflight.css.js'
+
+import './vars.css.js'
+
+import './reset.css.js'
+
+import './global.css.js'
+
+import './twoslash.css.js'
diff --git a/app/styles/preflight.css.ts b/app/styles/preflight.css.ts
new file mode 100644
index 00000000..4c994f43
--- /dev/null
+++ b/app/styles/preflight.css.ts
@@ -0,0 +1,6 @@
+import { layer } from '@vanilla-extract/css'
+
+// Preflight layer designed to be used by consumers so
+// consumer styles don't override internal Vocs styles.
+// Example case: @tailwind CSS directives.
+layer()
diff --git a/app/styles/reset.css.ts b/app/styles/reset.css.ts
new file mode 100644
index 00000000..a69b5c40
--- /dev/null
+++ b/app/styles/reset.css.ts
@@ -0,0 +1,258 @@
+import { globalStyle, layer } from '@vanilla-extract/css'
+import { root as DocsLayout } from '../layouts/DocsLayout.css.js'
+import { fontFamilyVars, fontSizeVars, primitiveColorVars } from './vars.css.js'
+
+const resetLayer = layer('reset')
+
+globalStyle(['*', '::before', '::after'].join(','), {
+ '@layer': {
+ [resetLayer]: {
+ boxSizing: 'border-box',
+ borderWidth: '0',
+ borderStyle: 'solid',
+ },
+ },
+})
+
+globalStyle('*:focus-visible', {
+ '@layer': {
+ [resetLayer]: {
+ outline: `2px solid ${primitiveColorVars.borderAccent}`,
+ outlineOffset: '2px',
+ outlineStyle: 'dashed',
+ },
+ },
+})
+
+globalStyle('html, body', {
+ '@layer': {
+ [resetLayer]: {
+ textSizeAdjust: '100%',
+ tabSize: 4,
+ lineHeight: 'inherit',
+ margin: 0,
+ padding: 0,
+ border: 0,
+ textRendering: 'optimizeLegibility',
+ },
+ },
+})
+
+globalStyle(`html, body, ${DocsLayout}`, {
+ fontFamily: fontFamilyVars.default,
+ fontFeatureSettings: '"rlig" 1, "calt" 1',
+ fontSize: fontSizeVars.root,
+})
+
+globalStyle('hr', {
+ '@layer': {
+ [resetLayer]: {
+ height: 0,
+ color: 'inherit',
+ borderTopWidth: '1px',
+ },
+ },
+})
+
+globalStyle('abbr:where([title])', {
+ '@layer': {
+ [resetLayer]: {
+ textDecoration: 'underline dotted',
+ },
+ },
+})
+
+globalStyle('h1,h2,h3,h4,h5,h6', {
+ '@layer': {
+ [resetLayer]: {
+ fontSize: 'inherit',
+ fontWeight: 'inherit',
+ // @ts-expect-error
+ textWrap: 'balance',
+ },
+ },
+})
+
+globalStyle('a', {
+ '@layer': {
+ [resetLayer]: {
+ color: 'inherit',
+ textDecoration: 'inherit',
+ },
+ },
+})
+
+globalStyle('b,strong', {
+ '@layer': {
+ [resetLayer]: {
+ fontWeight: 'bolder',
+ },
+ },
+})
+
+globalStyle('code,kbd,samp,pre', {
+ '@layer': {
+ [resetLayer]: {
+ fontFamily: fontFamilyVars.mono,
+ fontSize: fontSizeVars.root,
+ },
+ },
+})
+
+globalStyle('small', {
+ '@layer': {
+ [resetLayer]: {
+ fontSize: '80%',
+ },
+ },
+})
+
+globalStyle('sub,sup', {
+ '@layer': {
+ [resetLayer]: {
+ fontSize: '75%',
+ lineHeight: 0,
+ position: 'relative',
+ verticalAlign: 'baseline',
+ },
+ },
+})
+
+globalStyle('sub', {
+ '@layer': {
+ [resetLayer]: {
+ bottom: '-0.25em',
+ },
+ },
+})
+
+globalStyle('sup', {
+ '@layer': {
+ [resetLayer]: {
+ top: '-0.5em',
+ },
+ },
+})
+
+globalStyle('table', {
+ '@layer': {
+ [resetLayer]: {
+ borderColor: 'inherit',
+ borderCollapse: 'collapse',
+ textIndent: '0',
+ },
+ },
+})
+
+globalStyle('button,input,optgroup,select,textarea', {
+ '@layer': {
+ [resetLayer]: {
+ fontFamily: 'inherit',
+ fontFeatureSettings: 'inherit',
+ fontVariationSettings: 'inherit',
+ fontSize: '100%',
+ fontWeight: 'inherit',
+ lineHeight: 'inherit',
+ color: 'inherit',
+ margin: 0,
+ padding: 0,
+ },
+ },
+})
+
+globalStyle('button,select', {
+ textTransform: 'none',
+})
+
+globalStyle('button,select', {
+ appearance: 'button',
+ backgroundColor: 'transparent',
+ backgroundImage: 'none',
+})
+
+globalStyle(':-moz-focusring', {
+ outline: 'auto',
+})
+
+globalStyle(':-moz-ui-invalid', {
+ outline: 'auto',
+})
+
+globalStyle('progress', {
+ verticalAlign: 'baseline',
+})
+
+globalStyle('::-webkit-inner-spin-button, ::-webkit-outer-spin-button', {
+ height: 'auto',
+})
+
+globalStyle('[type="search"]', {
+ appearance: 'textfield',
+ outlineOffset: '-2px',
+})
+
+globalStyle('::-webkit-search-decoration', {
+ appearance: 'none',
+})
+
+globalStyle('::-webkit-file-upload-button', {
+ appearance: 'button',
+ font: 'inherit',
+})
+
+globalStyle('summary', {
+ display: 'list-item',
+})
+
+globalStyle('blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre', {
+ margin: 0,
+})
+
+globalStyle('fieldset', {
+ margin: 0,
+ padding: 0,
+})
+
+globalStyle('legend', {
+ padding: 0,
+})
+
+globalStyle('ol,ul,menu', {
+ listStyle: 'none',
+ margin: 0,
+ padding: 0,
+})
+
+globalStyle('dialog', {
+ padding: 0,
+})
+
+globalStyle('textarea', {
+ resize: 'vertical',
+})
+
+globalStyle('input::placeholder,textarea::placeholder', {
+ opacity: 1,
+})
+
+globalStyle('button,[role="button"]', {
+ cursor: 'pointer',
+})
+
+globalStyle(':disabled', {
+ overflow: 'default',
+})
+
+globalStyle('img,svg,video,canvas,audio,iframe,embed,object', {
+ display: 'block',
+ verticalAlign: 'middle',
+})
+
+globalStyle('img,video', {
+ maxWidth: '100%',
+ height: 'auto',
+})
+
+globalStyle('[hidden]', {
+ display: 'none',
+})
diff --git a/app/styles/twoslash.css.ts b/app/styles/twoslash.css.ts
new file mode 100644
index 00000000..68a1735f
--- /dev/null
+++ b/app/styles/twoslash.css.ts
@@ -0,0 +1,330 @@
+import { createGlobalTheme, createGlobalThemeContract, globalStyle } from '@vanilla-extract/css'
+import { borderRadiusVars, primitiveColorVars, semanticColorVars } from './vars.css.js'
+
+const getVarName = (scope: string) => (_: string | null, path: string[]) =>
+ `vocs-${scope}_${path.join('-')}`
+
+export const twoslashVars = createGlobalThemeContract(
+ {
+ borderColor: 'borderColor',
+ underlineColor: 'underlineColor',
+ popupBackground: 'popupBackground',
+ popupShadow: 'popupShadow',
+ matchedColor: 'matchedColor',
+ unmatchedColor: 'unmatchedColor',
+ cursorColor: 'cursorColor',
+ errorColor: 'errorColor',
+ errorBackground: 'errorBackground',
+ highlightedBackground: 'highlightedBackground',
+ highlightedBorder: 'highlightedBorder',
+ tagColor: 'tagColor',
+ tagBackground: 'tagBackground',
+ tagWarnColor: 'tagWarnColor',
+ tagWarnBackground: 'tagWarnBackground',
+ tagAnnotateColor: 'tagAnnotateColor',
+ tagAnnotateBackground: 'tagAnnotateBackground',
+ },
+ getVarName('twoslash'),
+)
+
+createGlobalTheme(':root', twoslashVars, {
+ borderColor: primitiveColorVars.border2,
+ underlineColor: 'currentColor',
+ popupBackground: primitiveColorVars.background2,
+ popupShadow: 'rgba(0, 0, 0, 0.08) 0px 1px 4px',
+ matchedColor: 'inherit',
+ unmatchedColor: '#888',
+ cursorColor: '#8888',
+ errorColor: primitiveColorVars.textRed,
+ errorBackground: primitiveColorVars.backgroundRedTint2,
+ highlightedBackground: primitiveColorVars.background,
+ highlightedBorder: primitiveColorVars.background,
+ tagColor: primitiveColorVars.textBlue,
+ tagBackground: primitiveColorVars.backgroundBlueTint,
+ tagWarnColor: primitiveColorVars.textYellow,
+ tagWarnBackground: primitiveColorVars.backgroundYellowTint,
+ tagAnnotateColor: primitiveColorVars.textGreen,
+ tagAnnotateBackground: primitiveColorVars.backgroundGreenTint2,
+})
+createGlobalTheme(':root.dark', twoslashVars, {
+ borderColor: primitiveColorVars.border2,
+ underlineColor: 'currentColor',
+ popupBackground: primitiveColorVars.background5,
+ popupShadow: 'rgba(0, 0, 0, 0.08) 0px 1px 4px',
+ matchedColor: 'inherit',
+ unmatchedColor: '#888',
+ cursorColor: '#8888',
+ errorColor: primitiveColorVars.textRed,
+ errorBackground: primitiveColorVars.backgroundRedTint2,
+ highlightedBackground: primitiveColorVars.background,
+ highlightedBorder: primitiveColorVars.background,
+ tagColor: primitiveColorVars.textBlue,
+ tagBackground: primitiveColorVars.backgroundBlueTint,
+ tagWarnColor: primitiveColorVars.textYellow,
+ tagWarnBackground: primitiveColorVars.backgroundYellowTint,
+ tagAnnotateColor: primitiveColorVars.textGreen,
+ tagAnnotateBackground: primitiveColorVars.backgroundGreenTint2,
+})
+
+/* Respect people's wishes to not have animations */
+globalStyle('.twoslash *', {
+ '@media': {
+ '(prefers-reduced-motion: reduce)': {
+ transition: 'none !important',
+ },
+ },
+})
+
+globalStyle(':root .twoslash-popup-info-hover, :root .twoslash-popup-info', {
+ vars: {
+ '--shiki-light-bg': primitiveColorVars.background2,
+ },
+})
+globalStyle(':root .twoslash-popup-info', {
+ width: 'max-content',
+})
+
+globalStyle(':root.dark .twoslash-popup-info, :root.dark .twoslash-popup-info-hover', {
+ vars: {
+ '--shiki-dark-bg': primitiveColorVars.background5,
+ },
+})
+
+globalStyle('.twoslash-query-persisted > .twoslash-popup-info', {
+ zIndex: 1,
+})
+
+globalStyle(':not(.twoslash-query-persisted) > .twoslash-popup-info', {
+ zIndex: 2,
+})
+
+globalStyle('.twoslash:hover .twoslash-hover', {
+ borderColor: twoslashVars.underlineColor,
+})
+
+globalStyle('.twoslash .twoslash-hover', {
+ borderBottom: '1px dotted transparent',
+ transitionTimingFunction: 'ease',
+ transition: 'border-color 0.3s',
+})
+
+globalStyle('.twoslash-query-persisted', {
+ position: 'relative',
+})
+
+globalStyle('.twoslash .twoslash-popup-info', {
+ position: 'absolute',
+ top: '0',
+ left: '0',
+ opacity: '0',
+ display: 'inline-block',
+ transform: 'translateY(1.1em)',
+ background: twoslashVars.popupBackground,
+ border: `1px solid ${twoslashVars.borderColor}`,
+ transition: 'opacity 0.3s',
+ borderRadius: '4px',
+ maxWidth: '540px',
+ padding: '4px 6px',
+ pointerEvents: 'none',
+ textAlign: 'left',
+ zIndex: 20,
+ whiteSpace: 'pre-wrap',
+ userSelect: 'none',
+ boxShadow: twoslashVars.popupShadow,
+})
+
+globalStyle('.twoslash .twoslash-popup-info-hover', {
+ background: twoslashVars.popupBackground,
+ border: `1px solid ${twoslashVars.borderColor}`,
+ borderRadius: '4px',
+ boxShadow: twoslashVars.popupShadow,
+ display: 'inline-block',
+ maxWidth: '500px',
+ pointerEvents: 'auto',
+ position: 'fixed',
+ opacity: 1,
+ transition: 'opacity 0.3s',
+ whiteSpace: 'pre-wrap',
+ userSelect: 'none',
+ zIndex: 20,
+})
+
+globalStyle('.twoslash .twoslash-popup-scroll-container', {
+ maxHeight: '300px',
+ padding: '4px 0px',
+ overflowY: 'auto',
+ msOverflowStyle: 'none',
+ scrollbarWidth: 'none',
+})
+
+globalStyle('.twoslash-popup-arrow', {
+ position: 'absolute',
+ top: '-4px',
+ left: '1em',
+ borderTop: `1px solid ${twoslashVars.borderColor}`,
+ borderRight: `1px solid ${twoslashVars.borderColor}`,
+ background: twoslashVars.popupBackground,
+ transform: 'rotate(-45deg)',
+ width: '6px',
+ height: '6px',
+ pointerEvents: 'none',
+})
+
+globalStyle('.twoslash .twoslash-popup-scroll-container::-webkit-scrollbar', {
+ display: 'none',
+})
+
+globalStyle('.twoslash .twoslash-popup-jsdoc', {
+ borderTop: `1px solid ${primitiveColorVars.border2}`,
+ color: primitiveColorVars.text,
+ fontFamily: 'sans-serif',
+ fontWeight: '500',
+ marginTop: '4px',
+ padding: '4px 10px 0px 10px',
+})
+
+globalStyle('.twoslash-tag-line + .twoslash-tag-line', {
+ marginTop: '-0.2em',
+})
+
+globalStyle('.twoslash-query-persisted .twoslash-popup-info', {
+ zIndex: 9,
+ transform: 'translateY(1.5em)',
+})
+
+globalStyle(
+ '.twoslash-hover:hover .twoslash-popup-info, .twoslash-query-persisted .twoslash-popup-info',
+ {
+ opacity: 1,
+ pointerEvents: 'auto',
+ },
+)
+
+globalStyle('.twoslash-popup-info:hover, .twoslash-popup-info-hover:hover', {
+ userSelect: 'auto',
+})
+
+globalStyle('.twoslash-error-line', {
+ position: 'relative',
+ backgroundColor: twoslashVars.errorBackground,
+ borderLeft: `2px solid ${twoslashVars.errorColor}`,
+ color: twoslashVars.errorColor,
+ margin: '0.2em 0',
+})
+
+globalStyle('.twoslash-error', {
+ background: `url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") repeat-x bottom left`,
+ paddingBottom: '2px',
+})
+
+globalStyle('.twoslash-completion-cursor', {
+ position: 'relative',
+})
+
+globalStyle('.twoslash-completion-cursor .twoslash-completion-list', {
+ userSelect: 'none',
+ position: 'absolute',
+ top: '0',
+ left: '0',
+ transform: 'translate(0, 1.2em)',
+ margin: '3px 0 0 -1px',
+ zIndex: 8,
+ boxShadow: twoslashVars.popupShadow,
+ background: twoslashVars.popupBackground,
+ border: `1px solid ${twoslashVars.borderColor}`,
+})
+
+globalStyle('.twoslash-completion-list', {
+ borderRadius: '4px',
+ fontSize: '0.8rem',
+ padding: '4px',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '4px',
+ width: '240px',
+})
+
+globalStyle('.twoslash-completion-list:hover', {
+ userSelect: 'auto',
+})
+
+globalStyle('.twoslash-completion-list::before', {
+ backgroundColor: twoslashVars.cursorColor,
+ width: '2px',
+ position: 'absolute',
+ top: '-1.6em',
+ height: '1.4em',
+ left: '-1px',
+ content: ' ',
+})
+
+globalStyle('.twoslash-completion-list .twoslash-completion-list-item', {
+ overflow: 'hidden',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '0.5em',
+ lineHeight: '1em',
+})
+
+globalStyle(
+ '.twoslash-completion-list .twoslash-completion-list-item span.twoslash-completions-unmatched.twoslash-completions-unmatched.twoslash-completions-unmatched',
+ {
+ color: `${twoslashVars.unmatchedColor} !important`,
+ },
+)
+
+globalStyle('.twoslash-completion-list .deprecated', {
+ textDecoration: 'line-through',
+ opacity: 0.5,
+})
+
+globalStyle(
+ '.twoslash-completion-list .twoslash-completion-list-item span.twoslash-completions-matched.twoslash-completions-unmatched.twoslash-completions-unmatched',
+ {
+ color: `${twoslashVars.matchedColor} !important`,
+ },
+)
+
+globalStyle('.twoslash-tag-line', {
+ position: 'relative',
+ backgroundColor: twoslashVars.tagBackground,
+ borderLeft: `2px solid ${twoslashVars.tagColor}`,
+ color: twoslashVars.tagColor,
+ margin: '0.2em 0',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '0.3em',
+})
+
+globalStyle('.twoslash-tag-line+.line[data-empty-line]+.twoslash-tag-line', {
+ marginTop: 'calc(-1.75em - 0.2em)',
+})
+
+globalStyle('.twoslash-tag-line .twoslash-tag-icon', {
+ width: '1.1em',
+ color: 'inherit',
+})
+
+globalStyle('.twoslash-tag-line.twoslash-tag-error-line', {
+ backgroundColor: twoslashVars.errorBackground,
+ borderLeft: `2px solid ${twoslashVars.errorColor}`,
+ color: twoslashVars.errorColor,
+})
+
+globalStyle('.twoslash-tag-line.twoslash-tag-warn-line', {
+ backgroundColor: twoslashVars.tagWarnBackground,
+ borderLeft: `2px solid ${twoslashVars.tagWarnColor}`,
+ color: twoslashVars.tagWarnColor,
+})
+
+globalStyle('.twoslash-tag-line.twoslash-tag-annotate-line', {
+ backgroundColor: twoslashVars.tagAnnotateBackground,
+ borderLeft: `2px solid ${twoslashVars.tagAnnotateColor}`,
+ color: twoslashVars.tagAnnotateColor,
+})
+
+globalStyle('.twoslash-highlighted', {
+ borderRadius: borderRadiusVars['2'],
+ backgroundColor: `${semanticColorVars.codeCharacterHighlightBackground} !important`,
+ boxShadow: `0 0 0 4px ${semanticColorVars.codeCharacterHighlightBackground}`,
+})
diff --git a/app/styles/utils.css.ts b/app/styles/utils.css.ts
new file mode 100644
index 00000000..e69ea170
--- /dev/null
+++ b/app/styles/utils.css.ts
@@ -0,0 +1,24 @@
+import { globalStyle, style } from '@vanilla-extract/css'
+
+export const visibleDark = style({}, 'visibleDark')
+globalStyle(`:root:not(.dark) ${visibleDark}`, {
+ display: 'none',
+})
+
+export const visibleLight = style({}, 'visibleLight')
+globalStyle(`:root.dark ${visibleLight}`, {
+ display: 'none',
+})
+
+export const visuallyHidden = style(
+ {
+ clip: 'rect(0 0 0 0)',
+ clipPath: 'inset(50%)',
+ height: 1,
+ overflow: 'hidden',
+ position: 'absolute',
+ whiteSpace: 'nowrap',
+ width: 1,
+ },
+ 'visuallyHidden',
+)
diff --git a/app/styles/vars.css.ts b/app/styles/vars.css.ts
new file mode 100644
index 00000000..6af1b327
--- /dev/null
+++ b/app/styles/vars.css.ts
@@ -0,0 +1,555 @@
+import * as globalColors from '@radix-ui/colors'
+import { createGlobalTheme, createGlobalThemeContract, globalStyle } from '@vanilla-extract/css'
+
+const white = 'rgba(255 255 255 / 100%)'
+const black = 'rgba(0 0 0 / 100%)'
+
+const getVarName = (scope: string) => (_: string | null, path: string[]) =>
+ `vocs-${scope}_${path.join('-')}`
+
+export const primitiveColorVars = createGlobalThemeContract(
+ {
+ white: 'white',
+ black: 'black',
+ background: 'background',
+ background2: 'background2',
+ background3: 'background3',
+ background4: 'background4',
+ background5: 'background5',
+ backgroundAccent: 'backgroundAccent',
+ backgroundAccentHover: 'backgroundAccentHover',
+ backgroundAccentText: 'backgroundAccentText',
+ backgroundBlueTint: 'backgroundBlueTint',
+ backgroundDark: 'backgroundDark',
+ backgroundGreenTint: 'backgroundGreenTint',
+ backgroundGreenTint2: 'backgroundGreenTint2',
+ backgroundIrisTint: 'backgroundIrisTint',
+ backgroundRedTint: 'backgroundRedTint',
+ backgroundRedTint2: 'backgroundRedTint2',
+ backgroundYellowTint: 'backgroundYellowTint',
+ border: 'border',
+ border2: 'border2',
+ borderAccent: 'borderAccent',
+ borderBlue: 'borderBlue',
+ borderGreen: 'borderGreen',
+ borderIris: 'borderIris',
+ borderRed: 'borderRed',
+ borderYellow: 'borderYellow',
+ heading: 'heading',
+ inverted: 'inverted',
+ shadow: 'shadow',
+ shadow2: 'shadow2',
+ text: 'text',
+ text2: 'text2',
+ text3: 'text3',
+ text4: 'text4',
+ textAccent: 'textAccent',
+ textAccentHover: 'textAccentHover',
+ textBlue: 'textBlue',
+ textBlueHover: 'textBlueHover',
+ textGreen: 'textGreen',
+ textGreenHover: 'textGreenHover',
+ textIris: 'textIris',
+ textIrisHover: 'textIrisHover',
+ textRed: 'textRed',
+ textRedHover: 'textRedHover',
+ textYellow: 'textYellow',
+ textYellowHover: 'textYellowHover',
+ title: 'title',
+ },
+ getVarName('color'),
+)
+createGlobalTheme(':root', primitiveColorVars, {
+ white,
+ black,
+ background: white,
+ background2: globalColors.gray.gray2,
+ background3: '#f6f6f6',
+ background4: globalColors.gray.gray3,
+ background5: globalColors.gray.gray4,
+ backgroundAccent: globalColors.iris.iris9,
+ backgroundAccentHover: globalColors.iris.iris10,
+ backgroundAccentText: white,
+ backgroundBlueTint: globalColors.blueA.blueA2,
+ backgroundDark: globalColors.gray.gray2,
+ backgroundGreenTint: globalColors.greenA.greenA2,
+ backgroundGreenTint2: globalColors.greenA.greenA3,
+ backgroundIrisTint: globalColors.irisA.irisA2,
+ backgroundRedTint: globalColors.redA.redA2,
+ backgroundRedTint2: globalColors.redA.redA3,
+ backgroundYellowTint: globalColors.yellowA.yellowA2,
+ border: '#ececec',
+ border2: globalColors.gray.gray7,
+ borderAccent: globalColors.iris.iris11,
+ borderBlue: globalColors.blueA.blueA4,
+ borderGreen: globalColors.greenA.greenA5,
+ borderIris: globalColors.iris.iris5,
+ borderRed: globalColors.redA.redA4,
+ borderYellow: globalColors.yellowA.yellowA5,
+ heading: globalColors.gray.gray12,
+ inverted: black,
+ shadow: globalColors.grayA.grayA3,
+ shadow2: globalColors.grayA.grayA2,
+ text: '#4c4c4c',
+ text2: globalColors.gray.gray11,
+ text3: globalColors.gray.gray10,
+ text4: globalColors.gray.gray8,
+ textAccent: globalColors.iris.iris11,
+ textAccentHover: globalColors.iris.iris12,
+ textBlue: globalColors.blue.blue11,
+ textBlueHover: globalColors.blue.blue12,
+ textGreen: globalColors.green.green11,
+ textGreenHover: globalColors.green.green12,
+ textIris: globalColors.iris.iris11,
+ textIrisHover: globalColors.iris.iris12,
+ textRed: globalColors.red.red11,
+ textRedHover: globalColors.red.red12,
+ textYellow: globalColors.yellow.yellow11,
+ textYellowHover: globalColors.yellow.yellow12,
+ title: globalColors.gray.gray12,
+})
+createGlobalTheme(':root.dark', primitiveColorVars, {
+ white,
+ black,
+ background: globalColors.mauveDark.mauve3,
+ background2: globalColors.mauveDark.mauve4,
+ background3: '#2e2c31',
+ background4: globalColors.mauveDark.mauve5,
+ background5: globalColors.mauveDark.mauve6,
+ backgroundAccent: globalColors.irisDark.iris9,
+ backgroundAccentHover: globalColors.iris.iris11,
+ backgroundAccentText: white,
+ backgroundBlueTint: globalColors.blueA.blueA3,
+ backgroundDark: '#1e1d1f',
+ backgroundGreenTint: globalColors.greenA.greenA3,
+ backgroundGreenTint2: globalColors.greenA.greenA4,
+ backgroundIrisTint: globalColors.irisA.irisA4,
+ backgroundRedTint: globalColors.redA.redA3,
+ backgroundRedTint2: globalColors.redA.redA4,
+ backgroundYellowTint: globalColors.yellowA.yellowA2,
+ border: globalColors.mauveDark.mauve6,
+ border2: globalColors.mauveDark.mauve9,
+ borderAccent: globalColors.irisDark.iris10,
+ borderBlue: globalColors.blueA.blueA4,
+ borderGreen: globalColors.greenA.greenA5,
+ borderIris: globalColors.irisDark.iris5,
+ borderRed: globalColors.redA.redA4,
+ borderYellow: globalColors.yellowA.yellowA2,
+ heading: '#e9e9ea',
+ inverted: white,
+ shadow: globalColors.grayDarkA.grayA1,
+ shadow2: globalColors.blackA.blackA1,
+ text: '#cfcfcf',
+ text2: '#bdbdbe',
+ text3: '#a7a7a8',
+ text4: '#656567',
+ textAccent: globalColors.irisDark.iris11,
+ textAccentHover: globalColors.irisDark.iris10,
+ textBlue: globalColors.blueDark.blue11,
+ textBlueHover: globalColors.blueDark.blue10,
+ textGreen: globalColors.greenDark.green11,
+ textGreenHover: globalColors.greenDark.green10,
+ textIris: globalColors.irisDark.iris11,
+ textIrisHover: globalColors.irisDark.iris10,
+ textRed: globalColors.redDark.red11,
+ textRedHover: globalColors.redDark.red10,
+ textYellow: globalColors.yellowDark.yellow11,
+ textYellowHover: globalColors.amber.amber8,
+ title: white,
+})
+
+export const semanticColorVars = createGlobalThemeContract(
+ {
+ blockquoteBorder: 'blockquoteBorder',
+ blockquoteText: 'blockquoteText',
+
+ dangerBackground: 'dangerBackground',
+ dangerBorder: 'dangerBorder',
+ dangerText: 'dangerText',
+ dangerTextHover: 'dangerTextHover',
+ infoBackground: 'infoBackground',
+ infoBorder: 'infoBorder',
+ infoText: 'infoText',
+ infoTextHover: 'infoTextHover',
+ noteBackground: 'noteBackground',
+ noteBorder: 'noteBorder',
+ noteText: 'noteText',
+ successBackground: 'successBackground',
+ successBorder: 'successBorder',
+ successText: 'successText',
+ successTextHover: 'successTextHover',
+ tipBackground: 'tipBackground',
+ tipBorder: 'tipBorder',
+ tipText: 'tipText',
+ tipTextHover: 'tipTextHover',
+ warningBackground: 'warningBackground',
+ warningBorder: 'warningBorder',
+ warningText: 'warningText',
+ warningTextHover: 'warningTextHover',
+
+ codeBlockBackground: 'codeBlockBackground',
+ codeCharacterHighlightBackground: 'codeCharacterHighlightBackground',
+ codeHighlightBackground: 'codeHighlightBackground',
+ codeHighlightBorder: 'codeHighlightBorder',
+ codeInlineBackground: 'codeInlineBackground',
+ codeInlineBorder: 'codeInlineBorder',
+ codeInlineText: 'codeInlineText',
+ codeTitleBackground: 'codeTitleBackground',
+ lineNumber: 'lineNumber',
+
+ hr: 'hr',
+
+ link: 'link',
+ linkHover: 'linkHover',
+
+ searchHighlightBackground: 'searchHighlightBackground',
+ searchHighlightText: 'searchHighlightText',
+
+ tableBorder: 'tableBorder',
+ tableHeaderBackground: 'tableHeaderBackground',
+ tableHeaderText: 'tableHeaderText',
+ },
+ getVarName('color'),
+)
+createGlobalTheme(':root', semanticColorVars, {
+ blockquoteBorder: primitiveColorVars.border,
+ blockquoteText: primitiveColorVars.text3,
+ dangerBackground: primitiveColorVars.backgroundRedTint,
+ dangerBorder: primitiveColorVars.borderRed,
+ dangerText: primitiveColorVars.textRed,
+ dangerTextHover: primitiveColorVars.textRedHover,
+ infoBackground: primitiveColorVars.backgroundBlueTint,
+ infoBorder: primitiveColorVars.borderBlue,
+ infoText: primitiveColorVars.textBlue,
+ infoTextHover: primitiveColorVars.textBlueHover,
+ noteBackground: primitiveColorVars.background2,
+ noteBorder: primitiveColorVars.border,
+ noteText: primitiveColorVars.text2,
+ successBackground: primitiveColorVars.backgroundGreenTint,
+ successBorder: primitiveColorVars.borderGreen,
+ successText: primitiveColorVars.textGreen,
+ successTextHover: primitiveColorVars.textGreenHover,
+ tipBackground: primitiveColorVars.backgroundIrisTint,
+ tipBorder: primitiveColorVars.borderIris,
+ tipText: primitiveColorVars.textIris,
+ tipTextHover: primitiveColorVars.textIrisHover,
+ warningBackground: primitiveColorVars.backgroundYellowTint,
+ warningBorder: primitiveColorVars.borderYellow,
+ warningText: primitiveColorVars.textYellow,
+ warningTextHover: primitiveColorVars.textYellowHover,
+ codeBlockBackground: primitiveColorVars.background2,
+ codeCharacterHighlightBackground: primitiveColorVars.background5,
+ codeHighlightBackground: primitiveColorVars.background4,
+ codeHighlightBorder: primitiveColorVars.border2,
+ codeInlineBackground: primitiveColorVars.background4,
+ codeInlineBorder: primitiveColorVars.border,
+ codeInlineText: primitiveColorVars.textAccent,
+ codeTitleBackground: primitiveColorVars.background4,
+ lineNumber: primitiveColorVars.text4,
+ hr: primitiveColorVars.border,
+ link: primitiveColorVars.textAccent,
+ linkHover: primitiveColorVars.textAccentHover,
+ searchHighlightBackground: primitiveColorVars.borderAccent,
+ searchHighlightText: primitiveColorVars.background,
+ tableBorder: primitiveColorVars.border,
+ tableHeaderBackground: primitiveColorVars.background2,
+ tableHeaderText: primitiveColorVars.text2,
+})
+
+export const borderRadiusVars = createGlobalThemeContract(
+ {
+ '0': '0',
+ '2': '2',
+ '3': '3',
+ '4': '4',
+ '6': '6',
+ '8': '8',
+ },
+ getVarName('borderRadius'),
+)
+createGlobalTheme(':root', borderRadiusVars, {
+ '0': '0',
+ '2': '2px',
+ '3': '3px',
+ '4': '4px',
+ '6': '6px',
+ '8': '8px',
+})
+
+export const defaultFontFamily = {
+ default:
+ "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif",
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
+}
+export const fontFamilyVars = createGlobalThemeContract(
+ {
+ default: 'default',
+ mono: 'mono',
+ },
+ getVarName('fontFamily'),
+)
+createGlobalTheme(':root', fontFamilyVars, {
+ default: defaultFontFamily.default,
+ mono: defaultFontFamily.mono,
+})
+
+export const fontSizeVars = createGlobalThemeContract(
+ {
+ root: 'root',
+ '9': '9',
+ '11': '11',
+ '12': '12',
+ '13': '13',
+ '14': '14',
+ '15': '15',
+ '16': '16',
+ '18': '18',
+ '20': '20',
+ '24': '24',
+ '32': '32',
+ '64': '64',
+ h1: 'h1',
+ h2: 'h2',
+ h3: 'h3',
+ h4: 'h4',
+ h5: 'h5',
+ h6: 'h6',
+ calloutCodeBlock: 'calloutCodeBlock',
+ code: 'code',
+ codeBlock: 'codeBlock',
+ lineNumber: 'lineNumber',
+ subtitle: 'subtitle',
+ th: 'th',
+ td: 'td',
+ },
+ getVarName('fontSize'),
+)
+createGlobalTheme(':root', fontSizeVars, {
+ root: '16px',
+ '9': '0.5625rem',
+ '11': '0.6875rem',
+ '12': '0.75rem',
+ '13': '0.8125rem',
+ '14': '0.875rem',
+ '15': '0.9375rem',
+ '16': '1rem',
+ '18': '1.125rem',
+ '20': '1.25rem',
+ '24': '1.5rem',
+ '32': '2rem',
+ '64': '3rem',
+ h1: fontSizeVars['32'],
+ h2: fontSizeVars['24'],
+ h3: fontSizeVars['20'],
+ h4: fontSizeVars['18'],
+ h5: fontSizeVars['16'],
+ h6: fontSizeVars['16'],
+ calloutCodeBlock: '0.8em',
+ code: '0.875em',
+ codeBlock: fontSizeVars['14'],
+ lineNumber: fontSizeVars['15'],
+ subtitle: fontSizeVars['20'],
+ th: fontSizeVars['14'],
+ td: fontSizeVars['14'],
+})
+
+export const fontWeightVars = createGlobalThemeContract(
+ {
+ regular: 'regular',
+ medium: 'medium',
+ semibold: 'semibold',
+ },
+ getVarName('fontWeight'),
+)
+createGlobalTheme(':root', fontWeightVars, {
+ regular: '300',
+ medium: '400',
+ semibold: '500',
+})
+
+export const lineHeightVars = createGlobalThemeContract(
+ {
+ code: 'code',
+ heading: 'heading',
+ listItem: 'listItem',
+ outlineItem: 'outlineItem',
+ paragraph: 'paragraph',
+ sidebarItem: 'sidebarItem',
+ },
+ getVarName('lineHeight'),
+)
+createGlobalTheme(':root', lineHeightVars, {
+ code: '1.75em',
+ heading: '1.5em',
+ listItem: '1.5em',
+ outlineItem: '1em',
+ paragraph: '1.75em',
+ sidebarItem: '1.375em',
+})
+
+export const spaceVars = createGlobalThemeContract(
+ {
+ '0': '0',
+ '1': '1',
+ '2': '2',
+ '3': '3',
+ '4': '4',
+ '6': '6',
+ '8': '8',
+ '12': '12',
+ '14': '14',
+ '16': '16',
+ '18': '18',
+ '20': '20',
+ '22': '22',
+ '24': '24',
+ '28': '28',
+ '32': '32',
+ '36': '36',
+ '40': '40',
+ '44': '44',
+ '48': '48',
+ '56': '56',
+ '64': '64',
+ '72': '72',
+ '80': '80',
+ },
+ getVarName('space'),
+)
+createGlobalTheme(':root', spaceVars, {
+ '0': '0px',
+ '1': '1px',
+ '2': '0.125rem',
+ '3': '0.1875rem',
+ '4': '0.25rem',
+ '6': '0.375rem',
+ '8': '0.5rem',
+ '12': '0.75rem',
+ '14': '0.875rem',
+ '16': '1rem',
+ '18': '1.125rem',
+ '20': '1.25rem',
+ '22': '1.375rem',
+ '24': '1.5rem',
+ '28': '1.75rem',
+ '32': '2rem',
+ '36': '2.25rem',
+ '40': '2.5rem',
+ '44': '2.75rem',
+ '48': '3rem',
+ '56': '3.5rem',
+ '64': '4rem',
+ '72': '4.5rem',
+ '80': '5rem',
+})
+
+export const viewportVars = {
+ 'max-480px': 'screen and (max-width: 480px)',
+ 'min-480px': 'screen and (min-width: 481px)',
+ 'max-720px': 'screen and (max-width: 720px)',
+ 'min-720px': 'screen and (min-width: 721px)',
+ 'max-1080px': 'screen and (max-width: 1080px)',
+ 'min-1080px': 'screen and (min-width: 1081px)',
+ 'max-1280px': 'screen and (max-width: 1280px)',
+ 'min-1280px': 'screen and (min-width: 1281px)',
+}
+
+export const zIndexVars = createGlobalThemeContract(
+ {
+ backdrop: 'backdrop',
+ drawer: 'drawer',
+ gutterLeft: 'gutterLeft',
+ gutterRight: 'gutterRight',
+ gutterTop: 'gutterTop',
+ gutterTopCurtain: 'gutterTopCurtain',
+ popover: 'popover',
+ surface: 'surface',
+ },
+ getVarName('zIndex'),
+)
+createGlobalTheme(':root', zIndexVars, {
+ backdrop: '69420',
+ drawer: '69421',
+ gutterRight: '11',
+ gutterLeft: '14',
+ gutterTop: '13',
+ gutterTopCurtain: '12',
+ popover: '69422',
+ surface: '10',
+})
+
+/////////////////////////////////////////////////////////////////////
+// Misc.
+
+export const contentVars = createGlobalThemeContract(
+ {
+ horizontalPadding: 'horizontalPadding',
+ verticalPadding: 'verticalPadding',
+ width: 'width',
+ },
+ getVarName('content'),
+)
+createGlobalTheme(':root', contentVars, {
+ horizontalPadding: spaceVars['48'],
+ verticalPadding: spaceVars['32'],
+ width: `calc(70ch + (${contentVars.horizontalPadding} * 2))`,
+})
+
+export const outlineVars = createGlobalThemeContract(
+ {
+ width: 'width',
+ },
+ getVarName('outline'),
+)
+createGlobalTheme(':root', outlineVars, {
+ width: '280px',
+})
+
+export const sidebarVars = createGlobalThemeContract(
+ {
+ horizontalPadding: 'horizontalPadding',
+ verticalPadding: 'verticalPadding',
+ width: 'width',
+ },
+ getVarName('sidebar'),
+)
+createGlobalTheme(':root', sidebarVars, {
+ horizontalPadding: spaceVars['24'],
+ verticalPadding: spaceVars['0'],
+ width: '300px',
+})
+
+export const topNavVars = createGlobalThemeContract(
+ {
+ height: 'height',
+ horizontalPadding: 'horizontalPadding',
+ curtainHeight: 'curtainHeight',
+ },
+ getVarName('topNav'),
+)
+createGlobalTheme(':root', topNavVars, {
+ height: '60px',
+ horizontalPadding: contentVars.horizontalPadding,
+ curtainHeight: '40px',
+})
+
+globalStyle(':root', {
+ '@media': {
+ [viewportVars['max-1080px']]: {
+ vars: {
+ [contentVars.verticalPadding]: spaceVars['48'],
+ [contentVars.horizontalPadding]: spaceVars['24'],
+ [sidebarVars.horizontalPadding]: spaceVars['16'],
+ [sidebarVars.verticalPadding]: spaceVars['16'],
+ [sidebarVars.width]: '300px',
+ [topNavVars.height]: '48px',
+ },
+ },
+ [viewportVars['max-720px']]: {
+ vars: {
+ [contentVars.horizontalPadding]: spaceVars['16'],
+ [contentVars.verticalPadding]: spaceVars['32'],
+ },
+ },
+ },
+})
diff --git a/app/types.ts b/app/types.ts
new file mode 100644
index 00000000..e3bcff0c
--- /dev/null
+++ b/app/types.ts
@@ -0,0 +1,48 @@
+import * as React from 'react'
+
+export type BlogPost = {
+ authors?: string | string[]
+ date?: string
+ path: string
+ title: string
+ description: string
+}
+
+export type Frontmatter = {
+ [key: string]: unknown
+ authors?: string | string[]
+ content?: {
+ horizontalPadding?: string
+ width?: string
+ verticalPadding?: string
+ }
+ date?: string
+ description?: string
+ title?: string
+} & Partial
+
+export type Layout = {
+ layout: 'docs' | 'landing' | 'minimal'
+ showLogo: boolean
+ showOutline: number | boolean
+ showSidebar: boolean
+ showTopNav: boolean
+}
+
+export type Module = {
+ default: React.ComponentType
+ frontmatter?: Frontmatter
+}
+
+export type PageData = {
+ filePath: string
+ frontmatter?: Frontmatter
+}
+
+export type Route = {
+ filePath: string
+ lazy: () => Promise
+ lastUpdatedAt?: number
+ path: string
+ type: 'jsx' | 'mdx'
+}
diff --git a/app/utils/createFetchRequest.ts b/app/utils/createFetchRequest.ts
new file mode 100644
index 00000000..83712b71
--- /dev/null
+++ b/app/utils/createFetchRequest.ts
@@ -0,0 +1,26 @@
+export function createFetchRequest(req: any) {
+ const origin = `${req.protocol}://${req.headers.host}`
+ const url = new URL(req.originalUrl || req.url, origin)
+
+ const controller = new AbortController()
+ req.on('close', () => controller.abort())
+
+ const headers = new Headers()
+
+ for (const [key, values] of Object.entries(req.headers)) {
+ if (values) {
+ if (Array.isArray(values)) for (const value of values) headers.append(key, value)
+ else headers.set(key, values as any)
+ }
+ }
+
+ const init: RequestInit = {
+ method: req.method,
+ headers,
+ signal: controller.signal,
+ }
+
+ if (req.method !== 'GET' && req.method !== 'HEAD') init.body = req.body
+
+ return new Request(url.href, init)
+}
diff --git a/app/utils/debounce.ts b/app/utils/debounce.ts
new file mode 100644
index 00000000..d14f1ded
--- /dev/null
+++ b/app/utils/debounce.ts
@@ -0,0 +1,10 @@
+export function debounce(fn: () => void, delay: number): () => void {
+ let invoked = false
+ return () => {
+ invoked = true
+ setTimeout(() => {
+ if (invoked) fn()
+ invoked = false
+ }, delay)
+ }
+}
diff --git a/app/utils/deserializeElement.ts b/app/utils/deserializeElement.ts
new file mode 100644
index 00000000..58c0068d
--- /dev/null
+++ b/app/utils/deserializeElement.ts
@@ -0,0 +1,12 @@
+import React, { type ReactElement, type ReactNode } from 'react'
+
+export function deserializeElement(element: ReactElement, key?: number): ReactNode {
+ if (typeof element !== 'object') return element
+ if (element === null) return element
+ if (Array.isArray(element)) return element.map((el, i) => deserializeElement(el, i))
+
+ const props: any = element.props.children
+ ? { ...element.props, children: deserializeElement(element.props.children) }
+ : element.props
+ return React.createElement(element.type, { ...props, key })
+}
diff --git a/app/utils/hydrateLazyRoutes.ts b/app/utils/hydrateLazyRoutes.ts
new file mode 100644
index 00000000..decb35c6
--- /dev/null
+++ b/app/utils/hydrateLazyRoutes.ts
@@ -0,0 +1,20 @@
+import { type RouteObject, matchRoutes } from 'react-router-dom'
+
+export async function hydrateLazyRoutes(routes: RouteObject[], basePath: string | undefined) {
+ // Determine if any of the initial routes are lazy
+ const lazyMatches = matchRoutes(routes, window.location, basePath)?.filter((m) => m.route.lazy)
+
+ // Load the lazy matches and update the routes before creating your router
+ // so we can hydrate the SSR-rendered content synchronously
+ if (lazyMatches && lazyMatches?.length > 0) {
+ await Promise.all(
+ lazyMatches.map(async (m) => {
+ const routeModule = await m.route.lazy!()
+ Object.assign(m.route, {
+ ...routeModule,
+ lazy: undefined,
+ })
+ }),
+ )
+ }
+}
diff --git a/app/utils/initializeTheme.ts b/app/utils/initializeTheme.ts
new file mode 100644
index 00000000..801a1225
--- /dev/null
+++ b/app/utils/initializeTheme.ts
@@ -0,0 +1,19 @@
+const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
+
+const hasTheme =
+ document.documentElement.classList.contains('dark') ||
+ document.documentElement.classList.contains('light')
+
+if (!hasTheme) {
+ const storedTheme = localStorage.getItem('vocs.theme')
+ const theme = storedTheme || (darkModeMediaQuery.matches ? 'dark' : 'light')
+
+ if (theme === 'dark') document.documentElement.classList.add('dark')
+
+ if (!storedTheme)
+ // Update the theme if the user changes their OS preference
+ darkModeMediaQuery.addEventListener('change', ({ matches: isDark }) => {
+ if (isDark) document.documentElement.classList.add('dark')
+ else document.documentElement.classList.remove('dark')
+ })
+}
diff --git a/app/utils/mergeRefs.ts b/app/utils/mergeRefs.ts
new file mode 100644
index 00000000..57b01464
--- /dev/null
+++ b/app/utils/mergeRefs.ts
@@ -0,0 +1,20 @@
+import { type MutableRefObject, type RefCallback } from 'react'
+
+type MutableRefList = Array | MutableRefObject | undefined | null>
+
+export function mergeRefs(...refs: MutableRefList): RefCallback {
+ return (val: T) => {
+ setRef(val, ...refs)
+ }
+}
+
+export function setRef(val: T, ...refs: MutableRefList): void {
+ // biome-ignore lint/complexity/noForEach:
+ refs.forEach((ref) => {
+ if (typeof ref === 'function') {
+ ref(val)
+ } else if (ref != null) {
+ ref.current = val
+ }
+ })
+}
diff --git a/app/utils/removeTempStyles.ts b/app/utils/removeTempStyles.ts
new file mode 100644
index 00000000..3ebb3408
--- /dev/null
+++ b/app/utils/removeTempStyles.ts
@@ -0,0 +1,4 @@
+export function removeTempStyles() {
+ const tempStyles = document.querySelectorAll('style[data-vocs-temp-style="true"]')
+ for (const style of tempStyles) style.remove()
+}
diff --git a/app/vite-env.d.ts b/app/vite-env.d.ts
new file mode 100644
index 00000000..8f0e1e45
--- /dev/null
+++ b/app/vite-env.d.ts
@@ -0,0 +1,22 @@
+///
+
+declare module 'virtual:blog' {
+ export const posts: import('./types.js').BlogPost[]
+}
+
+declare module 'virtual:config' {
+ export const config: import('../config.js').ParsedConfig
+}
+
+declare module 'virtual:routes' {
+ export const routes: import('./types.js').Route[]
+}
+
+declare module 'virtual:consumer-components' {
+ export const Layout: import('react').ElementType
+ export const Footer: import('react').ElementType
+}
+
+declare module 'virtual:searchIndex' {
+ export const getSearchIndex: () => Promise
+}
diff --git a/cli/commands/build.ts b/cli/commands/build.ts
new file mode 100644
index 00000000..1392e792
--- /dev/null
+++ b/cli/commands/build.ts
@@ -0,0 +1,69 @@
+import ora from 'ora'
+import pc from 'picocolors'
+import { createLogger } from 'vite'
+import type { BuildParameters as BuildParameters_ } from '../../vite/build.js'
+import { version } from '../version.js'
+
+export type BuildParameters = Pick<
+ BuildParameters_,
+ 'clean' | 'logLevel' | 'outDir' | 'publicDir' | 'searchIndex'
+>
+
+export async function build({ clean, logLevel, outDir, publicDir, searchIndex }: BuildParameters) {
+ const { build } = await import('../../vite/build.js')
+
+ const useLogger = logLevel !== 'info'
+
+ const start = Date.now()
+
+ const logger = createLogger('info', { allowClearScreen: true })
+
+ const spinner = {
+ client: ora('building bundles...\n'),
+ prerender: ora('prerendering pages...\n'),
+ }
+
+ logger.clearScreen('info')
+ logger.info('')
+ logger.info(` ${pc.blue('[building]')} ${pc.bold('vocs')}@${pc.dim(`v${version}`)}\n`)
+ await build({
+ clean,
+ hooks: {
+ onBundleStart() {
+ if (useLogger) spinner.client.start()
+ },
+ onBundleEnd({ error }) {
+ if (error) {
+ if (useLogger) spinner.client.fail(`bundles failed to build: ${error.message}`)
+ process.exit(1)
+ }
+
+ if (useLogger) spinner.client.succeed('bundles built')
+ else logger.info('')
+ },
+ onPrerenderStart() {
+ if (useLogger) spinner.prerender.start()
+ },
+ onPrerenderEnd({ error }) {
+ if (error) {
+ if (useLogger) spinner.client.fail(`prerendering failed: ${error.message}`)
+ process.exit(1)
+ }
+
+ if (useLogger) spinner.prerender.succeed('prerendered pages')
+ },
+ onScriptsEnd() {
+ if (!useLogger) logger.info('')
+ },
+ },
+ logger,
+ logLevel,
+ outDir,
+ publicDir,
+ searchIndex,
+ })
+
+ const end = Date.now()
+ const time = end - start
+ logger.info(`\n ${pc.green('[built]')} in ${time / 1000}s`)
+}
diff --git a/cli/commands/dev.ts b/cli/commands/dev.ts
new file mode 100644
index 00000000..36a7a26b
--- /dev/null
+++ b/cli/commands/dev.ts
@@ -0,0 +1,19 @@
+import pc from 'picocolors'
+import { createLogger } from 'vite'
+import { version } from '../version.js'
+
+type DevParameters = { clean?: boolean; host?: boolean; port?: number }
+
+export async function dev(_: any, { clean, host, port }: DevParameters = {}) {
+ const { createDevServer } = await import('../../vite/devServer.js')
+
+ const server = await createDevServer({ clean, host, port })
+ await server.listen()
+
+ const logger = createLogger()
+ logger.clearScreen('info')
+ logger.info('')
+ logger.info(` ${pc.green('[running]')} ${pc.bold('vocs')}@${pc.dim(`v${version}`)}`)
+ logger.info('')
+ server.printUrls()
+}
diff --git a/cli/commands/preview.ts b/cli/commands/preview.ts
new file mode 100644
index 00000000..bd95e661
--- /dev/null
+++ b/cli/commands/preview.ts
@@ -0,0 +1,25 @@
+import pc from 'picocolors'
+import { createLogger } from 'vite'
+
+import { resolveVocsConfig } from '../../vite/utils/resolveVocsConfig.js'
+import { version } from '../version.js'
+
+export async function preview() {
+ const { preview } = await import('../../vite/preview.js')
+ const server = await preview()
+
+ const { config } = await resolveVocsConfig()
+ const { basePath } = config
+
+ const logger = createLogger()
+ logger.clearScreen('info')
+ logger.info('')
+ logger.info(` ${pc.green('[running]')} ${pc.bold('vocs')}@${pc.dim(`v${version}`)}`)
+ logger.info('')
+
+ logger.info(
+ ` ${pc.green('➜')} ${pc.bold('Local')}: ${pc.cyan(
+ `http://localhost:${server.port}${basePath}`,
+ )}`,
+ )
+}
diff --git a/cli/commands/search-index.ts b/cli/commands/search-index.ts
new file mode 100644
index 00000000..47a1115a
--- /dev/null
+++ b/cli/commands/search-index.ts
@@ -0,0 +1,23 @@
+import pc from 'picocolors'
+import { createLogger } from 'vite'
+import type { BuildSearchIndexParameters } from '../../vite/buildSearchIndex.js'
+import { buildSearchIndex } from '../../vite/buildSearchIndex.js'
+import { version } from '../version.js'
+
+export type SearchIndexParameters = BuildSearchIndexParameters
+
+export async function searchIndex({ outDir }: SearchIndexParameters) {
+ const start = Date.now()
+
+ const logger = createLogger('info', { allowClearScreen: true })
+
+ logger.clearScreen('info')
+ logger.info('')
+ logger.info(` ${pc.blue('[indexing]')} ${pc.bold('vocs')}@${pc.dim(`v${version}`)}\n`)
+
+ await buildSearchIndex({ outDir })
+
+ const end = Date.now()
+ const time = end - start
+ logger.info(`\n ${pc.green('[indexed]')} in ${time / 1000}s`)
+}
diff --git a/cli/index.ts b/cli/index.ts
new file mode 100644
index 00000000..3f911b80
--- /dev/null
+++ b/cli/index.ts
@@ -0,0 +1,36 @@
+#!/usr/bin/env node
+import { cac } from 'cac'
+
+import { build } from './commands/build.js'
+import { dev } from './commands/dev.js'
+import { preview } from './commands/preview.js'
+import { searchIndex } from './commands/search-index.js'
+import { version } from './version.js'
+
+export const cli = cac('vocs')
+
+cli
+ .command('[root]')
+ .alias('dev')
+ .option('-c, --clean', 'clean the cache and re-bundle')
+ .option('-h, --host', 'Expose host URL')
+ .option('-p, --port [number]', 'Port used by the server (default: 5173)')
+ .action(dev)
+cli
+ .command('build')
+ .option('-c, --clean', 'clean the cache and re-bundle')
+ .option('-l, --logLevel [level]', 'info | warn | error | silent')
+ .option('-o, --outDir [dir]', 'output directory (default: dist)')
+ .option('-p, --publicDir [dir]', 'public (asset) directory (default: public)')
+ .option('--searchIndex', 'builds the search index (default: true)')
+ .action(build)
+cli.command('preview').action(preview)
+cli
+ .command('search-index')
+ .option('-o, --outDir [dir]', 'output directory (default: dist)')
+ .action(searchIndex)
+
+cli.help()
+cli.version(version)
+
+cli.parse()
diff --git a/cli/version.ts b/cli/version.ts
new file mode 100644
index 00000000..e589b9c2
--- /dev/null
+++ b/cli/version.ts
@@ -0,0 +1 @@
+export const version = '1.0.0-alpha.51'
diff --git a/components.ts b/components.ts
new file mode 100644
index 00000000..fbf40d1a
--- /dev/null
+++ b/components.ts
@@ -0,0 +1,11 @@
+export { Authors, type AuthorsProps } from './app/components/Authors.js'
+export { BlogPosts } from './app/components/BlogPosts.js'
+export { Button } from './app/components/Button.js'
+export { Callout, type CalloutProps } from './app/components/Callout.js'
+export * as HomePage from './app/components/HomePage.js'
+export { Raw } from './app/components/Raw.js'
+export { Sponsors } from './app/components/Sponsors.js'
+export { Steps, type StepsProps } from './app/components/Steps.js'
+export { Step, type StepProps } from './app/components/Step.js'
+
+export { components as MDXComponents } from './app/components/mdx/index.js'
diff --git a/config.ts b/config.ts
new file mode 100644
index 00000000..7f27e286
--- /dev/null
+++ b/config.ts
@@ -0,0 +1,634 @@
+import type { RehypeShikiOptions } from '@shikijs/rehype'
+import type { SearchOptions } from 'minisearch'
+import type { ReactElement } from 'react'
+import type { TwoslashOptions } from 'twoslash'
+import type { PluggableList } from 'unified'
+import type { UserConfig } from 'vite'
+import type {
+ borderRadiusVars,
+ contentVars,
+ fontFamilyVars,
+ fontSizeVars,
+ fontWeightVars,
+ lineHeightVars,
+ outlineVars,
+ primitiveColorVars,
+ semanticColorVars,
+ sidebarVars,
+ spaceVars,
+ topNavVars,
+ viewportVars,
+ zIndexVars,
+} from './app/styles/vars.css.js'
+
+type RequiredBy = Omit & Required>
+
+type RequiredProperties = 'blogDir' | 'markdown' | 'rootDir' | 'title' | 'titleTemplate'
+
+export type Config<
+ parsed extends boolean = false,
+ colorScheme extends ColorScheme = ColorScheme,
+> = RequiredBy<
+ {
+ /**
+ * Configuration for the banner fixed to the top of the page.
+ *
+ * Can be a Markdown string, a React Element, or an object with the following properties:
+ * - `dismissable`: Whether or not the banner can be dismissed.
+ * - `backgroundColor`: The background color of the banner.
+ * - `content`: The content of the banner.
+ * - `height`: The height of the banner.
+ * - `textColor`: The text color of the banner.
+ */
+ banner?: Banner
+ /**
+ * The base path the documentation will be deployed at. All assets and pages
+ * will be prefixed with this path. This is useful for deploying to GitHub Pages.
+ * For example, if you are deploying to `https://example.github.io/foo`, then the
+ * basePath should be set to `/foo`.
+ *
+ * @example
+ * /docs
+ */
+ basePath?: string
+ /**
+ * The base URL for your documentation. This is used to populate the ` ` tag in the
+ * `` of the page, and is used to form the `%logo` variable for dynamic OG images.
+ *
+ * @example
+ * https://vocs.dev
+ */
+ baseUrl?: string
+ /**
+ * Path to blog pages relative to project root.
+ * Used to extract posts from the filesystem.
+ *
+ * @default "./pages/blog"
+ */
+ blogDir?: string
+ /**
+ * General description for the documentation.
+ */
+ description?: string
+ /**
+ * Edit location for the documentation.
+ */
+ editLink?: Normalize
+ /**
+ * Base font face.
+ *
+ * @default { google: "Inter" }
+ */
+ font?: Normalize>
+ /**
+ * Additional tags to include in the `` tag of the page HTML.
+ */
+ head?:
+ | ReactElement
+ | { [path: string]: ReactElement }
+ | ((params: { path: string }) => ReactElement | Promise)
+ /**
+ * Icon URL.
+ */
+ iconUrl?: Normalize
+ /**
+ * Logo URL.
+ */
+ logoUrl?: Normalize
+ /**
+ * OG Image URL. `null` to disable.
+ *
+ * Template variables: `%logo`, `%title`, `%description`
+ *
+ * @default "https://vocs.dev/api/og?logo=%logo&title=%title&description=%description"
+ */
+ ogImageUrl?: string | { [path: string]: string }
+ /**
+ * Outline footer.
+ */
+ outlineFooter?: ReactElement
+ /**
+ * Markdown configuration.
+ */
+ markdown?: Normalize>
+ /**
+ * Documentation root directory. Can be an absolute path, or a path relative from
+ * the location of the config file itself.
+ *
+ * @default "docs"
+ */
+ rootDir?: string
+ /**
+ * Configuration for docs search.
+ */
+ search?: Normalize
+ /**
+ * Navigation displayed on the sidebar.
+ */
+ sidebar?: Normalize
+ /**
+ * Social links displayed in the top navigation.
+ */
+ socials?: Normalize>
+ /**
+ * Set of sponsors to display on MDX directives and (optionally) the sidebar.
+ */
+ sponsors?: SponsorSet[]
+ /**
+ * Theme configuration.
+ */
+ theme?: Normalize>
+ /**
+ * Documentation title.
+ *
+ * @default "Docs"
+ */
+ title?: string
+ /**
+ * Template for the page title.
+ *
+ * @default `%s – ${title}`
+ */
+ titleTemplate?: string
+ /**
+ * Navigation displayed on the top.
+ */
+ topNav?: Normalize>
+ /**
+ * TwoSlash configuration.
+ */
+ twoslash?: Normalize
+ /**
+ * Vite configuration.
+ */
+ vite?: UserConfig
+ },
+ parsed extends true ? RequiredProperties : never
+>
+
+export type ParsedConfig = Config
+
+export async function defineConfig({
+ blogDir = './pages/blog',
+ head,
+ ogImageUrl,
+ rootDir = 'docs',
+ title = 'Docs',
+ titleTemplate = `%s – ${title}`,
+ ...config
+}: Config): Promise {
+ const basePath = parseBasePath(config.basePath)
+ return {
+ blogDir,
+ head,
+ ogImageUrl,
+ rootDir,
+ title,
+ titleTemplate,
+ ...config,
+ basePath,
+ banner: await parseBanner(config.banner ?? ''),
+ font: parseFont(config.font ?? {}),
+ iconUrl: parseImageUrl(config.iconUrl, {
+ basePath,
+ }),
+ logoUrl: parseImageUrl(config.logoUrl, {
+ basePath,
+ }),
+ markdown: parseMarkdown(config.markdown ?? {}),
+ socials: parseSocials(config.socials ?? []),
+ topNav: parseTopNav(config.topNav ?? []),
+ theme: await parseTheme(config.theme ?? ({} as Theme)),
+ vite: parseViteConfig(config.vite, {
+ basePath,
+ }),
+ }
+}
+
+export const getDefaultConfig = async () => await defineConfig({})
+
+//////////////////////////////////////////////////////
+// Parsers
+
+function parseBasePath(basePath_: string | undefined) {
+ let basePath = basePath_
+ if (!basePath) return ''
+ if (!basePath.startsWith('/')) basePath = `/${basePath}`
+ if (basePath.endsWith('/')) basePath = basePath.slice(0, -1)
+ return basePath
+}
+
+async function parseBanner(banner: Banner): Promise | undefined> {
+ if (!banner) return undefined
+
+ const bannerContent = (() => {
+ if (typeof banner === 'string') return banner
+ if (typeof banner === 'object' && 'content' in banner) return banner.content
+ return undefined
+ })()
+
+ const content = await (async () => {
+ if (typeof bannerContent !== 'string') return bannerContent
+
+ const { compile } = await import('@mdx-js/mdx')
+ const remarkGfm = (await import('remark-gfm')).default
+ return String(
+ await compile(bannerContent, {
+ outputFormat: 'function-body',
+ remarkPlugins: [remarkGfm],
+ }),
+ )
+ })()
+
+ if (!content) return undefined
+
+ const textColor = await (async () => {
+ if (typeof banner === 'string') return undefined
+ if (typeof banner === 'object') {
+ if ('textColor' in banner) return banner.textColor
+ if ('backgroundColor' in banner && banner.backgroundColor) {
+ const chroma = (await import('chroma-js')).default
+ return chroma.contrast(banner.backgroundColor, 'white') < 4.5 ? 'black' : 'white'
+ }
+ }
+ return undefined
+ })()
+
+ return {
+ height: '32px',
+ ...(typeof banner === 'object' ? banner : {}),
+ content,
+ textColor,
+ }
+}
+
+function parseFont(font: Font): Font {
+ if ('google' in font) return { default: font }
+ return font as Font
+}
+
+function parseImageUrl(
+ imageUrl: ImageUrl | undefined,
+ { basePath }: { basePath?: string },
+): ImageUrl | undefined {
+ if (!imageUrl) return
+ if (process.env.NODE_ENV === 'development') return imageUrl
+ if (typeof imageUrl === 'string') {
+ if (imageUrl.startsWith('http')) return imageUrl
+ return `${basePath}${imageUrl}`
+ }
+ return {
+ dark: imageUrl.dark.startsWith('http') ? imageUrl.dark : `${basePath}${imageUrl.dark}`,
+ light: imageUrl.light.startsWith('http') ? imageUrl.light : `${basePath}${imageUrl.light}`,
+ }
+}
+
+function parseMarkdown(markdown: Markdown): Markdown {
+ return {
+ ...markdown,
+ code: {
+ themes: {
+ dark: 'github-dark-dimmed',
+ light: 'github-light',
+ },
+ ...markdown.code,
+ },
+ }
+}
+
+const socialsMeta = {
+ discord: { label: 'Discord', type: 'discord' },
+ github: { label: 'GitHub', type: 'github' },
+ telegram: { label: 'Telegram', type: 'telegram' },
+ warpcast: { label: 'Warpcast', type: 'warpcast' },
+ x: { label: 'X (Twitter)', type: 'x' },
+} satisfies Record
+
+function parseSocials(socials: Socials): Socials {
+ return socials.map((social) => {
+ return {
+ icon: social.icon,
+ link: social.link,
+ ...socialsMeta[social.icon],
+ }
+ })
+}
+
+let id = 0
+
+function parseTopNav(topNav: TopNav): TopNav {
+ const parsedTopNav: ParsedTopNavItem[] = []
+ for (const item of topNav) {
+ parsedTopNav.push({
+ ...item,
+ id: id++,
+ items: item.items ? parseTopNav(item.items) : [],
+ })
+ }
+ return parsedTopNav
+}
+
+async function parseTheme(
+ theme: Theme,
+): Promise> {
+ const chroma = (await import('chroma-js')).default
+ const accentColor = (() => {
+ if (!theme.accentColor) return theme.accentColor
+ if (
+ typeof theme.accentColor === 'object' &&
+ !Object.keys(theme.accentColor).includes('light') &&
+ !Object.keys(theme.accentColor).includes('dark')
+ )
+ return theme.accentColor
+
+ const accentColor = theme.accentColor as string | { light: string; dark: string }
+ const accentColorLight = typeof accentColor === 'object' ? accentColor.light : accentColor
+ const accentColorDark = typeof accentColor === 'object' ? accentColor.dark : accentColor
+ return {
+ backgroundAccent: {
+ dark: accentColorDark,
+ light: accentColorLight,
+ },
+ backgroundAccentHover: {
+ dark: chroma(accentColorDark).darken(0.25).hex(),
+ light: chroma(accentColorLight).darken(0.25).hex(),
+ },
+ backgroundAccentText: {
+ dark: chroma.contrast(accentColorDark, 'white') < 4.5 ? 'black' : 'white',
+ light: chroma.contrast(accentColorLight, 'white') < 4.5 ? 'black' : 'white',
+ },
+ borderAccent: {
+ dark: chroma(accentColorDark).brighten(0.5).hex(),
+ light: chroma(accentColorLight).darken(0.25).hex(),
+ },
+ textAccent: {
+ dark: accentColorDark,
+ light: accentColorLight,
+ },
+ textAccentHover: {
+ dark: chroma(accentColorDark).darken(0.5).hex(),
+ light: chroma(accentColorLight).darken(0.5).hex(),
+ },
+ } satisfies Theme['accentColor']
+ })()
+ return {
+ ...theme,
+ accentColor,
+ } as Theme
+}
+
+export function parseViteConfig(
+ viteConfig: UserConfig | undefined,
+ { basePath }: { basePath?: string },
+): UserConfig {
+ return {
+ ...viteConfig,
+ ...(basePath ? { base: basePath } : {}),
+ }
+}
+
+//////////////////////////////////////////////////////
+// Types
+
+type Normalize = {
+ [K in keyof T]: T[K]
+} & {}
+
+export type Banner = Exclude<
+ | string
+ | ReactElement
+ | {
+ /** Whether or not the banner can be dismissed. */
+ dismissable?: boolean
+ /** The background color of the banner. */
+ backgroundColor?: string
+ /** The content of the banner. */
+ content: string | ReactElement
+ /** The height of the banner. */
+ height?: string
+ /** The text color of the banner. */
+ textColor?: string
+ }
+ | undefined,
+ parsed extends true ? string | ReactElement : never
+>
+
+export type ColorScheme = 'light' | 'dark' | 'system' | undefined
+
+export type EditLink = {
+ /**
+ * Link pattern
+ */
+ pattern: string | (() => string)
+ /**
+ * Link text
+ *
+ * @default "Edit page"
+ */
+ text?: string
+}
+
+type FontSource = Normalize<{
+ /** Name of the Google Font to use. */
+ google?: string
+}>
+type ParsedFont = {
+ default?: FontSource
+ mono?: FontSource
+}
+export type Font = parsed extends true
+ ? ParsedFont
+ : FontSource | ParsedFont
+
+export type ImageUrl = string | { light: string; dark: string }
+
+export type IconUrl = ImageUrl
+
+export type LogoUrl = ImageUrl
+
+export type Markdown = RequiredBy<
+ {
+ code?: Normalize
+ remarkPlugins?: PluggableList
+ rehypePlugins?: PluggableList
+ },
+ parsed extends true ? 'code' : never
+>
+
+export type Search = SearchOptions
+
+export type SidebarItem = {
+ /** Whether or not to collapse the sidebar item by default. */
+ collapsed?: boolean
+ /** Text to display on the sidebar. */
+ text: string
+ /** Optional pathname to the target documentation page. */
+ // TODO: support external links
+ link?: string
+ /** Optional children to nest under this item. */
+ items?: SidebarItem[]
+}
+
+export type Sidebar =
+ | SidebarItem[]
+ | { [path: string]: SidebarItem[] | { backLink?: boolean; items: SidebarItem[] } }
+
+export type SocialType = 'discord' | 'github' | 'telegram' | 'warpcast' | 'x'
+export type SocialItem = {
+ /** Social icon to display. */
+ icon: SocialType // TODO: Support custom SVG icons
+ /** Label for the social. */
+ label?: string
+ /** Link to the social. */
+ link: string
+}
+export type ParsedSocialItem = Required & {
+ /** The type of social item. */
+ type: SocialType
+}
+
+export type Socials = parsed extends true
+ ? ParsedSocialItem[]
+ : SocialItem[]
+
+export type Sponsor = {
+ /** The name of the sponsor. */
+ name: string
+ /** The link to the sponsor's website. */
+ link: string
+ /** The image to display for the sponsor. */
+ image: string
+}
+export type SponsorSet = {
+ /** The list of sponsors to display. */
+ items: (Sponsor | null)[][]
+ /** The name of the sponsor set (e.g. "Gold Sponsors", "Collaborators", etc). */
+ name: string
+ /** The height of the sponsor images. */
+ height?: number
+}
+
+export type ThemeVariables, value> = {
+ [key in keyof variables]?: value
+}
+export type Theme<
+ parsed extends boolean = false,
+ colorScheme extends ColorScheme = ColorScheme,
+ ///
+ colorValue = colorScheme extends 'system' | undefined ? { light: string; dark: string } : string,
+> = {
+ accentColor?: Exclude<
+ | string
+ | (colorScheme extends 'system' | undefined ? { light: string; dark: string } : never)
+ | Required<
+ ThemeVariables<
+ Pick<
+ typeof primitiveColorVars,
+ | 'backgroundAccent'
+ | 'backgroundAccentHover'
+ | 'backgroundAccentText'
+ | 'borderAccent'
+ | 'textAccent'
+ | 'textAccentHover'
+ >,
+ colorValue
+ >
+ >,
+ parsed extends true ? string | { light: string; dark: string } : never
+ >
+ colorScheme?: colorScheme
+ variables?: {
+ borderRadius?: ThemeVariables
+ color?: ThemeVariables