From bd2fcd725c6e287990997b5c1b9046e0ed5c0e66 Mon Sep 17 00:00:00 2001 From: Rasmus Viloma Date: Sun, 11 Oct 2026 23:52:49 +0300 Subject: [PATCH] feat(docs): code block tabs with a sliding indicator --- components/docs/code-block.tsx | 4 +- components/docs/code-files.tsx | 35 +++++--- components/docs/code-frame.tsx | 93 ++++++++++++++------- components/docs/installation-block-tabs.tsx | 41 --------- components/docs/installation-block.tsx | 13 +-- 5 files changed, 97 insertions(+), 89 deletions(-) delete mode 100644 components/docs/installation-block-tabs.tsx diff --git a/components/docs/code-block.tsx b/components/docs/code-block.tsx index ebeaea7..00c197e 100644 --- a/components/docs/code-block.tsx +++ b/components/docs/code-block.tsx @@ -2,7 +2,7 @@ import { highlight } from "fumadocs-core/highlight"; import type { ReactNode } from "react"; import { codeTheme } from "@/lib/docs/code-theme"; import { cn } from "@/lib/utils"; -import { CodeFrame, CodeTab } from "./code-frame"; +import { CodeFrame, CodeTitle } from "./code-frame"; type CodeBlockProps = { code: string; @@ -34,7 +34,7 @@ export const highlightCode = (code: string, lang = "tsx") => export const CodeBlock = async ({ code, lang = "tsx", title, className }: CodeBlockProps) => ( {title ?? lang}} + tabs={{title ?? lang}} code={code} className={className} > diff --git a/components/docs/code-files.tsx b/components/docs/code-files.tsx index e73c2f2..f8fa374 100644 --- a/components/docs/code-files.tsx +++ b/components/docs/code-files.tsx @@ -1,8 +1,9 @@ "use client"; +import { Tabs } from "@base-ui/react/tabs"; import type { ReactNode } from "react"; import { useState } from "react"; -import { CodeFrame, CodeTab } from "./code-frame"; +import { CodeFrame, CodePanel, CodeTab, CodeTabList } from "./code-frame"; export type CodeFileEntry = { name: string; @@ -18,16 +19,26 @@ export const CodeFiles = ({ files, className }: { files: CodeFileEntry[]; classN const current = files.find((file) => file.name === name) ?? files[0]; return ( - ( - setName(file.name)}> - {file.name} - - ))} - > - {current.code} - + + + {files.map((file) => ( + + {file.name} + + ))} + + } + > + {files.map((file) => ( + + {file.code} + + ))} + + ); }; diff --git a/components/docs/code-frame.tsx b/components/docs/code-frame.tsx index fbd12f2..f87329a 100644 --- a/components/docs/code-frame.tsx +++ b/components/docs/code-frame.tsx @@ -1,5 +1,6 @@ "use client"; +import { Tabs } from "@base-ui/react/tabs"; import { Check, Copy } from "@keyline-icons/react"; import type { ReactNode } from "react"; import { useCopy } from "@/hooks/use-copy"; @@ -9,6 +10,8 @@ import { cn } from "@/lib/utils"; * The dark code chrome shared by every block in the docs: a header of file (or * package-manager) tabs with a copy button, over an inset code panel. Dark in * both themes, tinted by the theme's seeds (see --code-bg in globals.css). + * A tabbed frame sits inside a `Tabs.Root`, with a `CodeTabList` as its + * `tabs` and a `CodePanel` per tab as its children. */ export const CodeFrame = ({ tabs, @@ -25,12 +28,12 @@ export const CodeFrame = ({
-
-
{tabs}
+
+
{tabs}
@@ -39,36 +42,70 @@ export const CodeFrame = ({
); -/** A header tab. Without `onClick` it is a plain label (a single file's name). */ -export const CodeTab = ({ +/** The header's tab strip. The indicator slides to the active tab's label. */ +export const CodeTabList = ({ children }: { children: ReactNode }) => ( + + {children} + + +); + +/** A header tab, inside a `CodeTabList`. */ +export const CodeTab = ({ value, children }: { value: string; children: ReactNode }) => ( + + {children} + +); + +/** One tab's code, as a child of a tabbed `CodeFrame`. */ +export const CodePanel = ({ value, children }: { value: string; children: ReactNode }) => ( + + {children} + +); + +/** A plain header label in place of tabs: a single file's name, or dimmed, its language. */ +export const CodeTitle = ({ active = true, - onClick, children, }: { active?: boolean; - onClick?: () => void; children: ReactNode; -}) => { - const className = cn( - "flex h-full shrink-0 items-center font-medium font-mono text-xs transition-colors", - active - ? "text-white/92 shadow-[inset_0_-1px_0_rgb(255_255_255/0.92)]" - : "text-white/50 hover:text-white/80", - ); +}) => ( + + {children} + +); - return onClick ? ( - - ) : ( - {children} - ); -}; +const tabLabel = + "relative flex h-full shrink-0 items-center px-2 font-medium font-mono text-xs transition-colors"; const CodeCopyButton = ({ code }: { code: string }) => { const { copy, copied } = useCopy(); @@ -79,7 +116,7 @@ const CodeCopyButton = ({ code }: { code: string }) => { type="button" onClick={() => copy(code)} aria-label={isCopied ? "Copied" : "Copy code"} - className="grid size-7 shrink-0 cursor-pointer place-items-center rounded-full text-white/56 transition-colors hover:bg-white/8 hover:text-white focus-visible:outline-2 focus-visible:outline-white/60" + className="grid size-7 shrink-0 cursor-pointer place-items-center rounded-md text-white/56 transition-colors hover:bg-white/8 hover:text-white focus-visible:outline-2 focus-visible:outline-white/60" > {isCopied ? : } diff --git a/components/docs/installation-block-tabs.tsx b/components/docs/installation-block-tabs.tsx deleted file mode 100644 index 1749318..0000000 --- a/components/docs/installation-block-tabs.tsx +++ /dev/null @@ -1,41 +0,0 @@ -"use client"; - -import type { ReactNode } from "react"; -import { useState } from "react"; -import { CodeFrame, CodeTab } from "./code-frame"; - -export type InstallationEntry = { - manager: string; - command: string; - /** Pre-highlighted command, rendered on the server. */ - code: ReactNode; -}; - -type InstallationBlockTabsProps = { - entries: InstallationEntry[]; -}; - -// Client shell for the package-manager tabs. The server highlights every -// command up front; this only picks which one shows. -export const InstallationBlockTabs = ({ entries }: InstallationBlockTabsProps) => { - const [manager, setManager] = useState(entries[0].manager); - const current = entries.find((entry) => entry.manager === manager) ?? entries[0]; - - return ( - ( - setManager(entry.manager)} - > - {entry.manager} - - ))} - > - {current.code} - - ); -}; diff --git a/components/docs/installation-block.tsx b/components/docs/installation-block.tsx index bde9734..03ae3f7 100644 --- a/components/docs/installation-block.tsx +++ b/components/docs/installation-block.tsx @@ -1,20 +1,21 @@ import { PACKAGE_MANAGERS } from "@/lib/docs/package-managers"; import { highlightCode } from "./code-block"; -import { InstallationBlockTabs } from "./installation-block-tabs"; +import { CodeFiles } from "./code-files"; type InstallationBlockProps = { packageName: string; }; // Server component: highlights one install command per package manager and -// hands them all to the client shell, which shows the selected one. +// shows them as tabs, one per manager. export const InstallationBlock = async ({ packageName }: InstallationBlockProps) => ( - { - const command = `${install} ${packageName}`; - return { manager, command, code: await highlightCode(command, "bash") }; + const source = `${install} ${packageName}`; + return { name: manager, source, code: await highlightCode(source, "bash") }; }), ) }