1
- import { domMax , LazyMotion } from "motion/react" ;
2
- import React , { useEffect , useMemo } from "react" ;
3
- import { SpeakeasyCodeSamplesCore } from "../core.js" ;
4
- import {
5
- GetCodeSamplesRequest ,
6
- MethodPaths ,
7
- } from "../models/operations/getcodesamples.js" ;
8
- import { OperationId } from "../types/custom.js" ;
9
- import { getMethodPath , useCodeSampleState } from "./code-sample.state.js" ;
1
+ import { domMax , LazyMotion } from "motion/react" ;
2
+ import React , { useEffect , useMemo } from "react" ;
3
+ import { SpeakeasyCodeSamplesCore } from "../core.js" ;
4
+ import { GetCodeSamplesRequest , MethodPaths , } from "../models/operations/getcodesamples.js" ;
5
+ import { OperationId } from "../types/custom.js" ;
6
+ import { getMethodPath , useCodeSampleState } from "./code-sample.state.js" ;
10
7
import classes from "./code-sample.styles.js" ;
11
- import { CodeViewer , ErrorDisplay } from "./code-viewer.js" ;
8
+ import { CodeViewer , ErrorDisplay } from "./code-viewer.js" ;
12
9
import codehikeTheme from "./codehike/theme.js" ;
13
- import { CopyButton } from "./copy-button.js" ;
14
- import { LanguageSelectorSkeleton , LoadingSkeleton } from "./skeleton.js" ;
15
- import { getCssVars , useSystemColorMode } from "./styles.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" ;
10
+ import { CopyButton } from "./copy-button.js" ;
11
+ import { LanguageSelectorSkeleton , LoadingSkeleton } from "./skeleton.js" ;
12
+ import { getCssVars , useSystemColorMode } from "./styles.js" ;
13
+ import { CodeSampleFilenameTitle , CodeSampleTitle , type CodeSampleTitleComponent , } from "./titles.js" ;
14
+ import { prettyLanguageName } from "./utils.js" ;
15
+ import { Selector } from "./selector" ;
16
+ import { UsageSnippet } from "../models/components" ;
24
17
25
18
export type CodeSamplesViewerProps = {
26
19
/** Whether the code snippet should be copyable. */
@@ -47,7 +40,8 @@ export type CodeSamplesViewerProps = {
47
40
* @default CodeSampleMethodTitle
48
41
*/
49
42
title ?: CodeSampleTitleComponent | React . ReactNode | string | false ;
50
- /** The operations to get code samples for. If only one is provided, no selector will be shown.
43
+ /**
44
+ * The operations to get code samples for. If only one is provided, no selector will be shown.
51
45
* Can be queried by either operationId or method+path.
52
46
*/
53
47
operations ?: MethodPaths [ ] | OperationId [ ] ;
@@ -60,40 +54,47 @@ export type CodeSamplesViewerProps = {
60
54
* Sets the style of the code window.
61
55
*/
62
56
codeWindowStyle ?: React . CSSProperties ;
57
+ /**
58
+ * If true, the code window will be fixed to the height of the longest code snippet.
59
+ * This can be useful for preventing layout shifts when switching between code snippets.
60
+ * Overrides any height set in codeWindowStyle.
61
+ */
62
+ fixedHeight ?: boolean ;
63
63
64
64
className ?: string | undefined ;
65
65
style ?: React . CSSProperties ;
66
66
} ;
67
67
68
68
export function CodeSamplesViewer ( {
69
- theme = "system" ,
70
- title = CodeSampleFilenameTitle ,
71
- defaultLanguage,
72
- operations,
73
- copyable,
74
- client : clientProp ,
75
- style,
76
- codeWindowStyle,
77
- className,
78
- } : CodeSamplesViewerProps ) {
69
+ theme = "system" ,
70
+ title = CodeSampleFilenameTitle ,
71
+ defaultLanguage,
72
+ operations,
73
+ copyable,
74
+ client : clientProp ,
75
+ style,
76
+ codeWindowStyle,
77
+ fixedHeight,
78
+ className,
79
+ } : CodeSamplesViewerProps ) {
79
80
const requestParams : GetCodeSamplesRequest = React . useMemo ( ( ) => {
80
81
if ( typeof operations ?. [ 0 ] === "string" )
81
- return { operationIds : operations as OperationId [ ] } ;
82
+ return { operationIds : operations as OperationId [ ] } ;
82
83
else if ( operations ?. [ 0 ] ?. method && operations [ 0 ] . path )
83
- return { methodPaths : operations as MethodPaths [ ] } ;
84
+ return { methodPaths : operations as MethodPaths [ ] } ;
84
85
85
86
return { } ;
86
87
} , [ operations ] ) ;
87
88
88
- const { state, selectSnippet } = useCodeSampleState ( {
89
+ const { state, selectSnippet} = useCodeSampleState ( {
89
90
client : clientProp ,
90
91
requestParams,
91
92
} ) ;
92
93
93
94
// On mount, select the defaults
94
95
useEffect ( ( ) => {
95
96
if ( ! state . snippets || state . status !== "success" ) return ;
96
- selectSnippet ( { language : defaultLanguage } ) ;
97
+ selectSnippet ( { language : defaultLanguage } ) ;
97
98
} , [ state . status ] ) ;
98
99
99
100
const systemColorMode = useSystemColorMode ( ) ;
@@ -105,13 +106,13 @@ export function CodeSamplesViewer({
105
106
const languages : string [ ] = useMemo ( ( ) => {
106
107
return [
107
108
...new Set (
108
- state . snippets ?. map ( ( { raw } ) => prettyLanguageName ( raw . language ) ) ,
109
+ state . snippets ?. map ( ( { raw} ) => prettyLanguageName ( raw . language ) ) ,
109
110
) ,
110
111
] ;
111
112
} , [ state . snippets ] ) ;
112
113
113
114
const getOperationKey = ( snippet : UsageSnippet | undefined ) : string => {
114
- let { operationId } = snippet ;
115
+ let { operationId} = snippet ;
115
116
const methodPathDisplay = getMethodPath ( snippet ) ;
116
117
if ( ! operationId ) {
117
118
operationId = methodPathDisplay ;
@@ -123,7 +124,7 @@ export function CodeSamplesViewer({
123
124
// For the selector, we try to show operation ID but fall back on method+path if it's missing
124
125
const operationIdToMethodAndPath : Record < string , string > = useMemo ( ( ) => {
125
126
return Object . fromEntries (
126
- state . snippets ?. map ( ( { raw } ) => [
127
+ state . snippets ?. map ( ( { raw} ) => [
127
128
getOperationKey ( raw ) ,
128
129
getMethodPath ( raw ) ,
129
130
] ) ?? [ ] ,
@@ -132,6 +133,25 @@ export function CodeSamplesViewer({
132
133
133
134
const operationIds = Object . keys ( operationIdToMethodAndPath ) ;
134
135
136
+ const longestCodeHeight = React . useMemo ( ( ) => {
137
+ const largestLines = Math . max (
138
+ ...Object . values ( state . snippets ?? [ ] )
139
+ . filter ( ( snippet ) => snippet . code !== undefined )
140
+ . map ( ( code ) => code . code ! . split ( "\n" ) . length ) ,
141
+ ) ;
142
+
143
+ const lineHeight = 23 ;
144
+ const padding = 12 ;
145
+ return largestLines * lineHeight + padding * 2 ;
146
+ } , [ state . snippets ] ) ;
147
+
148
+ if ( fixedHeight ) {
149
+ codeWindowStyle = {
150
+ ...codeWindowStyle ,
151
+ height : longestCodeHeight ,
152
+ } ;
153
+ }
154
+
135
155
return (
136
156
< LazyMotion strict features = { domMax } >
137
157
< div
@@ -151,10 +171,10 @@ export function CodeSamplesViewer({
151
171
status = { state . status }
152
172
data = { state . selectedSnippet ?. raw }
153
173
/>
154
- < div style = { { display : "flex" , gap : "0.75rem" } } >
174
+ < div style = { { display : "flex" , gap : "0.75rem" } } >
155
175
{ state . status === "loading" && (
156
- < div style = { { width : "180px" } } >
157
- < LanguageSelectorSkeleton />
176
+ < div style = { { width : "180px" } } >
177
+ < LanguageSelectorSkeleton />
158
178
</ div >
159
179
) }
160
180
{ state . status === "success" && operationIds . length > 1 && (
@@ -175,7 +195,7 @@ export function CodeSamplesViewer({
175
195
state . selectedSnippet ?. raw . language ,
176
196
) }
177
197
values = { languages }
178
- onChange = { ( language : string ) => selectSnippet ( { language } ) }
198
+ onChange = { ( language : string ) => selectSnippet ( { language} ) }
179
199
className = { classes . selector }
180
200
/>
181
201
) }
@@ -184,10 +204,10 @@ export function CodeSamplesViewer({
184
204
) }
185
205
< div className = { classes . codeContainer } >
186
206
{ state . status === "success" && copyable && (
187
- < CopyButton code = { state . selectedSnippet . code } />
207
+ < CopyButton code = { state . selectedSnippet . code } />
188
208
) }
189
- { state . status === "loading" && < LoadingSkeleton /> }
190
- { state . status === "error" && < ErrorDisplay error = { state . error } /> }
209
+ { state . status === "loading" && < LoadingSkeleton /> }
210
+ { state . status === "error" && < ErrorDisplay error = { state . error } /> }
191
211
{ state . status === "success" && (
192
212
< CodeViewer
193
213
status = { state . status }
0 commit comments