From 2a932ee7f5cae6ed507d8d0a9d771a8616f8838f Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Fri, 9 Oct 2026 19:08:04 +0200 Subject: [PATCH] refactor: Docs layout --- apps/web/content/docs/ui/copy-button.mdx | 31 +- apps/web/content/docs/ui/tooltip.mdx | 6 +- apps/web/package.json | 4 +- apps/web/src/docs/callout.tsx | 18 +- apps/web/src/docs/card.tsx | 18 + apps/web/src/docs/docs.css | 14 + apps/web/src/docs/error-views.tsx | 2 +- apps/web/src/docs/mdx-components.tsx | 2 + apps/web/src/docs/page-content.tsx | 39 +- apps/web/src/docs/page-footer.tsx | 51 ++ apps/web/src/docs/page-header.tsx | 112 +++ apps/web/src/docs/pending.tsx | 62 +- apps/web/src/docs/shell-content.tsx | 49 +- apps/web/src/styles.css | 32 +- .../copy-of-vitnode-app/root/src/styles.css | 32 +- .../src/components/ui/copy-button.test.tsx | 85 ++ .../vitnode/src/components/ui/copy-button.tsx | 33 +- .../src/components/ui/sidebar-glide.ts | 192 +++++ .../src/components/ui/sidebar.test.tsx | 156 ++++ .../vitnode/src/components/ui/sidebar.tsx | 46 +- .../src/components/ui/tooltip.test.tsx | 56 ++ .../vitnode/src/components/ui/tooltip.tsx | 33 +- packages/vitnode/src/tanstack/admin/shell.tsx | 2 +- .../core/dashboard/grid/board-provider.tsx | 14 +- .../core/dashboard/grid/widget-panel.tsx | 8 +- pnpm-lock.yaml | 806 ++---------------- pnpm-workspace.yaml | 4 + 27 files changed, 1045 insertions(+), 862 deletions(-) create mode 100644 apps/web/src/docs/card.tsx create mode 100644 apps/web/src/docs/page-footer.tsx create mode 100644 apps/web/src/docs/page-header.tsx create mode 100644 packages/vitnode/src/components/ui/sidebar-glide.ts create mode 100644 packages/vitnode/src/components/ui/sidebar.test.tsx create mode 100644 packages/vitnode/src/components/ui/tooltip.test.tsx diff --git a/apps/web/content/docs/ui/copy-button.mdx b/apps/web/content/docs/ui/copy-button.mdx index b4b0b9467f..a37374bd20 100644 --- a/apps/web/content/docs/ui/copy-button.mdx +++ b/apps/web/content/docs/ui/copy-button.mdx @@ -56,6 +56,32 @@ const [copied, setCopied] = React.useState(false) ``` +## 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 + { + 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 + +``` + +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". @@ -70,8 +96,9 @@ import { TypeTable } from 'fumadocs-ui/components/type-table' Promise)', required: true, }, copied: { description: 'Controls the copied state.', type: 'boolean' }, diff --git a/apps/web/content/docs/ui/tooltip.mdx b/apps/web/content/docs/ui/tooltip.mdx index 84dc1de194..b0e4db8788 100644 --- a/apps/web/content/docs/ui/tooltip.mdx +++ b/apps/web/content/docs/ui/tooltip.mdx @@ -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 --- @@ -55,7 +55,7 @@ import { TooltipWithContent } from '@vitnode/core/components/ui/tooltip' -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 @@ -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: diff --git a/apps/web/package.json b/apps/web/package.json index 5f821250d9..9802f5f0de 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", diff --git a/apps/web/src/docs/callout.tsx b/apps/web/src/docs/callout.tsx index aa8037519f..27e0b65435 100644 --- a/apps/web/src/docs/callout.tsx +++ b/apps/web/src/docs/callout.tsx @@ -18,6 +18,16 @@ const CALLOUT_VARIANTS = { warning: 'warning', } as const satisfies Record +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, @@ -36,7 +46,13 @@ export const Callout = ({ variant={CALLOUT_VARIANTS[type]} {...props} > - {title ? {title} : null} + {title ? ( + + {title} + + ) : null} {children} ) diff --git a/apps/web/src/docs/card.tsx b/apps/web/src/docs/card.tsx new file mode 100644 index 0000000000..8b72e19279 --- /dev/null +++ b/apps/web/src/docs/card.tsx @@ -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) => ( + {icon} : null} + {...props} + /> +) diff --git a/apps/web/src/docs/docs.css b/apps/web/src/docs/docs.css index 54a455e390..f97f516506 100644 --- a/apps/web/src/docs/docs.css +++ b/apps/web/src/docs/docs.css @@ -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); +} diff --git a/apps/web/src/docs/error-views.tsx b/apps/web/src/docs/error-views.tsx index 0aa3ab905e..abc2b6c306 100644 --- a/apps/web/src/docs/error-views.tsx +++ b/apps/web/src/docs/error-views.tsx @@ -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 }) => ( ({ ...defaultMdxComponents, Callout, + Card, Preview, Step, Steps, diff --git a/apps/web/src/docs/page-content.tsx b/apps/web/src/docs/page-content.tsx index 2bb05d3799..4bddc8e595 100644 --- a/apps/web/src/docs/page-content.tsx +++ b/apps/web/src/docs/page-content.tsx @@ -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 = ({ @@ -26,20 +21,20 @@ export const DocsPageContent = ({ const MDX = page.body return ( - - {page.title} - - {page.description} - - -
-
- - -
-
- - + + + + diff --git a/apps/web/src/docs/page-footer.tsx b/apps/web/src/docs/page-footer.tsx new file mode 100644 index 0000000000..742a82ec2a --- /dev/null +++ b/apps/web/src/docs/page-footer.tsx @@ -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 ( + + ) +} diff --git a/apps/web/src/docs/page-header.tsx b/apps/web/src/docs/page-header.tsx new file mode 100644 index 0000000000..1b8e4da2f0 --- /dev/null +++ b/apps/web/src/docs/page-header.tsx @@ -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() + +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[] }) => ( +

+ {folders.map((folder, index) => ( + + {index > 0 && ( + + ))} +

+) + +const IconArtwork = ({ icon }: { icon: React.ReactNode }) => ( + +) + +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 ( +
+
+ {(icon || sectionPath.length > 0) && ( +
+ {icon && ( + + )} + {sectionPath.length > 0 && } +
+ )} + + {title} + + + {description} + +
+ await loadMarkdown(markdownUrl)} + size="sm" + variant="secondary" + > + Copy Markdown + + +
+
+ {icon && } +
+ ) +} diff --git a/apps/web/src/docs/pending.tsx b/apps/web/src/docs/pending.tsx index 112cde6d1b..a2d4539239 100644 --- a/apps/web/src/docs/pending.tsx +++ b/apps/web/src/docs/pending.tsx @@ -2,18 +2,26 @@ import { Skeleton } from '@vitnode/core/components/ui/skeleton' import { cn } from 'cn' const DocsHeadingPendingSkeleton = () => ( -