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" ;
3
3
import { SpeakeasyCodeSamplesCore } from "../core.js" ;
4
4
import {
5
5
GetCodeSamplesRequest ,
6
6
MethodPaths ,
7
7
} from "../models/operations/getcodesamples.js" ;
8
8
import { OperationId } from "../types/custom.js" ;
9
- import { useCodeSampleState } from "./code-sample.state.js" ;
9
+ import { getMethodPath , useCodeSampleState } from "./code-sample.state.js" ;
10
10
import classes from "./code-sample.styles.js" ;
11
11
import { CodeViewer , ErrorDisplay } from "./code-viewer.js" ;
12
12
import codehikeTheme from "./codehike/theme.js" ;
13
13
import { CopyButton } from "./copy-button.js" ;
14
- import { LanguageSelector } from "./language-selector.js" ;
15
14
import { LanguageSelectorSkeleton , LoadingSkeleton } from "./skeleton.js" ;
16
15
import { 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" ;
18
24
19
25
export type CodeSamplesViewerProps = {
20
26
/** Whether the code snippet should be copyable. */
21
27
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
+
24
32
/**
25
33
* The color mode for the code playground. If "system", the component will
26
34
* detect the system color scheme automagically.
@@ -32,50 +40,106 @@ export type CodeSamplesViewerProps = {
32
40
* A component to render as the snippet title in the upper-right corner of
33
41
* the component. Receives data about the selected code sample. The library
34
42
* comes pre-packaged with some sensible options.
43
+ * If set to false, no title bar will be shown.
35
44
*
36
- * @see CodeSampleMethodTitle
45
+ * @see CodeSampleTitle
37
46
* @see CodeSampleFilenameTitle
38
47
* @default CodeSampleMethodTitle
39
48
*/
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.
43
53
*/
44
- operation : MethodPaths | OperationId ;
54
+ operations ? : MethodPaths [ ] | OperationId [ ] ;
45
55
/**
46
56
* Optional client. Use this if the component is being used outside of
47
57
* SpeakeasyCodeSamplesContext.
48
58
*/
49
59
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
+
50
71
className ?: string | undefined ;
51
72
style ?: React . CSSProperties ;
52
73
} ;
53
74
54
75
export function CodeSamplesViewer ( {
55
76
theme = "system" ,
56
- className,
57
- title,
58
- operation,
59
- style,
77
+ title = CodeSampleFilenameTitle ,
78
+ defaultLanguage,
79
+ operations,
60
80
copyable,
61
81
client : clientProp ,
82
+ style,
83
+ codeWindowStyle,
84
+ fixedHeight,
85
+ className,
62
86
} : 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 [ ] } ;
67
92
68
- const { state, setSelectedLanguage } = useCodeSampleState ( {
93
+ return { } ;
94
+ } , [ operations ] ) ;
95
+
96
+ const { state, selectSnippet } = useCodeSampleState ( {
69
97
client : clientProp ,
70
- requestParams : request ,
98
+ requestParams,
71
99
} ) ;
72
100
101
+ // On mount, select the defaults
102
+ useEffect ( ( ) => {
103
+ if ( ! state . snippets || state . status !== "success" ) return ;
104
+ selectSnippet ( { language : defaultLanguage } ) ;
105
+ } , [ state . status ] ) ;
106
+
73
107
const systemColorMode = useSystemColorMode ( ) ;
74
108
const codeTheme = React . useMemo ( ( ) => {
75
109
if ( theme === "system" ) return codehikeTheme [ systemColorMode ] ;
76
110
return codehikeTheme [ theme ] ;
77
111
} , [ theme , systemColorMode ] ) ;
78
112
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
+
79
143
const longestCodeHeight = React . useMemo ( ( ) => {
80
144
const largestLines = Math . max (
81
145
...Object . values ( state . snippets ?? [ ] )
@@ -88,6 +152,13 @@ export function CodeSamplesViewer({
88
152
return largestLines * lineHeight + padding * 2 ;
89
153
} , [ state . snippets ] ) ;
90
154
155
+ if ( fixedHeight ) {
156
+ codeWindowStyle = {
157
+ ...codeWindowStyle ,
158
+ height : longestCodeHeight ,
159
+ } ;
160
+ }
161
+
91
162
return (
92
163
< LazyMotion strict features = { domMax } >
93
164
< div
@@ -100,24 +171,44 @@ export function CodeSamplesViewer({
100
171
} }
101
172
className = { `${ classes . root } ${ className ?? "" } ` }
102
173
>
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
+ ) }
121
212
< div className = { classes . codeContainer } >
122
213
{ state . status === "success" && copyable && (
123
214
< CopyButton code = { state . selectedSnippet . code } />
@@ -128,7 +219,7 @@ export function CodeSamplesViewer({
128
219
< CodeViewer
129
220
status = { state . status }
130
221
code = { state . selectedSnippet }
131
- longestCodeHeight = { longestCodeHeight }
222
+ style = { codeWindowStyle }
132
223
/>
133
224
) }
134
225
</ div >
0 commit comments