feat: better home page

This commit is contained in:
rzmk 2025-12-15 11:57:30 -05:00
parent 261b4ba21a
commit 1de7c29050
15 changed files with 1251 additions and 99 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

View file

@ -1,42 +1,437 @@
/** biome-ignore-all lint/suspicious/noArrayIndexKey: Would need to look into this trivial issue */
"use client";
import { cva } from "class-variance-authority";
import { CodeBlock } from "fumadocs-ui/components/codeblock";
import defaultMdxComponents from "fumadocs-ui/mdx";
import { cn } from "fumadocs-ui/utils/cn";
import {
BlocksIcon,
GiftIcon,
GitMergeIcon,
HomeIcon,
SailboatIcon,
TerminalIcon,
Trash2Icon,
ZapIcon,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { type HTMLProps, type ReactNode, useState } from "react";
import { Pre } from "@/components/codeblock";
import { Button, buttonVariants } from "@/components/ui/button";
import { RainbowButton } from "@/components/ui/rainbow-button";
import CkanactionDemo1 from "./ckanaction-demo-1.gif";
import CkanactionRustDemo from "./ckanaction-rust-demo.gif";
export default function HomePage() {
const gridColor =
"color-mix(in oklab, var(--color-fd-primary) 10%, transparent)";
const { Card, Cards } = defaultMdxComponents;
return (
<main className="flex flex-1 flex-col justify-center text-center">
<h1 className="mb-4 text-2xl font-bold">ckanaction</h1>
<p className="text-fd-muted-foreground">
You can open{" "}
<Link
href="/docs"
className="text-fd-foreground font-semibold underline"
<>
<div
className="absolute inset-x-0 top-[360px] h-[250px] max-md:hidden"
style={{
background: `repeating-linear-gradient(to right, ${gridColor}, ${gridColor} 1px,transparent 1px,transparent 50px), repeating-linear-gradient(to bottom, ${gridColor}, ${gridColor} 1px,transparent 1px,transparent 50px)`,
}}
/>
<main className="container relative max-w-[1100px] mx-auto px-2 py-4 z-2 lg:py-8">
<div
style={{
background:
"repeating-linear-gradient(to bottom, transparent, color-mix(in oklab, var(--color-fd-primary) 1%, transparent) 500px, transparent 1000px)",
}}
>
/docs
</Link>{" "}
and see the interactive documentation.
</p>
<footer className="absolute bottom-2 ml-4 flex flex-col bg-brand-secondary text-brand-secondary-foreground rounded-2xl">
<p className="text-xs">
Provided by{" "}
<a
href="https://dathere.com"
target="_blank"
className="font-medium text-blue-400"
rel="noopener"
>
datHere
</a>
.{" "}
<a
href="https://dathere.com/privacy-policy/"
target="_blank"
className="font-medium text-blue-400"
rel="noopener"
>
Privacy Policy
</a>
.
</p>
</footer>
</main>
<div className="relative mb-4">
<Hero />
{/* <Why /> */}
</div>
</div>
<hr className="mt-12 mb-4" />
<footer className="flex flex-col bg-brand-secondary pb-12 text-brand-secondary-foreground rounded-2xl">
<p className="mb-1 text-xl font-semibold">ckanaction</p>
<p className="text-xs">
Provided by{" "}
<a
href="https://dathere.com"
target="_blank"
className="font-medium text-blue-400"
rel="noopener"
>
datHere
</a>
.{" "}
<a
href="https://dathere.com/privacy-policy/"
target="_blank"
className="font-medium text-blue-400"
rel="noopener"
>
Privacy Policy
</a>
.
</p>
</footer>
</main>
</>
);
}
function Hero() {
const { Card, Cards } = defaultMdxComponents;
return (
<div className="relative z-2 flex flex-col border-x border-t bg-fd-background/80 px-4 pt-12 max-md:text-center md:px-12 md:pt-16 [.uwu_&]:hidden overflow-hidden">
<div
className="absolute inset-0 z-[-1] blur-2xl hidden dark:block"
style={{
maskImage:
"linear-gradient(to bottom, transparent, white, transparent)",
background:
"repeating-linear-gradient(65deg, var(--color-blue-500), var(--color-blue-500) 12px, color-mix(in oklab, var(--color-blue-600) 30%, transparent) 20px, transparent 200px)",
}}
/>
<div
className="absolute inset-0 z-[-1] blur-2xl dark:hidden"
style={{
maskImage:
"linear-gradient(to bottom, transparent, white, transparent)",
background:
"repeating-linear-gradient(65deg, var(--color-purple-300), var(--color-purple-300) 12px, color-mix(in oklab, var(--color-blue-600) 30%, transparent) 20px, transparent 200px)",
}}
/>
<h1 className="mb-8 text-4xl font-medium md:hidden">ckan-devstaller</h1>
<h1 className="mb-8 max-w-[800px] text-4xl font-medium max-md:hidden">
<span className="text-5xl">
ckanaction <GiftIcon className="inline-block w-10 h-10 pb-1" />
</span>
<br />
Rust crate & web GUI based on the CKAN API.
</h1>
<p className="mb-2 text-fd-muted-foreground md:max-w-[80%] md:text-xl">
ckanaction is a Rust library crate for interacting with the CKAN Actions
API, and this web app provides an interactive web GUI based on the
OpenAPI specification.
</p>
<p className="mb-8 text-fd-muted-foreground md:max-w-[80%] md:text-sm">
Provided by{" "}
<Link className="text-fd-info" href="https://dathere.com">
datHere
</Link>
.
</p>
<div className="inline-flex items-center gap-3 max-md:mx-auto mb-8">
<Link href="/docs">
<RainbowButton className="rounded-full">Get Started</RainbowButton>
</Link>
<Link
href="https://github.com/dathere/ckanaction"
className={cn(
buttonVariants({
variant: "outline",
size: "lg",
className: "rounded-full",
}),
)}
>
Source Code
</Link>
</div>
{/* <Cards>
<Card icon={<ZapIcon />} href="/docs/builder" title="Quick start">
Get started with ckan-devstaller and install CKAN within minutes
</Card>
<Card icon={<BlocksIcon />} href="/docs/builder" title="Builder">
Customize your installation with an interactive web GUI
</Card>
<Card
icon={<HomeIcon />}
href="/docs/reference/installation-architecture"
title="Installation architecture"
>
Learn about where files are installed after running ckan-devstaller
</Card>
<Card
icon={<GitMergeIcon />}
href="https://github.com/dathere/ckan-devstaller"
title="Source code"
>
View the source code of ckan-devstaller on GitHub
</Card>
</Cards> */}
<PreviewImages />
</div>
);
}
const previewButtonVariants = cva(
"w-48 h-8 text-sm font-medium transition-colors rounded-full",
{
variants: {
active: {
true: "text-fd-primary-foreground",
false: "text-fd-muted-foreground",
},
},
},
);
function PreviewImages() {
const [active, setActive] = useState(0);
const previews = [
{
image: CkanactionRustDemo,
name: "Rust library crate",
},
{
image: CkanactionDemo1,
name: "Interactive web GUI",
},
];
return (
<div className="hidden md:block p-8 min-w-[600px] md:min-w-[800px] overflow-hidden xl:-mx-12 dark:[mask-image:linear-gradient(to_top,transparent,white_40px)]">
<div className="absolute flex flex-row left-1/2 -translate-1/2 bottom-4 z-2 p-1 rounded-full bg-fd-card border shadow-xl dark:shadow-fd-background">
<div
role="none"
className="absolute bg-fd-primary rounded-full w-48 h-9 transition-transform z-[-1]"
style={{
transform: `translateX(calc(var(--spacing) * 48 * ${active}))`,
}}
/>
<div className="space-x-2">
{previews.map((item, i) => (
<Button
key={i}
className={`${cn(previewButtonVariants({ active: active === i }))} h-full`}
onClick={() => setActive(i)}
>
{item.name}
</Button>
))}
</div>
</div>
{previews.map((item, i) => (
<Image
key={i}
src={item.image}
alt="preview"
priority
className={cn(
"rounded-xl w-full select-none duration-1000 animate-in fade-in md:-mb-60 slide-in-from-bottom-12 lg:-mb-0",
active !== i && "hidden",
)}
/>
))}
</div>
);
}
function Why() {
return (
<div className="relative overflow-hidden border-x border-t p-2">
<WhyInteractive
codeblockInstall={
<CodeBlock lang="bash">
<Pre className="text-wrap pl-4">./ckan-devstaller</Pre>
</CodeBlock>
}
codeblockUninstall={
<CodeBlock lang="bash">
<Pre className="text-wrap pl-4">./ckan-devstaller uninstall</Pre>
</CodeBlock>
}
/>
</div>
);
}
function WhyInteractive(props: {
codeblockInstall: ReactNode;
codeblockUninstall: ReactNode;
}) {
const [active, setActive] = useState(0);
const items = [
[
<ZapIcon className="w-4 h-4 inline-block" key={0} />,
"Install CKAN within minutes",
],
[
<BlocksIcon className="w-4 h-4 inline-block" key={1} />,
"Customize your installation",
],
[
<TerminalIcon className="w-4 h-4 inline-block" key={2} />,
"Designed for developers",
],
[
<Trash2Icon className="w-4 h-4 inline-block" key={3} />,
"Uninstall with ease",
],
];
return (
<div
id="why-interactive"
className="flex flex-col-reverse gap-3 md:flex-row md:min-h-[200px]"
>
<div className="flex flex-col">
{items.map((item, i) => (
<button
key={item[1] as string}
ref={(element) => {
if (!element || i !== active) return;
}}
type="button"
className={cn(
"transition-colors text-nowrap border border-transparent rounded-lg px-3 py-2.5 text-start text-sm text-fd-muted-foreground font-medium",
i === active
? "text-fd-primary bg-fd-primary/10 border-fd-primary/10"
: "hover:text-fd-accent-foreground/80 cursor-pointer",
)}
onClick={() => {
setActive(i);
}}
>
{item[0]} {item[1]}
</button>
))}
</div>
<style>
{`
@keyframes why-interactive-x {
from {
width: 0px;
}
to {
width: 100%;
}
}`}
</style>
<div className="flex-1 p-4 border border-fd-primary/10 bg-fd-card/40 rounded-lg shadow-lg">
{active === 0 ? (
<WhyPanel>
<h3>
<ZapIcon className="w-4 h-4 inline-block mr-1 mb-1" />
Install CKAN within minutes.
</h3>
<p>
One of the primary goals of ckan-devstaller is to ease
installation of CKAN for development. Built with Rust for speed
and streamlining installation with{" "}
<a href="https://github.com/tino097/ckan-compose/tree/ckan-devstaller">
ckan-compose
</a>
, ckan-devstaller improves installation speeds{" "}
<strong>from hours/days to just minutes</strong> depending on your
download speed.
</p>
<div className="flex gap-2">
<Link
href="/docs/builder"
className={cn(
buttonVariants({ size: "lg", className: "rounded-full" }),
)}
>
<Button>Get Started</Button>
</Link>
</div>
</WhyPanel>
) : null}
{active === 1 ? (
<WhyPanel>
<h3>
<BlocksIcon className="w-4 h-4 inline-block mr-1 mb-1" />
Customize your installation with the Builder.
</h3>
<p>
Try out the interactive web GUI for customizing your CKAN
installation. You can select:
</p>
<ul>
<li>Presets</li>
<li>CKAN version</li>
<li>Extensions</li>
<li>Features</li>
</ul>
<p>
Then you can copy the provided ckan-devstaller command to run your
selected configuration.
</p>
<div className="mt-4 flex flex-row items-center gap-1.5 not-prose">
<Link href="/docs/builder" className={cn(buttonVariants())}>
Try out the Builder
</Link>
</div>
</WhyPanel>
) : null}
{active === 2 ? (
<WhyPanel>
<h3>
<TerminalIcon className="w-4 h-4 inline-block mr-1 mb-1" />
Designed for developers.
</h3>
<p>
We've kept development use cases in mind while developing
ckan-devstaller, such as:
</p>
<ul>
<li>Trying out a new version of CKAN</li>
<li>Developing CKAN extensions and themes</li>
</ul>
<div className="flex gap-2">
<Link
href="/docs/reference/installation-architecture"
className={cn(buttonVariants(), "not-prose")}
>
View the installation architecture
</Link>
<Link
href="https://github.com/dathere/ckan-devstaller"
className={cn(buttonVariants({ variant: "ghost" }))}
>
Source code
</Link>
</div>
</WhyPanel>
) : null}
{active === 3 ? (
<WhyPanel>
<h3>
<Trash2Icon className="w-4 h-4 inline-block mr-1 mb-1" />
Uninstall CKAN with ease.
</h3>
<p>
After you've installed CKAN with ckan-devstaller, you can
uninstall CKAN with ease. This allows for quickly re-installing
CKAN for a different use case.
</p>
{props.codeblockUninstall}
<Link
href="/docs/tutorials/uninstall-ckan"
className={cn(buttonVariants(), "not-prose")}
>
Learn more about uninstalling
</Link>
</WhyPanel>
) : null}
</div>
</div>
);
}
function WhyPanel(props: HTMLProps<HTMLDivElement>) {
return (
<div
{...props}
className={cn(
"duration-700 animate-in fade-in text-sm prose",
props.className,
)}
>
{props.children}
</div>
);
}

View file

@ -1,5 +1,154 @@
@import 'tailwindcss';
@import "tailwindcss";
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import 'fumadocs-openapi/css/preset.css';
@import "fumadocs-ui/css/ocean.css";
@import "fumadocs-ui/css/preset.css";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-rainbow: rainbow var(--speed, 2s) infinite linear;
--color-color-5: var(--color-5);
--color-color-4: var(--color-4);
--color-color-3: var(--color-3);
--color-color-2: var(--color-2);
--color-color-1: var(--color-1);
@keyframes rainbow {
0% {
background-position: 0%;
}
100% {
background-position: 200%;
}
}
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--color-1: oklch(66.2% 0.225 25.9);
--color-2: oklch(60.4% 0.26 302);
--color-3: oklch(69.6% 0.165 251);
--color-4: oklch(80.2% 0.134 225);
--color-5: oklch(90.7% 0.231 133);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--color-1: oklch(66.2% 0.225 25.9);
--color-2: oklch(60.4% 0.26 302);
--color-3: oklch(69.6% 0.165 251);
--color-4: oklch(80.2% 0.134 225);
--color-5: oklch(90.7% 0.231 133);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background dark:bg-[#101022] text-foreground;
}
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}

View file

@ -30,5 +30,10 @@
"organizeImports": "on"
}
}
},
"css": {
"parser": {
"tailwindDirectives": true
}
}
}

View file

@ -1,31 +1,36 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"configVersion": 1,
"workspaces": {
"": {
"name": "docs",
"dependencies": {
"fumadocs-core": "latest",
"fumadocs-mdx": "latest",
"fumadocs-openapi": "latest",
"fumadocs-ui": "latest",
"lucide-react": "latest",
"next": "latest",
"react": "latest",
"react-dom": "latest",
"shiki": "latest",
"@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fumadocs-core": "16.2.5",
"fumadocs-mdx": "^14.1.0",
"fumadocs-openapi": "^10.1.4",
"fumadocs-ui": "16.2.5",
"lucide-react": "^0.561.0",
"next": "^16.0.10",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"shiki": "^3.20.0",
"tailwind-merge": "^3.4.0",
},
"devDependencies": {
"@biomejs/biome": "latest",
"@tailwindcss/postcss": "latest",
"@types/mdx": "latest",
"@types/node": "latest",
"@types/react": "latest",
"@types/react-dom": "latest",
"openapi-types": "latest",
"postcss": "latest",
"tailwindcss": "latest",
"typescript": "latest",
"@biomejs/biome": "^2.3.8",
"@tailwindcss/postcss": "^4.1.18",
"@types/mdx": "^2.0.13",
"@types/node": "25.0.2",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"openapi-types": "^12.1.3",
"postcss": "8.5.6",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
},
},
},
@ -50,7 +55,7 @@
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w=="],
"@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="],
@ -118,49 +123,53 @@
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.3" }, "os": "darwin", "cpu": "x64" }, "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.3", "", { "os": "linux", "cpu": "arm" }, "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.3" }, "os": "linux", "cpu": "arm" }, "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.3" }, "os": "linux", "cpu": "s390x" }, "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.4", "", { "dependencies": { "@emnapi/runtime": "^1.5.0" }, "cpu": "none" }, "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.4", "", { "os": "win32", "cpu": "x64" }, "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
@ -192,7 +201,7 @@
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.10", "", { "os": "win32", "cpu": "x64" }, "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q=="],
"@orama/orama": ["@orama/orama@3.1.16", "", {}, "sha512-scSmQBD8eANlMUOglxHrN1JdSW8tDghsPuS83otqealBiIeMukCQMOf/wc0JJjDXomqwNdEQFLXLGHrU6PGxuA=="],
"@orama/orama": ["@orama/orama@3.1.17", "", {}, "sha512-APwpZ+FTGMryo4QEeD6ti+Ei8suBkvxe8PeWdUcQHVfJDpjpt4c1dKojjNswcBmdeWSiiTYcnkKKH+yuo6727g=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
@ -370,7 +379,7 @@
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"caniuse-lite": ["caniuse-lite@1.0.30001750", "", {}, "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
@ -412,7 +421,7 @@
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
@ -478,7 +487,7 @@
"image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
@ -564,7 +573,7 @@
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
@ -682,7 +691,7 @@
"react-medium-image-zoom": ["react-medium-image-zoom@5.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-BsE+EnFVQzFIlyuuQrZ9iTwyKpKkqdFZV1ImEQN573QPqGrIUuNni7aF+sZwDcxlsuOMayCr6oO/PZR/yJnbRg=="],
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
@ -698,7 +707,7 @@
"recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="],
"regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
@ -720,7 +729,7 @@
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="],
"sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
@ -728,7 +737,7 @@
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"sharp": ["sharp@0.34.4", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="],
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"shiki": ["shiki@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/engine-javascript": "3.20.0", "@shikijs/engine-oniguruma": "3.20.0", "@shikijs/langs": "3.20.0", "@shikijs/themes": "3.20.0", "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg=="],
@ -742,9 +751,9 @@
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
"style-to-js": ["style-to-js@1.1.18", "", { "dependencies": { "style-to-object": "1.0.11" } }, "sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg=="],
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
"style-to-object": ["style-to-object@1.0.11", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow=="],
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
@ -764,13 +773,15 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
@ -782,7 +793,7 @@
"unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
@ -796,9 +807,9 @@
"xml-js": ["xml-js@1.6.11", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="],
"yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
"zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="],
"zod": ["zod@4.2.0", "", {}, "sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],

24
docs/components.json Normal file
View file

@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@magicui": "https://magicui.design/r/{name}.json"
}
}

View file

@ -0,0 +1,256 @@
"use client";
import { useCopyButton } from "fumadocs-ui/utils/use-copy-button";
import { Check, Clipboard } from "lucide-react";
import {
type ComponentProps,
createContext,
type HTMLAttributes,
type ReactNode,
type RefObject,
useContext,
useMemo,
useRef,
} from "react";
import { cn } from "../lib/cn";
import { mergeRefs } from "../lib/merge-refs";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs.unstyled";
import { buttonVariants } from "./ui/button";
export interface CodeBlockProps extends ComponentProps<"figure"> {
/**
* Icon of code block
*
* When passed as a string, it assumes the value is the HTML of icon
*/
icon?: ReactNode;
/**
* Allow to copy code with copy button
*
* @defaultValue true
*/
allowCopy?: boolean;
/**
* Keep original background color generated by Shiki or Rehype Code
*
* @defaultValue false
*/
keepBackground?: boolean;
viewportProps?: HTMLAttributes<HTMLElement>;
/**
* show line numbers
*/
"data-line-numbers"?: boolean;
/**
* @defaultValue 1
*/
"data-line-numbers-start"?: number;
Actions?: (props: { className?: string; children?: ReactNode }) => ReactNode;
}
const TabsContext = createContext<{
containerRef: RefObject<HTMLDivElement | null>;
nested: boolean;
} | null>(null);
export function Pre(props: ComponentProps<"pre">) {
return (
<pre
{...props}
className={cn("min-w-full w-max *:flex *:flex-col", props.className)}
>
{props.children}
</pre>
);
}
export function CodeBlock({
ref,
title,
allowCopy = true,
keepBackground = false,
icon,
viewportProps = {},
children,
Actions = (props) => (
<div {...props} className={cn("empty:hidden", props.className)} />
),
...props
}: CodeBlockProps) {
const inTab = useContext(TabsContext) !== null;
const areaRef = useRef<HTMLDivElement>(null);
return (
<figure
ref={ref}
dir="ltr"
{...props}
className={cn(
inTab
? "bg-fd-secondary -mx-px -mb-px last:rounded-b-xl"
: "my-4 bg-fd-card rounded-xl",
keepBackground && "bg-(--shiki-light-bg) dark:bg-(--shiki-dark-bg)",
"shiki relative border shadow-sm outline-none not-prose overflow-hidden text-sm",
props.className,
)}
>
{title ? (
<div className="flex text-fd-muted-foreground items-center gap-2 h-9.5 border-b px-4">
{typeof icon === "string" ? (
<div
className="[&_svg]:size-3.5"
dangerouslySetInnerHTML={{
__html: icon,
}}
/>
) : (
icon
)}
<figcaption className="flex-1 truncate">{title}</figcaption>
{Actions({
className: "-me-2",
children: allowCopy && <CopyButton containerRef={areaRef} />,
})}
</div>
) : (
Actions({
className:
"absolute top-2 right-2 z-2 backdrop-blur-lg rounded-lg text-fd-muted-foreground",
children: allowCopy && <CopyButton containerRef={areaRef} />,
})
)}
<div
ref={areaRef}
{...viewportProps}
className={cn(
"text-[13px] py-3.5 overflow-auto max-h-[600px] fd-scroll-container",
viewportProps.className,
)}
style={
{
// space for toolbar
"--padding-right": !title ? "calc(var(--spacing) * 8)" : undefined,
counterSet: props["data-line-numbers"]
? `line ${Number(props["data-line-numbers-start"] ?? 1) - 1}`
: undefined,
...viewportProps.style,
} as object
}
>
{children}
</div>
</figure>
);
}
function CopyButton({
className,
containerRef,
...props
}: ComponentProps<"button"> & {
containerRef: RefObject<HTMLElement | null>;
}) {
const [checked, onClick] = useCopyButton(() => {
const pre = containerRef.current?.getElementsByTagName("pre").item(0);
if (!pre) return;
const clone = pre.cloneNode(true) as HTMLElement;
clone.querySelectorAll(".nd-copy-ignore").forEach((node) => {
node.replaceWith("\n");
});
void navigator.clipboard.writeText(clone.textContent ?? "");
});
return (
<button
type="button"
data-checked={checked || undefined}
className={cn(
buttonVariants({
className:
"hover:text-fd-accent-foreground data-checked:text-fd-accent-foreground",
size: "icon-xs",
}),
className,
)}
aria-label={checked ? "Copied Text" : "Copy Text"}
onClick={onClick}
{...props}
>
{checked ? <Check /> : <Clipboard />}
</button>
);
}
export function CodeBlockTabs({ ref, ...props }: ComponentProps<typeof Tabs>) {
const containerRef = useRef<HTMLDivElement>(null);
const nested = useContext(TabsContext) !== null;
return (
<Tabs
ref={mergeRefs(containerRef, ref)}
{...props}
className={cn(
"bg-fd-card rounded-xl border",
!nested && "my-4",
props.className,
)}
>
<TabsContext.Provider
value={useMemo(
() => ({
containerRef,
nested,
}),
[nested],
)}
>
{props.children}
</TabsContext.Provider>
</Tabs>
);
}
export function CodeBlockTabsList(props: ComponentProps<typeof TabsList>) {
return (
<TabsList
{...props}
className={cn(
"flex flex-row px-2 overflow-x-auto text-fd-muted-foreground",
props.className,
)}
>
{props.children}
</TabsList>
);
}
export function CodeBlockTabsTrigger({
children,
...props
}: ComponentProps<typeof TabsTrigger>) {
return (
<TabsTrigger
{...props}
className={cn(
"relative group inline-flex text-sm font-medium text-nowrap items-center transition-colors gap-2 px-2 py-1.5 hover:text-fd-accent-foreground data-[state=active]:text-fd-primary [&_svg]:size-3.5",
props.className,
)}
>
<div className="absolute inset-x-2 bottom-0 h-px group-data-[state=active]:bg-fd-primary" />
{children}
</TabsTrigger>
);
}
// TODO: currently Vite RSC plugin has problem with `asChild` due to children is automatically wrapped in <Fragment />, maybe revisit this in future
export function CodeBlockTab(props: ComponentProps<typeof TabsContent>) {
return <TabsContent {...props} />;
}

View file

@ -0,0 +1,163 @@
"use client";
import * as Primitive from "@radix-ui/react-tabs";
import { useEffectEvent } from "fumadocs-core/utils/use-effect-event";
import {
type ComponentProps,
createContext,
useContext,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { mergeRefs } from "../lib/merge-refs";
type ChangeListener = (v: string) => void;
const listeners = new Map<string, ChangeListener[]>();
function addChangeListener(id: string, listener: ChangeListener): void {
const list = listeners.get(id) ?? [];
list.push(listener);
listeners.set(id, list);
}
function removeChangeListener(id: string, listener: ChangeListener): void {
const list = listeners.get(id) ?? [];
listeners.set(
id,
list.filter((item) => item !== listener),
);
}
export interface TabsProps extends ComponentProps<typeof Primitive.Tabs> {
/**
* Identifier for Sharing value of tabs
*/
groupId?: string;
/**
* Enable persistent
*/
persist?: boolean;
/**
* If true, updates the URL hash based on the tab's id
*/
updateAnchor?: boolean;
}
const TabsContext = createContext<{
valueToIdMap: Map<string, string>;
} | null>(null);
function useTabContext() {
const ctx = useContext(TabsContext);
if (!ctx) throw new Error("You must wrap your component in <Tabs>");
return ctx;
}
export const TabsList = Primitive.TabsList;
export const TabsTrigger = Primitive.TabsTrigger;
/**
* @internal You better not use it
*/
export function Tabs({
ref,
groupId,
persist = false,
updateAnchor = false,
defaultValue,
value: _value,
onValueChange: _onValueChange,
...props
}: TabsProps) {
const tabsRef = useRef<HTMLDivElement>(null);
const [value, setValue] =
_value === undefined
? // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled
useState(defaultValue)
: [_value, _onValueChange ?? (() => undefined)];
const onChange = useEffectEvent((v: string) => setValue(v));
const valueToIdMap = useMemo(() => new Map<string, string>(), []);
useLayoutEffect(() => {
if (!groupId) return;
const previous = persist
? localStorage.getItem(groupId)
: sessionStorage.getItem(groupId);
if (previous) onChange(previous);
addChangeListener(groupId, onChange);
return () => {
removeChangeListener(groupId, onChange);
};
}, [groupId, persist]);
useLayoutEffect(() => {
const hash = window.location.hash.slice(1);
if (!hash) return;
for (const [value, id] of valueToIdMap.entries()) {
if (id === hash) {
onChange(value);
tabsRef.current?.scrollIntoView();
break;
}
}
}, [valueToIdMap]);
return (
<Primitive.Tabs
ref={mergeRefs(ref, tabsRef)}
value={value}
onValueChange={(v: string) => {
if (updateAnchor) {
const id = valueToIdMap.get(v);
if (id) {
window.history.replaceState(null, "", `#${id}`);
}
}
if (groupId) {
listeners.get(groupId)?.forEach((item) => {
item(v);
});
if (persist) localStorage.setItem(groupId, v);
else sessionStorage.setItem(groupId, v);
} else {
setValue(v);
}
}}
{...props}
>
<TabsContext.Provider
value={useMemo(() => ({ valueToIdMap }), [valueToIdMap])}
>
{props.children}
</TabsContext.Provider>
</Primitive.Tabs>
);
}
export function TabsContent({
value,
...props
}: ComponentProps<typeof Primitive.TabsContent>) {
const { valueToIdMap } = useTabContext();
if (props.id) {
valueToIdMap.set(value, props.id);
}
return (
<Primitive.TabsContent value={value} {...props}>
{props.children}
</Primitive.TabsContent>
);
}

View file

@ -0,0 +1,62 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View file

@ -0,0 +1,60 @@
import React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const rainbowButtonVariants = cva(
cn(
"relative cursor-pointer group transition-all animate-rainbow",
"inline-flex items-center justify-center gap-2 shrink-0",
"rounded-sm outline-none focus-visible:ring-[3px] aria-invalid:border-destructive",
"text-sm font-medium whitespace-nowrap",
"disabled:pointer-events-none disabled:opacity-50",
"[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0"
),
{
variants: {
variant: {
default:
"border-0 bg-[linear-gradient(#121213,#121213),linear-gradient(#121213_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] bg-[length:200%] text-primary-foreground [background-clip:padding-box,border-box,border-box] [background-origin:border-box] [border:calc(0.125rem)_solid_transparent] before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] before:[filter:blur(0.75rem)] dark:bg-[linear-gradient(#fff,#fff),linear-gradient(#fff_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))]",
outline:
"border border-input border-b-transparent bg-[linear-gradient(#ffffff,#ffffff),linear-gradient(#ffffff_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] bg-[length:200%] text-accent-foreground [background-clip:padding-box,border-box,border-box] [background-origin:border-box] before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] before:[filter:blur(0.75rem)] dark:bg-[linear-gradient(#0a0a0a,#0a0a0a),linear-gradient(#0a0a0a_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))]",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-xl px-3 text-xs",
lg: "h-11 rounded-xl px-8",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
interface RainbowButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof rainbowButtonVariants> {
asChild?: boolean
}
const RainbowButton = React.forwardRef<HTMLButtonElement, RainbowButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(rainbowButtonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
RainbowButton.displayName = "RainbowButton"
export { RainbowButton, rainbowButtonVariants, type RainbowButtonProps }

1
docs/lib/cn.ts Normal file
View file

@ -0,0 +1 @@
export { twMerge as cn } from 'tailwind-merge';

15
docs/lib/merge-refs.ts Normal file
View file

@ -0,0 +1,15 @@
import type * as React from 'react';
export function mergeRefs<T>(
...refs: (React.Ref<T> | undefined)[]
): React.RefCallback<T> {
return (value) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
ref(value);
} else if (ref) {
ref.current = value;
}
});
};
}

6
docs/lib/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View file

@ -4,13 +4,16 @@
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev --turbo",
"dev": "next dev",
"start": "next start",
"postinstall": "fumadocs-mdx",
"lint": "biome check",
"format": "biome format --write"
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fumadocs-core": "16.2.5",
"fumadocs-mdx": "^14.1.0",
"fumadocs-openapi": "^10.1.4",
@ -19,7 +22,8 @@
"next": "^16.0.10",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"shiki": "^3.20.0"
"shiki": "^3.20.0",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@biomejs/biome": "^2.3.8",
@ -29,8 +33,9 @@
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"openapi-types": "^12.1.3",
"postcss": "^8.5.6",
"postcss": "8.5.6",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3"
}
}