Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions apps/web/content/docs/ui/copy-button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,32 @@ const [copied, setCopied] = React.useState(false)
<CopyButton content={inviteLink} copied={copied} onCopiedChange={setCopied} />
```

## Async content

Text that has to be fetched first? Pass a function instead of a string. It runs
on click, the button is `aria-busy` while it waits, and extra clicks are ignored
until it settles.

```tsx
<CopyButton
content={async () => {
const res = await fetch('/docs/dev.md')
if (!res.ok) throw new Error(`Failed to load ${res.url}`)

return await res.text()
}}
size="sm"
variant="secondary"
>
Copy Markdown
</CopyButton>
```

Safari only lets a page write to the clipboard during the click itself, so the
pending text goes in as a `ClipboardItem` promise. Browsers without
`ClipboardItem` wait for the text, then copy it. A rejected promise shows the
same error toast as a blocked clipboard.

## Accessibility

- A polite live region announces "Copied".
Expand All @@ -70,8 +96,9 @@ import { TypeTable } from 'fumadocs-ui/components/type-table'
<TypeTable
type={{
content: {
description: 'Text written to the clipboard.',
type: 'string',
description:
'Text written to the clipboard, or an async function that resolves it on click.',
type: 'string | (() => Promise<string>)',
required: true,
},
copied: { description: 'Controls the copied state.', type: 'boolean' },
Expand Down
6 changes: 3 additions & 3 deletions apps/web/content/docs/ui/tooltip.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Tooltip
description: A small hint on hover or focus, with a half-second delay, warm-up between neighbours and a gliding group mode for toolbars.
description: A small hint on hover or focus, with a short delay, warm-up between neighbours and a gliding group mode for toolbars.
icon: MessageCircleMore
---

Expand Down Expand Up @@ -55,7 +55,7 @@ import { TooltipWithContent } from '@vitnode/core/components/ui/tooltip'

<Preview name="tooltip-delay" />

Tooltips wait 500ms, so they don't flash as the cursor passes by. Once one is
Tooltips wait 200ms, so they don't flash as the cursor passes by. Once one is
open, its neighbours open instantly. Hover the left row, then slide along it.

VitNode already mounts a `TooltipProvider` for the whole app. Wrap a section in
Expand Down Expand Up @@ -126,7 +126,7 @@ import { TypeTable } from 'fumadocs-ui/components/type-table'
'TooltipProvider.delay': {
description: 'Wait before opening, in milliseconds.',
type: 'number',
default: '500',
default: '200',
},
'TooltipProvider.timeout': {
description:
Expand Down
4 changes: 2 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
"dotenv": "^18.0.6",
"drizzle-kit": "1.0.0-rc.4",
"drizzle-orm": "1.0.0-rc.4",
"fumadocs-core": "^16.16.2",
"fumadocs-core": "16.17.1",
"fumadocs-mdx": "^15.4.6",
"fumadocs-ui": "^16.16.2",
"fumadocs-ui": "npm:@fumadocs/base-ui@16.17.1",
"hono": "^4.13.13",
"lucide-react": "^1.52.0",
"motion": "^14.0.0",
Expand Down
18 changes: 17 additions & 1 deletion apps/web/src/docs/callout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ const CALLOUT_VARIANTS = {
warning: 'warning',
} as const satisfies Record<string, AlertVariant>

const CALLOUT_TITLE_COLORS = {
destructive: 'text-destructive',
info: 'text-primary',
success: 'text-success',
warning: 'text-warn',
} as const satisfies Record<
(typeof CALLOUT_VARIANTS)[keyof typeof CALLOUT_VARIANTS],
string
>

export const Callout = ({
children,
className,
Expand All @@ -36,7 +46,13 @@ export const Callout = ({
variant={CALLOUT_VARIANTS[type]}
{...props}
>
{title ? <AlertTitle className="my-0">{title}</AlertTitle> : null}
{title ? (
<AlertTitle
className={cn('my-0', CALLOUT_TITLE_COLORS[CALLOUT_VARIANTS[type]])}
>
{title}
</AlertTitle>
) : null}
<AlertDescription className="prose-no-margin">{children}</AlertDescription>
</Alert>
)
18 changes: 18 additions & 0 deletions apps/web/src/docs/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type React from 'react'

import { cn } from 'cn'
import { Card as FumadocsCard } from 'fumadocs-ui/components/card'

export const Card = ({
className,
href,
icon,
...props
}: React.ComponentProps<typeof FumadocsCard>) => (
<FumadocsCard
className={cn(href && 'hover:border-fd-primary/60', className)}
href={href}
icon={icon ? <span className="text-fd-primary flex">{icon}</span> : null}
{...props}
/>
)
14 changes: 14 additions & 0 deletions apps/web/src/docs/docs.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
@import '../styles.css';
@import 'fumadocs-ui/css/generated/spacious.css';
@import 'fumadocs-ui/css/shadcn.css';
@import 'fumadocs-ui/css/preset.css';

@source '../../node_modules/fumadocs-ui/dist/**/*.js';

:root:not(.dark) #fd-spacious-layout {
background-color: var(--color-fd-background);
}

:root:not(.dark) #nd-sidebar {
background-color: var(--background);
--color-fd-accent: var(--accent);
}

:root:not(.dark) #fd-spacious-layout .prose {
--tw-prose-body: oklch(0.373 0.003 264);
}
2 changes: 1 addition & 1 deletion apps/web/src/docs/error-views.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
ErrorActions,
NotFound,
} from '@vitnode/core/tanstack/layout'
import { DocsPage } from 'fumadocs-ui/layouts/notebook/page'
import { DocsPage } from 'fumadocs-ui/layouts/spacious/page'

const DocsErrorFrame = ({ children }: { children: React.ReactNode }) => (
<DocsPage
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/docs/mdx-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { Step, Steps } from 'fumadocs-ui/components/steps'
import defaultMdxComponents from 'fumadocs-ui/mdx'

import { Callout } from './callout'
import { Card } from './card'
import { Preview } from './preview'

export const getMDXComponents = (components?: MDXComponents) =>
({
...defaultMdxComponents,
Callout,
Card,
Preview,
Step,
Steps,
Expand Down
39 changes: 17 additions & 22 deletions apps/web/src/docs/page-content.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
MarkdownCopyButton,
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/notebook/page'
import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/spacious/page'
import { use } from 'react'

import type { DocsPageData } from './transport'

import { getMDXComponents } from './mdx-components'
import { DocsPageFooter } from './page-footer'
import { DocsPageHeader } from './page-header'
import { docs } from './source'

export const DocsPageContent = ({
Expand All @@ -26,20 +21,20 @@ export const DocsPageContent = ({
const MDX = page.body

return (
<DocsPage tableOfContent={{ single: false, style: 'clerk' }} toc={toc}>
<DocsTitle className="text-balance">{page.title}</DocsTitle>
<DocsDescription className="text-pretty">
{page.description}
</DocsDescription>

<div className="border-b pb-6">
<div className="flex flex-wrap items-center gap-2">
<MarkdownCopyButton markdownUrl={markdownUrl} />
<ViewOptionsPopover githubUrl={githubUrl} markdownUrl={markdownUrl} />
</div>
</div>

<DocsBody>
<DocsPage
breadcrumb={{ enabled: false }}
slots={{ footer: DocsPageFooter }}
tableOfContent={{ single: false, style: 'clerk' }}
toc={toc}
>
<DocsPageHeader
description={page.description}
githubUrl={githubUrl}
markdownUrl={markdownUrl}
title={page.title}
/>

<DocsBody className="pt-2">
<MDX components={getMDXComponents()} />
</DocsBody>
</DocsPage>
Expand Down
51 changes: 51 additions & 0 deletions apps/web/src/docs/page-footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { usePathname } from 'fumadocs-core/framework'
import Link from 'fumadocs-core/link'
import { findNeighbour } from 'fumadocs-core/page-tree'
import { useTreeContext } from 'fumadocs-ui/contexts/tree'
import { ArrowLeft, ArrowRight } from 'lucide-react'

export const DocsPageFooter = () => {
const { full } = useTreeContext()
const { next, previous } = findNeighbour(full, usePathname())

if (!(next || previous)) return null

return (
<nav
aria-label="Previous and next page"
className="mt-10 flex flex-col gap-4"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Separate gap from margin and padding utilities

The new footer mixes mt-10 with gap-4, and repeats the same pattern with gap-6 and padding on the next-page link. Split the outer spacing onto a wrapper or use one spacing mechanism so this component follows the repository's explicit layout convention.

AGENTS.md reference: AGENTS.md:L73-L73

Useful? React with 👍 / 👎.

>
{next && (
<Link
className="group border-fd-primary/20 bg-fd-primary/5 hover:bg-fd-primary/10 flex items-center justify-between gap-6 rounded-2xl border p-5 transition-colors md:p-6"
href={next.url}
>
<span className="flex min-w-0 flex-col gap-1">
<span className="text-fd-primary text-sm font-medium">Next up</span>
<span className="text-fd-foreground text-lg font-semibold text-balance">
{next.name}
</span>
{next.description && (
<span className="text-fd-muted-foreground text-sm leading-relaxed text-pretty">
{next.description}
</span>
)}
</span>
<ArrowRight
aria-hidden="true"
className="text-fd-primary size-5 shrink-0 transition-transform group-hover:translate-x-0.5 motion-reduce:transition-none"
/>
</Link>
)}
{previous && (
<Link
className="text-fd-muted-foreground hover:text-fd-foreground inline-flex items-center gap-2 self-start py-2 text-sm transition-colors"
href={previous.url}
>
<ArrowLeft aria-hidden="true" className="size-4" />
{previous.name}
</Link>
)}
</nav>
)
}
112 changes: 112 additions & 0 deletions apps/web/src/docs/page-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { Folder } from 'fumadocs-core/page-tree'

import { buttonVariants } from '@vitnode/core/components/ui/button'
import { CopyButton } from '@vitnode/core/components/ui/copy-button'
import { useTreePath } from 'fumadocs-ui/contexts/tree'
import {
DocsDescription,
DocsTitle,
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/spacious/page'
import { ChevronRight } from 'lucide-react'
import { Fragment } from 'react'

const markdownCache = new Map<string, string>()

const loadMarkdown = async (markdownUrl: string) => {
const cached = markdownCache.get(markdownUrl)
if (cached !== undefined) return cached

const res = await fetch(markdownUrl)
if (!res.ok) throw new Error(`Failed to load ${markdownUrl}: ${res.status}`)

const markdown = await res.text()
if (import.meta.env.PROD) markdownCache.set(markdownUrl, markdown)

return markdown
}

const SectionPath = ({ folders }: { folders: Folder[] }) => (
<p className="text-fd-primary flex flex-wrap items-center gap-1 text-sm font-medium [&_.fd-page-tree-item-name]:w-auto!">
{folders.map((folder, index) => (
<Fragment key={folder.$id ?? folder.index?.url}>
{index > 0 && (
<ChevronRight aria-hidden="true" className="size-3.5 opacity-60" />
)}
{folder.name}
</Fragment>
))}
</p>
)

const IconArtwork = ({ icon }: { icon: React.ReactNode }) => (
<div
aria-hidden="true"
className="relative flex size-40 shrink-0 items-center justify-center select-none max-md:hidden"
>
<span className="border-fd-primary/10 absolute inset-0 rounded-4xl border" />
<span className="border-fd-primary/15 bg-fd-primary/5 absolute inset-5 rounded-xl border" />
<span className="bg-fd-background text-fd-primary relative flex size-20 items-center justify-center rounded-2xl border shadow-sm [&_svg]:size-9">
{icon}
</span>
</div>
)

export const DocsPageHeader = ({
description,
githubUrl,
markdownUrl,
title,
}: {
description?: string
githubUrl: string
markdownUrl: string
title: string
}) => {
const path = useTreePath()
const icon = path.at(-1)?.icon
const sectionPath = path.filter(
(node): node is Folder => node.type === 'folder' && !node.root,
)

return (
<header className="flex items-center gap-10 border-b pt-2 pb-10">
<div className="flex min-w-0 flex-1 flex-col gap-4">
{(icon || sectionPath.length > 0) && (
<div className="flex items-center gap-3">
{icon && (
<span
aria-hidden="true"
className="bg-fd-background text-fd-primary flex size-10 shrink-0 items-center justify-center rounded-xl border shadow-xs md:hidden [&_svg]:size-5"
>
{icon}
</span>
)}
{sectionPath.length > 0 && <SectionPath folders={sectionPath} />}
</div>
)}
<DocsTitle className="text-3xl tracking-tight text-balance md:text-4xl">
{title}
</DocsTitle>
<DocsDescription className="mb-0 text-lg leading-relaxed text-pretty">
{description}
</DocsDescription>
<div className="flex flex-wrap items-center gap-2 pt-1">
<CopyButton
content={async () => await loadMarkdown(markdownUrl)}
size="sm"
variant="secondary"
>
Copy Markdown
</CopyButton>
<ViewOptionsPopover
className={buttonVariants({ size: 'sm', variant: 'secondary' })}
githubUrl={githubUrl}
markdownUrl={markdownUrl}
/>
</div>
</div>
{icon && <IconArtwork icon={icon} />}
</header>
)
}
Loading
Loading