diff --git a/components/globe.tsx b/components/globe.tsx
deleted file mode 100644
index bc18e2f58..000000000
--- a/components/globe.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import createGlobe, { COBEOptions } from "cobe";
-import { useCallback, useEffect, useRef } from "react";
-import { useSpring } from "react-spring";
-
-const GLOBE_CONFIG: COBEOptions = {
- width: 800,
- height: 800,
- onRender: () => {},
- devicePixelRatio: 2,
- phi: 0,
- theta: 0.3,
- dark: 0,
- diffuse: 0.4,
- mapSamples: 16000,
- mapBrightness: 1.2,
- baseColor: [1, 1, 1],
- markerColor: [251 / 255, 100 / 255, 21 / 255],
- glowColor: [1, 1, 1],
- markers: [
- { location: [14.5995, 120.9842], size: 0.03 },
- { location: [19.076, 72.8777], size: 0.1 },
- { location: [23.8103, 90.4125], size: 0.05 },
- { location: [30.0444, 31.2357], size: 0.07 },
- { location: [39.9042, 116.4074], size: 0.08 },
- { location: [-23.5505, -46.6333], size: 0.1 },
- { location: [19.4326, -99.1332], size: 0.1 },
- { location: [40.7128, -74.006], size: 0.1 },
- { location: [34.6937, 135.5022], size: 0.05 },
- { location: [41.0082, 28.9784], size: 0.06 },
- ],
-};
-
-export default function Globe({
- className,
- config = GLOBE_CONFIG,
-}: {
- className?: string;
- config?: COBEOptions;
-}) {
- let phi = 0;
- let width = 0;
- const canvasRef = useRef
(null);
- const pointerInteracting = useRef(null);
- const pointerInteractionMovement = useRef(0);
- const [{ r }, api] = useSpring(() => ({
- r: 0,
- config: {
- mass: 1,
- tension: 280,
- friction: 40,
- precision: 0.001,
- },
- }));
-
- const updatePointerInteraction = (value: any) => {
- pointerInteracting.current = value;
- canvasRef.current!.style.cursor = value ? "grabbing" : "grab";
- };
-
- const updateMovement = (clientX: any) => {
- if (pointerInteracting.current !== null) {
- const delta = clientX - pointerInteracting.current;
- pointerInteractionMovement.current = delta;
- api.start({ r: delta / 200 });
- }
- };
-
- const onRender = useCallback(
- (state: Record) => {
- if (!pointerInteracting.current) phi += 0.005;
- state.phi = phi + r.get();
- state.width = width * 2;
- state.height = width * 2;
- },
- [pointerInteracting, phi, r]
- );
-
- const onResize = () => {
- if (canvasRef.current) {
- width = canvasRef.current.offsetWidth;
- }
- };
-
- useEffect(() => {
- window.addEventListener("resize", onResize);
- onResize();
-
- const globe = createGlobe(canvasRef.current!, {
- ...config,
- width: width * 2,
- height: width * 2,
- onRender,
- });
-
- setTimeout(() => (canvasRef.current!.style.opacity = "1"));
- return () => globe.destroy();
- }, []);
-
- return (
-
-
- );
-}
diff --git a/components/grid-pattern.tsx b/components/grid-pattern.tsx
deleted file mode 100644
index 7569e4538..000000000
--- a/components/grid-pattern.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { useId } from "react";
-import { cn } from "@/lib/utils";
-
-interface GridPatternProps {
- width?: any;
- height?: any;
- x?: any;
- y?: any;
- squares?: Array<[x: number, y: number]>;
- strokeDasharray?: any;
- className?: string;
- [key: string]: any;
-}
-
-export function GridPattern({
- width = 40,
- height = 40,
- x = -1,
- y = -1,
- strokeDasharray = 0,
- squares,
- className,
- ...props
-}: GridPatternProps) {
- const id = useId();
-
- return (
-
- );
-}
-
-export default GridPattern;
diff --git a/components/highlight.tsx b/components/highlight.tsx
deleted file mode 100644
index 9965df0eb..000000000
--- a/components/highlight.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { LucideIcon } from "lucide-react";
-import type { ReactNode } from "react";
-
-export function Highlight({
- icon: Icon,
- heading,
- children,
-}: {
- icon: LucideIcon;
- heading: ReactNode;
- children: ReactNode;
-}): JSX.Element {
- return (
-
-
-
-
{heading}
-
-
{children}
-
- );
-}
diff --git a/components/icons.tsx b/components/icons.tsx
new file mode 100644
index 000000000..ae036fea5
--- /dev/null
+++ b/components/icons.tsx
@@ -0,0 +1,148 @@
+type IconProps = React.HTMLAttributes;
+
+export const Icons = {
+ logo: (props: IconProps) => (
+
+
+
+
+
+ ),
+ twitter: (props: IconProps) => (
+
+
+
+ ),
+ gitHub: (props: IconProps) => (
+
+
+
+ ),
+ radix: (props: IconProps) => (
+
+
+
+
+
+ ),
+ aria: (props: IconProps) => (
+
+
+
+ ),
+ npm: (props: IconProps) => (
+
+
+
+ ),
+ yarn: (props: IconProps) => (
+
+
+
+ ),
+ pnpm: (props: IconProps) => (
+
+
+
+ ),
+ react: (props: IconProps) => (
+
+
+
+ ),
+ tailwind: (props: IconProps) => (
+
+
+
+ ),
+ google: (props: IconProps) => (
+
+
+
+ ),
+ apple: (props: IconProps) => (
+
+
+
+ ),
+ paypal: (props: IconProps) => (
+
+
+
+ ),
+ spinner: (props: IconProps) => (
+
+
+
+ ),
+};
diff --git a/components/inline-code.tsx b/components/inline-code.tsx
deleted file mode 100644
index 80c63d488..000000000
--- a/components/inline-code.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from "react";
-import { cn } from "@/lib/utils";
-
-const InlineCode = ({
- children,
- className,
-}: {
- children: React.ReactNode;
- className?: string;
-}) => {
- return (
-
- {children}
-
- );
-};
-
-export { InlineCode };
diff --git a/components/layout/api-example.tsx b/components/layout/api-example.tsx
deleted file mode 100644
index e4cf7feba..000000000
--- a/components/layout/api-example.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from "react";
-
-import { APIExample as FumaAPIExample } from "fumadocs-openapi/ui";
-
-const APIExample: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
- return {children};
-};
-
-export { APIExample };
diff --git a/components/layout/api.tsx b/components/layout/api.tsx
deleted file mode 100644
index 8be29a0a3..000000000
--- a/components/layout/api.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from "react";
-
-import { API as FumaAPI } from "fumadocs-openapi/ui";
-import { cn } from "@/lib/utils";
-
-const API: React.FC<{ children?: React.ReactNode; className?: string }> = ({
- children,
- className,
-}) => {
- return (
- {children}
- );
-};
-
-export { API };
diff --git a/components/layout/index.ts b/components/layout/index.ts
index 9a2f6a7cf..a2cca3f6f 100644
--- a/components/layout/index.ts
+++ b/components/layout/index.ts
@@ -1,5 +1,6 @@
-export { Root } from "./root";
-export { API } from "./api";
-export { APIExample } from "./api-example";
-export { Property } from "./property";
-export { Info } from "./info";
+export { Section, SectionHeader, SectionContent } from "./section";
+export type {
+ SectionProps,
+ SectionHeaderProps,
+ SectionContentProps,
+} from "./section";
diff --git a/components/layout/info.tsx b/components/layout/info.tsx
deleted file mode 100644
index 3b3c13ee6..000000000
--- a/components/layout/info.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from "react";
-
-import { cn } from "@/lib/utils";
-
-const Info: React.FC<{ children?: React.ReactNode; className?: string }> = ({
- children,
- className,
-}) => {
- return (
-
- {children}
-
- );
-};
-
-export { Info };
diff --git a/components/layout/language-toggle.tsx b/components/layout/language-toggle.tsx
new file mode 100644
index 000000000..e3b4c819d
--- /dev/null
+++ b/components/layout/language-toggle.tsx
@@ -0,0 +1,64 @@
+"use client";
+import { type ButtonHTMLAttributes, type HTMLAttributes } from "react";
+import { useI18n } from "fumadocs-ui/contexts/i18n";
+import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
+import { cn } from "@/lib/utils";
+import { buttonVariants } from "../ui/button";
+
+export type LanguageSelectProps = ButtonHTMLAttributes;
+
+export function LanguageToggle(props: LanguageSelectProps): React.ReactElement {
+ const context = useI18n();
+ if (!context.locales) throw new Error("Missing ``");
+
+ return (
+
+
+ {props.children}
+
+
+
+ {context.text.chooseLanguage}
+
+ {context.locales.map((item) => (
+
+ ))}
+
+
+ );
+}
+
+export function LanguageToggleText(
+ props: HTMLAttributes
+): React.ReactElement {
+ const context = useI18n();
+ const text = context.locales?.find(
+ (item) => item.locale === context.locale
+ )?.name;
+
+ return {text};
+}
diff --git a/components/layout/mobile-menu-button.tsx b/components/layout/mobile-menu-button.tsx
new file mode 100644
index 000000000..193c5d889
--- /dev/null
+++ b/components/layout/mobile-menu-button.tsx
@@ -0,0 +1,32 @@
+'use client';
+
+import { Menu } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { useMobileMenu } from '@/contexts/mobile-menu';
+import { MobileNavigation } from './mobile-navigation';
+import type { PageTree } from 'fumadocs-core/server';
+
+interface MobileMenuButtonProps {
+ tree?: PageTree.Root;
+}
+
+export function MobileMenuButton({ tree }: MobileMenuButtonProps) {
+ const { open, isOpen, close } = useMobileMenu();
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/components/layout/mobile-navigation.tsx b/components/layout/mobile-navigation.tsx
new file mode 100644
index 000000000..c0fee0ccd
--- /dev/null
+++ b/components/layout/mobile-navigation.tsx
@@ -0,0 +1,336 @@
+"use client";
+
+import { X, ChevronRight } from "lucide-react";
+import type { PageTree } from "fumadocs-core/server";
+import { SearchToggle } from "../layout/search-toggle";
+import { TreeContextProvider } from "fumadocs-ui/contexts/tree";
+import { baseOptions } from "@/app/layout.config";
+import Link from "next/link";
+import { useState, useMemo } from "react";
+import { usePathname } from "next/navigation";
+import React from "react";
+import { Sidebar } from "@/components/layouts/docs";
+import {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb";
+import { DocsLogo } from "@/components/ui/icon";
+import { cn } from "@/lib/utils";
+
+interface MobileNavigationProps {
+ isOpen?: boolean;
+ onClose?: () => void;
+ tree?: PageTree.Root;
+}
+
+export function MobileNavigation({
+ isOpen = false,
+ onClose,
+ tree,
+}: MobileNavigationProps) {
+ const [activeSubmenu, setActiveSubmenu] = useState(null);
+ const [showMainMenu, setShowMainMenu] = useState(false);
+ const pathname = usePathname();
+
+ const isInDocsContext = !!tree;
+
+ if (!isOpen) return null;
+
+ const handleClose = () => {
+ setActiveSubmenu(null);
+ setShowMainMenu(false);
+ onClose?.();
+ };
+
+ const getMobileBreadcrumb = () => {
+ if (!tree || !pathname) return Navigation;
+
+ const segments = pathname.split("/").filter(Boolean);
+ if (segments.length === 0) return Navigation;
+
+ const displaySegments = [...segments];
+ let firstSegmentMenu = null;
+
+ if (segments[0] === "reference") {
+ displaySegments[0] = "Libraries & SDKs";
+ firstSegmentMenu = "Libraries & SDKs";
+ } else {
+ // Check if first segment matches a menu item
+ const firstSegment = segments[0];
+ const menuItem = baseOptions.links?.find(
+ (link) =>
+ link.type === "menu" &&
+ typeof link.text === "string" &&
+ link.text.toLowerCase() === firstSegment.toLowerCase()
+ );
+ if (menuItem && "text" in menuItem && typeof menuItem.text === "string") {
+ firstSegmentMenu = menuItem.text;
+ displaySegments[0] = menuItem.text; // Use the properly cased menu text
+ }
+ }
+
+ // FIXME: Limit to 2 levels deep (e.g., Tools/Clarinet, not Tools/Clarinet/Quickstart)
+ const maxDepth = 2;
+ const limitedSegments = displaySegments.slice(0, maxDepth);
+
+ // FIXME: Special formatting
+ const formattedSegments = limitedSegments.map((segment, index) => {
+ if (index === 0 && (segment === "Libraries & SDKs" || firstSegmentMenu)) {
+ return segment;
+ }
+
+ if (segment.toLowerCase() === "stacks.js") {
+ return "Stacks.js";
+ }
+
+ if (index === 1 && displaySegments[0].toLowerCase() === "apis") {
+ const apiMappings: { [key: string]: string } = {
+ "stacks-blockchain-api": "Stacks Blockchain API",
+ "token-metadata-api": "Token Metadata API",
+ "platform-api": "Platform API",
+ "ordinals-api": "Ordinals API",
+ "runes-api": "Runes API",
+ "signer-metrics-api": "Signer Metrics API",
+ };
+
+ if (apiMappings[segment.toLowerCase()]) {
+ return apiMappings[segment.toLowerCase()];
+ }
+ }
+
+ if (index === 1 && displaySegments[0] === "Tools") {
+ const toolMappings: { [key: string]: string } = {
+ "bitcoin-indexer": "Bitcoin Indexer",
+ "contract-monitoring": "Contract Monitoring",
+ };
+
+ if (toolMappings[segment.toLowerCase()]) {
+ return toolMappings[segment.toLowerCase()];
+ }
+ }
+
+ return segment.charAt(0).toUpperCase() + segment.slice(1);
+ });
+
+ return (
+
+
+
+
+
+
+
+
+ {formattedSegments.map((segment, index) => {
+ const isLast = index === formattedSegments.length - 1;
+ const displayName = segment;
+
+ const handleClick = () => {
+ if (index === 0 && firstSegmentMenu) {
+ setActiveSubmenu(firstSegmentMenu);
+ } else {
+ handleClose();
+ }
+ };
+
+ return (
+
+
+ /
+
+
+ {isLast ? (
+
+ {displayName}
+
+ ) : (
+
+
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+ };
+
+ return (
+
+
e.key === "Escape" && handleClose()}
+ tabIndex={-1}
+ />
+
+
+
+
+
+ {activeSubmenu ? (
+
+
+
+
+
+
+
+
+ /
+
+
+
+ {activeSubmenu}
+
+
+
+
+ ) : isInDocsContext && !showMainMenu ? (
+ getMobileBreadcrumb()
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/layout/nav.tsx b/components/layout/nav.tsx
new file mode 100644
index 000000000..182f7c2ec
--- /dev/null
+++ b/components/layout/nav.tsx
@@ -0,0 +1,93 @@
+"use client";
+import Link, { type LinkProps } from "fumadocs-core/link";
+import {
+ createContext,
+ type ReactNode,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
+import { cn } from "../../lib/utils";
+import { useI18n } from "fumadocs-ui/provider";
+
+export interface NavProviderProps {
+ /**
+ * Use transparent background
+ *
+ * @defaultValue none
+ */
+ transparentMode?: "always" | "top" | "none";
+}
+
+export interface TitleProps {
+ title?: ReactNode;
+
+ /**
+ * Redirect url of title
+ * @defaultValue '/'
+ */
+ url?: string;
+}
+
+interface NavContextType {
+ isTransparent: boolean;
+}
+
+const NavContext = createContext
({
+ isTransparent: false,
+});
+
+export function NavProvider({
+ transparentMode = "none",
+ children,
+}: NavProviderProps & { children: ReactNode }) {
+ const [transparent, setTransparent] = useState(transparentMode !== "none");
+
+ useEffect(() => {
+ if (transparentMode !== "top") return;
+
+ const listener = () => {
+ setTransparent(window.scrollY < 10);
+ };
+
+ listener();
+ window.addEventListener("scroll", listener);
+ return () => {
+ window.removeEventListener("scroll", listener);
+ };
+ }, [transparentMode]);
+
+ return (
+ ({ isTransparent: transparent }), [transparent])}
+ >
+ {children}
+
+ );
+}
+
+export function useNav(): NavContextType {
+ return useContext(NavContext);
+}
+
+export function Title({
+ title,
+ url,
+ ...props
+}: TitleProps & Omit) {
+ const { locale } = useI18n();
+
+ return (
+
+ {title}
+
+ );
+}
diff --git a/components/layout/property.tsx b/components/layout/property.tsx
deleted file mode 100644
index fd3cf2ab1..000000000
--- a/components/layout/property.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from "react";
-
-import { Property as FumaProperty } from "fumadocs-openapi/ui";
-
-const Property: React.FC<{
- children?: React.ReactNode;
- required: boolean;
- deprecated?: boolean;
- name: string;
- type: string;
-}> = (props) => {
- return (
-
- {props.children}
-
- );
-};
-
-export { Property };
diff --git a/components/layout/root-toggle.tsx b/components/layout/root-toggle.tsx
new file mode 100644
index 000000000..242f69c6a
--- /dev/null
+++ b/components/layout/root-toggle.tsx
@@ -0,0 +1,109 @@
+"use client";
+import { ChevronsUpDown } from "lucide-react";
+import { type HTMLAttributes, type ReactNode, useMemo, useState } from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { cn } from "@/lib/utils";
+import { isActive } from "@/lib/is-active";
+import { useSidebar } from "fumadocs-ui/provider";
+import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
+
+export interface Option {
+ /**
+ * Redirect URL of the folder, usually the index page
+ */
+ url: string;
+
+ icon?: ReactNode;
+ title: ReactNode;
+ description?: ReactNode;
+
+ /**
+ * Detect from a list of urls
+ */
+ urls?: Set;
+
+ props?: HTMLAttributes;
+}
+
+export function RootToggle({
+ options,
+ placeholder,
+ ...props
+}: {
+ placeholder?: ReactNode;
+ options: Option[];
+} & HTMLAttributes) {
+ const [open, setOpen] = useState(false);
+ const { closeOnRedirect } = useSidebar();
+ const pathname = usePathname();
+
+ const selected = useMemo(() => {
+ return options.findLast((item) =>
+ item.urls
+ ? item.urls.has(
+ pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
+ )
+ : isActive(item.url, pathname, true)
+ );
+ }, [options, pathname]);
+
+ const onClick = () => {
+ closeOnRedirect.current = false;
+ setOpen(false);
+ };
+
+ const item = selected ? : placeholder;
+
+ return (
+
+ {item ? (
+
+ {item}
+
+
+ ) : null}
+
+ {options.map((item) => (
+
+
+
+ ))}
+
+
+ );
+}
+
+function Item(props: Option) {
+ return (
+ <>
+ {props.icon}
+
+
{props.title}
+ {props.description ? (
+
+ {props.description}
+
+ ) : null}
+
+ >
+ );
+}
diff --git a/components/layout/root.tsx b/components/layout/root.tsx
deleted file mode 100644
index fa39ed35e..000000000
--- a/components/layout/root.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from "react";
-
-import { Root as FumaRoot } from "fumadocs-openapi/ui";
-
-const Root: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
- return {children};
-};
-
-export { Root };
diff --git a/components/layout/search-toggle.tsx b/components/layout/search-toggle.tsx
new file mode 100644
index 000000000..7f3341b9e
--- /dev/null
+++ b/components/layout/search-toggle.tsx
@@ -0,0 +1,48 @@
+"use client";
+import type { ComponentProps } from "react";
+import { Search, SearchIcon } from "lucide-react";
+import { useSearchContext } from "fumadocs-ui/contexts/search";
+import { useI18n } from "fumadocs-ui/contexts/i18n";
+import { cn } from "@/lib/utils";
+import { Kbd } from "../ui/kbd";
+
+export function SearchToggle(props: ComponentProps<"button">) {
+ const { enabled, setOpenSearch } = useSearchContext();
+ if (!enabled) return;
+
+ return (
+ <>
+ {/* For mobile, show the search icon */}
+
+
+ {/* For desktop, show the search bar */}
+
+ >
+ );
+}
diff --git a/components/layout/section-icons.tsx b/components/layout/section-icons.tsx
new file mode 100644
index 000000000..d6c4880c9
--- /dev/null
+++ b/components/layout/section-icons.tsx
@@ -0,0 +1,52 @@
+type IconProps = React.HTMLAttributes;
+
+export const DeveloperToolsIcon = (props: IconProps) => (
+
+
+
+);
+
+export const ApiServicesIcon = (props: IconProps) => (
+
+
+
+);
+
+export const BitcoinInnovationIcon = (props: IconProps) => (
+
+
+
+);
diff --git a/components/layout/section.tsx b/components/layout/section.tsx
new file mode 100644
index 000000000..fd275e0f3
--- /dev/null
+++ b/components/layout/section.tsx
@@ -0,0 +1,140 @@
+import type { HTMLAttributes, ReactNode } from "react";
+import { cn } from "@/lib/utils";
+
+type SectionLayout = "grid" | "flex" | "stack";
+type SectionVariant = "bordered" | "clean" | "hero";
+
+export interface SectionProps extends HTMLAttributes {
+ layout?: SectionLayout;
+ variant?: SectionVariant;
+ columns?: number;
+ gap?: number;
+ padding?: "none" | "normal" | "large";
+}
+
+export function Section({
+ layout = "grid",
+ variant = "clean",
+ columns = 12,
+ gap = 6,
+ padding = "normal",
+ className,
+ children,
+ ...props
+}: SectionProps): React.ReactElement {
+ const layoutClasses = {
+ grid: "flex flex-col lg:grid lg:grid-cols-12 items-start w-full",
+ flex: "flex flex-col w-full",
+ stack: "space-y-6 w-full",
+ };
+
+ const variantClasses = {
+ bordered: "border-b",
+ clean: "",
+ hero: "border-b bg-muted/30",
+ };
+
+ const paddingClasses = {
+ none: "",
+ normal: "py-12",
+ large: "py-16",
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export interface SectionHeaderProps extends HTMLAttributes {
+ span?: number;
+ icon?: ReactNode;
+ title: string;
+ description?: string;
+ color?: "purple" | "blue" | "orange" | "green" | "gray";
+}
+
+export function SectionHeader({
+ span = 4,
+ icon,
+ title,
+ description,
+ color = "purple",
+ className,
+ ...props
+}: SectionHeaderProps): React.ReactElement {
+ const colorClasses = {
+ purple:
+ "bg-purple-100 dark:bg-purple-900 text-purple-600 dark:text-purple-400",
+ blue: "bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400",
+ orange:
+ "bg-orange-100 dark:bg-orange-900 text-orange-600 dark:text-orange-400",
+ green: "bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-400",
+ gray: "bg-gray-100 dark:bg-gray-900 text-gray-600 dark:text-gray-400",
+ };
+
+ return (
+
+
+
+ {icon && (
+
+ {icon}
+
+ )}
+
{title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ );
+}
+
+export interface SectionContentProps extends HTMLAttributes {
+ span?: number;
+ gridColumns?: number;
+}
+
+export function SectionContent({
+ span = 8,
+ gridColumns = 12,
+ className,
+ children,
+ ...props
+}: SectionContentProps): React.ReactElement {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components/layout/theme-toggle.tsx b/components/layout/theme-toggle.tsx
new file mode 100644
index 000000000..0d94f2113
--- /dev/null
+++ b/components/layout/theme-toggle.tsx
@@ -0,0 +1,55 @@
+"use client";
+
+import { cva } from "class-variance-authority";
+import { Moon, Sun, Airplay } from "lucide-react";
+import { useTheme } from "next-themes";
+import { type HTMLAttributes, useLayoutEffect, useState } from "react";
+import { cn } from "../../lib/utils";
+
+const itemVariants = cva("rounded p-1.5 text-fd-muted-foreground", {
+ variants: {
+ active: {
+ true: "bg-white dark:bg-neutral-900 text-primary",
+ false: "text-muted-foreground dark:text-neutral-300 cursor-pointer",
+ },
+ },
+});
+
+const full = [["light", Sun] as const, ["dark", Moon] as const];
+
+export function ThemeToggle({
+ className,
+ mode = "light-dark",
+ ...props
+}: HTMLAttributes & {
+ mode?: "light-dark";
+}) {
+ const { setTheme, resolvedTheme } = useTheme();
+ const [mounted, setMounted] = useState(false);
+
+ useLayoutEffect(() => {
+ setMounted(true);
+ }, []);
+
+ const container = cn(
+ "bg-neutral-150 dark:bg-neutral-700 inline-flex items-center rounded p-1",
+ className
+ );
+
+ const value = mounted ? resolvedTheme : null;
+
+ return (
+
+ {full.map(([key, Icon]) => (
+
+ ))}
+
+ );
+}
diff --git a/components/layout/toc-clerk.tsx b/components/layout/toc-clerk.tsx
new file mode 100644
index 000000000..2b8797dcc
--- /dev/null
+++ b/components/layout/toc-clerk.tsx
@@ -0,0 +1,162 @@
+"use client";
+import type { TOCItemType } from "fumadocs-core/server";
+import * as Primitive from "fumadocs-core/toc";
+import { useEffect, useRef, useState } from "react";
+import { cn } from "../../lib/utils";
+import { TocThumb } from "./toc-thumb";
+import { TocItemsEmpty } from "./toc";
+
+export default function ClerkTOCItems({ items }: { items: TOCItemType[] }) {
+ const containerRef = useRef(null);
+
+ const [svg, setSvg] = useState<{
+ path: string;
+ width: number;
+ height: number;
+ }>();
+
+ useEffect(() => {
+ if (!containerRef.current) return;
+ const container = containerRef.current;
+
+ function onResize(): void {
+ if (container.clientHeight === 0) return;
+ let w = 0,
+ h = 0;
+ const d: string[] = [];
+ for (let i = 0; i < items.length; i++) {
+ const element: HTMLElement | null = container.querySelector(
+ `a[href="#${items[i].url.slice(1)}"]`
+ );
+ if (!element) continue;
+
+ const styles = getComputedStyle(element);
+ const offset = getLineOffset(items[i].depth) + 1,
+ top = element.offsetTop + parseFloat(styles.paddingTop),
+ bottom =
+ element.offsetTop +
+ element.clientHeight -
+ parseFloat(styles.paddingBottom);
+
+ w = Math.max(offset, w);
+ h = Math.max(h, bottom);
+
+ d.push(`${i === 0 ? "M" : "L"}${offset} ${top}`);
+ d.push(`L${offset} ${bottom}`);
+ }
+
+ setSvg({
+ path: d.join(" "),
+ width: w + 1,
+ height: h,
+ });
+ }
+
+ const observer = new ResizeObserver(onResize);
+ onResize();
+
+ observer.observe(container);
+ return () => {
+ observer.disconnect();
+ };
+ }, [items]);
+
+ if (items.length === 0) return ;
+
+ return (
+ <>
+ {svg ? (
+ `
+ )
+ }")`,
+ }}
+ >
+
+
+ ) : null}
+
+ {items.map((item, i) => (
+
+ ))}
+
+ >
+ );
+}
+
+function getItemOffset(depth: number): number {
+ if (depth <= 2) return 14;
+ if (depth === 3) return 26;
+ return 36;
+}
+
+function getLineOffset(depth: number): number {
+ return depth >= 3 ? 10 : 0;
+}
+
+function TOCItem({
+ item,
+ upper = item.depth,
+ lower = item.depth,
+}: {
+ item: TOCItemType;
+ upper?: number;
+ lower?: number;
+}) {
+ const offset = getLineOffset(item.depth),
+ upperOffset = getLineOffset(upper),
+ lowerOffset = getLineOffset(lower);
+
+ return (
+
+ {offset !== upperOffset ? (
+
+
+
+ ) : null}
+
+ {item.title}
+
+ );
+}
diff --git a/components/layout/toc-thumb.tsx b/components/layout/toc-thumb.tsx
new file mode 100644
index 000000000..bdced31ad
--- /dev/null
+++ b/components/layout/toc-thumb.tsx
@@ -0,0 +1,73 @@
+import { type HTMLAttributes, type RefObject, useEffect, useRef } from 'react';
+import * as Primitive from 'fumadocs-core/toc';
+import { useOnChange } from 'fumadocs-core/utils/use-on-change';
+import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event';
+
+export type TOCThumb = [top: number, height: number];
+
+function calc(container: HTMLElement, active: string[]): TOCThumb {
+ if (active.length === 0 || container.clientHeight === 0) {
+ return [0, 0];
+ }
+
+ let upper = Number.MAX_VALUE,
+ lower = 0;
+
+ for (const item of active) {
+ const element = container.querySelector(`a[href="#${item}"]`);
+ if (!element) continue;
+
+ const styles = getComputedStyle(element);
+ upper = Math.min(upper, element.offsetTop + parseFloat(styles.paddingTop));
+ lower = Math.max(
+ lower,
+ element.offsetTop +
+ element.clientHeight -
+ parseFloat(styles.paddingBottom),
+ );
+ }
+
+ return [upper, lower - upper];
+}
+
+function update(element: HTMLElement, info: TOCThumb): void {
+ element.style.setProperty('--fd-top', `${info[0]}px`);
+ element.style.setProperty('--fd-height', `${info[1]}px`);
+}
+
+export function TocThumb({
+ containerRef,
+ ...props
+}: HTMLAttributes & {
+ containerRef: RefObject;
+}) {
+ const active = Primitive.useActiveAnchors();
+ const thumbRef = useRef(null);
+
+ const onResize = useEffectEvent(() => {
+ if (!containerRef.current || !thumbRef.current) return;
+
+ update(thumbRef.current, calc(containerRef.current, active));
+ });
+
+ useEffect(() => {
+ if (!containerRef.current) return;
+ const container = containerRef.current;
+
+ onResize();
+ const observer = new ResizeObserver(onResize);
+ observer.observe(container);
+
+ return () => {
+ observer.disconnect();
+ };
+ }, [containerRef, onResize]);
+
+ useOnChange(active, () => {
+ if (!containerRef.current || !thumbRef.current) return;
+
+ update(thumbRef.current, calc(containerRef.current, active));
+ });
+
+ return ;
+}
diff --git a/components/layout/toc.tsx b/components/layout/toc.tsx
new file mode 100644
index 000000000..b34d1173e
--- /dev/null
+++ b/components/layout/toc.tsx
@@ -0,0 +1,227 @@
+"use client";
+import type { TOCItemType } from "fumadocs-core/server";
+import * as Primitive from "fumadocs-core/toc";
+import {
+ type ComponentProps,
+ createContext,
+ type HTMLAttributes,
+ type ReactNode,
+ use,
+ useMemo,
+ useRef,
+} from "react";
+import { cn } from "../../lib/utils";
+import { useI18n } from "fumadocs-ui/provider";
+import { TocThumb } from "./toc-thumb";
+import { ScrollArea, ScrollViewport } from "../ui/scroll-area";
+import type {
+ PopoverContentProps,
+ PopoverTriggerProps,
+} from "@radix-ui/react-popover";
+import { ChevronRight, Text } from "lucide-react";
+import { usePageStyles } from "fumadocs-ui/provider";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "../ui/collapsible";
+
+export interface TOCProps {
+ /**
+ * Custom content in TOC container, before the main TOC
+ */
+ header?: ReactNode;
+
+ /**
+ * Custom content in TOC container, after the main TOC
+ */
+ footer?: ReactNode;
+
+ children: ReactNode;
+}
+
+export function Toc(props: HTMLAttributes) {
+ const { toc } = usePageStyles();
+
+ return (
+
+ );
+}
+
+export function TocItemsEmpty() {
+ const { text } = useI18n();
+
+ return (
+
+ {text.tocNoHeadings}
+
+ );
+}
+
+export function TOCScrollArea({
+ isMenu,
+ ...props
+}: ComponentProps & { isMenu?: boolean }) {
+ const viewRef = useRef(null);
+
+ return (
+
+
+
+ {props.children}
+
+
+
+ );
+}
+
+export function TOCItems({ items }: { items: TOCItemType[] }) {
+ const containerRef = useRef(null);
+
+ if (items.length === 0) return ;
+
+ return (
+ <>
+
+
+ {items.map((item) => (
+
+ ))}
+
+ >
+ );
+}
+
+function TOCItem({ item }: { item: TOCItemType }) {
+ return (
+ = 4 && "ps-8"
+ )}
+ >
+ {item.title}
+
+ );
+}
+
+type MakeRequired = T & { [P in K]-?: T[P] };
+
+const Context = createContext<{
+ open: boolean;
+ setOpen: (open: boolean) => void;
+} | null>(null);
+
+const TocProvider = Context.Provider || Context;
+
+export function TocPopover({
+ open,
+ onOpenChange,
+ ref: _ref,
+ ...props
+}: MakeRequired, "open" | "onOpenChange">) {
+ return (
+
+ ({
+ open,
+ setOpen: onOpenChange,
+ }),
+ [onOpenChange, open]
+ )}
+ >
+ {props.children}
+
+
+ );
+}
+
+export function TocPopoverTrigger({
+ items,
+ ...props
+}: PopoverTriggerProps & { items: TOCItemType[] }) {
+ const { text } = useI18n();
+ const { open } = use(Context)!;
+ const active = Primitive.useActiveAnchor();
+ const current = useMemo(() => {
+ return items.find((item) => active === item.url.slice(1))?.title;
+ }, [items, active]);
+
+ return (
+
+
+ {text.toc}
+
+
+ {current}
+
+
+ );
+}
+
+export function TocPopoverContent(props: PopoverContentProps) {
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/components/layouts/docs.tsx b/components/layouts/docs.tsx
new file mode 100644
index 000000000..a6a6dc257
--- /dev/null
+++ b/components/layouts/docs.tsx
@@ -0,0 +1,461 @@
+"use client";
+
+import React, { ButtonHTMLAttributes } from "react";
+import type { PageTree } from "fumadocs-core/server";
+import { type ReactNode, useMemo } from "react";
+import { cn } from "@/lib/utils";
+import { TreeContextProvider, useTreeContext } from "fumadocs-ui/contexts/tree";
+import Link from "fumadocs-core/link";
+import { useSidebar } from "fumadocs-ui/contexts/sidebar";
+import { cva } from "class-variance-authority";
+import { usePathname } from "fumadocs-core/framework";
+import { Button } from "../ui/button";
+import { ThemeToggle } from "../layout/theme-toggle";
+import {
+ ArrowUpRight,
+ SidebarIcon,
+ ChevronRight,
+ ChevronDown,
+} from "lucide-react";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "../ui/accordion";
+import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
+import { DocsLogo } from "../ui/icon";
+import { SearchToggle } from "../layout/search-toggle";
+import { NavigationMenu, NavigationMenuList } from "../ui/navigation-menu";
+import { renderNavItem } from "./links";
+import { baseOptions } from "@/app/layout.config";
+import { MobileMenuButton } from "../layout/mobile-menu-button";
+import { MobileMenuProvider } from "@/contexts/mobile-menu";
+
+export interface DocsLayoutProps {
+ tree: PageTree.Root;
+ children: ReactNode;
+}
+
+export function DocsLayout({ tree, children }: DocsLayoutProps) {
+ const [isScrolled, setIsScrolled] = React.useState(false);
+ const { registerShortcut } = useKeyboardShortcuts();
+ const { collapsed } = useSidebar();
+
+ React.useEffect(() => {
+ const handleScroll = () => {
+ setIsScrolled(window.scrollY > 45);
+ };
+
+ window.addEventListener("scroll", handleScroll);
+ handleScroll();
+
+ return () => window.removeEventListener("scroll", handleScroll);
+ }, []);
+
+ React.useEffect(() => {
+ // register 'p' shortcut for platform navigation
+ const platformShortcut = registerShortcut({
+ key: "p",
+ callback: () => {
+ window.open(
+ "https://platform.hiro.so",
+ "_blank",
+ "noopener,noreferrer"
+ );
+ },
+ preventDefault: true,
+ });
+
+ // register 't' shortcut for calendar scheduling
+ const calendarShortcut = registerShortcut({
+ key: "t",
+ callback: () => {
+ window.open(
+ "https://cal.com/waits/15min",
+ "_blank",
+ "noopener,noreferrer"
+ );
+ },
+ preventDefault: true,
+ });
+
+ return () => {
+ platformShortcut();
+ calendarShortcut();
+ };
+ }, [registerShortcut]);
+
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
+
+export function Sidebar() {
+ const { root } = useTreeContext();
+ const { open, collapsed } = useSidebar();
+ const pathname = usePathname();
+
+ const children = useMemo(() => {
+ const filterCriteria = ["tools", "apis", "reference", "resources"];
+
+ const shouldFilterItem = (item: PageTree.Node): boolean => {
+ const isCurrentSection = filterCriteria.some(
+ (criteria) =>
+ pathname?.includes(`/${criteria}/`) || pathname === `/${criteria}`
+ );
+
+ if (isCurrentSection) {
+ const currentSectionFilter = filterCriteria.find(
+ (criteria) =>
+ pathname?.includes(`/${criteria}/`) || pathname === `/${criteria}`
+ );
+ return (
+ !item.$id?.includes(currentSectionFilter ?? "") &&
+ filterCriteria.some((criteria) => {
+ // Check if item.$id matches the exact criteria as a path segment
+ const itemPath = item.$id || "";
+ return (
+ itemPath === criteria ||
+ itemPath.startsWith(`${criteria}/`) ||
+ itemPath.includes(`/${criteria}/`) ||
+ itemPath.endsWith(`/${criteria}`)
+ );
+ })
+ );
+ }
+
+ return filterCriteria.some((criteria) => {
+ // Check if item.$id matches the exact criteria as a path segment
+ const itemPath = item.$id || "";
+ return (
+ itemPath === criteria ||
+ itemPath.startsWith(`${criteria}/`) ||
+ itemPath.includes(`/${criteria}/`) ||
+ itemPath.endsWith(`/${criteria}`)
+ );
+ });
+ };
+
+ function renderItems(items: PageTree.Node[]) {
+ const filteredItems = items.filter((item) => !shouldFilterItem(item));
+
+ return filteredItems.map((item) => (
+
+ {item.type === "folder" ? renderItems(item.children) : null}
+
+ ));
+ }
+
+ return renderItems(root.children);
+ }, [root, pathname]);
+
+ return (
+
+ );
+}
+
+export const linkVariants = cva(
+ "flex items-center gap-3 w-full py-1.5 px-2 rounded-lg text-muted-foreground !font-sans [&_svg]:size-3",
+ {
+ variants: {
+ active: {
+ true: "text-primary bg-neutral-150 dark:bg-neutral-700 font-medium",
+ false: "hover:text-muted-foreground hover:bg-card",
+ },
+ },
+ }
+);
+
+export function NavbarSidebarTrigger(
+ props: ButtonHTMLAttributes
+) {
+ const { collapsed, setCollapsed } = useSidebar();
+
+ return (
+
+ );
+}
+
+export function SidebarItem({
+ item,
+ children,
+}: {
+ item: PageTree.Node;
+ children: ReactNode;
+}) {
+ const pathname = usePathname();
+
+ const isPathInFolder = (
+ folderItem: PageTree.Node,
+ currentPath: string
+ ): boolean => {
+ if (folderItem.type !== "folder") return false;
+
+ if (folderItem.index?.url === currentPath) return true;
+ const checkChildren = (children: PageTree.Node[]): boolean => {
+ return children.some((child) => {
+ if (child.type === "page" && child.url === currentPath) return true;
+ if (child.type === "folder") {
+ if (child.index?.url === currentPath) return true;
+ return checkChildren(child.children);
+ }
+ return false;
+ });
+ };
+
+ return checkChildren(folderItem.children);
+ };
+
+ const shouldExpand =
+ item.type === "folder" &&
+ (isPathInFolder(item, pathname) || (item as any).defaultOpen === true);
+
+ const [isOpen, setIsOpen] = React.useState(shouldExpand);
+
+ if (item.type === "page") {
+ const sidebarTitle = (item as any).data?.sidebarTitle;
+ const displayName = sidebarTitle || item.name;
+ const isRootPage = (item as any).data?.root === true;
+
+ return (
+
+
+ {item.icon}
+ {displayName}
+
+
+
+ );
+ }
+
+ if (item.type === "separator") {
+ return (
+
+ {item.name}
+
+ );
+ }
+
+ const getStringValue = (value: any): string => {
+ if (typeof value === "string") return value;
+ if (typeof value === "number") return value.toString();
+ return "folder";
+ };
+
+ const accordionValue =
+ getStringValue(item.$id) || getStringValue(item.name) || "folder";
+
+ return (
+
+
setIsOpen(value === accordionValue)}
+ >
+
+
+
+
+ {item.index ? (
+ e.stopPropagation()}
+ className={cn(
+ "flex items-center gap-2 font-sans hover:no-underline",
+ pathname === item.index.url
+ ? "font-normal text-primary"
+ : "font-normal text-muted-foreground"
+ )}
+ >
+ {item.index.icon}
+ {item.index.name}
+
+ ) : (
+ <>
+ {item.icon}
+ {item.name}
+ >
+ )}
+
+
+ {item.index &&
}
+ {isOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export function PageBadges({ item }: { item: PageTree.Node }) {
+ if (item.type !== "page") return null;
+
+ const badges: React.ReactNode[] = [];
+
+ const isNew = (item as any).data?.isNew;
+
+ if (isNew) {
+ badges.push(
+
+ New
+
+ );
+ }
+
+ const openapi = (item as any).data?.openapi;
+ const operations = openapi?.operations || [];
+
+ const methods = new Set(operations.map((op: any) => op.method.toUpperCase()));
+
+ for (const method of methods) {
+ const colors = {
+ GET: "bg-[#e7f7e7] text-[#4B714D] border-[#c2ebc4] dark:bg-background dark:text-[#c2ebc4] dark:border-[#c2ebc4]",
+ POST: "bg-[#e7f0ff] text-[#4B5F8A] border-[#c2d9ff] dark:bg-background dark:text-[#c2d9ff] dark:border-[#c2d9ff]",
+ PUT: "bg-[#fff4e7] text-[#8A6B4B] border-[#ffd9c2] dark:bg-background dark:text-[#ffd9c2] dark:border-[#ffd9c2]",
+ PATCH:
+ "bg-[#fffce7] text-[#8A864B] border-[#fff9c2] dark:bg-background dark:text-[#fff9c2] dark:border-[#fff9c2]",
+ DELETE:
+ "bg-[#ffe7e7] text-[#8A4B4B] border-[#ffc2c2] dark:bg-background dark:text-[#ffc2c2] dark:border-[#ffc2c2]",
+ };
+
+ badges.push(
+
+ {String(method)}
+
+ );
+ }
+
+ if (badges.length === 0) return null;
+
+ return {badges}
;
+}
diff --git a/components/layouts/home.tsx b/components/layouts/home.tsx
new file mode 100644
index 000000000..b45b19f6e
--- /dev/null
+++ b/components/layouts/home.tsx
@@ -0,0 +1,128 @@
+"use client";
+import React, { type HTMLAttributes, useMemo } from "react";
+import { type NavOptions, slot } from "./shared";
+import { cn } from "@/lib/utils";
+import { type BaseLayoutProps, getLinks } from "./shared";
+import { NavProvider } from "fumadocs-ui/contexts/layout";
+import { SearchToggle } from "../layout/search-toggle";
+import { ThemeToggle } from "../layout/theme-toggle";
+import { ArrowUpRight } from "lucide-react";
+import Link from "fumadocs-core/link";
+import { Button } from "../ui/button";
+import { DocsLogo } from "../ui/icon";
+import { NavigationMenu, NavigationMenuList } from "../ui/navigation-menu";
+import { renderNavItem } from "./links";
+import { baseOptions } from "@/app/layout.config";
+import { MobileMenuButton } from "../layout/mobile-menu-button";
+import { MobileMenuProvider } from "@/contexts/mobile-menu";
+
+export interface HomeLayoutProps extends BaseLayoutProps {
+ nav?: Partial<
+ NavOptions & {
+ /**
+ * Open mobile menu when hovering the trigger
+ */
+ enableHoverToOpen?: boolean;
+ }
+ >;
+}
+
+export function HomeLayout(
+ props: HomeLayoutProps & HTMLAttributes
+) {
+ const {
+ nav,
+ links,
+ githubUrl,
+ i18n,
+ disableThemeSwitch = false,
+ themeSwitch = { enabled: !disableThemeSwitch },
+ searchToggle,
+ ...rest
+ } = props;
+
+ return (
+
+
+
+ {slot(
+ nav,
+
+ )}
+ {props.children}
+
+
+
+ );
+}
+
+export function Header({
+ nav = {},
+ i18n = false,
+ links,
+ githubUrl,
+ themeSwitch,
+ searchToggle,
+}: HomeLayoutProps) {
+ return (
+
+
+
+ );
+}
diff --git a/components/layouts/home/menu.tsx b/components/layouts/home/menu.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/components/layouts/home/navbar.tsx b/components/layouts/home/navbar.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/components/layouts/interactive.tsx b/components/layouts/interactive.tsx
new file mode 100644
index 000000000..475588816
--- /dev/null
+++ b/components/layouts/interactive.tsx
@@ -0,0 +1,140 @@
+"use client";
+
+import { type ReactNode } from "react";
+import Link from "next/link";
+import { Check } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { usePageData } from "./page";
+
+// Interactive Header container component
+interface InteractiveHeaderProps {
+ children: ReactNode;
+ className?: string;
+}
+
+function InteractiveHeader({ children, className }: InteractiveHeaderProps) {
+ return {children}
;
+}
+
+// Interactive Title component
+interface InteractiveTitleProps {
+ className?: string;
+}
+
+function InteractiveTitle({ className }: InteractiveTitleProps) {
+ const { title } = usePageData();
+ if (!title) return null;
+
+ return (
+ {title}
+ );
+}
+
+// Interactive Description component
+interface InteractiveDescriptionProps {
+ className?: string;
+}
+
+function InteractiveDescription({ className }: InteractiveDescriptionProps) {
+ const { description } = usePageData();
+ if (!description) return null;
+
+ return (
+
+ {description}
+
+ );
+}
+
+// Interactive Features component
+interface InteractiveFeaturesProps {
+ className?: string;
+}
+
+function InteractiveFeatures({ className }: InteractiveFeaturesProps) {
+ const { interactiveFeatures = [] } = usePageData();
+ if (interactiveFeatures.length === 0) return null;
+
+ return (
+
+ {interactiveFeatures.map((feature, index) => (
+ -
+
+
+
+
+ {feature}
+
+
+ ))}
+
+ );
+}
+
+// Interactive Links component
+interface InteractiveLinksProps {
+ className?: string;
+}
+
+function InteractiveLinks({ className }: InteractiveLinksProps) {
+ const { interactiveLinks = [] } = usePageData();
+ if (interactiveLinks.length === 0) return null;
+
+ return (
+
+ {interactiveLinks.map((link, index) => (
+ -
+
+ {link.icon && {link.icon}}
+ {link.title}
+ →
+
+
+ ))}
+
+ );
+}
+
+// Interactive Layout - default composition
+function InteractiveLayout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+// Export as compound component
+export const Interactive = {
+ Header: InteractiveHeader,
+ Title: InteractiveTitle,
+ Description: InteractiveDescription,
+ Features: InteractiveFeatures,
+ Links: InteractiveLinks,
+ Layout: InteractiveLayout,
+};
+
+// Also export individual components for flexibility
+export {
+ InteractiveHeader,
+ InteractiveTitle,
+ InteractiveDescription,
+ InteractiveFeatures,
+ InteractiveLinks,
+ InteractiveLayout,
+};
diff --git a/components/layouts/links.tsx b/components/layouts/links.tsx
new file mode 100644
index 000000000..485cf46a4
--- /dev/null
+++ b/components/layouts/links.tsx
@@ -0,0 +1,430 @@
+"use client";
+import Link from "fumadocs-core/link";
+import { usePathname } from "fumadocs-core/framework";
+import React, {
+ type AnchorHTMLAttributes,
+ forwardRef,
+ type HTMLAttributes,
+ type ReactNode,
+ useState,
+ useRef,
+ useEffect,
+} from "react";
+import { isActive } from "../../lib/is-active";
+import {
+ NavigationMenuItem,
+ NavigationMenuLink,
+ NavigationMenuTrigger,
+ NavigationMenuContent,
+} from "../ui/navigation-menu";
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+} from "../ui/dropdown-menu";
+import { ChevronDown } from "lucide-react";
+import { cn } from "../../lib/utils";
+
+interface BaseItem {
+ /**
+ * Restrict where the item is displayed
+ *
+ * @defaultValue 'all'
+ */
+ on?: "menu" | "nav" | "all";
+}
+
+export interface BaseLinkType extends BaseItem {
+ url: string;
+ /**
+ * When the item is marked as active
+ *
+ * @defaultValue 'url'
+ */
+ active?: "url" | "nested-url" | "none";
+ external?: boolean;
+}
+
+export interface MainItemType extends BaseLinkType {
+ type?: "main";
+ icon?: ReactNode;
+ text: ReactNode;
+ description?: ReactNode;
+ isNew?: boolean;
+}
+
+export interface IconItemType extends BaseLinkType {
+ type: "icon";
+ /**
+ * `aria-label` of icon button
+ */
+ label?: string;
+ icon: ReactNode;
+ text: ReactNode;
+ /**
+ * @defaultValue true
+ */
+ secondary?: boolean;
+}
+
+interface ButtonItem extends BaseLinkType {
+ type: "button";
+ icon?: ReactNode;
+ text: ReactNode;
+ /**
+ * @defaultValue false
+ */
+ secondary?: boolean;
+}
+
+export interface MenuItemType extends BaseItem {
+ type: "menu";
+ icon?: ReactNode;
+ text: ReactNode;
+
+ url?: string;
+ items: (
+ | (MainItemType & {
+ /**
+ * Options when displayed on navigation menu
+ */
+ menu?: HTMLAttributes & {
+ banner?: ReactNode;
+ };
+ })
+ | CustomItem
+ )[];
+
+ /**
+ * @defaultValue false
+ */
+ secondary?: boolean;
+}
+
+export interface DropdownItemType extends BaseItem {
+ type: "dropdown";
+ icon?: ReactNode;
+ text: ReactNode;
+ url?: string; // Optional URL for main item click navigation
+ items: (
+ | (MainItemType & {
+ /**
+ * Options when displayed on dropdown menu
+ */
+ dropdown?: HTMLAttributes;
+ })
+ | CustomItem
+ )[];
+ /**
+ * @defaultValue false
+ */
+ secondary?: boolean;
+}
+
+interface CustomItem extends BaseItem {
+ type: "custom";
+ /**
+ * @defaultValue false
+ */
+ secondary?: boolean;
+ children: ReactNode;
+}
+
+export type LinkItemType =
+ | MainItemType
+ | IconItemType
+ | ButtonItem
+ | MenuItemType
+ | DropdownItemType
+ | CustomItem;
+
+export const BaseLinkItem = forwardRef<
+ HTMLAnchorElement,
+ Omit, "href"> & { item: BaseLinkType }
+>(({ item, ...props }, ref) => {
+ const pathname = usePathname();
+ const activeType = item.active ?? "url";
+ const active =
+ activeType !== "none" &&
+ isActive(item.url, pathname, activeType === "nested-url");
+
+ return (
+
+ {props.children}
+
+ );
+});
+
+BaseLinkItem.displayName = "BaseLinkItem";
+
+// Helper function to determine if a navigation item should be active
+function isNavItemActive(item: LinkItemType, pathname: string): boolean {
+ if (!("url" in item) || !item.url) return false;
+
+ // Check if item has active property (only certain types have it)
+ // For menu items, default to "nested-url" behavior since they should match child routes
+ const activeType =
+ "active" in item
+ ? (item.active ?? "url")
+ : item.type === "menu"
+ ? "nested-url"
+ : "url";
+ if (activeType === "none") return false;
+
+ // Special handling for "Get started" link
+ if (item.url === "/start") {
+ // Define the main sections that should override "Get started"
+ const mainSections = ["/tools", "/apis", "/reference", "/resources"];
+
+ // Check if current path is in any main section
+ const isInMainSection = mainSections.some(
+ (section) => pathname === section || pathname.startsWith(`${section}/`)
+ );
+
+ // "Get started" is active if:
+ // 1. We're exactly on the start page, OR
+ // 2. We're on any path that's not root ("/") AND not in the main sections
+ return pathname === "/start" || (pathname !== "/" && !isInMainSection);
+ }
+
+ // For other items, use the standard isActive logic
+ return isActive(item.url, pathname, activeType === "nested-url");
+}
+
+export function renderNavItem(item: LinkItemType): ReactNode {
+ const itemType = item.type ?? "main";
+ const pathname = usePathname();
+
+ switch (itemType) {
+ case "main": {
+ if (!("url" in item)) return null;
+
+ const isActive = isNavItemActive(item, pathname);
+
+ return (
+
+
+
+ {item.text}
+
+
+
+ );
+ }
+
+ case "menu": {
+ if (!("items" in item)) return null;
+
+ return (
+
+ {item.url ? (
+
+
+
+ {item.text}
+
+
+
+
+ ) : (
+ // When no URL, use default button behavior
+
+ {item.text}
+
+
+ )}
+
+ {/* DEMO: Grid layout with banner
+
+ -
+
+
+
+
+ {item.text}
+
+
+ Explore our {String(item.text).toLowerCase()} for building
+ on Stacks.
+
+
+
+
+ {item.items.map((menuItem, index) => {
+ if (menuItem.type === "custom") {
+ return - {menuItem.children}
;
+ }
+
+ if (!("url" in menuItem)) return null;
+
+ return (
+ -
+
+
+
+ {menuItem.text}
+
+
+ {menuItem.description}
+
+
+
+
+ );
+ })}
+
+ */}
+
+ {/* Simple two-column layout */}
+
+ {item.items.map((menuItem, index) => {
+ if (menuItem.type === "custom") {
+ return
{menuItem.children}
;
+ }
+
+ if (!("url" in menuItem)) return null;
+
+ const isMenuItemActive = isNavItemActive(menuItem, pathname);
+
+ return (
+
+
+
+ {menuItem.text}
+
+ {menuItem.isNew && (
+
+ New
+
+ )}
+
+
+ );
+ })}
+
+
+
+ );
+ }
+
+ case "dropdown":
+ if (!("items" in item)) return null;
+ return (
+
+ );
+
+ case "icon":
+ case "button":
+ case "custom":
+ // These types are not part of the current PRD scope
+ return null;
+
+ default:
+ return null;
+ }
+}
+
+// Separate component for dropdown
+function DropdownNavItem({ item }: { item: DropdownItemType }) {
+ return (
+
+
+
+
+
+
+ {item.items.map((dropdownItem, index) => {
+ if (dropdownItem.type === "custom") {
+ return (
+
+ {dropdownItem.children}
+
+ );
+ }
+
+ if (!("url" in dropdownItem)) return null;
+
+ return (
+
+
+ {dropdownItem.text}
+ {dropdownItem.description && (
+
+ {dropdownItem.description}
+
+ )}
+
+
+ );
+ })}
+ {/* If dropdown has a main URL, add a footer link */}
+ {item.url && (
+ <>
+
+
+
+ View all {item.text}
+ →
+
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/components/layouts/page.tsx b/components/layouts/page.tsx
new file mode 100644
index 000000000..afb9d5154
--- /dev/null
+++ b/components/layouts/page.tsx
@@ -0,0 +1,270 @@
+"use client";
+
+import React, {
+ createContext,
+ useContext,
+ type ReactNode,
+ type ComponentProps,
+ useRef,
+} from "react";
+import type { TableOfContents, TOCItemType } from "fumadocs-core/server";
+import { AnchorProvider } from "fumadocs-core/toc";
+import * as Primitive from "fumadocs-core/toc";
+import { cn } from "@/lib/utils";
+import { AlignLeft } from "lucide-react";
+import { TocThumb } from "@/components/layout/toc-thumb";
+import { BreadcrumbNav as OriginalBreadcrumb } from "@/components/breadcrumb-nav";
+
+export interface PageData {
+ toc?: TableOfContents;
+ full?: boolean;
+ interactive?: boolean;
+ title?: string;
+ description?: string;
+ interactiveFeatures?: string[];
+ interactiveLinks?: Array<{
+ title: string;
+ href: string;
+ icon?: ReactNode;
+ }>;
+}
+
+export const PageContext = createContext({});
+
+export function usePageData() {
+ const context = useContext(PageContext);
+ if (!context) {
+ throw new Error("usePageData must be used within a Page component");
+ }
+ return context;
+}
+
+interface DocsPageProps {
+ data: PageData;
+ children: ReactNode;
+}
+
+export function DocsPage({ data, children }: DocsPageProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+type LayoutVariant = "standard" | "interactive" | "hero" | "minimal";
+
+interface PageLayoutProps {
+ children: ReactNode;
+ variant?: LayoutVariant;
+ className?: string;
+}
+
+function PageLayout({
+ children,
+ variant = "standard",
+ className,
+}: PageLayoutProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+// Internal wrapper component for content + TOC
+interface ContentWrapperProps {
+ children: ReactNode;
+ className?: string;
+}
+
+function ContentWrapper({ children, className }: ContentWrapperProps) {
+ const { toc = [], full } = usePageData();
+ const shouldShowTOC = toc.length > 0 && !full;
+
+ return shouldShowTOC ? (
+
+ ) : (
+ <>{children}>
+ );
+}
+
+interface PageHeaderProps {
+ children: ReactNode;
+ className?: string;
+}
+
+function PageHeader({ children, className }: PageHeaderProps) {
+ const { interactive } = usePageData();
+
+ return (
+
+ );
+}
+
+interface PageContentProps extends ComponentProps<"div"> {
+ children: ReactNode;
+ className?: string;
+}
+
+function PageContent({ children, className, ...props }: PageContentProps) {
+ const { full, interactive } = usePageData();
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+function TocItem({ item }: { item: TOCItemType }) {
+ return (
+ = 4 && "ps-8"
+ )}
+ >
+ {item.title}
+
+ );
+}
+
+function PageTOC() {
+ const { toc = [], full } = usePageData();
+ const containerRef = useRef(null);
+
+ if (toc.length === 0 || full) return null;
+
+ return (
+
+
+
+
+
+ {toc.map((item) => (
+
+ ))}
+
+
+
+ );
+}
+
+function PageBreadcrumb() {
+ return ;
+}
+
+function PageTitle(props: ComponentProps<"h1">) {
+ const { title } = usePageData();
+ if (!title && !props.children) return null;
+
+ return (
+
+ {props.children || title}
+
+ );
+}
+
+function PageDescription(props: ComponentProps<"p">) {
+ const { description } = usePageData();
+ if (!description && !props.children) return null;
+
+ return (
+
+ {props.children || description}
+
+ );
+}
+
+interface PageProseProps {
+ children: ReactNode;
+ className?: string;
+}
+
+function PageProse({ children, className }: PageProseProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export {
+ PageLayout as DocsPageLayout,
+ PageHeader as DocsPageHeader,
+ PageContent as DocsPageContent,
+ ContentWrapper as DocsPageContentWrapper,
+ PageTOC as DocsPageTOC,
+ PageBreadcrumb as DocsPageBreadcrumb,
+ PageTitle as DocsPageTitle,
+ PageDescription as DocsPageDescription,
+ PageProse as DocsPageProse,
+};
+
+export {
+ PageTitle as DocsTitle,
+ PageDescription as DocsDescription,
+ PageProse as DocsBody,
+};
+
+// DocsPage.Layout = PageLayout;
+// DocsPage.Header = PageHeader;
+// DocsPage.Content = PageContent;
+// DocsPage.ContentWrapper = ContentWrapper;
+// DocsPage.TOC = PageTOC;
+// DocsPage.Breadcrumb = PageBreadcrumb;
+// DocsPage.Title = PageTitle;
+// DocsPage.Description = PageDescription;
+// DocsPage.Prose = PageProse;
diff --git a/components/layouts/shared.tsx b/components/layouts/shared.tsx
new file mode 100644
index 000000000..4e24c0dc4
--- /dev/null
+++ b/components/layouts/shared.tsx
@@ -0,0 +1,140 @@
+import type { ReactNode } from 'react';
+import type { LinkItemType } from './links';
+import type { NavProviderProps } from 'fumadocs-ui/contexts/layout';
+import { Slot } from '@radix-ui/react-slot';
+import type { I18nConfig } from 'fumadocs-core/i18n';
+
+export interface NavOptions extends NavProviderProps {
+ enabled: boolean;
+ component: ReactNode;
+
+ title?: ReactNode;
+
+ /**
+ * Redirect url of title
+ * @defaultValue '/'
+ */
+ url?: string;
+
+ children?: ReactNode;
+}
+
+export interface BaseLayoutProps {
+ themeSwitch?: {
+ enabled?: boolean;
+ component?: ReactNode;
+ mode?: 'light-dark' | 'light-dark-system';
+ };
+
+ searchToggle?: Partial<{
+ enabled: boolean;
+ components: Partial<{
+ sm: ReactNode;
+ lg: ReactNode;
+ }>;
+ }>;
+
+ /**
+ * Remove theme switcher component
+ *
+ * @deprecated Use `themeSwitch.enabled` instead.
+ */
+ disableThemeSwitch?: boolean;
+
+ /**
+ * I18n options
+ *
+ * @defaultValue false
+ */
+ i18n?: boolean | I18nConfig;
+
+ /**
+ * GitHub url
+ */
+ githubUrl?: string;
+
+ links?: LinkItemType[];
+ /**
+ * Replace or disable navbar
+ */
+ nav?: Partial;
+
+ children?: ReactNode;
+}
+
+export { type LinkItemType };
+
+/**
+ * Get Links Items with shortcuts
+ */
+export function getLinks(
+ links: LinkItemType[] = [],
+ githubUrl?: string,
+): LinkItemType[] {
+ let result = links ?? [];
+
+ if (githubUrl)
+ result = [
+ ...result,
+ {
+ type: 'icon',
+ url: githubUrl,
+ text: 'Github',
+ label: 'GitHub',
+ icon: (
+
+
+
+ ),
+ external: true,
+ },
+ ];
+
+ return result;
+}
+
+export function slot(
+ obj:
+ | {
+ enabled?: boolean;
+ component?: ReactNode;
+ }
+ | undefined,
+ def: ReactNode,
+ customComponentProps?: object,
+ disabled?: ReactNode,
+): ReactNode {
+ if (obj?.enabled === false) return disabled;
+ if (obj?.component !== undefined)
+ return {obj.component};
+
+ return def;
+}
+
+export function slots>(
+ variant: keyof Comp,
+ obj:
+ | {
+ enabled?: boolean;
+ components?: Comp;
+ }
+ | undefined,
+ def: ReactNode,
+): ReactNode {
+ if (obj?.enabled === false) return;
+ if (obj?.components?.[variant] !== undefined)
+ return {obj.components[variant]};
+
+ return def;
+}
+
+export function omit, Keys extends keyof T>(
+ obj: T,
+ ...keys: Keys[]
+): Omit {
+ const clone = { ...obj };
+ for (const key of keys) {
+ delete clone[key];
+ }
+ return clone;
+}
diff --git a/components/lists.tsx b/components/lists.tsx
index 9c52f5097..9bc8d8fce 100644
--- a/components/lists.tsx
+++ b/components/lists.tsx
@@ -11,8 +11,8 @@ function OrderedList({ children, items }: ListProps) {
return (
{items.map((item, index) => (
- -
-
+
-
+
{index + 1}
{item}
@@ -24,7 +24,10 @@ function OrderedList({ children, items }: ListProps) {
// For MDX usage, filter and process children
const validChildren = React.Children.toArray(children).filter(
- (child) => React.isValidElement(child) && child.type === "li"
+ (child) =>
+ React.isValidElement(child) &&
+ typeof child.type === "string" &&
+ child.type === "li"
);
return (
@@ -32,12 +35,14 @@ function OrderedList({ children, items }: ListProps) {
{validChildren.map((child, index) => {
if (!React.isValidElement(child)) return null;
+ const element = child as React.ReactElement<{ children?: ReactNode }>;
+
return (
-
-
-
+
-
+
{index + 1}
- {child.props.children}
+ {element.props.children}
);
})}
@@ -49,7 +54,7 @@ function UnorderedList({ children, items }: ListProps) {
// If items prop is provided, use it directly
if (items) {
return (
-
+
{items.map((item, index) => (
-
@@ -64,20 +69,105 @@ function UnorderedList({ children, items }: ListProps) {
// For MDX usage, filter and process children
const validChildren = React.Children.toArray(children).filter(
- (child) => React.isValidElement(child) && child.type === "li"
+ (child) =>
+ React.isValidElement(child) &&
+ typeof child.type === "string" &&
+ child.type === "li"
);
+ // Helper function to recursively check for checkboxes in React elements
+ const hasCheckboxInElement = (element: any): boolean => {
+ if (!element) return false;
+
+ if (React.isValidElement(element)) {
+ // Check if this element is an input with type checkbox
+ if ((element.props as any)?.type === "checkbox") {
+ return true;
+ }
+
+ // Recursively check children
+ if ((element.props as any)?.children) {
+ const children = React.Children.toArray(
+ (element.props as any).children
+ );
+ return children.some(hasCheckboxInElement);
+ }
+ }
+
+ return false;
+ };
+
+ // Helper function to extract text content from React elements
+ const getTextContent = (element: any): string => {
+ if (typeof element === "string") return element;
+ if (typeof element === "number") return element.toString();
+ if (!element) return "";
+
+ // Handle arrays directly
+ if (Array.isArray(element)) {
+ return element.map(getTextContent).join("");
+ }
+
+ if (React.isValidElement(element)) {
+ if ((element.props as any)?.children) {
+ const children = (element.props as any).children;
+ // Handle both arrays and single children
+ if (Array.isArray(children)) {
+ return children.map(getTextContent).join("");
+ } else {
+ return getTextContent(children);
+ }
+ }
+ }
+
+ return "";
+ };
+
+ // Check if any child contains a checkbox
+ const hasCheckboxes = validChildren.some((child) => {
+ if (!React.isValidElement(child)) return false;
+ return hasCheckboxInElement(child);
+ });
+
+ // Check if any child starts with special characters (like ✓, ✗, ★, etc.)
+ const hasSpecialPrefixes = validChildren.some((child) => {
+ if (!React.isValidElement(child)) return false;
+ const element = child as React.ReactElement<{ children?: ReactNode }>;
+ const childText = getTextContent(element.props.children);
+ // Only look for specific special characters, not a broad range
+ return childText.includes("✓") || childText.includes("✗");
+ });
+
+ const shouldRemoveDashes = hasCheckboxes || hasSpecialPrefixes;
+
return (
-
- }
- />
- );
-}
diff --git a/components/steps.tsx b/components/steps.tsx
new file mode 100644
index 000000000..5139885c2
--- /dev/null
+++ b/components/steps.tsx
@@ -0,0 +1,9 @@
+import type { ReactNode } from "react";
+
+export function Steps({ children }: { children: ReactNode }) {
+ return {children}
;
+}
+
+export function Step({ children }: { children: ReactNode }) {
+ return {children}
;
+}
diff --git a/components/table.tsx b/components/table.tsx
index 9f6e999db..f4875c9c8 100644
--- a/components/table.tsx
+++ b/components/table.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { type ReactNode } from "react";
import {
Table,
TableBody,
@@ -20,8 +20,9 @@ interface NetworkBadgeProps {
const NetworkBadge = ({ network }: NetworkBadgeProps) => (
<>
{typeof network === "object" && network !== null ? (
-
- {network.props.children}
+
+ {(network as React.ReactElement<{ children?: ReactNode }>).props
+ .children ?? ""}
) : (
@@ -42,28 +43,41 @@ function CustomTable({ className, ...props }: TableProps) {
React.isValidElement(child) && child.type === "tbody"
);
- const headerRows = thead ? React.Children.toArray(thead.props.children) : [];
- const bodyRows = tbody ? React.Children.toArray(tbody.props.children) : [];
+ const headerRows = thead
+ ? React.Children.toArray(
+ (thead as React.ReactElement<{ children?: ReactNode }>).props.children
+ )
+ : [];
+ const bodyRows = tbody
+ ? React.Children.toArray(
+ (tbody as React.ReactElement<{ children?: ReactNode }>).props.children
+ )
+ : [];
const rows = [...headerRows, ...bodyRows].filter(
(row): row is React.ReactElement => React.isValidElement(row)
);
if (rows.length === 0) return null;
- const headers = React.Children.toArray(rows[0].props.children).map((cell) =>
- React.isValidElement(cell) ? cell.props.children : cell
+ const headers = React.Children.toArray(
+ (rows[0] as React.ReactElement<{ children?: ReactNode }>).props.children
+ ).map((cell) =>
+ React.isValidElement(cell)
+ ? ((cell as React.ReactElement<{ children?: ReactNode }>).props
+ .children ?? "")
+ : cell
);
const dataRows = rows.slice(1);
return (
-
-
+
+
-
+
{headers.map((header, i) => (
{header}
@@ -72,19 +86,33 @@ function CustomTable({ className, ...props }: TableProps) {
{dataRows.map((row, i) => {
- const cells = React.Children.toArray(row.props.children).map(
- (cell) =>
- React.isValidElement(cell) ? cell.props.children : cell
+ const cells = React.Children.toArray(
+ (row as React.ReactElement<{ children?: ReactNode }>).props
+ .children
+ ).map((cell) =>
+ React.isValidElement(cell)
+ ? ((cell as React.ReactElement<{ children?: ReactNode }>).props
+ .children ?? "")
+ : cell
);
return (
-
-
-
+
+
+
{cells.slice(1).map((cell, j) => (
{cell}
diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx
new file mode 100644
index 000000000..189a2b1a1
--- /dev/null
+++ b/components/theme-provider.tsx
@@ -0,0 +1,11 @@
+"use client";
+
+import * as React from "react";
+import { ThemeProvider as NextThemesProvider } from "next-themes";
+
+export function ThemeProvider({
+ children,
+ ...props
+}: React.ComponentProps) {
+ return {children};
+}
diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx
index 61a00a455..8d813e95a 100644
--- a/components/ui/accordion.tsx
+++ b/components/ui/accordion.tsx
@@ -13,7 +13,7 @@ const Accordion = React.forwardRef<
,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
-
+
svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+ {
+ variants: {
+ variant: {
+ default: "bg-card text-card-foreground",
+ destructive:
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+);
+
+function Alert({
+ className,
+ variant,
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ );
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function AlertDescription({
className,
...props
-}: HTMLAttributes): JSX.Element {
+}: React.ComponentProps<"div">) {
return (
-
-
We are renaming to Fumadocs
-
+ />
);
}
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
index e4284916e..935f078c8 100644
--- a/components/ui/badge.tsx
+++ b/components/ui/badge.tsx
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
- "inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold font-aeonikFono transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 group-data-[state=active]:bg-inverted group-data-[state=active]:text-background",
+ "inline-flex items-center rounded border px-2 py-1 text-xs font-regular font-aeonik-fono transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 group-data-[state=active]:bg-inverted group-data-[state=active]:text-background",
{
variants: {
variant: {
diff --git a/components/ui/banner.tsx b/components/ui/banner.tsx
index 1a697af2c..7cfbf1486 100644
--- a/components/ui/banner.tsx
+++ b/components/ui/banner.tsx
@@ -74,7 +74,7 @@ export function Banner({
id={id}
{...props}
className={cn(
- "relative flex h-12 flex-row items-center justify-center bg-[#ff7733] px-4 text-center text-sm text-[#141312] font-medium font-aeonikFono",
+ "relative flex h-12 flex-row items-center justify-center bg-[#ff7733] px-4 text-center text-sm text-primary font-medium font-aeonik-fono",
!open && "hidden",
props.className
)}
@@ -105,7 +105,7 @@ export function Banner({
) : null}
diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx
index cfdc9ac2e..b4372df51 100644
--- a/components/ui/breadcrumb.tsx
+++ b/components/ui/breadcrumb.tsx
@@ -1,16 +1,16 @@
-import * as React from "react"
-import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
-import { Slot } from "@radix-ui/react-slot"
+import * as React from "react";
+import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
+import { Slot } from "@radix-ui/react-slot";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
- separator?: React.ReactNode
+ separator?: React.ReactNode;
}
->(({ ...props }, ref) => )
-Breadcrumb.displayName = "Breadcrumb"
+>(({ ...props }, ref) => );
+Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
@@ -24,8 +24,8 @@ const BreadcrumbList = React.forwardRef<
)}
{...props}
/>
-))
-BreadcrumbList.displayName = "BreadcrumbList"
+));
+BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
@@ -36,26 +36,29 @@ const BreadcrumbItem = React.forwardRef<
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
-))
-BreadcrumbItem.displayName = "BreadcrumbItem"
+));
+BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
- asChild?: boolean
+ asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
- const Comp = asChild ? Slot : "a"
+ const Comp = asChild ? Slot : "a";
return (
- )
-})
-BreadcrumbLink.displayName = "BreadcrumbLink"
+ );
+});
+BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
@@ -69,8 +72,8 @@ const BreadcrumbPage = React.forwardRef<
className={cn("font-normal text-foreground", className)}
{...props}
/>
-))
-BreadcrumbPage.displayName = "BreadcrumbPage"
+));
+BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({
children,
@@ -85,8 +88,8 @@ const BreadcrumbSeparator = ({
>
{children ?? }
-)
-BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+);
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({
className,
@@ -101,8 +104,8 @@ const BreadcrumbEllipsis = ({
More
-)
-BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+);
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
@@ -112,4 +115,4 @@ export {
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
-}
+};
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
index 673671e53..caaaf6bd4 100644
--- a/components/ui/button.tsx
+++ b/components/ui/button.tsx
@@ -1,45 +1,39 @@
-import * as React from "react";
-import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
-
+import { Slot } from "@radix-ui/react-slot";
+import { forwardRef } from "react";
import { cn } from "@/lib/utils";
-const buttonVariants = cva(
- "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+export const buttonVariants = cva(
+ "cursor-pointer inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
- default: "bg-primary text-background hover:bg-primary/90",
- destructive:
- "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ primary: "bg-primary text-primary-foreground",
outline:
- "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
- secondary:
- "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ "border text-primary hover:bg-neutral-150 hover:dark:bg-neutral-700 hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
- link: "text-primary underline-offset-4 hover:underline",
+ secondary:
+ "border bg-fd-secondary text-secondary-foreground hover:bg-fd-accent hover:text-accent-foreground",
},
size: {
- default: "h-10 px-4 py-2",
- sm: "h-8 rounded-md px-3",
- lg: "h-11 rounded-md px-8",
- icon: "h-10 w-10",
+ sm: "gap-1 p-0.5 text-xs",
+ icon: "p-1.5 [&_svg]:size-5",
+ "icon-sm": "p-1.5 [&_svg]:size-4.5",
},
},
defaultVariants: {
- variant: "default",
- size: "default",
+ variant: "primary",
},
}
);
export interface ButtonProps
- extends React.ButtonHTMLAttributes,
+ extends Omit, "color">,
VariantProps {
asChild?: boolean;
}
-const Button = React.forwardRef(
+export const Button = forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
@@ -52,5 +46,3 @@ const Button = React.forwardRef(
}
);
Button.displayName = "Button";
-
-export { Button, buttonVariants };
diff --git a/components/ui/calendar.tsx b/components/ui/calendar.tsx
deleted file mode 100644
index af40edcb3..000000000
--- a/components/ui/calendar.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-"use client";
-
-import * as React from "react";
-import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
-import { DayPicker } from "react-day-picker";
-
-import { cn } from "@/lib/utils";
-import { buttonVariants } from "@/components/ui/button";
-
-export type CalendarProps = React.ComponentProps;
-
-function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- ...props
-}: CalendarProps) {
- return (
- .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
- : "[&:has([aria-selected])]:rounded-md"
- ),
- day: cn(
- buttonVariants({ variant: "ghost" }),
- "h-8 w-8 p-0 font-normal aria-selected:opacity-100"
- ),
- day_range_start: "day-range-start",
- day_range_end: "day-range-end",
- day_selected:
- "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
- day_today: "bg-accent text-accent-foreground",
- day_outside:
- "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
- day_disabled: "text-muted-foreground opacity-50",
- day_range_middle:
- "aria-selected:bg-accent aria-selected:text-accent-foreground",
- day_hidden: "invisible",
- ...classNames,
- }}
- components={{
- IconLeft: ({ ...props }) => ,
- IconRight: ({ ...props }) => ,
- }}
- {...props}
- />
- );
-}
-Calendar.displayName = "Calendar";
-
-export { Calendar };
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
index 8db8c4e4d..14c863446 100644
--- a/components/ui/card.tsx
+++ b/components/ui/card.tsx
@@ -9,7 +9,7 @@ const Card = React.forwardRef<
-type CarouselOptions = UseCarouselParameters[0]
-type CarouselPlugin = UseCarouselParameters[1]
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters
;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
- opts?: CarouselOptions
- plugins?: CarouselPlugin
- orientation?: "horizontal" | "vertical"
- setApi?: (api: CarouselApi) => void
-}
+ opts?: CarouselOptions;
+ plugins?: CarouselPlugin;
+ orientation?: "horizontal" | "vertical";
+ setApi?: (api: CarouselApi) => void;
+};
type CarouselContextProps = {
- carouselRef: ReturnType[0]
- api: ReturnType[1]
- scrollPrev: () => void
- scrollNext: () => void
- canScrollPrev: boolean
- canScrollNext: boolean
-} & CarouselProps
+ carouselRef: ReturnType[0];
+ api: ReturnType[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
+} & CarouselProps;
-const CarouselContext = React.createContext(null)
+const CarouselContext = React.createContext(null);
function useCarousel() {
- const context = React.useContext(CarouselContext)
+ const context = React.useContext(CarouselContext);
if (!context) {
- throw new Error("useCarousel must be used within a ")
+ throw new Error("useCarousel must be used within a ");
}
- return context
+ return context;
}
const Carousel = React.forwardRef<
@@ -63,61 +63,61 @@ const Carousel = React.forwardRef<
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
- )
- const [canScrollPrev, setCanScrollPrev] = React.useState(false)
- const [canScrollNext, setCanScrollNext] = React.useState(false)
+ );
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
- return
+ return;
}
- setCanScrollPrev(api.canScrollPrev())
- setCanScrollNext(api.canScrollNext())
- }, [])
+ setCanScrollPrev(api.canScrollPrev());
+ setCanScrollNext(api.canScrollNext());
+ }, []);
const scrollPrev = React.useCallback(() => {
- api?.scrollPrev()
- }, [api])
+ api?.scrollPrev();
+ }, [api]);
const scrollNext = React.useCallback(() => {
- api?.scrollNext()
- }, [api])
+ api?.scrollNext();
+ }, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent) => {
if (event.key === "ArrowLeft") {
- event.preventDefault()
- scrollPrev()
+ event.preventDefault();
+ scrollPrev();
} else if (event.key === "ArrowRight") {
- event.preventDefault()
- scrollNext()
+ event.preventDefault();
+ scrollNext();
}
},
[scrollPrev, scrollNext]
- )
+ );
React.useEffect(() => {
if (!api || !setApi) {
- return
+ return;
}
- setApi(api)
- }, [api, setApi])
+ setApi(api);
+ }, [api, setApi]);
React.useEffect(() => {
if (!api) {
- return
+ return;
}
- onSelect(api)
- api.on("reInit", onSelect)
- api.on("select", onSelect)
+ onSelect(api);
+ api.on("reInit", onSelect);
+ api.on("select", onSelect);
return () => {
- api?.off("select", onSelect)
- }
- }, [api, onSelect])
+ api?.off("select", onSelect);
+ };
+ }, [api, onSelect]);
return (
- )
+ );
}
-)
-Carousel.displayName = "Carousel"
+);
+Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
- const { carouselRef, orientation } = useCarousel()
+ const { carouselRef, orientation } = useCarousel();
return (
@@ -167,15 +167,15 @@ const CarouselContent = React.forwardRef<
{...props}
/>
- )
-})
-CarouselContent.displayName = "CarouselContent"
+ );
+});
+CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
- const { orientation } = useCarousel()
+ const { orientation } = useCarousel();
return (
- )
-})
-CarouselItem.displayName = "CarouselItem"
+ );
+});
+CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps
->(({ className, variant = "outline", size = "icon", ...props }, ref) => {
- const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+>(({ className, size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
Previous slide
- )
-})
-CarouselPrevious.displayName = "CarouselPrevious"
+ );
+});
+CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps
->(({ className, variant = "outline", size = "icon", ...props }, ref) => {
- const { orientation, scrollNext, canScrollNext } = useCarousel()
+>(({ className, size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
Next slide
- )
-})
-CarouselNext.displayName = "CarouselNext"
+ );
+});
+CarouselNext.displayName = "CarouselNext";
export {
type CarouselApi,
@@ -258,4 +256,4 @@ export {
CarouselItem,
CarouselPrevious,
CarouselNext,
-}
+};
diff --git a/components/ui/collapsible.tsx b/components/ui/collapsible.tsx
index cb003d175..695733e70 100644
--- a/components/ui/collapsible.tsx
+++ b/components/ui/collapsible.tsx
@@ -1,11 +1,39 @@
"use client";
-
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
+import { forwardRef, useEffect, useState } from "react";
+import { cn } from "@/lib/utils";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
-const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
+const CollapsibleContent = forwardRef<
+ HTMLDivElement,
+ React.ComponentPropsWithoutRef
+>(({ children, ...props }, ref) => {
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+});
+
+CollapsibleContent.displayName =
+ CollapsiblePrimitive.CollapsibleContent.displayName;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
diff --git a/components/ui/command.tsx b/components/ui/command.tsx
index 012c90c9f..e3aa4fdfa 100644
--- a/components/ui/command.tsx
+++ b/components/ui/command.tsx
@@ -2,11 +2,11 @@
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
-import { MagnetIcon } from "lucide-react";
import { Command as CommandPrimitive } from "cmdk";
import { cn } from "@/lib/utils";
-import { Dialog, DialogContent } from "@/components/ui/dialog";
+import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
+import { SearchIcon } from "lucide-react";
const Command = React.forwardRef<
React.ElementRef,
@@ -28,7 +28,8 @@ interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (