1- import { LazyMotion , domMax } from "motion/react" ;
2- import React from "react" ;
1+ import { domMax , LazyMotion } from "motion/react" ;
2+ import React , { useEffect , useMemo } from "react" ;
33import { SpeakeasyCodeSamplesCore } from "../core.js" ;
44import {
55 GetCodeSamplesRequest ,
66 MethodPaths ,
77} from "../models/operations/getcodesamples.js" ;
88import { OperationId } from "../types/custom.js" ;
9- import { useCodeSampleState } from "./code-sample.state.js" ;
9+ import { getMethodPath , useCodeSampleState } from "./code-sample.state.js" ;
1010import classes from "./code-sample.styles.js" ;
1111import { CodeViewer , ErrorDisplay } from "./code-viewer.js" ;
1212import codehikeTheme from "./codehike/theme.js" ;
1313import { CopyButton } from "./copy-button.js" ;
14- import { LanguageSelector } from "./language-selector.js" ;
1514import { LanguageSelectorSkeleton , LoadingSkeleton } from "./skeleton.js" ;
1615import { getCssVars , useSystemColorMode } from "./styles.js" ;
17- import { type CodeSampleTitleComponent , CodeSampleTitle } from "./titles.js" ;
16+ import {
17+ CodeSampleFilenameTitle ,
18+ CodeSampleTitle ,
19+ type CodeSampleTitleComponent ,
20+ } from "./titles.js" ;
21+ import { prettyLanguageName } from "./utils.js" ;
22+ import { Selector } from "./selector" ;
23+ import { UsageSnippet } from "../models/components" ;
1824
1925export type CodeSamplesViewerProps = {
2026 /** Whether the code snippet should be copyable. */
2127 copyable ?: boolean ;
22- /** Default language to show in the code playground. */
23- defaultLang ?: string ;
28+
29+ /** Default language to show in the code playground. If not found in the snippets, the first one will be used. */
30+ defaultLanguage ?: string ;
31+
2432 /**
2533 * The color mode for the code playground. If "system", the component will
2634 * detect the system color scheme automagically.
@@ -32,50 +40,106 @@ export type CodeSamplesViewerProps = {
3240 * A component to render as the snippet title in the upper-right corner of
3341 * the component. Receives data about the selected code sample. The library
3442 * comes pre-packaged with some sensible options.
43+ * If set to false, no title bar will be shown.
3544 *
36- * @see CodeSampleMethodTitle
45+ * @see CodeSampleTitle
3746 * @see CodeSampleFilenameTitle
3847 * @default CodeSampleMethodTitle
3948 */
40- title ?: CodeSampleTitleComponent | React . ReactNode | string ;
41- /** The operation to get a code sample for. Can be queried by either
42- * operationId or method+path.
49+ title ?: CodeSampleTitleComponent | React . ReactNode | string | false ;
50+ /**
51+ * The operations to get code samples for. If only one is provided, no selector will be shown.
52+ * Can be queried by either operationId or method+path.
4353 */
44- operation : MethodPaths | OperationId ;
54+ operations ? : MethodPaths [ ] | OperationId [ ] ;
4555 /**
4656 * Optional client. Use this if the component is being used outside of
4757 * SpeakeasyCodeSamplesContext.
4858 */
4959 client ?: SpeakeasyCodeSamplesCore ;
60+ /**
61+ * Sets the style of the code window.
62+ */
63+ codeWindowStyle ?: React . CSSProperties ;
64+ /**
65+ * If true, the code window will be fixed to the height of the longest code snippet.
66+ * This can be useful for preventing layout shifts when switching between code snippets.
67+ * Overrides any height set in codeWindowStyle.
68+ */
69+ fixedHeight ?: boolean ;
70+
5071 className ?: string | undefined ;
5172 style ?: React . CSSProperties ;
5273} ;
5374
5475export function CodeSamplesViewer ( {
5576 theme = "system" ,
56- className,
57- title,
58- operation,
59- style,
77+ title = CodeSampleFilenameTitle ,
78+ defaultLanguage,
79+ operations,
6080 copyable,
6181 client : clientProp ,
82+ style,
83+ codeWindowStyle,
84+ fixedHeight,
85+ className,
6286} : CodeSamplesViewerProps ) {
63- const request : GetCodeSamplesRequest = React . useMemo ( ( ) => {
64- if ( typeof operation === "string" ) return { operationIds : [ operation ] } ;
65- return { methoPaths : [ operation ] } ;
66- } , [ operation ] ) ;
87+ const requestParams : GetCodeSamplesRequest = React . useMemo ( ( ) => {
88+ if ( typeof operations ?. [ 0 ] === "string" )
89+ return { operationIds : operations as OperationId [ ] } ;
90+ else if ( operations ?. [ 0 ] ?. method && operations [ 0 ] . path )
91+ return { methodPaths : operations as MethodPaths [ ] } ;
6792
68- const { state, setSelectedLanguage } = useCodeSampleState ( {
93+ return { } ;
94+ } , [ operations ] ) ;
95+
96+ const { state, selectSnippet } = useCodeSampleState ( {
6997 client : clientProp ,
70- requestParams : request ,
98+ requestParams,
7199 } ) ;
72100
101+ // On mount, select the defaults
102+ useEffect ( ( ) => {
103+ if ( ! state . snippets || state . status !== "success" ) return ;
104+ selectSnippet ( { language : defaultLanguage } ) ;
105+ } , [ state . status ] ) ;
106+
73107 const systemColorMode = useSystemColorMode ( ) ;
74108 const codeTheme = React . useMemo ( ( ) => {
75109 if ( theme === "system" ) return codehikeTheme [ systemColorMode ] ;
76110 return codehikeTheme [ theme ] ;
77111 } , [ theme , systemColorMode ] ) ;
78112
113+ const languages : string [ ] = useMemo ( ( ) => {
114+ return [
115+ ...new Set (
116+ state . snippets ?. map ( ( { raw } ) => prettyLanguageName ( raw . language ) ) ,
117+ ) ,
118+ ] ;
119+ } , [ state . snippets ] ) ;
120+
121+ const getOperationKey = ( snippet : UsageSnippet | undefined ) : string => {
122+ let { operationId } = snippet ;
123+ const methodPathDisplay = getMethodPath ( snippet ) ;
124+ if ( ! operationId ) {
125+ operationId = methodPathDisplay ;
126+ }
127+ return operationId ;
128+ } ;
129+
130+ // We need this methodAndPath stuff because not all snippets will have operation ids
131+ // For the selector, we try to show operation ID but fall back on method+path if it's missing
132+ const operationIdToMethodAndPath : Record < string , string > = useMemo ( ( ) => {
133+ return Object . fromEntries (
134+ state . snippets ?. map ( ( { raw } ) => [
135+ getOperationKey ( raw ) ,
136+ getMethodPath ( raw ) ,
137+ ] ) ?? [ ] ,
138+ ) ;
139+ } , [ state . snippets ] ) ;
140+
141+ const operationIds = Object . keys ( operationIdToMethodAndPath ) ;
142+
79143 const longestCodeHeight = React . useMemo ( ( ) => {
80144 const largestLines = Math . max (
81145 ...Object . values ( state . snippets ?? [ ] )
@@ -88,6 +152,13 @@ export function CodeSamplesViewer({
88152 return largestLines * lineHeight + padding * 2 ;
89153 } , [ state . snippets ] ) ;
90154
155+ if ( fixedHeight ) {
156+ codeWindowStyle = {
157+ ...codeWindowStyle ,
158+ height : longestCodeHeight ,
159+ } ;
160+ }
161+
91162 return (
92163 < LazyMotion strict features = { domMax } >
93164 < div
@@ -100,24 +171,44 @@ export function CodeSamplesViewer({
100171 } }
101172 className = { `${ classes . root } ${ className ?? "" } ` }
102173 >
103- < div className = { classes . heading } >
104- < CodeSampleTitle
105- component = { title }
106- status = { state . status }
107- data = { state . selectedSnippet ?. raw }
108- />
109- < >
110- { state . status === "loading" && < LanguageSelectorSkeleton /> }
111- { state . status === "success" && (
112- < LanguageSelector
113- value = { state . selectedSnippet ?. lang }
114- onChange = { setSelectedLanguage }
115- snippets = { state . snippets ?? [ ] }
116- className = { classes . selector }
117- />
118- ) }
119- </ >
120- </ div >
174+ { title !== false && (
175+ < div className = { classes . heading } >
176+ < CodeSampleTitle
177+ component = { title }
178+ status = { state . status }
179+ data = { state . selectedSnippet ?. raw }
180+ />
181+ < div style = { { display : "flex" , gap : "0.75rem" } } >
182+ { state . status === "loading" && (
183+ < div style = { { width : "180px" } } >
184+ < LanguageSelectorSkeleton />
185+ </ div >
186+ ) }
187+ { state . status === "success" && operationIds . length > 1 && (
188+ < Selector
189+ value = { getOperationKey ( state . selectedSnippet ?. raw ) }
190+ values = { operationIds }
191+ onChange = { ( operationId : string ) =>
192+ selectSnippet ( {
193+ methodPath : operationIdToMethodAndPath [ operationId ] ,
194+ } )
195+ }
196+ className = { classes . selector }
197+ />
198+ ) }
199+ { state . status === "success" && (
200+ < Selector
201+ value = { prettyLanguageName (
202+ state . selectedSnippet ?. raw . language ,
203+ ) }
204+ values = { languages }
205+ onChange = { ( language : string ) => selectSnippet ( { language } ) }
206+ className = { classes . selector }
207+ />
208+ ) }
209+ </ div >
210+ </ div >
211+ ) }
121212 < div className = { classes . codeContainer } >
122213 { state . status === "success" && copyable && (
123214 < CopyButton code = { state . selectedSnippet . code } />
@@ -128,7 +219,7 @@ export function CodeSamplesViewer({
128219 < CodeViewer
129220 status = { state . status }
130221 code = { state . selectedSnippet }
131- longestCodeHeight = { longestCodeHeight }
222+ style = { codeWindowStyle }
132223 />
133224 ) }
134225 </ div >
0 commit comments