diff --git a/packages/components/src/components/code-group/code-group-select.stories.tsx b/packages/components/src/components/code-group/code-group-select.stories.tsx new file mode 100644 index 0000000..d841ac0 --- /dev/null +++ b/packages/components/src/components/code-group/code-group-select.stories.tsx @@ -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 = { + title: "Components/CodeGroupSelect", + component: CodeGroupSelect, +}; + +export default meta; +type Story = StoryObj; + +const pythonSnippets: Record = { + 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 = { + 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 = { + 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> = { + Python: pythonSnippets, + TypeScript: typescriptSnippets, + Go: goSnippets, +}; + +export const Default: Story = { + render: (_, { globals }) => ( + + ), +}; + +export const DarkTheme: Story = { + render: () => , +}; + +export const WithShikiTheme: Story = { + render: () => ( + + ), +}; + +export const WithLightDarkShikiThemes: Story = { + render: (_, { globals }) => ( + + ), +}; + +export const SingleGroup: Story = { + render: (_, { globals }) => ( + + ), +}; + +export const SingleOption: Story = { + render: (_, { globals }) => ( + + ), +}; + +export const WithAudioUrl: Story = { + render: (_, { globals }) => ( + + ), +}; + +export const WithCustomClassName: Story = { + render: (_, { globals }) => ( + + ), +}; diff --git a/packages/components/src/components/code-group/code-group-select.tsx b/packages/components/src/components/code-group/code-group-select.tsx new file mode 100644 index 0000000..edf4180 --- /dev/null +++ b/packages/components/src/components/code-group/code-group-select.tsx @@ -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>; + 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 ( +
+
+ +
+ {options && ( + + )} +
+ + {askAiButton && askAiButton} +
+
+
+ +
+
+ {snippet?.audioUrl ? ( +
+ +
+ ) : ( + + {snippet?.code} + + )} +
+
+
+ ); +}; + +export { type ExampleCodeSnippet, type CodeGroupSelectProps, CodeGroupSelect }; diff --git a/packages/components/src/components/code-group/code-select-dropdown.tsx b/packages/components/src/components/code-group/code-select-dropdown.tsx index 5f28429..15e2fad 100644 --- a/packages/components/src/components/code-group/code-select-dropdown.tsx +++ b/packages/components/src/components/code-group/code-select-dropdown.tsx @@ -1,7 +1,6 @@ import { ChevronDownIcon } from "lucide-react"; import { cn } from "@/utils/cn"; -import type { CodeBlockTheme } from "@/utils/shiki/code-styling"; import { DropdownMenu, @@ -14,60 +13,35 @@ type CodeSelectDropdownProps = { selectedOption?: string; setSelectedOption: (option: string) => void; options: string[]; - codeBlockTheme?: CodeBlockTheme; }; const CodeSelectDropdown = ({ selectedOption, setSelectedOption, options, - codeBlockTheme = "system", }: CodeSelectDropdownProps) => { const hasOptions = options.length > 1; return ( -
+

{selectedOption}

{hasOptions && }
- + {options.map((option, i) => ( setSelectedOption(option)} diff --git a/packages/components/src/components/code-group/index.ts b/packages/components/src/components/code-group/index.ts index dd83128..0e705ba 100644 --- a/packages/components/src/components/code-group/index.ts +++ b/packages/components/src/components/code-group/index.ts @@ -1,4 +1,9 @@ export type { CodeGroupProps } from "./code-group"; export { CodeGroup } from "./code-group"; +export type { + CodeGroupSelectProps, + ExampleCodeSnippet, +} from "./code-group-select"; +export { CodeGroupSelect } from "./code-group-select"; export type { CodeSnippetProps } from "./code-snippet"; export { CodeSnippet } from "./code-snippet";