Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { ExampleCodeSnippet } from "./code-group-select";
import { CodeGroupSelect } from "./code-group-select";

const meta: Meta<typeof CodeGroupSelect> = {
title: "Components/CodeGroupSelect",
component: CodeGroupSelect,
};

export default meta;
type Story = StoryObj<typeof CodeGroupSelect>;

const pythonSnippets: Record<string, ExampleCodeSnippet> = {
Setup: {
filename: "install.sh",
code: "pip install my-package",
language: "bash",
},
"Basic usage": {
filename: "example.py",
code: `from my_package import Client

client = Client(api_key="your-key")
result = client.run(input="Hello, world")
print(result)`,
language: "python",
},
Advanced: {
filename: "advanced.py",
code: `from my_package import Client, Config

config = Config(
timeout=30,
retries=3,
verbose=True,
)
client = Client(api_key="your-key", config=config)

for item in client.stream(input="Process this"):
print(item)`,
language: "python",
},
};

const typescriptSnippets: Record<string, ExampleCodeSnippet> = {
Setup: {
filename: "install.sh",
code: "npm install my-package",
language: "bash",
},
"Basic usage": {
filename: "example.ts",
code: `import { Client } from "my-package";

const client = new Client({ apiKey: "your-key" });
const result = await client.run({ input: "Hello, world" });
console.log(result);`,
language: "typescript",
},
Advanced: {
filename: "advanced.ts",
code: `import { Client, type Config } from "my-package";

const config: Config = {
timeout: 30,
retries: 3,
verbose: true,
};
const client = new Client({ apiKey: "your-key", config });

for await (const item of client.stream({ input: "Process this" })) {
console.log(item);
}`,
language: "typescript",
},
};

const goSnippets: Record<string, ExampleCodeSnippet> = {
Setup: {
filename: "install.sh",
code: "go get github.com/example/my-package",
language: "bash",
},
"Basic usage": {
filename: "main.go",
code: `package main

import "github.com/example/my-package"

func main() {
client := mypackage.NewClient("your-key")
result, err := client.Run("Hello, world")
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
}`,
language: "go",
},
};

const snippets: Record<string, Record<string, ExampleCodeSnippet>> = {
Python: pythonSnippets,
TypeScript: typescriptSnippets,
Go: goSnippets,
};

export const Default: Story = {
render: (_, { globals }) => (
<CodeGroupSelect
codeBlockTheme={globals.theme === "dark" ? "dark" : "system"}
snippets={snippets}
/>
),
};

export const DarkTheme: Story = {
render: () => <CodeGroupSelect codeBlockTheme="dark" snippets={snippets} />,
};

export const WithShikiTheme: Story = {
render: () => (
<CodeGroupSelect
codeBlockTheme="system"
codeBlockThemeObject={{ theme: "andromeeda" }}
snippets={snippets}
/>
),
};

export const WithLightDarkShikiThemes: Story = {
render: (_, { globals }) => (
<CodeGroupSelect
codeBlockTheme={globals.theme === "dark" ? "dark" : "system"}
codeBlockThemeObject={{
theme: { light: "everforest-light", dark: "dracula" },
}}
snippets={snippets}
/>
),
};

export const SingleGroup: Story = {
render: (_, { globals }) => (
<CodeGroupSelect
codeBlockTheme={globals.theme === "dark" ? "dark" : "system"}
snippets={{ Python: pythonSnippets }}
/>
),
};

export const SingleOption: Story = {
render: (_, { globals }) => (
<CodeGroupSelect
codeBlockTheme={globals.theme === "dark" ? "dark" : "system"}
snippets={{
Python: {
Setup: pythonSnippets.Setup,
},
TypeScript: {
Setup: typescriptSnippets.Setup,
},
}}
/>
),
};

export const WithAudioUrl: Story = {
render: (_, { globals }) => (
<CodeGroupSelect
codeBlockTheme={globals.theme === "dark" ? "dark" : "system"}
snippets={{
English: {
Greeting: {
filename: "greeting.mp3",
code: "",
language: "text",
audioUrl: "/greeting.mp3",
},
},
Spanish: {
Greeting: {
filename: "saludo.mp3",
code: "",
language: "text",
audioUrl: "/saludo.mp3",
},
},
}}
/>
),
};

export const WithCustomClassName: Story = {
render: (_, { globals }) => (
<CodeGroupSelect
className="max-w-md shadow-lg"
codeBlockTheme={globals.theme === "dark" ? "dark" : "system"}
snippets={snippets}
/>
),
};
168 changes: 168 additions & 0 deletions packages/components/src/components/code-group/code-group-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { type ReactNode, useEffect, useMemo, useState } from "react";
import { cn } from "@/utils/cn";
import type { CodeBlockTheme, CodeStyling } from "@/utils/shiki/code-styling";
import { BaseCodeBlock } from "../code-block/base-code-block";
import {
CopyToClipboardButton,
type CopyToClipboardButtonProps,
} from "../code-block/copy-button";
import { CodeSelectDropdown } from "./code-select-dropdown";

const DEFAULT_CODE_SNIPPET_ARIA_LABEL = "Code snippet";

type ExampleCodeSnippet = {
filename: string;
code: string;
language: string;
audioUrl?: string;
};

interface CodeGroupSelectProps {
snippets: Record<string, Record<string, ExampleCodeSnippet>>;
className?: string;
syncedLabel?: string;
setSyncedLabel?: (label: string) => void;
setSelectedExampleIndex?: (index: number) => void;
askAiButton?: ReactNode;
codeBlockTheme?: CodeBlockTheme;
codeBlockThemeObject?: CodeStyling;
codeSnippetAriaLabel?: string;
copyButtonProps?: CopyToClipboardButtonProps;
}

const CodeGroupSelect = ({
snippets,
syncedLabel,
setSyncedLabel,
setSelectedExampleIndex,
askAiButton,
codeBlockTheme = "system",
codeBlockThemeObject,
codeSnippetAriaLabel = DEFAULT_CODE_SNIPPET_ARIA_LABEL,
copyButtonProps,
className,
}: CodeGroupSelectProps) => {
const groups = Object.keys(snippets);
const [selectedGroup, setSelectedGroup] = useState(groups[0]);

const safeSelectedGroup =
selectedGroup && groups.includes(selectedGroup) ? selectedGroup : groups[0];

const groupSnippets =
safeSelectedGroup !== undefined ? snippets[safeSelectedGroup] : undefined;
const options = useMemo(
() => (groupSnippets ? Object.keys(groupSnippets) : undefined),
[groupSnippets]
);
const [selectedOption, setSelectedOption] = useState(options?.[0]);

const safeSelectedOption =
selectedOption && options?.includes(selectedOption)
? selectedOption
: options?.[0];

const snippet =
groupSnippets !== undefined && safeSelectedOption !== undefined
? groupSnippets[safeSelectedOption]
: undefined;

const handleGroupSelect = (grp: string) => {
setSelectedGroup(grp);
const newOptions = snippets[grp] ? Object.keys(snippets[grp]) : [];
const firstOption = newOptions[0];
if (firstOption !== undefined) {
setSelectedOption(firstOption);
setSelectedExampleIndex?.(0);
if (firstOption !== syncedLabel) {
setSyncedLabel?.(firstOption);
}
}
};

const handleOptionSelect = (opt: string) => {
setSelectedOption(opt);
const index = options?.indexOf(opt) ?? -1;
setSelectedExampleIndex?.(index === -1 ? 0 : index);
if (opt !== syncedLabel) {
setSyncedLabel?.(opt);
}
};

useEffect(() => {
if (
syncedLabel &&
syncedLabel !== safeSelectedOption &&
options?.includes(syncedLabel)
) {
setSelectedOption(syncedLabel);
const index = options?.indexOf(syncedLabel) ?? -1;
setSelectedExampleIndex?.(index === -1 ? 0 : index);
}
}, [syncedLabel, options, safeSelectedOption, setSelectedExampleIndex]);

return (
<div
className={cn(
"not-prose relative flex min-w-full max-w-full flex-col overflow-hidden rounded-2xl p-0.5 text-xs leading-6",
"border border-gray-950/10 bg-gray-50 dark:border-white/10 dark:bg-white/5",
className
)}
data-testid="code-group-select"
>
<div className="flex w-full justify-between rounded-t-2xl text-xs leading-6">
<CodeSelectDropdown
options={groups}
selectedOption={safeSelectedGroup}
setSelectedOption={handleGroupSelect}
/>
<div className="flex overflow-hidden">
{options && (
<CodeSelectDropdown
options={options}
selectedOption={safeSelectedOption}
setSelectedOption={handleOptionSelect}
/>
)}
<div
className="flex items-center gap-1.5 pr-2.5"
data-testid="code-group-select-copy-button"
>
<CopyToClipboardButton
codeBlockTheme={codeBlockTheme}
textToCopy={snippet?.code ?? ""}
{...copyButtonProps}
/>
{askAiButton && askAiButton}
</div>
</div>
</div>

<section
aria-label={codeSnippetAriaLabel}
className="flex flex-1 overflow-hidden"
>
<div className="relative h-full max-h-full w-full min-w-full max-w-full">
{snippet?.audioUrl ? (
<div className="p-4">
<audio className="w-full" controls src={snippet.audioUrl}>
<track kind="captions" />
</audio>
</div>
) : (
<BaseCodeBlock
codeBlockTheme={codeBlockTheme}
codeBlockThemeObject={codeBlockThemeObject ?? codeBlockTheme}
isParentCodeGroup={true}
isSmallText
language={snippet?.language}
>
{snippet?.code}
</BaseCodeBlock>
)}
</div>
</section>
</div>
);
};

export { type ExampleCodeSnippet, type CodeGroupSelectProps, CodeGroupSelect };
Loading
Loading