From aeb3f7ca7b4deed9e53474c3d420ae6735882da3 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 4 Apr 2026 10:29:20 -0700 Subject: [PATCH 1/2] feat(website): add animated architecture flow diagram to introduction --- .../docs-v2/getting-started/introduction.mdx | 17 +- .../src/components/docs/ArchFlowDiagram.tsx | 264 ++++++++++++++++++ .../src/components/docs/MdxRenderer.tsx | 2 + 3 files changed, 268 insertions(+), 15 deletions(-) create mode 100644 apps/website/src/components/docs/ArchFlowDiagram.tsx diff --git a/apps/website/content/docs-v2/getting-started/introduction.mdx b/apps/website/content/docs-v2/getting-started/introduction.mdx index 3aeae6115..e31ab4ba0 100644 --- a/apps/website/content/docs-v2/getting-started/introduction.mdx +++ b/apps/website/content/docs-v2/getting-started/introduction.mdx @@ -27,22 +27,9 @@ No RxJS. No manual subscriptions. No async pipes. Just Signals that work with An ## The Architecture -StreamResource sits between your Angular app and LangGraph Platform: +Watch a full conversation turn flow through the stack — from user input to rendered response: - - -Creates a reactive resource bound to a specific agent. All state is exposed as Signals. - - -Sends HTTP POST to LangGraph Platform, receives Server-Sent Events with state updates. - - -Executes nodes, calls tools, manages checkpoints. Streams results back in real-time. - - -As tokens arrive, `messages()` updates. Angular re-renders only the affected components. - - + ## Build Your Agent diff --git a/apps/website/src/components/docs/ArchFlowDiagram.tsx b/apps/website/src/components/docs/ArchFlowDiagram.tsx new file mode 100644 index 000000000..5aaf9aa71 --- /dev/null +++ b/apps/website/src/components/docs/ArchFlowDiagram.tsx @@ -0,0 +1,264 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { tokens } from '../../../lib/design-tokens'; + +// Animation phases for a full AI turn +type Phase = 'idle' | 'submit' | 'processing' | 'streaming' | 'rendered' | 'complete'; + +const PHASE_LABELS: Record = { + idle: 'Waiting for user input...', + submit: 'User sends message', + processing: 'Agent processing...', + streaming: 'Tokens streaming back', + rendered: 'UI updated', + complete: 'Turn complete', +}; + +const PHASE_DURATION: Record = { + idle: 1200, + submit: 1000, + processing: 1500, + streaming: 2000, + rendered: 1200, + complete: 800, +}; + +export function ArchFlowDiagram() { + const [phase, setPhase] = useState('idle'); + const [tokenCount, setTokenCount] = useState(0); + const [streamedText, setStreamedText] = useState(''); + + const fullResponse = 'Hello! I can help you with that.'; + + useEffect(() => { + const phases: Phase[] = ['idle', 'submit', 'processing', 'streaming', 'rendered', 'complete']; + let phaseIdx = 0; + let tokenInterval: ReturnType; + + const advancePhase = () => { + const currentPhase = phases[phaseIdx]; + setPhase(currentPhase); + + if (currentPhase === 'streaming') { + let t = 0; + setStreamedText(''); + setTokenCount(0); + tokenInterval = setInterval(() => { + t++; + setTokenCount(t); + setStreamedText(fullResponse.slice(0, t * 2)); + if (t * 2 >= fullResponse.length) { + clearInterval(tokenInterval); + } + }, 100); + } + + if (currentPhase === 'idle') { + setStreamedText(''); + setTokenCount(0); + } + + phaseIdx = (phaseIdx + 1) % phases.length; + setTimeout(advancePhase, PHASE_DURATION[currentPhase]); + }; + + advancePhase(); + return () => { clearInterval(tokenInterval); }; + }, []); + + const isActive = (node: string) => { + if (node === 'angular' && (phase === 'idle' || phase === 'submit' || phase === 'rendered')) return true; + if (node === 'bridge' && (phase === 'submit' || phase === 'streaming' || phase === 'rendered')) return true; + if (node === 'transport' && (phase === 'submit' || phase === 'streaming')) return true; + if (node === 'langgraph' && (phase === 'processing' || phase === 'streaming')) return true; + return false; + }; + + const showDownFlow = phase === 'submit'; + const showUpFlow = phase === 'streaming' || phase === 'rendered'; + const showProcessing = phase === 'processing'; + + return ( +
+ {/* Phase indicator */} +
+ + + {PHASE_LABELS[phase]} + +
+ + + + {/* Node: Angular */} +
+
🅰️
+
+
Your Angular App
+
+ {phase === 'rendered' || phase === 'complete' + ? Rendering: chat.messages() + : 'Components call submit() and bind Signals in templates'} +
+ {/* Live message preview */} + {(phase === 'streaming' || phase === 'rendered' || phase === 'complete') && ( +
+ {streamedText} +
+ )} +
+ {['OnPush', 'computed()', 'signal()'].map(c => ( + {c} + ))} +
+
+
+ + {/* Connector 1 */} +
+
submit(input)
+
+ {showDownFlow &&
} +
+
↓ message
+
+ + {/* Node: streamResource */} +
+
+
+
streamResource()
+
+ {showUpFlow + ? Converting SSE events → Angular Signals + : 'Reactive bridge — BehaviorSubject → toSignal()'} +
+
+ {['toSignal()', 'throttle()', 'DestroyRef'].map(c => ( + {c} + ))} +
+
+
+ + {/* Connector 2 */} +
+
HTTP POST
+
+ {showDownFlow &&
} + {showUpFlow &&
} + {showUpFlow &&
} +
+
SSE ↑
+
+ + {/* Node: Transport */} +
+
📡
+
+
FetchStreamTransport
+
SSE connection, thread management, event parsing
+
+ {['langgraph-sdk', 'AsyncIterable', 'AbortSignal'].map(c => ( + {c} + ))} +
+
+
+ + {/* Connector 3 */} +
+
threads
+
+ {showDownFlow &&
} + {showUpFlow &&
} + {showUpFlow &&
} + {showUpFlow &&
} +
+
+ {showUpFlow ? `${tokenCount} tokens` : 'token stream'} +
+
+ + {/* Node: LangGraph */} +
+
+ {showProcessing ? ⚙️ : '🧠'} +
+
+
LangGraph Platform
+
+ {showProcessing + ? Running call_model node... + : 'Agent graph, state management, checkpoints, tool execution'} +
+
+ {['StateGraph', 'MessagesState', 'Checkpoints', 'Tools'].map(c => ( + {c} + ))} +
+
+
+
+ ); +} diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index d1f6001c8..d9c628242 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -5,6 +5,7 @@ import { Steps, Step } from './mdx/Steps'; import { Tabs, Tab } from './mdx/Tabs'; import { Card, CardGroup } from './mdx/Card'; import { CodeGroup } from './mdx/CodeGroup'; +import { ArchFlowDiagram } from './ArchFlowDiagram'; import { DocsBreadcrumb } from './DocsBreadcrumb'; import { DocsPrevNext } from './DocsPrevNext'; import rehypePrettyCode from 'rehype-pretty-code'; @@ -19,6 +20,7 @@ const mdxComponents = { Card, CardGroup, CodeGroup, + ArchFlowDiagram, }; const rehypeOptions = { From e38718c0b49ac9ddd1771e4b03ecb6e91ee2fee0 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 4 Apr 2026 10:38:17 -0700 Subject: [PATCH 2/2] feat(website): redesign arch diagram, fix tabs/table/deploy/copy button - Redesign ArchFlowDiagram as chat+console simulation showing full AI turn - Fix Tab labels using label prop instead of items array - Replace Key Concepts table with CardGroup (better MDX compat) - Expand Deploy section with 4 steps + env config + Angular build - Add copy button to all code blocks via Pre component - Use gpt-5-mini in code examples --- .../docs-v2/getting-started/introduction.mdx | 91 +++-- .../src/components/docs/ArchFlowDiagram.tsx | 369 +++++++----------- .../src/components/docs/MdxRenderer.tsx | 2 + .../src/components/docs/mdx/CodeBlock.tsx | 42 ++ apps/website/src/components/docs/mdx/Tabs.tsx | 18 +- 5 files changed, 264 insertions(+), 258 deletions(-) create mode 100644 apps/website/src/components/docs/mdx/CodeBlock.tsx diff --git a/apps/website/content/docs-v2/getting-started/introduction.mdx b/apps/website/content/docs-v2/getting-started/introduction.mdx index e31ab4ba0..eddc6d57d 100644 --- a/apps/website/content/docs-v2/getting-started/introduction.mdx +++ b/apps/website/content/docs-v2/getting-started/introduction.mdx @@ -35,8 +35,8 @@ Watch a full conversation turn flow through the stack — from user input to ren LangGraph agents are Python programs defined as directed graphs. Here's a minimal chat agent using the example from this repository: - - + + ```python # examples/chat-agent/src/chat_agent/agent.py @@ -45,7 +45,7 @@ from langchain_core.runnables import RunnableConfig from langgraph.graph import END, START, MessagesState, StateGraph from langchain_openai import ChatOpenAI -llm = ChatOpenAI(model="gpt-4o-mini") +llm = ChatOpenAI(model="gpt-5-mini") def call_model(state: MessagesState, config: RunnableConfig) -> dict: """Invoke the LLM with the current message history.""" @@ -66,7 +66,7 @@ graph = builder.compile() ``` - + ```json { @@ -149,8 +149,8 @@ export const appConfig: ApplicationConfig = { - - + + ```typescript // chat.component.ts @@ -189,7 +189,7 @@ export class ChatComponent { ``` - + ```html @@ -244,46 +244,91 @@ Open `http://localhost:4200` and start chatting with your agent. Messages stream Here's what streamResource() gives you out of the box: -| Feature | Signal | Description | -|---------|--------|-------------| -| **Messages** | `chat.messages()` | Live message list, updates as tokens arrive | -| **Status** | `chat.status()` | Current state: idle, loading, resolved, error | -| **Thread persistence** | `threadId` option | Conversations survive page refreshes | -| **Interrupts** | `chat.interrupt()` | Agent pauses for human input | -| **History** | `chat.history()` | Full checkpoint timeline for time-travel | -| **Subagents** | `chat.subagents()` | Track delegated agent work | -| **Tool calls** | `chat.toolCalls()` | See what tools the agent is using | + + + `chat.messages()` — live message list that updates as each token arrives from the agent + + + `chat.status()` — current state: idle, loading, resolved, or error + + + `threadId` option — conversations survive page refreshes via localStorage or backend + + + `chat.interrupt()` — agent pauses for human approval, your UI handles the decision + + + `chat.history()` — full checkpoint timeline for debugging and branching + + + `chat.subagents()` — track delegated agent work across multiple graphs + + + `chat.toolCalls()` — see what tools the agent is invoking in real-time + + + `MockStreamTransport` — deterministic testing without a running server + + ## Deploy to Production -When you're ready to go live, deploy your agent to LangGraph Cloud. +When you're ready to go live, deploy your agent to LangGraph Cloud and point your Angular app to the deployment URL. -Your agent code (the Python project with `langgraph.json`) needs to be in a GitHub repository. +Your agent code (the Python project with `langgraph.json`) needs to be in a GitHub repository. Make sure your `langgraph.json` references the correct graph entry point. + +```bash +git init && git add . && git commit -m "initial agent" +gh repo create my-agent --public --source=. --push +``` -Go to [LangSmith Deployments](https://smith.langchain.com) and click **+ New Deployment**. Connect your GitHub repo and deploy. This takes about 15 minutes. +Go to [LangSmith Deployments](https://smith.langchain.com) and click **+ New Deployment**. Connect your GitHub account, select your repository, and deploy. The first deployment takes about 15 minutes. + +You'll receive a deployment URL like `https://my-agent-abc123.langsmith.dev`. -Point `apiUrl` to your deployment URL: +Point `apiUrl` to your deployment URL and set up environment-based configuration: ```typescript +// environment.ts +export const environment = { + langgraphUrl: 'http://localhost:2024', // dev +}; + +// environment.prod.ts +export const environment = { + langgraphUrl: 'https://my-agent-abc123.langsmith.dev', // prod +}; + +// app.config.ts provideStreamResource({ - apiUrl: 'https://your-deployment.langsmith.dev', + apiUrl: environment.langgraphUrl, }) ``` + + + +Deploy your Angular frontend to any hosting platform — Vercel, Netlify, AWS, or your own infrastructure. Since streamResource() is a stateless client, your frontend has no server-side state requirements. + +```bash +ng build --configuration production +# Deploy dist/ to your hosting platform +``` + - -Your Angular app is a stateless client. All agent state lives on LangGraph Platform. This means you can deploy your Angular app anywhere — CDN, edge, SSR — without state management concerns. + +Your Angular app is a stateless client. All agent state — threads, checkpoints, memory — lives on LangGraph Platform. This means you can deploy your frontend anywhere (CDN, edge, SSR) without state management concerns. Scale your frontend independently of your agent infrastructure. ## What's Next diff --git a/apps/website/src/components/docs/ArchFlowDiagram.tsx b/apps/website/src/components/docs/ArchFlowDiagram.tsx index 5aaf9aa71..bef621fd7 100644 --- a/apps/website/src/components/docs/ArchFlowDiagram.tsx +++ b/apps/website/src/components/docs/ArchFlowDiagram.tsx @@ -1,264 +1,169 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { tokens } from '../../../lib/design-tokens'; -// Animation phases for a full AI turn -type Phase = 'idle' | 'submit' | 'processing' | 'streaming' | 'rendered' | 'complete'; - -const PHASE_LABELS: Record = { - idle: 'Waiting for user input...', - submit: 'User sends message', - processing: 'Agent processing...', - streaming: 'Tokens streaming back', - rendered: 'UI updated', - complete: 'Turn complete', -}; +interface LogEntry { + time: string; + source: 'angular' | 'transport' | 'langgraph' | 'signal'; + message: string; +} -const PHASE_DURATION: Record = { - idle: 1200, - submit: 1000, - processing: 1500, - streaming: 2000, - rendered: 1200, - complete: 800, +const SCENARIO: { delay: number; chatBubble?: { role: 'user' | 'assistant'; text: string; streaming?: boolean }; log: LogEntry }[] = [ + { delay: 0, chatBubble: { role: 'user', text: 'How do Angular Signals work with streaming?' }, log: { time: '0.00s', source: 'angular', message: 'chat.submit({ messages: [userMsg] })' } }, + { delay: 800, log: { time: '0.02s', source: 'transport', message: 'POST /threads/t_8f3a/runs/stream → 200' } }, + { delay: 1200, log: { time: '0.04s', source: 'langgraph', message: 'Executing node: call_model (gpt-5-mini)' } }, + { delay: 2200, log: { time: '0.82s', source: 'langgraph', message: 'SSE event: { type: "values", messages: [...] }' } }, + { delay: 2600, log: { time: '0.84s', source: 'transport', message: 'Received chunk → messages$.next([...])' } }, + { delay: 2800, log: { time: '0.85s', source: 'signal', message: 'messages() updated → 2 messages' } }, + { delay: 3000, chatBubble: { role: 'assistant', text: 'Angular Signals', streaming: true }, log: { time: '0.86s', source: 'signal', message: 'status() → "loading"' } }, + { delay: 3400, chatBubble: { role: 'assistant', text: 'Angular Signals provide a synchronous', streaming: true }, log: { time: '1.12s', source: 'transport', message: 'Received chunk → values event' } }, + { delay: 3900, chatBubble: { role: 'assistant', text: 'Angular Signals provide a synchronous, reactive way to', streaming: true }, log: { time: '1.45s', source: 'signal', message: 'messages() updated → streaming token' } }, + { delay: 4500, chatBubble: { role: 'assistant', text: 'Angular Signals provide a synchronous, reactive way to track streaming state.', streaming: true }, log: { time: '1.82s', source: 'langgraph', message: 'SSE event: { type: "values", status: "done" }' } }, + { delay: 5200, chatBubble: { role: 'assistant', text: 'Angular Signals provide a synchronous, reactive way to track streaming state. Each token updates the Signal, and OnPush change detection re-renders automatically.' }, log: { time: '2.10s', source: 'signal', message: 'status() → "resolved" ✓' } }, + { delay: 6000, log: { time: '2.12s', source: 'angular', message: 'Template re-rendered (OnPush) — 1 component' } }, +]; + +const SOURCE_COLORS: Record = { + angular: { bg: 'rgba(221,0,49,0.08)', text: '#c62828', label: 'ANGULAR' }, + transport: { bg: 'rgba(100,80,200,0.08)', text: '#5e35b1', label: 'TRANSPORT' }, + langgraph: { bg: 'rgba(0,64,144,0.08)', text: '#004090', label: 'LANGGRAPH' }, + signal: { bg: 'rgba(16,185,129,0.08)', text: '#059669', label: 'SIGNAL' }, }; export function ArchFlowDiagram() { - const [phase, setPhase] = useState('idle'); - const [tokenCount, setTokenCount] = useState(0); - const [streamedText, setStreamedText] = useState(''); - - const fullResponse = 'Hello! I can help you with that.'; + const [logs, setLogs] = useState([]); + const [bubbles, setBubbles] = useState<{ role: 'user' | 'assistant'; text: string; streaming?: boolean }[]>([]); + const [cycle, setCycle] = useState(0); + const logRef = useRef(null); useEffect(() => { - const phases: Phase[] = ['idle', 'submit', 'processing', 'streaming', 'rendered', 'complete']; - let phaseIdx = 0; - let tokenInterval: ReturnType; - - const advancePhase = () => { - const currentPhase = phases[phaseIdx]; - setPhase(currentPhase); - - if (currentPhase === 'streaming') { - let t = 0; - setStreamedText(''); - setTokenCount(0); - tokenInterval = setInterval(() => { - t++; - setTokenCount(t); - setStreamedText(fullResponse.slice(0, t * 2)); - if (t * 2 >= fullResponse.length) { - clearInterval(tokenInterval); + const timeouts: ReturnType[] = []; + + const runScenario = () => { + setLogs([]); + setBubbles([]); + + SCENARIO.forEach((step, i) => { + timeouts.push(setTimeout(() => { + setLogs(prev => [...prev, step.log]); + if (step.chatBubble) { + setBubbles(prev => { + const existing = prev.findIndex(b => b.role === step.chatBubble!.role && b.role === 'assistant'); + if (existing >= 0 && step.chatBubble!.role === 'assistant') { + const updated = [...prev]; + updated[existing] = step.chatBubble!; + return updated; + } + return [...prev, step.chatBubble!]; + }); } - }, 100); - } - - if (currentPhase === 'idle') { - setStreamedText(''); - setTokenCount(0); - } - - phaseIdx = (phaseIdx + 1) % phases.length; - setTimeout(advancePhase, PHASE_DURATION[currentPhase]); + if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; + }, step.delay)); + }); + + // Restart after completion + timeouts.push(setTimeout(() => { + setCycle(c => c + 1); + }, 8000)); }; - advancePhase(); - return () => { clearInterval(tokenInterval); }; - }, []); - - const isActive = (node: string) => { - if (node === 'angular' && (phase === 'idle' || phase === 'submit' || phase === 'rendered')) return true; - if (node === 'bridge' && (phase === 'submit' || phase === 'streaming' || phase === 'rendered')) return true; - if (node === 'transport' && (phase === 'submit' || phase === 'streaming')) return true; - if (node === 'langgraph' && (phase === 'processing' || phase === 'streaming')) return true; - return false; - }; - - const showDownFlow = phase === 'submit'; - const showUpFlow = phase === 'streaming' || phase === 'rendered'; - const showProcessing = phase === 'processing'; + runScenario(); + return () => timeouts.forEach(clearTimeout); + }, [cycle]); return (
- {/* Phase indicator */} -
- - - {PHASE_LABELS[phase]} - -
- - - - {/* Node: Angular */} -
-
🅰️
-
-
Your Angular App
-
- {phase === 'rendered' || phase === 'complete' - ? Rendering: chat.messages() - : 'Components call submit() and bind Signals in templates'} -
- {/* Live message preview */} - {(phase === 'streaming' || phase === 'rendered' || phase === 'complete') && ( -
- {streamedText} -
- )} -
- {['OnPush', 'computed()', 'signal()'].map(c => ( - {c} - ))} -
-
-
- - {/* Connector 1 */} -
-
submit(input)
-
- {showDownFlow &&
} -
-
↓ message
-
- - {/* Node: streamResource */} + {/* Header bar */}
-
-
-
streamResource()
-
- {showUpFlow - ? Converting SSE events → Angular Signals - : 'Reactive bridge — BehaviorSubject → toSignal()'} -
-
- {['toSignal()', 'throttle()', 'DestroyRef'].map(c => ( - {c} - ))} -
+
+
+
+
+ streamResource() — live architecture flow + localhost:4200
- {/* Connector 2 */} -
-
HTTP POST
-
- {showDownFlow &&
} - {showUpFlow &&
} - {showUpFlow &&
} -
-
SSE ↑
-
- - {/* Node: Transport */} -
-
📡
-
-
FetchStreamTransport
-
SSE connection, thread management, event parsing
-
- {['langgraph-sdk', 'AsyncIterable', 'AbortSignal'].map(c => ( - {c} +
+ {/* Left: Chat simulation */} +
+
Chat Interface
+ +
+ {bubbles.map((b, i) => ( +
+ {b.role === 'assistant' && ( +
AI
+ )} +
+ {b.text} + {b.streaming && } +
+
))}
-
- {/* Connector 3 */} -
-
threads
-
- {showDownFlow &&
} - {showUpFlow &&
} - {showUpFlow &&
} - {showUpFlow &&
} -
-
- {showUpFlow ? `${tokenCount} tokens` : 'token stream'} + {/* Right: Console log */} +
+
Developer Console
+ + {logs.map((log, i) => { + const sc = SOURCE_COLORS[log.source]; + return ( +
+ {log.time} + {sc.label} + {log.message} +
+ ); + })} + + {logs.length === 0 && ( +
Waiting for interaction...
+ )}
- {/* Node: LangGraph */} -
-
- {showProcessing ? ⚙️ : '🧠'} -
-
-
LangGraph Platform
-
- {showProcessing - ? Running call_model node... - : 'Agent graph, state management, checkpoints, tool execution'} -
-
- {['StateGraph', 'MessagesState', 'Checkpoints', 'Tools'].map(c => ( - {c} - ))} -
-
-
+
); } diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index d9c628242..aa67747eb 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -5,6 +5,7 @@ import { Steps, Step } from './mdx/Steps'; import { Tabs, Tab } from './mdx/Tabs'; import { Card, CardGroup } from './mdx/Card'; import { CodeGroup } from './mdx/CodeGroup'; +import { Pre } from './mdx/CodeBlock'; import { ArchFlowDiagram } from './ArchFlowDiagram'; import { DocsBreadcrumb } from './DocsBreadcrumb'; import { DocsPrevNext } from './DocsPrevNext'; @@ -21,6 +22,7 @@ const mdxComponents = { CardGroup, CodeGroup, ArchFlowDiagram, + pre: Pre, }; const rehypeOptions = { diff --git a/apps/website/src/components/docs/mdx/CodeBlock.tsx b/apps/website/src/components/docs/mdx/CodeBlock.tsx new file mode 100644 index 000000000..6b4d10647 --- /dev/null +++ b/apps/website/src/components/docs/mdx/CodeBlock.tsx @@ -0,0 +1,42 @@ +'use client'; +import { useRef, useState } from 'react'; + +export function Pre({ children, ...props }: React.HTMLAttributes) { + const ref = useRef(null); + const [copied, setCopied] = useState(false); + + const copy = async () => { + const text = ref.current?.textContent ?? ''; + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
{children}
+ +
+ ); +} diff --git a/apps/website/src/components/docs/mdx/Tabs.tsx b/apps/website/src/components/docs/mdx/Tabs.tsx index 772f8756d..89fac6851 100644 --- a/apps/website/src/components/docs/mdx/Tabs.tsx +++ b/apps/website/src/components/docs/mdx/Tabs.tsx @@ -1,11 +1,23 @@ 'use client'; -import { useState, Children } from 'react'; +import { useState, Children, isValidElement } from 'react'; import { tokens } from '../../../../lib/design-tokens'; +interface TabProps { + label?: string; + children: React.ReactNode; +} + export function Tabs({ items, children }: { items?: string[]; children: React.ReactNode }) { const [active, setActive] = useState(0); const tabs = Children.toArray(children); - const labels = items ?? tabs.map((_, i) => `Tab ${i + 1}`); + + // Extract labels: from items prop, from Tab label prop, or fallback + const labels = items ?? tabs.map((child, i) => { + if (isValidElement(child) && child.props.label) { + return child.props.label; + } + return `Tab ${i + 1}`; + }); return (
@@ -40,6 +52,6 @@ export function Tabs({ items, children }: { items?: string[]; children: React.Re ); } -export function Tab({ children }: { children: React.ReactNode }) { +export function Tab({ children }: TabProps) { return
{children}
; }