"use client"; import { Tag } from "@/components/tag"; import { Tab } from "@headlessui/react"; import clsx from "clsx"; import { Children, createContext, isValidElement, useContext, useEffect, useRef, useState } from "react"; import { create } from "zustand"; const languageNames: Record = { js: "JavaScript", ts: "TypeScript", javascript: "JavaScript", typescript: "TypeScript", php: "PHP", python: "Python", ruby: "Ruby", go: "Go", }; const getPanelTitle = ({ title, language }: { title?: string; language?: string }) => { if (title) { return title; } if (language && language in languageNames) { return languageNames[language]; } return "Code"; }; function ClipboardIcon(props: React.ComponentPropsWithoutRef<"svg">) { return ( ); } function CopyButton({ code }: { code: string }) { const [copyCount, setCopyCount] = useState(0); const copied = copyCount > 0; useEffect(() => { if (copyCount > 0) { const timeout = setTimeout(() => { setCopyCount(0); }, 1000); return () => { clearTimeout(timeout); }; } }, [copyCount]); return ( ); } function CodePanelHeader({ tag, label }: { tag?: string; label?: string }): React.ReactNode | null { if (!tag && !label) { return null; } return (
{tag ? (
{tag}
) : null} {tag && label ? : null} {label ? {label} : null}
); } function CodePanel({ children, tag, label, code, }: { children: React.ReactNode; tag?: string; label?: string; code?: string; }) { const child = Children.only(children) as { props: { tag?: string; label?: string; code?: string } }; let codePanelTag = tag; let codePanelLabel = label; let codePanelCode = code; if (isValidElement(child)) { codePanelTag = child.props.tag ?? tag; codePanelLabel = child.props.label ?? label; codePanelCode = child.props.code ?? code; } if (!codePanelCode) { throw new Error("`CodePanel` requires a `code` prop, or a child with a `code` prop."); } return (
{children}
); } function CodeGroupHeader({ title, children, selectedIndex, }: { title: string; children: React.ReactNode; selectedIndex: number; }) { const hasTabs = Children.count(children) >= 1; if (!title && !hasTabs) { return null; } return (
{title ?

{title}

: null} {hasTabs ? ( {Children.map(children, (child, childIndex) => { if (isValidElement(child)) { return ( {getPanelTitle(child.props as { title?: string; language?: string })} ); } return null; })} ) : null}
); } function CodeGroupPanels({ children, ...props }: React.ComponentPropsWithoutRef): React.JSX.Element { const hasTabs = Children.count(children) >= 1; if (hasTabs) { return ( {Children.map(children, (child) => ( {child} ))} ); } return {children}; } const usePreventLayoutShift = () => { const positionRef = useRef(null); const rafRef = useRef(); useEffect(() => { return () => { if (typeof rafRef.current !== "undefined") { window.cancelAnimationFrame(rafRef.current); } }; }, []); return { positionRef, preventLayoutShift: (callback: () => void) => { if (!positionRef.current) { return; } const initialTop = positionRef.current.getBoundingClientRect().top; callback(); rafRef.current = window.requestAnimationFrame(() => { const newTop = positionRef.current?.getBoundingClientRect().top ?? initialTop; window.scrollBy(0, newTop - initialTop); }); }, }; }; const usePreferredLanguageStore = create<{ preferredLanguages: string[]; addPreferredLanguage: (language: string) => void; }>()((set) => ({ preferredLanguages: [], addPreferredLanguage: (language) => { set((state) => ({ preferredLanguages: [ ...state.preferredLanguages.filter((preferredLanguage) => preferredLanguage !== language), language, ], })); }, })); const useTabGroupProps = (availableLanguages: string[]) => { const { preferredLanguages, addPreferredLanguage } = usePreferredLanguageStore(); const [selectedIndex, setSelectedIndex] = useState(0); const activeLanguage = [...availableLanguages].sort( (a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a) )[0]; const languageIndex = availableLanguages.indexOf(activeLanguage ?? ""); const newSelectedIndex = languageIndex === -1 ? selectedIndex : languageIndex; if (newSelectedIndex !== selectedIndex) { setSelectedIndex(newSelectedIndex); } const { positionRef, preventLayoutShift } = usePreventLayoutShift(); const onChange = (index: number) => { preventLayoutShift(() => { addPreferredLanguage(availableLanguages[index] ?? ""); }); }; return { as: "div" as const, ref: positionRef, selectedIndex, onChange, }; }; const CodeGroupContext = createContext(false); export function CodeGroup({ children, title, ...props }: React.ComponentPropsWithoutRef & { title: string }): React.JSX.Element { const languages = Children.map(children, (child) => { if (isValidElement(child)) { return getPanelTitle(child.props as { title?: string; language?: string }); } return "Code"; }) ?? []; const tabGroupProps = useTabGroupProps(languages); const hasTabs = Children.count(children) >= 1; const containerClassName = "not-prose my-6 overflow-hidden rounded-2xl bg-slate-900 shadow-md dark:ring-1 dark:ring-white/10"; const header = ( {children} ); const panels = {children}; return ( {hasTabs ? ( {header} {panels} ) : (
{header} {panels}
)}
); } export function Code({ children, ...props }: React.ComponentPropsWithoutRef<"code">): React.JSX.Element { const isGrouped = useContext(CodeGroupContext); if (isGrouped) { if (typeof children !== "string") { throw new Error("`Code` children must be a string when nested inside a `CodeGroup`."); } return ; } return {children}; } export function Pre({ children, ...props }: React.ComponentPropsWithoutRef): React.ReactNode { const isGrouped = useContext(CodeGroupContext); if (isGrouped) { return children; } return {children}; }