From 9c4e9cf2c02e0b5aa9fc9c2d7a0d53fd87346669 Mon Sep 17 00:00:00 2001 From: Kathryn Isabelle Lawrence Date: Tue, 10 Mar 2026 14:49:29 -0700 Subject: [PATCH 01/12] add code-group-select component --- .../code-group/code-group-select.tsx | 151 ++++++++++++++++++ .../src/components/code-group/index.ts | 2 + 2 files changed, 153 insertions(+) create mode 100644 packages/components/src/components/code-group/code-group-select.tsx 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..1c7e1f2 --- /dev/null +++ b/packages/components/src/components/code-group/code-group-select.tsx @@ -0,0 +1,151 @@ +import { type ReactNode, useEffect, useState } from "react"; +import { cn } from "@/utils/cn"; +import type { CodeBlockTheme } 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; + codeSnippetAriaLabel?: string; + copyButtonProps?: CopyToClipboardButtonProps; +} + +const CodeGroupSelect = ({ + snippets, + syncedLabel, + setSyncedLabel, + setSelectedExampleIndex, + askAiButton, + codeBlockTheme = "system", + codeSnippetAriaLabel = DEFAULT_CODE_SNIPPET_ARIA_LABEL, + copyButtonProps, + className, +}: CodeGroupSelectProps) => { + const groups = Object.keys(snippets); + const [selectedGroup, setSelectedGroup] = useState(groups[0]); + + const groupSnippets = + selectedGroup !== undefined ? snippets[selectedGroup] : undefined; + const options = groupSnippets ? Object.keys(groupSnippets) : undefined; + 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 handleOptionSelect = (opt: string) => { + setSelectedOption(opt); + setSelectedExampleIndex?.(options?.indexOf(opt) ?? 0); + if (opt !== syncedLabel) { + setSyncedLabel?.(opt); + } + }; + + useEffect(() => { + if ( + syncedLabel && + syncedLabel !== safeSelectedOption && + options?.includes(syncedLabel) + ) { + setSelectedOption(syncedLabel); + } + }, [syncedLabel, options, safeSelectedOption]); + + return ( +
+
+ +
+ {options && ( + + )} +
+ + {askAiButton && askAiButton} +
+
+
+ +
+
+ {snippet?.audioUrl ? ( +
+ +
+ ) : ( + + {snippet?.code} + + )} +
+
+
+ ); +}; + +export { type CodeGroupSelectProps, CodeGroupSelect }; diff --git a/packages/components/src/components/code-group/index.ts b/packages/components/src/components/code-group/index.ts index dd83128..6335998 100644 --- a/packages/components/src/components/code-group/index.ts +++ b/packages/components/src/components/code-group/index.ts @@ -1,4 +1,6 @@ export type { CodeGroupProps } from "./code-group"; export { CodeGroup } from "./code-group"; +export type { CodeGroupSelectProps } from "./code-group-select"; +export { CodeGroupSelect } from "./code-group-select"; export type { CodeSnippetProps } from "./code-snippet"; export { CodeSnippet } from "./code-snippet"; From ce5e413b8cbea7d43462009c56e19e8f9698b3e9 Mon Sep 17 00:00:00 2001 From: Kathryn Isabelle Lawrence Date: Tue, 10 Mar 2026 15:02:02 -0700 Subject: [PATCH 02/12] add code-group-select stories --- .../code-group/code-group-select.stories.tsx | 204 ++++++++++++++++++ .../code-group/code-group-select.tsx | 8 +- 2 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 packages/components/src/components/code-group/code-group-select.stories.tsx 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..42e3b3e --- /dev/null +++ b/packages/components/src/components/code-group/code-group-select.stories.tsx @@ -0,0 +1,204 @@ +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 SingleGroup: Story = { + render: (_, { globals }) => ( + + ), +}; + +export const SingleOption: Story = { + render: (_, { globals }) => ( + + ), +}; + +export const WithAudioUrl: Story = { + render: (_, { globals }) => ( + + ), +}; + +export const WithShikiTheme: Story = { + render: () => ( + + ), +}; + +export const WithLightDarkShikiThemes: 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 index 1c7e1f2..c35dc69 100644 --- a/packages/components/src/components/code-group/code-group-select.tsx +++ b/packages/components/src/components/code-group/code-group-select.tsx @@ -1,6 +1,6 @@ import { type ReactNode, useEffect, useState } from "react"; import { cn } from "@/utils/cn"; -import type { CodeBlockTheme } from "@/utils/shiki/code-styling"; +import type { CodeBlockTheme, CodeStyling } from "@/utils/shiki/code-styling"; import { BaseCodeBlock } from "../code-block/base-code-block"; import { CopyToClipboardButton, @@ -25,6 +25,7 @@ interface CodeGroupSelectProps { setSelectedExampleIndex?: (index: number) => void; askAiButton?: ReactNode; codeBlockTheme?: CodeBlockTheme; + codeBlockThemeObject?: CodeStyling; codeSnippetAriaLabel?: string; copyButtonProps?: CopyToClipboardButtonProps; } @@ -36,6 +37,7 @@ const CodeGroupSelect = ({ setSelectedExampleIndex, askAiButton, codeBlockTheme = "system", + codeBlockThemeObject, codeSnippetAriaLabel = DEFAULT_CODE_SNIPPET_ARIA_LABEL, copyButtonProps, className, @@ -135,6 +137,8 @@ const CodeGroupSelect = ({ ) : ( Date: Tue, 10 Mar 2026 15:15:09 -0700 Subject: [PATCH 03/12] fix passed props --- .../src/components/code-group/code-group-select.tsx | 6 +++--- packages/components/src/components/code-group/index.ts | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/components/src/components/code-group/code-group-select.tsx b/packages/components/src/components/code-group/code-group-select.tsx index c35dc69..61a53ff 100644 --- a/packages/components/src/components/code-group/code-group-select.tsx +++ b/packages/components/src/components/code-group/code-group-select.tsx @@ -96,7 +96,7 @@ const CodeGroupSelect = ({ >
{options && ( Date: Tue, 10 Mar 2026 15:22:42 -0700 Subject: [PATCH 04/12] fix dark theme example --- .../src/components/code-group/code-group-select.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 42e3b3e..9821e5d 100644 --- a/packages/components/src/components/code-group/code-group-select.stories.tsx +++ b/packages/components/src/components/code-group/code-group-select.stories.tsx @@ -116,7 +116,7 @@ export const Default: Story = { export const DarkTheme: Story = { render: () => ( - + ), }; From 4cd717ceb2438c25c8d3aa81b4ed47ec197c686c Mon Sep 17 00:00:00 2001 From: Kathryn Isabelle Lawrence Date: Tue, 10 Mar 2026 15:23:08 -0700 Subject: [PATCH 05/12] reorder theme stories --- .../code-group/code-group-select.stories.tsx | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) 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 index 9821e5d..10ffaec 100644 --- a/packages/components/src/components/code-group/code-group-select.stories.tsx +++ b/packages/components/src/components/code-group/code-group-select.stories.tsx @@ -120,6 +120,29 @@ export const DarkTheme: Story = { ), }; +export const WithShikiTheme: Story = { + render: () => ( + + ), +}; + +export const WithLightDarkShikiThemes: Story = { + render: (_, { globals }) => ( + + ), +}; + + export const SingleGroup: Story = { render: (_, { globals }) => ( ( - - ), -}; - -export const WithLightDarkShikiThemes: Story = { - render: (_, { globals }) => ( - - ), -}; - export const WithCustomClassName: Story = { render: (_, { globals }) => ( Date: Tue, 10 Mar 2026 15:24:35 -0700 Subject: [PATCH 06/12] lint --- .../src/components/code-group/code-group-select.stories.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 index 10ffaec..d841ac0 100644 --- a/packages/components/src/components/code-group/code-group-select.stories.tsx +++ b/packages/components/src/components/code-group/code-group-select.stories.tsx @@ -115,9 +115,7 @@ export const Default: Story = { }; export const DarkTheme: Story = { - render: () => ( - - ), + render: () => , }; export const WithShikiTheme: Story = { @@ -142,7 +140,6 @@ export const WithLightDarkShikiThemes: Story = { ), }; - export const SingleGroup: Story = { render: (_, { globals }) => ( Date: Tue, 10 Mar 2026 15:37:52 -0700 Subject: [PATCH 07/12] address bugbot comments --- .../code-group/code-group-select.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/components/src/components/code-group/code-group-select.tsx b/packages/components/src/components/code-group/code-group-select.tsx index 61a53ff..68713cd 100644 --- a/packages/components/src/components/code-group/code-group-select.tsx +++ b/packages/components/src/components/code-group/code-group-select.tsx @@ -1,4 +1,4 @@ -import { type ReactNode, useEffect, useState } from "react"; +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"; @@ -47,7 +47,10 @@ const CodeGroupSelect = ({ const groupSnippets = selectedGroup !== undefined ? snippets[selectedGroup] : undefined; - const options = groupSnippets ? Object.keys(groupSnippets) : undefined; + const options = useMemo( + () => (groupSnippets ? Object.keys(groupSnippets) : undefined), + [groupSnippets] + ); const [selectedOption, setSelectedOption] = useState(options?.[0]); const safeSelectedOption = @@ -62,11 +65,21 @@ const CodeGroupSelect = ({ 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); - setSelectedExampleIndex?.(options?.indexOf(opt) ?? 0); + const index = options?.indexOf(opt) ?? -1; + setSelectedExampleIndex?.(index === -1 ? 0 : index); if (opt !== syncedLabel) { setSyncedLabel?.(opt); } From 58876a391775823e8f99117725b35b43293bf110 Mon Sep 17 00:00:00 2001 From: Kathryn Isabelle Lawrence Date: Tue, 10 Mar 2026 15:50:36 -0700 Subject: [PATCH 08/12] address bugbot comment --- .../src/components/code-group/code-group-select.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/components/src/components/code-group/code-group-select.tsx b/packages/components/src/components/code-group/code-group-select.tsx index 68713cd..c527a5f 100644 --- a/packages/components/src/components/code-group/code-group-select.tsx +++ b/packages/components/src/components/code-group/code-group-select.tsx @@ -92,8 +92,10 @@ const CodeGroupSelect = ({ options?.includes(syncedLabel) ) { setSelectedOption(syncedLabel); + const index = options?.indexOf(syncedLabel) ?? -1; + setSelectedExampleIndex?.(index === -1 ? 0 : index); } - }, [syncedLabel, options, safeSelectedOption]); + }, [syncedLabel, options, safeSelectedOption, setSelectedExampleIndex]); return (
Date: Tue, 10 Mar 2026 16:31:22 -0700 Subject: [PATCH 09/12] add safe selected group --- .../src/components/code-group/code-group-select.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/components/src/components/code-group/code-group-select.tsx b/packages/components/src/components/code-group/code-group-select.tsx index c527a5f..82300d9 100644 --- a/packages/components/src/components/code-group/code-group-select.tsx +++ b/packages/components/src/components/code-group/code-group-select.tsx @@ -45,8 +45,11 @@ const CodeGroupSelect = ({ const groups = Object.keys(snippets); const [selectedGroup, setSelectedGroup] = useState(groups[0]); + const safeSelectedGroup = + selectedGroup && groups.includes(selectedGroup) ? selectedGroup : groups[0]; + const groupSnippets = - selectedGroup !== undefined ? snippets[selectedGroup] : undefined; + safeSelectedGroup !== undefined ? snippets[safeSelectedGroup] : undefined; const options = useMemo( () => (groupSnippets ? Object.keys(groupSnippets) : undefined), [groupSnippets] @@ -113,7 +116,7 @@ const CodeGroupSelect = ({
From f92909a27407fdb7ab8e474fd8c65c51e15ea21f Mon Sep 17 00:00:00 2001 From: dmytro Date: Wed, 11 Mar 2026 18:59:23 +0100 Subject: [PATCH 10/12] fix inner border radius for items --- .../src/components/code-group/code-select-dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..9c50e0a 100644 --- a/packages/components/src/components/code-group/code-select-dropdown.tsx +++ b/packages/components/src/components/code-group/code-select-dropdown.tsx @@ -51,7 +51,7 @@ const CodeSelectDropdown = ({ {options.map((option, i) => ( Date: Wed, 11 Mar 2026 13:57:24 -0700 Subject: [PATCH 11/12] fix big menu --- .../src/components/code-group/code-select-dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9c50e0a..9c3c2a7 100644 --- a/packages/components/src/components/code-group/code-select-dropdown.tsx +++ b/packages/components/src/components/code-group/code-select-dropdown.tsx @@ -29,7 +29,7 @@ const CodeSelectDropdown = ({ Date: Wed, 11 Mar 2026 14:14:33 -0700 Subject: [PATCH 12/12] fix dark mode --- .../code-group/code-group-select.tsx | 7 +--- .../code-group/code-select-dropdown.tsx | 36 +++---------------- 2 files changed, 6 insertions(+), 37 deletions(-) diff --git a/packages/components/src/components/code-group/code-group-select.tsx b/packages/components/src/components/code-group/code-group-select.tsx index 82300d9..edf4180 100644 --- a/packages/components/src/components/code-group/code-group-select.tsx +++ b/packages/components/src/components/code-group/code-group-select.tsx @@ -104,17 +104,13 @@ const CodeGroupSelect = ({
{options && ( 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)}