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" ;
107import classes from "./code-sample.styles.js" ;
11- import { CodeViewer , ErrorDisplay } from "./code-viewer.js" ;
8+ import { CodeViewer , ErrorDisplay } from "./code-viewer.js" ;
129import 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" ;
2417
2518export type CodeSamplesViewerProps = {
2619 /** Whether the code snippet should be copyable. */
@@ -47,7 +40,8 @@ export type CodeSamplesViewerProps = {
4740 * @default CodeSampleMethodTitle
4841 */
4942 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.
5145 * Can be queried by either operationId or method+path.
5246 */
5347 operations ?: MethodPaths [ ] | OperationId [ ] ;
@@ -60,40 +54,47 @@ export type CodeSamplesViewerProps = {
6054 * Sets the style of the code window.
6155 */
6256 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 ;
6363
6464 className ?: string | undefined ;
6565 style ?: React . CSSProperties ;
6666} ;
6767
6868export 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 ) {
7980 const requestParams : GetCodeSamplesRequest = React . useMemo ( ( ) => {
8081 if ( typeof operations ?. [ 0 ] === "string" )
81- return { operationIds : operations as OperationId [ ] } ;
82+ return { operationIds : operations as OperationId [ ] } ;
8283 else if ( operations ?. [ 0 ] ?. method && operations [ 0 ] . path )
83- return { methodPaths : operations as MethodPaths [ ] } ;
84+ return { methodPaths : operations as MethodPaths [ ] } ;
8485
8586 return { } ;
8687 } , [ operations ] ) ;
8788
88- const { state, selectSnippet } = useCodeSampleState ( {
89+ const { state, selectSnippet} = useCodeSampleState ( {
8990 client : clientProp ,
9091 requestParams,
9192 } ) ;
9293
9394 // On mount, select the defaults
9495 useEffect ( ( ) => {
9596 if ( ! state . snippets || state . status !== "success" ) return ;
96- selectSnippet ( { language : defaultLanguage } ) ;
97+ selectSnippet ( { language : defaultLanguage } ) ;
9798 } , [ state . status ] ) ;
9899
99100 const systemColorMode = useSystemColorMode ( ) ;
@@ -105,13 +106,13 @@ export function CodeSamplesViewer({
105106 const languages : string [ ] = useMemo ( ( ) => {
106107 return [
107108 ...new Set (
108- state . snippets ?. map ( ( { raw } ) => prettyLanguageName ( raw . language ) ) ,
109+ state . snippets ?. map ( ( { raw} ) => prettyLanguageName ( raw . language ) ) ,
109110 ) ,
110111 ] ;
111112 } , [ state . snippets ] ) ;
112113
113114 const getOperationKey = ( snippet : UsageSnippet | undefined ) : string => {
114- let { operationId } = snippet ;
115+ let { operationId} = snippet ;
115116 const methodPathDisplay = getMethodPath ( snippet ) ;
116117 if ( ! operationId ) {
117118 operationId = methodPathDisplay ;
@@ -123,7 +124,7 @@ export function CodeSamplesViewer({
123124 // For the selector, we try to show operation ID but fall back on method+path if it's missing
124125 const operationIdToMethodAndPath : Record < string , string > = useMemo ( ( ) => {
125126 return Object . fromEntries (
126- state . snippets ?. map ( ( { raw } ) => [
127+ state . snippets ?. map ( ( { raw} ) => [
127128 getOperationKey ( raw ) ,
128129 getMethodPath ( raw ) ,
129130 ] ) ?? [ ] ,
@@ -132,6 +133,25 @@ export function CodeSamplesViewer({
132133
133134 const operationIds = Object . keys ( operationIdToMethodAndPath ) ;
134135
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+
135155 return (
136156 < LazyMotion strict features = { domMax } >
137157 < div
@@ -151,10 +171,10 @@ export function CodeSamplesViewer({
151171 status = { state . status }
152172 data = { state . selectedSnippet ?. raw }
153173 />
154- < div style = { { display : "flex" , gap : "0.75rem" } } >
174+ < div style = { { display : "flex" , gap : "0.75rem" } } >
155175 { state . status === "loading" && (
156- < div style = { { width : "180px" } } >
157- < LanguageSelectorSkeleton />
176+ < div style = { { width : "180px" } } >
177+ < LanguageSelectorSkeleton />
158178 </ div >
159179 ) }
160180 { state . status === "success" && operationIds . length > 1 && (
@@ -175,7 +195,7 @@ export function CodeSamplesViewer({
175195 state . selectedSnippet ?. raw . language ,
176196 ) }
177197 values = { languages }
178- onChange = { ( language : string ) => selectSnippet ( { language } ) }
198+ onChange = { ( language : string ) => selectSnippet ( { language} ) }
179199 className = { classes . selector }
180200 />
181201 ) }
@@ -184,10 +204,10 @@ export function CodeSamplesViewer({
184204 ) }
185205 < div className = { classes . codeContainer } >
186206 { state . status === "success" && copyable && (
187- < CopyButton code = { state . selectedSnippet . code } />
207+ < CopyButton code = { state . selectedSnippet . code } />
188208 ) }
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 } /> }
191211 { state . status === "success" && (
192212 < CodeViewer
193213 status = { state . status }
0 commit comments