1
1
import * as React from 'react' ;
2
- import {
3
- makeStyles ,
4
- Menu ,
5
- MenuButtonProps ,
6
- MenuItem ,
7
- MenuList ,
8
- MenuPopover ,
9
- MenuTrigger ,
10
- Spinner ,
11
- SplitButton ,
12
- Toast ,
13
- Toaster ,
14
- ToastTitle ,
15
- useId ,
16
- useToastController ,
17
- } from '@fluentui/react-components' ;
2
+ import { SplitButton , type MenuButtonProps } from '@fluentui/react-button' ;
3
+ import { Menu , MenuItem , MenuList , MenuPopover , MenuTrigger } from '@fluentui/react-menu' ;
4
+ import { Spinner } from '@fluentui/react-spinner' ;
5
+ import { Toast , Toaster , ToastTitle , useToastController } from '@fluentui/react-toast' ;
6
+ import { useId } from '@fluentui/react-utilities' ;
7
+ import { makeStyles } from '@griffel/react' ;
18
8
import { bundleIcon , MarkdownFilled , MarkdownRegular } from '@fluentui/react-icons' ;
9
+ import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts' ;
19
10
20
11
const MarkdownIcon = bundleIcon ( MarkdownFilled , MarkdownRegular ) ;
21
12
@@ -35,6 +26,8 @@ export interface CopyAsMarkdownProps {
35
26
* The markdown content is fetched from the Storybook API and cached for subsequent requests.
36
27
*/
37
28
export const CopyAsMarkdownButton : React . FC < CopyAsMarkdownProps > = ( { storyId = '' } ) => {
29
+ const { targetDocument } = useFluent ( ) ;
30
+ const targetWindow = targetDocument ?. defaultView ;
38
31
const styles = useStyles ( ) ;
39
32
const toastId = useId ( 'copy-toast' ) ;
40
33
const toasterId = useId ( 'toaster' ) ;
@@ -48,8 +41,8 @@ export const CopyAsMarkdownButton: React.FC<CopyAsMarkdownProps> = ({ storyId =
48
41
49
42
// Full URL to the markdown endpoint for this story
50
43
const markdownUrl = React . useMemo ( ( ) => {
51
- return convertStoryIdToMarkdownUrl ( storyId ) ;
52
- } , [ storyId ] ) ;
44
+ return targetWindow ? convertStoryIdToMarkdownUrl ( targetWindow , storyId ) : '' ;
45
+ } , [ storyId , targetWindow ] ) ;
53
46
54
47
// Cleanup: abort pending requests on unmount
55
48
React . useEffect ( ( ) => {
@@ -69,6 +62,11 @@ export const CopyAsMarkdownButton: React.FC<CopyAsMarkdownProps> = ({ storyId =
69
62
return ;
70
63
}
71
64
65
+ // Ensure we have a window context to use for clipboard and fetch
66
+ if ( ! targetWindow ) {
67
+ return ;
68
+ }
69
+
72
70
// Create new AbortController for this request
73
71
const abortController = new AbortController ( ) ;
74
72
abortControllerRef . current = abortController ;
@@ -88,11 +86,11 @@ export const CopyAsMarkdownButton: React.FC<CopyAsMarkdownProps> = ({ storyId =
88
86
try {
89
87
// Use cached content if available, otherwise fetch from API
90
88
if ( ! markdownContentCache . current ) {
91
- markdownContentCache . current = await fetchMarkdownContent ( markdownUrl , abortController . signal ) ;
89
+ markdownContentCache . current = await fetchMarkdownContent ( targetWindow , markdownUrl , abortController . signal ) ;
92
90
}
93
91
94
92
// Copy to clipboard
95
- await navigator . clipboard . writeText ( markdownContentCache . current ) ;
93
+ await targetWindow ?. navigator . clipboard . writeText ( markdownContentCache . current ) ;
96
94
97
95
// Update toast to success
98
96
updateToast ( {
@@ -128,12 +126,12 @@ export const CopyAsMarkdownButton: React.FC<CopyAsMarkdownProps> = ({ storyId =
128
126
// Clear the abort controller ref to allow new requests
129
127
abortControllerRef . current = null ;
130
128
}
131
- } , [ dispatchToast , updateToast , toastId , markdownUrl ] ) ;
129
+ } , [ dispatchToast , updateToast , toastId , markdownUrl , targetWindow ] ) ;
132
130
133
131
/** Opens the markdown content in a new browser tab */
134
132
const openInNewTab = React . useCallback ( ( ) => {
135
- window . open ( markdownUrl , '_blank' ) ;
136
- } , [ markdownUrl ] ) ;
133
+ targetWindow ? .open ( markdownUrl , '_blank' ) ;
134
+ } , [ markdownUrl , targetWindow ] ) ;
137
135
138
136
if ( ! storyId ) {
139
137
return null ;
@@ -182,33 +180,40 @@ const STORYBOOK_VARIANT_SUFFIX_PATTERN = /--\w+$/g;
182
180
/**
183
181
* Gets the base URL for fetching markdown content from the Storybook LLM endpoint.
184
182
* Each story's markdown is available at: {BASE_URL}/{storyId}.txt
183
+ * @param targetWindow - The window object to use for location access
185
184
* @returns The base URL constructed from current location origin and pathname
186
185
*/
187
- function getStorybookMarkdownApiBaseUrl ( ) : string {
186
+ function getStorybookMarkdownApiBaseUrl ( targetWindow : Window ) : string {
188
187
// Remove the [page].html file from pathname and append /llms/
189
- const basePath = window . location . pathname . replace ( / \/ [ ^ / ] * \. h t m l $ / , '' ) ;
190
- return `${ window . location . origin } ${ basePath } /llms/` ;
188
+ const basePath = targetWindow . location . pathname . replace ( / \/ [ ^ / ] * \. h t m l $ / , '' ) ;
189
+ return `${ targetWindow . location . origin } ${ basePath } /llms/` ;
191
190
}
192
191
193
192
/**
194
193
* Converts a Storybook story ID to a markdown URL.
194
+ * @param targetWindow - The window object to use for location access
195
195
* @param storyId - The Storybook story ID
196
196
* @returns The full URL to the markdown endpoint for the story
197
197
* @example "button--primary" -> "https://storybooks.fluentui.dev/llms/button.txt"
198
198
*/
199
- function convertStoryIdToMarkdownUrl ( storyId : string ) : string {
200
- return `${ getStorybookMarkdownApiBaseUrl ( ) } ${ storyId . replace ( STORYBOOK_VARIANT_SUFFIX_PATTERN , '.txt' ) } ` ;
199
+ function convertStoryIdToMarkdownUrl ( targetWindow : Window , storyId : string ) : string {
200
+ return `${ getStorybookMarkdownApiBaseUrl ( targetWindow ) } ${ storyId . replace ( STORYBOOK_VARIANT_SUFFIX_PATTERN , '.txt' ) } ` ;
201
201
}
202
202
203
203
/**
204
204
* Fetches markdown content from the Storybook API.
205
+ * @param targetWindow - The window object to use for fetch access
205
206
* @param url - The URL to fetch markdown content from
206
207
* @param signal - Optional AbortSignal to cancel the request
207
208
* @returns Promise resolving to the markdown text content
208
209
* @throws Error if the fetch request fails or is aborted
209
210
*/
210
- async function fetchMarkdownContent ( url : string , signal ?: AbortSignal ) : Promise < string > {
211
- const response = await fetch ( url , {
211
+ async function fetchMarkdownContent (
212
+ targetWindow : Window ,
213
+ url : string ,
214
+ signal : AbortSignal | undefined ,
215
+ ) : Promise < string > {
216
+ const response = await targetWindow . fetch ( url , {
212
217
headers : {
213
218
'Content-Type' : 'text/plain' ,
214
219
} ,
0 commit comments