2025-08-20 04:12:49 -06:00

144 lines
4.0 KiB
TypeScript

'use client';
import * as React from 'react';
import { useTheme } from 'next-themes';
import { cn } from '@workspace/ui/lib/utils';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
TabsContents,
type TabsProps,
} from '@/registry/components/tabs';
import { CopyButton } from '@/registry/buttons/copy';
type CodeTabsProps = {
codes: Record<string, string>;
lang?: string;
themes?: {
light: string;
dark: string;
};
copyButton?: boolean;
onCopy?: (content: string) => void;
} & Omit<TabsProps, 'children'>;
function CodeTabs({
codes,
lang = 'bash',
themes = {
light: 'github-light',
dark: 'github-dark',
},
className,
defaultValue,
value,
onValueChange,
copyButton = true,
onCopy,
...props
}: CodeTabsProps) {
const { resolvedTheme } = useTheme();
const [highlightedCodes, setHighlightedCodes] = React.useState<Record<
string,
string
> | null>(null);
const [selectedCode, setSelectedCode] = React.useState<string>(
value ?? defaultValue ?? Object.keys(codes)[0] ?? '',
);
React.useEffect(() => {
async function loadHighlightedCode() {
try {
const { codeToHtml } = await import('shiki');
const newHighlightedCodes: Record<string, string> = {};
for (const [command, val] of Object.entries(codes)) {
const highlighted = await codeToHtml(val, {
lang,
themes: {
light: themes.light,
dark: themes.dark,
},
defaultColor: resolvedTheme === 'dark' ? 'dark' : 'light',
});
newHighlightedCodes[command] = highlighted;
}
setHighlightedCodes(newHighlightedCodes);
} catch (error) {
console.error('Error highlighting codes', error);
setHighlightedCodes(codes);
}
}
loadHighlightedCode();
}, [resolvedTheme, lang, themes.light, themes.dark, codes]);
return (
<Tabs
data-slot="install-tabs"
className={cn(
'w-full gap-0 bg-muted/50 rounded-xl border overflow-hidden',
className,
)}
{...props}
value={selectedCode}
onValueChange={(val) => {
setSelectedCode(val);
onValueChange?.(val);
}}
>
<TabsList
data-slot="install-tabs-list"
className="w-full relative justify-between rounded-none h-10 bg-muted border-b border-border/75 dark:border-border/50 text-current py-0 px-4"
activeClassName="rounded-none shadow-none bg-transparent after:content-[''] after:absolute after:inset-x-0 after:h-0.5 after:bottom-0 dark:after:bg-white after:bg-black after:rounded-t-full"
>
<div className="flex gap-x-3 h-full">
{highlightedCodes &&
Object.keys(highlightedCodes).map((code) => (
<TabsTrigger
key={code}
value={code}
className="text-muted-foreground data-[state=active]:text-current px-0"
>
{code}
</TabsTrigger>
))}
</div>
{copyButton && highlightedCodes && (
<CopyButton
content={codes[selectedCode]}
size="sm"
variant="ghost"
className="-me-2 bg-transparent hover:bg-black/5 dark:hover:bg-white/10"
onCopy={onCopy}
/>
)}
</TabsList>
<TabsContents data-slot="install-tabs-contents">
{highlightedCodes &&
Object.entries(highlightedCodes).map(([code, val]) => (
<TabsContent
data-slot="install-tabs-content"
key={code}
className="w-full text-sm flex items-center p-4 overflow-auto"
value={code}
>
<div
className="[&>pre,_&_code]:!bg-transparent [&>pre,_&_code]:[background:transparent_!important] [&>pre,_&_code]:border-none [&_code]:!text-[13px]"
dangerouslySetInnerHTML={{ __html: val }}
/>
</TabsContent>
))}
</TabsContents>
</Tabs>
);
}
export { CodeTabs, type CodeTabsProps };