@fluentui/react-storybook-addon 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +27 -2
  2. package/dist/index.d.ts +100 -0
  3. package/lib/components/DirectionSwitch.js +26 -0
  4. package/lib/components/DirectionSwitch.js.map +1 -0
  5. package/lib/components/ReactStrictMode.js +22 -0
  6. package/lib/components/ReactStrictMode.js.map +1 -0
  7. package/lib/components/ThemePicker.js +60 -0
  8. package/lib/components/ThemePicker.js.map +1 -0
  9. package/lib/constants.js +4 -0
  10. package/lib/constants.js.map +1 -0
  11. package/lib/decorators/withAriaLive.js +18 -0
  12. package/lib/decorators/withAriaLive.js.map +1 -0
  13. package/lib/decorators/withFluentProvider.js +48 -0
  14. package/lib/decorators/withFluentProvider.js.map +1 -0
  15. package/lib/decorators/withReactStrictMode.js +16 -0
  16. package/lib/decorators/withReactStrictMode.js.map +1 -0
  17. package/lib/docs/CopyAsMarkdownButton.js +177 -0
  18. package/lib/docs/CopyAsMarkdownButton.js.map +1 -0
  19. package/lib/docs/DirSwitch.js +51 -0
  20. package/lib/docs/DirSwitch.js.map +1 -0
  21. package/lib/docs/FluentCanvas.js +21 -0
  22. package/lib/docs/FluentCanvas.js.map +1 -0
  23. package/lib/docs/FluentDocsContainer.js +24 -0
  24. package/lib/docs/FluentDocsContainer.js.map +1 -0
  25. package/lib/docs/FluentDocsPage.js +308 -0
  26. package/lib/docs/FluentDocsPage.js.map +1 -0
  27. package/lib/docs/FluentStory.js +18 -0
  28. package/lib/docs/FluentStory.js.map +1 -0
  29. package/lib/docs/ThemePicker.js +61 -0
  30. package/lib/docs/ThemePicker.js.map +1 -0
  31. package/lib/docs/Toc.js +130 -0
  32. package/lib/docs/Toc.js.map +1 -0
  33. package/lib/docs/index.js +4 -0
  34. package/lib/docs/index.js.map +1 -0
  35. package/lib/docs/utils.js +74 -0
  36. package/lib/docs/utils.js.map +1 -0
  37. package/lib/hooks.js +16 -0
  38. package/lib/hooks.js.map +1 -0
  39. package/lib/index.js +4 -0
  40. package/lib/index.js.map +1 -0
  41. package/lib/preset/manager.js +25 -0
  42. package/lib/preset/manager.js.map +1 -0
  43. package/lib/preset/preview.js +26 -0
  44. package/lib/preset/preview.js.map +1 -0
  45. package/lib/theme.js +31 -0
  46. package/lib/theme.js.map +1 -0
  47. package/lib/utils/isDecoratorDisabled.js +6 -0
  48. package/lib/utils/isDecoratorDisabled.js.map +1 -0
  49. package/lib-commonjs/components/DirectionSwitch.js +37 -0
  50. package/lib-commonjs/components/DirectionSwitch.js.map +1 -0
  51. package/lib-commonjs/components/ReactStrictMode.js +33 -0
  52. package/lib-commonjs/components/ReactStrictMode.js.map +1 -0
  53. package/lib-commonjs/components/ThemePicker.js +71 -0
  54. package/lib-commonjs/components/ThemePicker.js.map +1 -0
  55. package/lib-commonjs/constants.js +28 -0
  56. package/lib-commonjs/constants.js.map +1 -0
  57. package/lib-commonjs/decorators/withAriaLive.js +29 -0
  58. package/lib-commonjs/decorators/withAriaLive.js.map +1 -0
  59. package/lib-commonjs/decorators/withFluentProvider.js +59 -0
  60. package/lib-commonjs/decorators/withFluentProvider.js.map +1 -0
  61. package/lib-commonjs/decorators/withReactStrictMode.js +27 -0
  62. package/lib-commonjs/decorators/withReactStrictMode.js.map +1 -0
  63. package/lib-commonjs/docs/CopyAsMarkdownButton.js +185 -0
  64. package/lib-commonjs/docs/CopyAsMarkdownButton.js.map +1 -0
  65. package/lib-commonjs/docs/DirSwitch.js +60 -0
  66. package/lib-commonjs/docs/DirSwitch.js.map +1 -0
  67. package/lib-commonjs/docs/FluentCanvas.js +29 -0
  68. package/lib-commonjs/docs/FluentCanvas.js.map +1 -0
  69. package/lib-commonjs/docs/FluentDocsContainer.js +33 -0
  70. package/lib-commonjs/docs/FluentDocsContainer.js.map +1 -0
  71. package/lib-commonjs/docs/FluentDocsPage.js +319 -0
  72. package/lib-commonjs/docs/FluentDocsPage.js.map +1 -0
  73. package/lib-commonjs/docs/FluentStory.js +26 -0
  74. package/lib-commonjs/docs/FluentStory.js.map +1 -0
  75. package/lib-commonjs/docs/ThemePicker.js +70 -0
  76. package/lib-commonjs/docs/ThemePicker.js.map +1 -0
  77. package/lib-commonjs/docs/Toc.js +149 -0
  78. package/lib-commonjs/docs/Toc.js.map +1 -0
  79. package/lib-commonjs/docs/index.js +28 -0
  80. package/lib-commonjs/docs/index.js.map +1 -0
  81. package/lib-commonjs/docs/utils.js +88 -0
  82. package/lib-commonjs/docs/utils.js.map +1 -0
  83. package/lib-commonjs/hooks.js +37 -0
  84. package/lib-commonjs/hooks.js.map +1 -0
  85. package/lib-commonjs/index.js +34 -0
  86. package/lib-commonjs/index.js.map +1 -0
  87. package/lib-commonjs/preset/manager.js +29 -0
  88. package/lib-commonjs/preset/manager.js.map +1 -0
  89. package/lib-commonjs/preset/preview.js +47 -0
  90. package/lib-commonjs/preset/preview.js.map +1 -0
  91. package/lib-commonjs/theme.js +49 -0
  92. package/lib-commonjs/theme.js.map +1 -0
  93. package/lib-commonjs/utils/isDecoratorDisabled.js +16 -0
  94. package/lib-commonjs/utils/isDecoratorDisabled.js.map +1 -0
  95. package/package.json +20 -27
package/CHANGELOG.md CHANGED
@@ -1,12 +1,37 @@
1
1
  # Change Log - @fluentui/react-storybook-addon
2
2
 
3
- This log was last generated on Fri, 31 Oct 2025 16:17:32 GMT and should not be manually modified.
3
+ This log was last generated on Tue, 03 Mar 2026 13:15:56 GMT and should not be manually modified.
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## [0.6.0](https://github.com/microsoft/fluentui/tree/@fluentui/react-storybook-addon_v0.6.0)
8
+
9
+ Tue, 03 Mar 2026 13:15:56 GMT
10
+ [Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-storybook-addon_v0.5.1..@fluentui/react-storybook-addon_v0.6.0)
11
+
12
+ ### Minor changes
13
+
14
+ - feat: upgrade to storybook 9 ([PR #35459](https://github.com/microsoft/fluentui/pull/35459) by dmytrokirpa@microsoft.com)
15
+ - BREAKING: upgrade storybook to v8 ([PR #35279](https://github.com/microsoft/fluentui/pull/35279) by dmytrokirpa@microsoft.com)
16
+
17
+ ### Patches
18
+
19
+ - chore: migrate source to react 19 ([PR #35434](https://github.com/microsoft/fluentui/pull/35434) by martinhochel@microsoft.com)
20
+ - chore: bump storybook to mitigate CVE ([PR #35748](https://github.com/microsoft/fluentui/pull/35748) by martinhochel@microsoft.com)
21
+ - chore: Bump @griffel/react package. ([PR #35469](https://github.com/microsoft/fluentui/pull/35469) by estebanmu@microsoft.com)
22
+
23
+ ## [0.5.1](https://github.com/microsoft/fluentui/tree/@fluentui/react-storybook-addon_v0.5.1)
24
+
25
+ Tue, 04 Nov 2025 14:47:00 GMT
26
+ [Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-storybook-addon_v0.5.0..@fluentui/react-storybook-addon_v0.5.1)
27
+
28
+ ### Patches
29
+
30
+ - fix: force release ([PR #35445](https://github.com/microsoft/fluentui/pull/35445) by dmytrokirpa@microsoft.com)
31
+
7
32
  ## [0.5.0](https://github.com/microsoft/fluentui/tree/@fluentui/react-storybook-addon_v0.5.0)
8
33
 
9
- Fri, 31 Oct 2025 16:17:32 GMT
34
+ Fri, 31 Oct 2025 16:22:00 GMT
10
35
  [Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-storybook-addon_v0.4.6..@fluentui/react-storybook-addon_v0.5.0)
11
36
 
12
37
  ### Minor changes
@@ -0,0 +1,100 @@
1
+ import { Args } from '@storybook/react-webpack5';
2
+ import type { JSXElement } from '@fluentui/react-utilities';
3
+ import { Parameters as Parameters_2 } from '@storybook/react-webpack5';
4
+ import * as React_2 from 'react';
5
+ import { StoryContext } from '@storybook/react-webpack5';
6
+
7
+ export declare const DIR_ID: "storybook_fluentui-react-addon_dir";
8
+
9
+ /**
10
+ * Canvas component to wrap stories in a styled container.
11
+ * Provides a similar experience to Storybook's v7 `Canvas` component.
12
+ */
13
+ export declare const FluentCanvas: (props: React_2.ComponentProps<"div">) => JSXElement;
14
+
15
+ /**
16
+ * Configuration for docs components
17
+ */
18
+ declare type FluentDocsConfig = boolean | {
19
+ tableOfContents?: boolean;
20
+ dirSwitcher?: boolean;
21
+ themePicker?: boolean;
22
+ copyAsMarkdown?: boolean;
23
+ argTable?: boolean | {
24
+ slotsApi?: boolean;
25
+ nativePropsApi?: boolean;
26
+ };
27
+ };
28
+
29
+ /**
30
+ * Extends the storybook globals object to include fluent specific properties
31
+ */
32
+ export declare interface FluentGlobals extends Args {
33
+ [DIR_ID]?: 'ltr' | 'rtl';
34
+ [THEME_ID]?: ThemeIds;
35
+ [STRICT_MODE_ID]?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Extends the storybook parameters object to include fluent specific properties
40
+ */
41
+ export declare interface FluentParameters extends Parameters_2 {
42
+ dir?: 'ltr' | 'rtl';
43
+ fluentTheme?: ThemeIds;
44
+ mode?: 'default' | 'vr-test';
45
+ reactStorybookAddon?: {
46
+ disabledDecorators?: ['AriaLive' | 'FluentProvider' | 'ReactStrictMode'];
47
+ docs?: FluentDocsConfig;
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Story component to render stories in an iframe.
53
+ * Provides a similar experience to Storybook's v7 `Story` component.
54
+ */
55
+ export declare const FluentStory: ({ id, height }: FluentStoryProps) => JSXElement;
56
+
57
+ export declare interface FluentStoryContext extends StoryContext {
58
+ globals: FluentGlobals;
59
+ parameters: FluentParameters;
60
+ }
61
+
62
+ declare type FluentStoryProps = {
63
+ /** The unique identifier for the story */
64
+ id: string;
65
+ /** The height of the iframe */
66
+ height?: string | number;
67
+ };
68
+
69
+ export declare function parameters(options?: FluentParameters): FluentParameters;
70
+
71
+ declare const STRICT_MODE_ID: "storybook_fluentui-react-addon_strict-mode";
72
+
73
+ export declare const THEME_ID: "storybook_fluentui-react-addon_theme";
74
+
75
+ export declare type ThemeIds = (typeof themes)[number]['id'];
76
+
77
+ export declare const themes: readonly [{
78
+ readonly id: "web-light";
79
+ readonly label: "Web Light";
80
+ }, {
81
+ readonly id: "web-dark";
82
+ readonly label: "Web Dark";
83
+ }, {
84
+ readonly id: "teams-light";
85
+ readonly label: "Teams Light";
86
+ }, {
87
+ readonly id: "teams-dark";
88
+ readonly label: "Teams Dark";
89
+ }, {
90
+ readonly id: "teams-light-v21";
91
+ readonly label: "Teams Light V2.1";
92
+ }, {
93
+ readonly id: "teams-dark-v21";
94
+ readonly label: "Teams Dark V2.1";
95
+ }, {
96
+ readonly id: "teams-high-contrast";
97
+ readonly label: "Teams High Contrast";
98
+ }];
99
+
100
+ export { }
@@ -0,0 +1,26 @@
1
+ import * as React from 'react';
2
+ import { IconButton } from 'storybook/internal/components';
3
+ import { styled } from 'storybook/theming';
4
+ import { DIR_ID } from '../constants';
5
+ import { useGlobals } from '../hooks';
6
+ const Monospace = styled.span({
7
+ fontFamily: "'Cascadia Code', Menlo, 'Courier New', Courier, monospace",
8
+ letterSpacing: '-0.05em'
9
+ });
10
+ export const DirectionSwitch = ()=>{
11
+ const [globals, updateGlobals] = useGlobals();
12
+ var _globals_DIR_ID;
13
+ const direction = (_globals_DIR_ID = globals[DIR_ID]) !== null && _globals_DIR_ID !== void 0 ? _globals_DIR_ID : 'ltr';
14
+ const isLTR = direction === 'ltr';
15
+ const toggleDirection = React.useCallback(()=>updateGlobals({
16
+ [DIR_ID]: isLTR ? 'rtl' : 'ltr'
17
+ }), [
18
+ isLTR,
19
+ updateGlobals
20
+ ]);
21
+ return /*#__PURE__*/ React.createElement(IconButton, {
22
+ key: DIR_ID,
23
+ title: "Change Direction",
24
+ onClick: toggleDirection
25
+ }, /*#__PURE__*/ React.createElement("div", null, "Direction: ", /*#__PURE__*/ React.createElement(Monospace, null, direction.toUpperCase())));
26
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/DirectionSwitch.tsx"],"sourcesContent":["import * as React from 'react';\nimport { IconButton } from 'storybook/internal/components';\nimport { styled } from 'storybook/theming';\n\nimport { JSXElement } from '@fluentui/react-utilities';\nimport { DIR_ID } from '../constants';\nimport { useGlobals } from '../hooks';\n\nconst Monospace = styled.span({\n fontFamily: \"'Cascadia Code', Menlo, 'Courier New', Courier, monospace\",\n letterSpacing: '-0.05em',\n});\n\nexport const DirectionSwitch = (): JSXElement => {\n const [globals, updateGlobals] = useGlobals();\n\n const direction = globals[DIR_ID] ?? 'ltr';\n const isLTR = direction === 'ltr';\n\n const toggleDirection = React.useCallback(\n () =>\n updateGlobals({\n [DIR_ID]: isLTR ? 'rtl' : 'ltr',\n }),\n [isLTR, updateGlobals],\n );\n\n return (\n <IconButton key={DIR_ID} title=\"Change Direction\" onClick={toggleDirection}>\n <div>\n Direction: <Monospace>{direction.toUpperCase()}</Monospace>\n </div>\n </IconButton>\n );\n};\n"],"names":["React","IconButton","styled","DIR_ID","useGlobals","Monospace","span","fontFamily","letterSpacing","DirectionSwitch","globals","updateGlobals","direction","isLTR","toggleDirection","useCallback","key","title","onClick","div","toUpperCase"],"mappings":"AAAA,YAAYA,WAAW,QAAQ;AAC/B,SAASC,UAAU,QAAQ,gCAAgC;AAC3D,SAASC,MAAM,QAAQ,oBAAoB;AAG3C,SAASC,MAAM,QAAQ,eAAe;AACtC,SAASC,UAAU,QAAQ,WAAW;AAEtC,MAAMC,YAAYH,OAAOI,IAAI,CAAC;IAC5BC,YAAY;IACZC,eAAe;AACjB;AAEA,OAAO,MAAMC,kBAAkB;IAC7B,MAAM,CAACC,SAASC,cAAc,GAAGP;QAEfM;IAAlB,MAAME,YAAYF,CAAAA,kBAAAA,OAAO,CAACP,OAAO,cAAfO,6BAAAA,kBAAmB;IACrC,MAAMG,QAAQD,cAAc;IAE5B,MAAME,kBAAkBd,MAAMe,WAAW,CACvC,IACEJ,cAAc;YACZ,CAACR,OAAO,EAAEU,QAAQ,QAAQ;QAC5B,IACF;QAACA;QAAOF;KAAc;IAGxB,qBACE,oBAACV;QAAWe,KAAKb;QAAQc,OAAM;QAAmBC,SAASJ;qBACzD,oBAACK,aAAI,6BACQ,oBAACd,iBAAWO,UAAUQ,WAAW;AAIpD,EAAE"}
@@ -0,0 +1,22 @@
1
+ import * as React from 'react';
2
+ import { IconButton } from 'storybook/internal/components';
3
+ import { LockIcon } from '@storybook/icons';
4
+ import { STRICT_MODE_ID } from '../constants';
5
+ import { useGlobals } from '../hooks';
6
+ export const ReactStrictMode = ()=>{
7
+ const [globals, updateGlobals] = useGlobals();
8
+ var _globals_STRICT_MODE_ID;
9
+ const isActive = (_globals_STRICT_MODE_ID = globals[STRICT_MODE_ID]) !== null && _globals_STRICT_MODE_ID !== void 0 ? _globals_STRICT_MODE_ID : false;
10
+ const toggleStrictMode = React.useCallback(()=>updateGlobals({
11
+ [STRICT_MODE_ID]: !isActive
12
+ }), [
13
+ isActive,
14
+ updateGlobals
15
+ ]);
16
+ return /*#__PURE__*/ React.createElement(IconButton, {
17
+ key: STRICT_MODE_ID,
18
+ active: isActive,
19
+ title: "Toggle React Strict mode",
20
+ onClick: toggleStrictMode
21
+ }, /*#__PURE__*/ React.createElement(LockIcon, null));
22
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/ReactStrictMode.tsx"],"sourcesContent":["import * as React from 'react';\nimport { IconButton } from 'storybook/internal/components';\nimport { LockIcon } from '@storybook/icons';\n\nimport type { JSXElement } from '@fluentui/react-utilities';\nimport { STRICT_MODE_ID } from '../constants';\nimport { useGlobals } from '../hooks';\n\nexport const ReactStrictMode = (): JSXElement => {\n const [globals, updateGlobals] = useGlobals();\n\n const isActive = globals[STRICT_MODE_ID] ?? false;\n\n const toggleStrictMode = React.useCallback(\n () =>\n updateGlobals({\n [STRICT_MODE_ID]: !isActive,\n }),\n [isActive, updateGlobals],\n );\n\n return (\n <IconButton key={STRICT_MODE_ID} active={isActive} title=\"Toggle React Strict mode\" onClick={toggleStrictMode}>\n <LockIcon />\n </IconButton>\n );\n};\n"],"names":["React","IconButton","LockIcon","STRICT_MODE_ID","useGlobals","ReactStrictMode","globals","updateGlobals","isActive","toggleStrictMode","useCallback","key","active","title","onClick"],"mappings":"AAAA,YAAYA,WAAW,QAAQ;AAC/B,SAASC,UAAU,QAAQ,gCAAgC;AAC3D,SAASC,QAAQ,QAAQ,mBAAmB;AAG5C,SAASC,cAAc,QAAQ,eAAe;AAC9C,SAASC,UAAU,QAAQ,WAAW;AAEtC,OAAO,MAAMC,kBAAkB;IAC7B,MAAM,CAACC,SAASC,cAAc,GAAGH;QAEhBE;IAAjB,MAAME,WAAWF,CAAAA,0BAAAA,OAAO,CAACH,eAAe,cAAvBG,qCAAAA,0BAA2B;IAE5C,MAAMG,mBAAmBT,MAAMU,WAAW,CACxC,IACEH,cAAc;YACZ,CAACJ,eAAe,EAAE,CAACK;QACrB,IACF;QAACA;QAAUD;KAAc;IAG3B,qBACE,oBAACN;QAAWU,KAAKR;QAAgBS,QAAQJ;QAAUK,OAAM;QAA2BC,SAASL;qBAC3F,oBAACP;AAGP,EAAE"}
@@ -0,0 +1,60 @@
1
+ import * as React from 'react';
2
+ import { IconButton, TooltipLinkList, WithTooltip } from 'storybook/internal/components';
3
+ import { ArrowDownIcon } from '@storybook/icons';
4
+ import { useParameter } from 'storybook/manager-api';
5
+ import { themes, defaultTheme } from '../theme';
6
+ import { THEME_ID } from '../constants';
7
+ import { useGlobals } from '../hooks';
8
+ function createThemeItems(value, changeTheme, getCurrentTheme) {
9
+ return value.map((item)=>{
10
+ return {
11
+ id: item.id,
12
+ title: item.id === defaultTheme.id ? `${item.label} (Default)` : item.label,
13
+ onClick: ()=>{
14
+ changeTheme(item.id);
15
+ },
16
+ value: item.id,
17
+ active: getCurrentTheme() === item.id
18
+ };
19
+ });
20
+ }
21
+ export const ThemePicker = ()=>{
22
+ const [globals, updateGlobals] = useGlobals();
23
+ const fluentTheme = useParameter('fluentTheme');
24
+ var _globals_THEME_ID;
25
+ const selectedThemeId = fluentTheme ? fluentTheme : (_globals_THEME_ID = globals[THEME_ID]) !== null && _globals_THEME_ID !== void 0 ? _globals_THEME_ID : defaultTheme.id;
26
+ const selectedTheme = themes.find((entry)=>entry.id === selectedThemeId);
27
+ const isActive = selectedThemeId !== defaultTheme.id;
28
+ const setTheme = React.useCallback((id)=>{
29
+ updateGlobals({
30
+ [THEME_ID]: id
31
+ });
32
+ }, [
33
+ updateGlobals
34
+ ]);
35
+ const renderTooltip = React.useCallback((props)=>{
36
+ return /*#__PURE__*/ React.createElement(TooltipLinkList, {
37
+ links: createThemeItems(themes, (id)=>{
38
+ setTheme(id);
39
+ props.onHide();
40
+ }, ()=>selectedThemeId)
41
+ });
42
+ }, [
43
+ selectedThemeId,
44
+ setTheme
45
+ ]);
46
+ return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(WithTooltip, {
47
+ placement: "top",
48
+ trigger: "click",
49
+ closeOnOutsideClick: true,
50
+ tooltip: renderTooltip
51
+ }, /*#__PURE__*/ React.createElement(IconButton, {
52
+ key: THEME_ID,
53
+ title: "Change Fluent theme",
54
+ active: isActive
55
+ }, /*#__PURE__*/ React.createElement(ArrowDownIcon, null), /*#__PURE__*/ React.createElement("span", {
56
+ style: {
57
+ marginLeft: 5
58
+ }
59
+ }, "Theme: ", selectedTheme === null || selectedTheme === void 0 ? void 0 : selectedTheme.label))));
60
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/ThemePicker.tsx"],"sourcesContent":["import * as React from 'react';\nimport { IconButton, TooltipLinkList, WithTooltip } from 'storybook/internal/components';\nimport { ArrowDownIcon } from '@storybook/icons';\nimport { useParameter } from 'storybook/manager-api';\n\nimport type { JSXElement } from '@fluentui/react-utilities';\nimport { ThemeIds, themes, defaultTheme } from '../theme';\nimport { THEME_ID } from '../constants';\nimport { useGlobals, FluentParameters } from '../hooks';\n\nexport interface ThemeSelectorItem {\n id: string;\n title: string;\n onClick: () => void;\n value: string;\n active: boolean;\n}\n\nfunction createThemeItems(\n value: typeof themes,\n changeTheme: (id: ThemeIds) => void,\n getCurrentTheme: () => ThemeIds,\n): ThemeSelectorItem[] {\n return value.map(item => {\n return {\n id: item.id,\n title: item.id === defaultTheme.id ? `${item.label} (Default)` : item.label,\n onClick: () => {\n changeTheme(item.id);\n },\n value: item.id,\n active: getCurrentTheme() === item.id,\n };\n });\n}\n\nexport const ThemePicker = (): JSXElement => {\n const [globals, updateGlobals] = useGlobals();\n const fluentTheme: FluentParameters['fluentTheme'] = useParameter('fluentTheme');\n\n const selectedThemeId = fluentTheme ? fluentTheme : globals[THEME_ID] ?? defaultTheme.id;\n const selectedTheme = themes.find(entry => entry.id === selectedThemeId);\n\n const isActive = selectedThemeId !== defaultTheme.id;\n\n const setTheme = React.useCallback(\n (id: ThemeIds) => {\n updateGlobals({ [THEME_ID]: id });\n },\n [updateGlobals],\n );\n\n const renderTooltip = React.useCallback(\n (props: { onHide: () => void }) => {\n return (\n <TooltipLinkList\n links={createThemeItems(\n themes,\n id => {\n setTheme(id);\n props.onHide();\n },\n () => selectedThemeId,\n )}\n />\n );\n },\n [selectedThemeId, setTheme],\n );\n\n return (\n <>\n <WithTooltip placement=\"top\" trigger=\"click\" closeOnOutsideClick tooltip={renderTooltip}>\n <IconButton key={THEME_ID} title=\"Change Fluent theme\" active={isActive}>\n <ArrowDownIcon />\n <span style={{ marginLeft: 5 }}>Theme: {selectedTheme?.label}</span>\n </IconButton>\n </WithTooltip>\n </>\n );\n};\n"],"names":["React","IconButton","TooltipLinkList","WithTooltip","ArrowDownIcon","useParameter","themes","defaultTheme","THEME_ID","useGlobals","createThemeItems","value","changeTheme","getCurrentTheme","map","item","id","title","label","onClick","active","ThemePicker","globals","updateGlobals","fluentTheme","selectedThemeId","selectedTheme","find","entry","isActive","setTheme","useCallback","renderTooltip","props","links","onHide","placement","trigger","closeOnOutsideClick","tooltip","key","span","style","marginLeft"],"mappings":"AAAA,YAAYA,WAAW,QAAQ;AAC/B,SAASC,UAAU,EAAEC,eAAe,EAAEC,WAAW,QAAQ,gCAAgC;AACzF,SAASC,aAAa,QAAQ,mBAAmB;AACjD,SAASC,YAAY,QAAQ,wBAAwB;AAGrD,SAAmBC,MAAM,EAAEC,YAAY,QAAQ,WAAW;AAC1D,SAASC,QAAQ,QAAQ,eAAe;AACxC,SAASC,UAAU,QAA0B,WAAW;AAUxD,SAASC,iBACPC,KAAoB,EACpBC,WAAmC,EACnCC,eAA+B;IAE/B,OAAOF,MAAMG,GAAG,CAACC,CAAAA;QACf,OAAO;YACLC,IAAID,KAAKC,EAAE;YACXC,OAAOF,KAAKC,EAAE,KAAKT,aAAaS,EAAE,GAAG,GAAGD,KAAKG,KAAK,CAAC,UAAU,CAAC,GAAGH,KAAKG,KAAK;YAC3EC,SAAS;gBACPP,YAAYG,KAAKC,EAAE;YACrB;YACAL,OAAOI,KAAKC,EAAE;YACdI,QAAQP,sBAAsBE,KAAKC,EAAE;QACvC;IACF;AACF;AAEA,OAAO,MAAMK,cAAc;IACzB,MAAM,CAACC,SAASC,cAAc,GAAGd;IACjC,MAAMe,cAA+CnB,aAAa;QAEdiB;IAApD,MAAMG,kBAAkBD,cAAcA,cAAcF,CAAAA,oBAAAA,OAAO,CAACd,SAAS,cAAjBc,+BAAAA,oBAAqBf,aAAaS,EAAE;IACxF,MAAMU,gBAAgBpB,OAAOqB,IAAI,CAACC,CAAAA,QAASA,MAAMZ,EAAE,KAAKS;IAExD,MAAMI,WAAWJ,oBAAoBlB,aAAaS,EAAE;IAEpD,MAAMc,WAAW9B,MAAM+B,WAAW,CAChC,CAACf;QACCO,cAAc;YAAE,CAACf,SAAS,EAAEQ;QAAG;IACjC,GACA;QAACO;KAAc;IAGjB,MAAMS,gBAAgBhC,MAAM+B,WAAW,CACrC,CAACE;QACC,qBACE,oBAAC/B;YACCgC,OAAOxB,iBACLJ,QACAU,CAAAA;gBACEc,SAASd;gBACTiB,MAAME,MAAM;YACd,GACA,IAAMV;;IAId,GACA;QAACA;QAAiBK;KAAS;IAG7B,qBACE,wDACE,oBAAC3B;QAAYiC,WAAU;QAAMC,SAAQ;QAAQC,qBAAAA;QAAoBC,SAASP;qBACxE,oBAAC/B;QAAWuC,KAAKhC;QAAUS,OAAM;QAAsBG,QAAQS;qBAC7D,oBAACzB,oCACD,oBAACqC;QAAKC,OAAO;YAAEC,YAAY;QAAE;OAAG,WAAQjB,0BAAAA,oCAAAA,cAAeR,KAAK;AAKtE,EAAE"}
@@ -0,0 +1,4 @@
1
+ export const ADDON_ID = 'storybook_fluentui-react-addon';
2
+ export const DIR_ID = `${ADDON_ID}_dir`;
3
+ export const STRICT_MODE_ID = `${ADDON_ID}_strict-mode`;
4
+ export const THEME_ID = `${ADDON_ID}_theme`;
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts"],"sourcesContent":["export const ADDON_ID = 'storybook_fluentui-react-addon';\n\nexport const DIR_ID = `${ADDON_ID}_dir` as const;\nexport const STRICT_MODE_ID = `${ADDON_ID}_strict-mode` as const;\nexport const THEME_ID = `${ADDON_ID}_theme` as const;\n"],"names":["ADDON_ID","DIR_ID","STRICT_MODE_ID","THEME_ID"],"mappings":"AAAA,OAAO,MAAMA,WAAW,iCAAiC;AAEzD,OAAO,MAAMC,SAAS,GAAGD,SAAS,IAAI,CAAC,CAAU;AACjD,OAAO,MAAME,iBAAiB,GAAGF,SAAS,YAAY,CAAC,CAAU;AACjE,OAAO,MAAMG,WAAW,GAAGH,SAAS,MAAM,CAAC,CAAU"}
@@ -0,0 +1,18 @@
1
+ import { AriaLiveAnnouncer } from '@fluentui/react-aria';
2
+ import * as React from 'react';
3
+ import { isDecoratorDisabled } from '../utils/isDecoratorDisabled';
4
+ export const withAriaLive = (Story, context)=>{
5
+ if (isDecoratorDisabled(context, 'AriaLive')) {
6
+ return Story();
7
+ }
8
+ return /*#__PURE__*/ React.createElement(AriaLiveWrapper, null, /*#__PURE__*/ React.createElement(Story, null));
9
+ };
10
+ const AriaLiveWrapper = (props)=>{
11
+ const [mounted, setMounted] = React.useState(false);
12
+ React.useEffect(()=>{
13
+ // The AriaLiveAnnouncer appends an element to DOM in an effect
14
+ // Trigger an extra renderer to make sure that doc examples that need to announce on mount can do so
15
+ setMounted(true);
16
+ }, []);
17
+ return /*#__PURE__*/ React.createElement(AriaLiveAnnouncer, null, mounted && props.children);
18
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/decorators/withAriaLive.tsx"],"sourcesContent":["import { AriaLiveAnnouncer } from '@fluentui/react-aria';\nimport * as React from 'react';\n\nimport type { FluentStoryContext } from '../hooks';\nimport { isDecoratorDisabled } from '../utils/isDecoratorDisabled';\nimport type { JSXElement } from '@fluentui/react-utilities';\n\nexport const withAriaLive = (Story: () => JSXElement, context: FluentStoryContext): JSXElement => {\n if (isDecoratorDisabled(context, 'AriaLive')) {\n return Story();\n }\n\n return (\n <AriaLiveWrapper>\n <Story />\n </AriaLiveWrapper>\n );\n};\n\nconst AriaLiveWrapper: React.FC<{ children: React.ReactNode }> = props => {\n const [mounted, setMounted] = React.useState(false);\n\n React.useEffect(() => {\n // The AriaLiveAnnouncer appends an element to DOM in an effect\n // Trigger an extra renderer to make sure that doc examples that need to announce on mount can do so\n setMounted(true);\n }, []);\n\n return <AriaLiveAnnouncer>{mounted && props.children}</AriaLiveAnnouncer>;\n};\n"],"names":["AriaLiveAnnouncer","React","isDecoratorDisabled","withAriaLive","Story","context","AriaLiveWrapper","props","mounted","setMounted","useState","useEffect","children"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,uBAAuB;AACzD,YAAYC,WAAW,QAAQ;AAG/B,SAASC,mBAAmB,QAAQ,+BAA+B;AAGnE,OAAO,MAAMC,eAAe,CAACC,OAAyBC;IACpD,IAAIH,oBAAoBG,SAAS,aAAa;QAC5C,OAAOD;IACT;IAEA,qBACE,oBAACE,qCACC,oBAACF;AAGP,EAAE;AAEF,MAAME,kBAA2DC,CAAAA;IAC/D,MAAM,CAACC,SAASC,WAAW,GAAGR,MAAMS,QAAQ,CAAC;IAE7CT,MAAMU,SAAS,CAAC;QACd,+DAA+D;QAC/D,oGAAoG;QACpGF,WAAW;IACb,GAAG,EAAE;IAEL,qBAAO,oBAACT,yBAAmBQ,WAAWD,MAAMK,QAAQ;AACtD"}
@@ -0,0 +1,48 @@
1
+ import * as React from 'react';
2
+ import { FluentProvider } from '@fluentui/react-provider';
3
+ import { teamsDarkTheme, teamsDarkV21Theme, teamsHighContrastTheme, teamsLightTheme, teamsLightV21Theme, webDarkTheme, webLightTheme } from '@fluentui/react-theme';
4
+ import { defaultTheme } from '../theme';
5
+ import { DIR_ID, THEME_ID } from '../constants';
6
+ import { isDecoratorDisabled } from '../utils/isDecoratorDisabled';
7
+ const themes = {
8
+ 'web-light': webLightTheme,
9
+ 'web-dark': webDarkTheme,
10
+ 'teams-light': teamsLightTheme,
11
+ 'teams-dark': teamsDarkTheme,
12
+ 'teams-high-contrast': teamsHighContrastTheme,
13
+ 'teams-light-v21': teamsLightV21Theme,
14
+ 'teams-dark-v21': teamsDarkV21Theme
15
+ };
16
+ const findTheme = (themeId)=>{
17
+ return themeId ? themes[themeId] : null;
18
+ };
19
+ export const withFluentProvider = (StoryFn, context)=>{
20
+ const { globals, parameters } = context;
21
+ const { mode } = parameters;
22
+ if (isDecoratorDisabled(context, 'FluentProvider')) {
23
+ return StoryFn();
24
+ }
25
+ const isVrTest = mode === 'vr-test';
26
+ var _parameters_dir, _ref;
27
+ const dir = (_ref = (_parameters_dir = parameters.dir) !== null && _parameters_dir !== void 0 ? _parameters_dir : globals[DIR_ID]) !== null && _ref !== void 0 ? _ref : 'ltr';
28
+ const globalTheme = findTheme(globals[THEME_ID]);
29
+ const paramTheme = findTheme(parameters.fluentTheme);
30
+ var _ref1;
31
+ const theme = (_ref1 = paramTheme !== null && paramTheme !== void 0 ? paramTheme : globalTheme) !== null && _ref1 !== void 0 ? _ref1 : themes[defaultTheme.id];
32
+ return /*#__PURE__*/ React.createElement(FluentProvider, {
33
+ theme: theme,
34
+ dir: dir
35
+ }, isVrTest ? StoryFn() : /*#__PURE__*/ React.createElement(FluentExampleContainer, {
36
+ theme: theme
37
+ }, StoryFn()));
38
+ };
39
+ const FluentExampleContainer = (props)=>{
40
+ const { theme } = props;
41
+ const backgroundColor = theme.colorNeutralBackground2;
42
+ return /*#__PURE__*/ React.createElement("div", {
43
+ style: {
44
+ padding: '48px 24px',
45
+ backgroundColor
46
+ }
47
+ }, props.children);
48
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/decorators/withFluentProvider.tsx"],"sourcesContent":["import * as React from 'react';\n\nimport { FluentProvider } from '@fluentui/react-provider';\nimport type { JSXElement } from '@fluentui/react-utilities';\nimport {\n Theme,\n teamsDarkTheme,\n teamsDarkV21Theme,\n teamsHighContrastTheme,\n teamsLightTheme,\n teamsLightV21Theme,\n webDarkTheme,\n webLightTheme,\n} from '@fluentui/react-theme';\nimport { defaultTheme, ThemeIds } from '../theme';\nimport { DIR_ID, THEME_ID } from '../constants';\nimport { FluentStoryContext } from '../hooks';\nimport { isDecoratorDisabled } from '../utils/isDecoratorDisabled';\n\nconst themes: Record<ThemeIds, Theme> = {\n 'web-light': webLightTheme,\n 'web-dark': webDarkTheme,\n 'teams-light': teamsLightTheme,\n 'teams-dark': teamsDarkTheme,\n 'teams-high-contrast': teamsHighContrastTheme,\n 'teams-light-v21': teamsLightV21Theme,\n 'teams-dark-v21': teamsDarkV21Theme,\n} as const;\n\nconst findTheme = (themeId?: ThemeIds) => {\n return themeId ? themes[themeId] : null;\n};\n\nexport const withFluentProvider = (StoryFn: () => JSXElement, context: FluentStoryContext): JSXElement => {\n const { globals, parameters } = context;\n const { mode } = parameters;\n\n if (isDecoratorDisabled(context, 'FluentProvider')) {\n return StoryFn();\n }\n\n const isVrTest = mode === 'vr-test';\n const dir = parameters.dir ?? globals[DIR_ID] ?? 'ltr';\n const globalTheme = findTheme(globals[THEME_ID]);\n const paramTheme = findTheme(parameters.fluentTheme);\n const theme = paramTheme ?? globalTheme ?? themes[defaultTheme.id];\n\n return (\n <FluentProvider theme={theme} dir={dir}>\n {isVrTest ? StoryFn() : <FluentExampleContainer theme={theme}>{StoryFn()}</FluentExampleContainer>}\n </FluentProvider>\n );\n};\n\nconst FluentExampleContainer: React.FC<{ children: React.ReactNode; theme: Theme }> = props => {\n const { theme } = props;\n\n const backgroundColor = theme.colorNeutralBackground2;\n return <div style={{ padding: '48px 24px', backgroundColor }}>{props.children}</div>;\n};\n"],"names":["React","FluentProvider","teamsDarkTheme","teamsDarkV21Theme","teamsHighContrastTheme","teamsLightTheme","teamsLightV21Theme","webDarkTheme","webLightTheme","defaultTheme","DIR_ID","THEME_ID","isDecoratorDisabled","themes","findTheme","themeId","withFluentProvider","StoryFn","context","globals","parameters","mode","isVrTest","dir","globalTheme","paramTheme","fluentTheme","theme","id","FluentExampleContainer","props","backgroundColor","colorNeutralBackground2","div","style","padding","children"],"mappings":"AAAA,YAAYA,WAAW,QAAQ;AAE/B,SAASC,cAAc,QAAQ,2BAA2B;AAE1D,SAEEC,cAAc,EACdC,iBAAiB,EACjBC,sBAAsB,EACtBC,eAAe,EACfC,kBAAkB,EAClBC,YAAY,EACZC,aAAa,QACR,wBAAwB;AAC/B,SAASC,YAAY,QAAkB,WAAW;AAClD,SAASC,MAAM,EAAEC,QAAQ,QAAQ,eAAe;AAEhD,SAASC,mBAAmB,QAAQ,+BAA+B;AAEnE,MAAMC,SAAkC;IACtC,aAAaL;IACb,YAAYD;IACZ,eAAeF;IACf,cAAcH;IACd,uBAAuBE;IACvB,mBAAmBE;IACnB,kBAAkBH;AACpB;AAEA,MAAMW,YAAY,CAACC;IACjB,OAAOA,UAAUF,MAAM,CAACE,QAAQ,GAAG;AACrC;AAEA,OAAO,MAAMC,qBAAqB,CAACC,SAA2BC;IAC5D,MAAM,EAAEC,OAAO,EAAEC,UAAU,EAAE,GAAGF;IAChC,MAAM,EAAEG,IAAI,EAAE,GAAGD;IAEjB,IAAIR,oBAAoBM,SAAS,mBAAmB;QAClD,OAAOD;IACT;IAEA,MAAMK,WAAWD,SAAS;QACdD,iBAAAA;IAAZ,MAAMG,MAAMH,CAAAA,OAAAA,CAAAA,kBAAAA,WAAWG,GAAG,cAAdH,6BAAAA,kBAAkBD,OAAO,CAACT,OAAO,cAAjCU,kBAAAA,OAAqC;IACjD,MAAMI,cAAcV,UAAUK,OAAO,CAACR,SAAS;IAC/C,MAAMc,aAAaX,UAAUM,WAAWM,WAAW;QACrCD;IAAd,MAAME,QAAQF,CAAAA,QAAAA,uBAAAA,wBAAAA,aAAcD,yBAAdC,mBAAAA,QAA6BZ,MAAM,CAACJ,aAAamB,EAAE,CAAC;IAElE,qBACE,oBAAC3B;QAAe0B,OAAOA;QAAOJ,KAAKA;OAChCD,WAAWL,0BAAY,oBAACY;QAAuBF,OAAOA;OAAQV;AAGrE,EAAE;AAEF,MAAMY,yBAAgFC,CAAAA;IACpF,MAAM,EAAEH,KAAK,EAAE,GAAGG;IAElB,MAAMC,kBAAkBJ,MAAMK,uBAAuB;IACrD,qBAAO,oBAACC;QAAIC,OAAO;YAAEC,SAAS;YAAaJ;QAAgB;OAAID,MAAMM,QAAQ;AAC/E"}
@@ -0,0 +1,16 @@
1
+ import * as React from 'react';
2
+ import { STRICT_MODE_ID } from '../constants';
3
+ import { isDecoratorDisabled } from '../utils/isDecoratorDisabled';
4
+ export const withReactStrictMode = (StoryFn, context)=>{
5
+ if (isDecoratorDisabled(context, 'ReactStrictMode')) {
6
+ return StoryFn();
7
+ }
8
+ var _context_globals_STRICT_MODE_ID;
9
+ const isActive = (_context_globals_STRICT_MODE_ID = context.globals[STRICT_MODE_ID]) !== null && _context_globals_STRICT_MODE_ID !== void 0 ? _context_globals_STRICT_MODE_ID : false;
10
+ return /*#__PURE__*/ React.createElement(StrictModeWrapper, {
11
+ strictMode: isActive
12
+ }, StoryFn());
13
+ };
14
+ const StrictModeWrapper = (props)=>{
15
+ return props.strictMode ? /*#__PURE__*/ React.createElement(React.StrictMode, null, props.children) : props.children;
16
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/decorators/withReactStrictMode.tsx"],"sourcesContent":["import * as React from 'react';\nimport type { JSXElement } from '@fluentui/react-utilities';\n\nimport { STRICT_MODE_ID } from '../constants';\nimport { FluentStoryContext } from '../hooks';\nimport { isDecoratorDisabled } from '../utils/isDecoratorDisabled';\n\nexport const withReactStrictMode = (StoryFn: () => JSXElement, context: FluentStoryContext): JSXElement => {\n if (isDecoratorDisabled(context, 'ReactStrictMode')) {\n return StoryFn();\n }\n\n const isActive = context.globals[STRICT_MODE_ID] ?? false;\n\n return <StrictModeWrapper strictMode={isActive}>{StoryFn()}</StrictModeWrapper>;\n};\n\nconst StrictModeWrapper = (props: { strictMode: boolean; children: React.ReactElement }) => {\n return props.strictMode ? <React.StrictMode>{props.children}</React.StrictMode> : props.children;\n};\n"],"names":["React","STRICT_MODE_ID","isDecoratorDisabled","withReactStrictMode","StoryFn","context","isActive","globals","StrictModeWrapper","strictMode","props","StrictMode","children"],"mappings":"AAAA,YAAYA,WAAW,QAAQ;AAG/B,SAASC,cAAc,QAAQ,eAAe;AAE9C,SAASC,mBAAmB,QAAQ,+BAA+B;AAEnE,OAAO,MAAMC,sBAAsB,CAACC,SAA2BC;IAC7D,IAAIH,oBAAoBG,SAAS,oBAAoB;QACnD,OAAOD;IACT;QAEiBC;IAAjB,MAAMC,WAAWD,CAAAA,kCAAAA,QAAQE,OAAO,CAACN,eAAe,cAA/BI,6CAAAA,kCAAmC;IAEpD,qBAAO,oBAACG;QAAkBC,YAAYH;OAAWF;AACnD,EAAE;AAEF,MAAMI,oBAAoB,CAACE;IACzB,OAAOA,MAAMD,UAAU,iBAAG,oBAACT,MAAMW,UAAU,QAAED,MAAME,QAAQ,IAAuBF,MAAME,QAAQ;AAClG"}
@@ -0,0 +1,177 @@
1
+ import * as React from 'react';
2
+ import { SplitButton } 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';
8
+ import { bundleIcon, MarkdownFilled, MarkdownRegular } from '@fluentui/react-icons';
9
+ import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
10
+ const MarkdownIcon = bundleIcon(MarkdownFilled, MarkdownRegular);
11
+ const useStyles = makeStyles({
12
+ button: {
13
+ marginInlineStart: 'auto'
14
+ }
15
+ });
16
+ /**
17
+ * A button that allows users to copy the current page as markdown to their clipboard or view it in a new tab.
18
+ * The markdown content is fetched from the Storybook API and cached for subsequent requests.
19
+ */ export const CopyAsMarkdownButton = ({ storyId = '' })=>{
20
+ const { targetDocument } = useFluent();
21
+ const targetWindow = targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.defaultView;
22
+ const styles = useStyles();
23
+ const toastId = useId('copy-toast');
24
+ const toasterId = useId('toaster');
25
+ const { dispatchToast, updateToast } = useToastController(toasterId);
26
+ // Cache for the fetched markdown content to avoid redundant network requests
27
+ const markdownContentCache = React.useRef(null);
28
+ // AbortController to track and cancel fetch requests
29
+ const abortControllerRef = React.useRef(null);
30
+ // Full URL to the markdown endpoint for this story
31
+ const markdownUrl = React.useMemo(()=>{
32
+ return targetWindow ? convertStoryIdToMarkdownUrl(targetWindow, storyId) : '';
33
+ }, [
34
+ storyId,
35
+ targetWindow
36
+ ]);
37
+ // Cleanup: abort pending requests on unmount
38
+ React.useEffect(()=>{
39
+ return ()=>{
40
+ var _abortControllerRef_current;
41
+ (_abortControllerRef_current = abortControllerRef.current) === null || _abortControllerRef_current === void 0 ? void 0 : _abortControllerRef_current.abort();
42
+ };
43
+ }, []);
44
+ /**
45
+ * Fetches the markdown content (with caching) and copies it to the clipboard.
46
+ * Shows a toast notification with loading, success, or error states.
47
+ * Skips the request if one is already in progress.
48
+ */ const copyPageContentToClipboard = React.useCallback(async ()=>{
49
+ // Skip if a request is already in progress (abort controller exists and not aborted)
50
+ if (abortControllerRef.current && !abortControllerRef.current.signal.aborted) {
51
+ return;
52
+ }
53
+ // Ensure we have a window context to use for clipboard and fetch
54
+ if (!targetWindow) {
55
+ return;
56
+ }
57
+ // Create new AbortController for this request
58
+ const abortController = new AbortController();
59
+ abortControllerRef.current = abortController;
60
+ // Show loading toast that persists until updated
61
+ dispatchToast(/*#__PURE__*/ React.createElement(Toast, null, /*#__PURE__*/ React.createElement(ToastTitle, {
62
+ media: /*#__PURE__*/ React.createElement(Spinner, null)
63
+ }, "Copying page content...")), {
64
+ toastId,
65
+ intent: 'info',
66
+ timeout: -1
67
+ });
68
+ try {
69
+ // Use cached content if available, otherwise fetch from API
70
+ if (!markdownContentCache.current) {
71
+ markdownContentCache.current = await fetchMarkdownContent(targetWindow, markdownUrl, abortController.signal);
72
+ }
73
+ // Copy to clipboard
74
+ await (targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.navigator.clipboard.writeText(markdownContentCache.current));
75
+ // Update toast to success
76
+ updateToast({
77
+ content: /*#__PURE__*/ React.createElement(Toast, null, /*#__PURE__*/ React.createElement(ToastTitle, null, "Page content copied to clipboard!")),
78
+ intent: 'success',
79
+ toastId,
80
+ timeout: 3000
81
+ });
82
+ } catch (error) {
83
+ // Don't show error if request was aborted
84
+ if (abortController.signal.aborted) {
85
+ return;
86
+ }
87
+ const errorMessage = error instanceof Error ? error.message : String(error);
88
+ // Update toast to error
89
+ updateToast({
90
+ content: /*#__PURE__*/ React.createElement(Toast, null, /*#__PURE__*/ React.createElement(ToastTitle, null, "Failed to copy page content: ", errorMessage)),
91
+ intent: 'error',
92
+ toastId,
93
+ timeout: 3000
94
+ });
95
+ } finally{
96
+ // Clear the abort controller ref to allow new requests
97
+ abortControllerRef.current = null;
98
+ }
99
+ }, [
100
+ dispatchToast,
101
+ updateToast,
102
+ toastId,
103
+ markdownUrl,
104
+ targetWindow
105
+ ]);
106
+ /** Opens the markdown content in a new browser tab */ const openInNewTab = React.useCallback(()=>{
107
+ targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.open(markdownUrl, '_blank');
108
+ }, [
109
+ markdownUrl,
110
+ targetWindow
111
+ ]);
112
+ if (!storyId) {
113
+ return null;
114
+ }
115
+ return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(Menu, {
116
+ positioning: "below-end"
117
+ }, /*#__PURE__*/ React.createElement(MenuTrigger, {
118
+ disableButtonEnhancement: true
119
+ }, (triggerProps)=>/*#__PURE__*/ React.createElement(SplitButton, {
120
+ className: styles.button,
121
+ menuButton: triggerProps,
122
+ primaryActionButton: {
123
+ appearance: 'secondary',
124
+ icon: /*#__PURE__*/ React.createElement(MarkdownIcon, null),
125
+ onClick: copyPageContentToClipboard,
126
+ 'aria-label': 'Copy page content as markdown to clipboard'
127
+ },
128
+ "aria-label": "Copy page as markdown"
129
+ }, "Copy Page")), /*#__PURE__*/ React.createElement(MenuPopover, null, /*#__PURE__*/ React.createElement(MenuList, null, /*#__PURE__*/ React.createElement(MenuItem, {
130
+ icon: /*#__PURE__*/ React.createElement(MarkdownIcon, null),
131
+ onClick: openInNewTab
132
+ }, "View as Markdown")))), /*#__PURE__*/ React.createElement(Toaster, {
133
+ toasterId: toasterId
134
+ }));
135
+ };
136
+ /**
137
+ * Regex pattern to remove the story variant suffix from Storybook story IDs.
138
+ * @example "button--primary" -> "button"
139
+ */ const STORYBOOK_VARIANT_SUFFIX_PATTERN = /--\w+$/g;
140
+ /**
141
+ * Gets the base URL for fetching markdown content from the Storybook LLM endpoint.
142
+ * Each story's markdown is available at: {BASE_URL}/{storyId}.txt
143
+ * @param targetWindow - The window object to use for location access
144
+ * @returns The base URL constructed from current location origin and pathname
145
+ */ function getStorybookMarkdownApiBaseUrl(targetWindow) {
146
+ // Remove the [page].html file from pathname and append /llms/
147
+ const basePath = targetWindow.location.pathname.replace(/\/[^/]*\.html$/, '');
148
+ return `${targetWindow.location.origin}${basePath}/llms/`;
149
+ }
150
+ /**
151
+ * Converts a Storybook story ID to a markdown URL.
152
+ * @param targetWindow - The window object to use for location access
153
+ * @param storyId - The Storybook story ID
154
+ * @returns The full URL to the markdown endpoint for the story
155
+ * @example "button--primary" -> "https://storybooks.fluentui.dev/llms/button.txt"
156
+ */ function convertStoryIdToMarkdownUrl(targetWindow, storyId) {
157
+ return `${getStorybookMarkdownApiBaseUrl(targetWindow)}${storyId.replace(STORYBOOK_VARIANT_SUFFIX_PATTERN, '.txt')}`;
158
+ }
159
+ /**
160
+ * Fetches markdown content from the Storybook API.
161
+ * @param targetWindow - The window object to use for fetch access
162
+ * @param url - The URL to fetch markdown content from
163
+ * @param signal - Optional AbortSignal to cancel the request
164
+ * @returns Promise resolving to the markdown text content
165
+ * @throws Error if the fetch request fails or is aborted
166
+ */ async function fetchMarkdownContent(targetWindow, url, signal) {
167
+ const response = await targetWindow.fetch(url, {
168
+ headers: {
169
+ 'Content-Type': 'text/plain'
170
+ },
171
+ signal
172
+ });
173
+ if (!response.ok) {
174
+ throw new Error(`Failed to fetch markdown: ${response.status} ${response.statusText}`);
175
+ }
176
+ return response.text();
177
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/docs/CopyAsMarkdownButton.tsx"],"sourcesContent":["import * as React from 'react';\nimport { SplitButton, type MenuButtonProps } from '@fluentui/react-button';\nimport { Menu, MenuItem, MenuList, MenuPopover, MenuTrigger } from '@fluentui/react-menu';\nimport { Spinner } from '@fluentui/react-spinner';\nimport { Toast, Toaster, ToastTitle, useToastController } from '@fluentui/react-toast';\nimport { useId } from '@fluentui/react-utilities';\nimport { makeStyles } from '@griffel/react';\nimport { bundleIcon, MarkdownFilled, MarkdownRegular } from '@fluentui/react-icons';\nimport { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';\n\nconst MarkdownIcon = bundleIcon(MarkdownFilled, MarkdownRegular);\n\nconst useStyles = makeStyles({\n button: {\n marginInlineStart: 'auto',\n },\n});\n\nexport interface CopyAsMarkdownProps {\n /** The Storybook story ID used to generate the markdown URL */\n storyId?: string;\n}\n\n/**\n * A button that allows users to copy the current page as markdown to their clipboard or view it in a new tab.\n * The markdown content is fetched from the Storybook API and cached for subsequent requests.\n */\nexport const CopyAsMarkdownButton: React.FC<CopyAsMarkdownProps> = ({ storyId = '' }) => {\n const { targetDocument } = useFluent();\n const targetWindow = targetDocument?.defaultView;\n const styles = useStyles();\n const toastId = useId('copy-toast');\n const toasterId = useId('toaster');\n const { dispatchToast, updateToast } = useToastController(toasterId);\n\n // Cache for the fetched markdown content to avoid redundant network requests\n const markdownContentCache = React.useRef<string | null>(null);\n\n // AbortController to track and cancel fetch requests\n const abortControllerRef = React.useRef<AbortController | null>(null);\n\n // Full URL to the markdown endpoint for this story\n const markdownUrl = React.useMemo(() => {\n return targetWindow ? convertStoryIdToMarkdownUrl(targetWindow, storyId) : '';\n }, [storyId, targetWindow]);\n\n // Cleanup: abort pending requests on unmount\n React.useEffect(() => {\n return () => {\n abortControllerRef.current?.abort();\n };\n }, []);\n\n /**\n * Fetches the markdown content (with caching) and copies it to the clipboard.\n * Shows a toast notification with loading, success, or error states.\n * Skips the request if one is already in progress.\n */\n const copyPageContentToClipboard = React.useCallback(async () => {\n // Skip if a request is already in progress (abort controller exists and not aborted)\n if (abortControllerRef.current && !abortControllerRef.current.signal.aborted) {\n return;\n }\n\n // Ensure we have a window context to use for clipboard and fetch\n if (!targetWindow) {\n return;\n }\n\n // Create new AbortController for this request\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n // Show loading toast that persists until updated\n dispatchToast(\n <Toast>\n <ToastTitle media={<Spinner />}>Copying page content...</ToastTitle>\n </Toast>,\n {\n toastId,\n intent: 'info',\n timeout: -1, // Never auto-dismiss\n },\n );\n\n try {\n // Use cached content if available, otherwise fetch from API\n if (!markdownContentCache.current) {\n markdownContentCache.current = await fetchMarkdownContent(targetWindow, markdownUrl, abortController.signal);\n }\n\n // Copy to clipboard\n await targetWindow?.navigator.clipboard.writeText(markdownContentCache.current);\n\n // Update toast to success\n updateToast({\n content: (\n <Toast>\n <ToastTitle>Page content copied to clipboard!</ToastTitle>\n </Toast>\n ),\n intent: 'success',\n toastId,\n timeout: 3000,\n });\n } catch (error) {\n // Don't show error if request was aborted\n if (abortController.signal.aborted) {\n return;\n }\n\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n // Update toast to error\n updateToast({\n content: (\n <Toast>\n <ToastTitle>Failed to copy page content: {errorMessage}</ToastTitle>\n </Toast>\n ),\n intent: 'error',\n toastId,\n timeout: 3000,\n });\n } finally {\n // Clear the abort controller ref to allow new requests\n abortControllerRef.current = null;\n }\n }, [dispatchToast, updateToast, toastId, markdownUrl, targetWindow]);\n\n /** Opens the markdown content in a new browser tab */\n const openInNewTab = React.useCallback(() => {\n targetWindow?.open(markdownUrl, '_blank');\n }, [markdownUrl, targetWindow]);\n\n if (!storyId) {\n return null;\n }\n\n return (\n <>\n <Menu positioning=\"below-end\">\n <MenuTrigger disableButtonEnhancement>\n {(triggerProps: MenuButtonProps) => (\n <SplitButton\n className={styles.button}\n menuButton={triggerProps}\n primaryActionButton={{\n appearance: 'secondary',\n icon: <MarkdownIcon />,\n onClick: copyPageContentToClipboard,\n 'aria-label': 'Copy page content as markdown to clipboard',\n }}\n aria-label=\"Copy page as markdown\"\n >\n Copy Page\n </SplitButton>\n )}\n </MenuTrigger>\n\n <MenuPopover>\n <MenuList>\n <MenuItem icon={<MarkdownIcon />} onClick={openInNewTab}>\n View as Markdown\n </MenuItem>\n </MenuList>\n </MenuPopover>\n </Menu>\n <Toaster toasterId={toasterId} />\n </>\n );\n};\n\n/**\n * Regex pattern to remove the story variant suffix from Storybook story IDs.\n * @example \"button--primary\" -> \"button\"\n */\nconst STORYBOOK_VARIANT_SUFFIX_PATTERN = /--\\w+$/g;\n\n/**\n * Gets the base URL for fetching markdown content from the Storybook LLM endpoint.\n * Each story's markdown is available at: {BASE_URL}/{storyId}.txt\n * @param targetWindow - The window object to use for location access\n * @returns The base URL constructed from current location origin and pathname\n */\nfunction getStorybookMarkdownApiBaseUrl(targetWindow: Window): string {\n // Remove the [page].html file from pathname and append /llms/\n const basePath = targetWindow.location.pathname.replace(/\\/[^/]*\\.html$/, '');\n return `${targetWindow.location.origin}${basePath}/llms/`;\n}\n\n/**\n * Converts a Storybook story ID to a markdown URL.\n * @param targetWindow - The window object to use for location access\n * @param storyId - The Storybook story ID\n * @returns The full URL to the markdown endpoint for the story\n * @example \"button--primary\" -> \"https://storybooks.fluentui.dev/llms/button.txt\"\n */\nfunction convertStoryIdToMarkdownUrl(targetWindow: Window, storyId: string): string {\n return `${getStorybookMarkdownApiBaseUrl(targetWindow)}${storyId.replace(STORYBOOK_VARIANT_SUFFIX_PATTERN, '.txt')}`;\n}\n\n/**\n * Fetches markdown content from the Storybook API.\n * @param targetWindow - The window object to use for fetch access\n * @param url - The URL to fetch markdown content from\n * @param signal - Optional AbortSignal to cancel the request\n * @returns Promise resolving to the markdown text content\n * @throws Error if the fetch request fails or is aborted\n */\nasync function fetchMarkdownContent(\n targetWindow: Window,\n url: string,\n signal: AbortSignal | undefined,\n): Promise<string> {\n const response = await targetWindow.fetch(url, {\n headers: {\n 'Content-Type': 'text/plain',\n },\n signal,\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch markdown: ${response.status} ${response.statusText}`);\n }\n\n return response.text();\n}\n"],"names":["React","SplitButton","Menu","MenuItem","MenuList","MenuPopover","MenuTrigger","Spinner","Toast","Toaster","ToastTitle","useToastController","useId","makeStyles","bundleIcon","MarkdownFilled","MarkdownRegular","useFluent_unstable","useFluent","MarkdownIcon","useStyles","button","marginInlineStart","CopyAsMarkdownButton","storyId","targetDocument","targetWindow","defaultView","styles","toastId","toasterId","dispatchToast","updateToast","markdownContentCache","useRef","abortControllerRef","markdownUrl","useMemo","convertStoryIdToMarkdownUrl","useEffect","current","abort","copyPageContentToClipboard","useCallback","signal","aborted","abortController","AbortController","media","intent","timeout","fetchMarkdownContent","navigator","clipboard","writeText","content","error","errorMessage","Error","message","String","openInNewTab","open","positioning","disableButtonEnhancement","triggerProps","className","menuButton","primaryActionButton","appearance","icon","onClick","aria-label","STORYBOOK_VARIANT_SUFFIX_PATTERN","getStorybookMarkdownApiBaseUrl","basePath","location","pathname","replace","origin","url","response","fetch","headers","ok","status","statusText","text"],"mappings":"AAAA,YAAYA,WAAW,QAAQ;AAC/B,SAASC,WAAW,QAA8B,yBAAyB;AAC3E,SAASC,IAAI,EAAEC,QAAQ,EAAEC,QAAQ,EAAEC,WAAW,EAAEC,WAAW,QAAQ,uBAAuB;AAC1F,SAASC,OAAO,QAAQ,0BAA0B;AAClD,SAASC,KAAK,EAAEC,OAAO,EAAEC,UAAU,EAAEC,kBAAkB,QAAQ,wBAAwB;AACvF,SAASC,KAAK,QAAQ,4BAA4B;AAClD,SAASC,UAAU,QAAQ,iBAAiB;AAC5C,SAASC,UAAU,EAAEC,cAAc,EAAEC,eAAe,QAAQ,wBAAwB;AACpF,SAASC,sBAAsBC,SAAS,QAAQ,kCAAkC;AAElF,MAAMC,eAAeL,WAAWC,gBAAgBC;AAEhD,MAAMI,YAAYP,WAAW;IAC3BQ,QAAQ;QACNC,mBAAmB;IACrB;AACF;AAOA;;;CAGC,GACD,OAAO,MAAMC,uBAAsD,CAAC,EAAEC,UAAU,EAAE,EAAE;IAClF,MAAM,EAAEC,cAAc,EAAE,GAAGP;IAC3B,MAAMQ,eAAeD,2BAAAA,qCAAAA,eAAgBE,WAAW;IAChD,MAAMC,SAASR;IACf,MAAMS,UAAUjB,MAAM;IACtB,MAAMkB,YAAYlB,MAAM;IACxB,MAAM,EAAEmB,aAAa,EAAEC,WAAW,EAAE,GAAGrB,mBAAmBmB;IAE1D,6EAA6E;IAC7E,MAAMG,uBAAuBjC,MAAMkC,MAAM,CAAgB;IAEzD,qDAAqD;IACrD,MAAMC,qBAAqBnC,MAAMkC,MAAM,CAAyB;IAEhE,mDAAmD;IACnD,MAAME,cAAcpC,MAAMqC,OAAO,CAAC;QAChC,OAAOX,eAAeY,4BAA4BZ,cAAcF,WAAW;IAC7E,GAAG;QAACA;QAASE;KAAa;IAE1B,6CAA6C;IAC7C1B,MAAMuC,SAAS,CAAC;QACd,OAAO;gBACLJ;aAAAA,8BAAAA,mBAAmBK,OAAO,cAA1BL,kDAAAA,4BAA4BM,KAAK;QACnC;IACF,GAAG,EAAE;IAEL;;;;GAIC,GACD,MAAMC,6BAA6B1C,MAAM2C,WAAW,CAAC;QACnD,qFAAqF;QACrF,IAAIR,mBAAmBK,OAAO,IAAI,CAACL,mBAAmBK,OAAO,CAACI,MAAM,CAACC,OAAO,EAAE;YAC5E;QACF;QAEA,iEAAiE;QACjE,IAAI,CAACnB,cAAc;YACjB;QACF;QAEA,8CAA8C;QAC9C,MAAMoB,kBAAkB,IAAIC;QAC5BZ,mBAAmBK,OAAO,GAAGM;QAE7B,iDAAiD;QACjDf,4BACE,oBAACvB,2BACC,oBAACE;YAAWsC,qBAAO,oBAACzC;WAAY,6BAElC;YACEsB;YACAoB,QAAQ;YACRC,SAAS,CAAC;QACZ;QAGF,IAAI;YACF,4DAA4D;YAC5D,IAAI,CAACjB,qBAAqBO,OAAO,EAAE;gBACjCP,qBAAqBO,OAAO,GAAG,MAAMW,qBAAqBzB,cAAcU,aAAaU,gBAAgBF,MAAM;YAC7G;YAEA,oBAAoB;YACpB,OAAMlB,yBAAAA,mCAAAA,aAAc0B,SAAS,CAACC,SAAS,CAACC,SAAS,CAACrB,qBAAqBO,OAAO;YAE9E,0BAA0B;YAC1BR,YAAY;gBACVuB,uBACE,oBAAC/C,2BACC,oBAACE,kBAAW;gBAGhBuC,QAAQ;gBACRpB;gBACAqB,SAAS;YACX;QACF,EAAE,OAAOM,OAAO;YACd,0CAA0C;YAC1C,IAAIV,gBAAgBF,MAAM,CAACC,OAAO,EAAE;gBAClC;YACF;YAEA,MAAMY,eAAeD,iBAAiBE,QAAQF,MAAMG,OAAO,GAAGC,OAAOJ;YAErE,wBAAwB;YACxBxB,YAAY;gBACVuB,uBACE,oBAAC/C,2BACC,oBAACE,kBAAW,iCAA8B+C;gBAG9CR,QAAQ;gBACRpB;gBACAqB,SAAS;YACX;QACF,SAAU;YACR,uDAAuD;YACvDf,mBAAmBK,OAAO,GAAG;QAC/B;IACF,GAAG;QAACT;QAAeC;QAAaH;QAASO;QAAaV;KAAa;IAEnE,oDAAoD,GACpD,MAAMmC,eAAe7D,MAAM2C,WAAW,CAAC;QACrCjB,yBAAAA,mCAAAA,aAAcoC,IAAI,CAAC1B,aAAa;IAClC,GAAG;QAACA;QAAaV;KAAa;IAE9B,IAAI,CAACF,SAAS;QACZ,OAAO;IACT;IAEA,qBACE,wDACE,oBAACtB;QAAK6D,aAAY;qBAChB,oBAACzD;QAAY0D,0BAAAA;OACV,CAACC,6BACA,oBAAChE;YACCiE,WAAWtC,OAAOP,MAAM;YACxB8C,YAAYF;YACZG,qBAAqB;gBACnBC,YAAY;gBACZC,oBAAM,oBAACnD;gBACPoD,SAAS7B;gBACT,cAAc;YAChB;YACA8B,cAAW;WACZ,6BAML,oBAACnE,iCACC,oBAACD,8BACC,oBAACD;QAASmE,oBAAM,oBAACnD;QAAiBoD,SAASV;OAAc,sCAM/D,oBAACpD;QAAQqB,WAAWA;;AAG1B,EAAE;AAEF;;;CAGC,GACD,MAAM2C,mCAAmC;AAEzC;;;;;CAKC,GACD,SAASC,+BAA+BhD,YAAoB;IAC1D,8DAA8D;IAC9D,MAAMiD,WAAWjD,aAAakD,QAAQ,CAACC,QAAQ,CAACC,OAAO,CAAC,kBAAkB;IAC1E,OAAO,GAAGpD,aAAakD,QAAQ,CAACG,MAAM,GAAGJ,SAAS,MAAM,CAAC;AAC3D;AAEA;;;;;;CAMC,GACD,SAASrC,4BAA4BZ,YAAoB,EAAEF,OAAe;IACxE,OAAO,GAAGkD,+BAA+BhD,gBAAgBF,QAAQsD,OAAO,CAACL,kCAAkC,SAAS;AACtH;AAEA;;;;;;;CAOC,GACD,eAAetB,qBACbzB,YAAoB,EACpBsD,GAAW,EACXpC,MAA+B;IAE/B,MAAMqC,WAAW,MAAMvD,aAAawD,KAAK,CAACF,KAAK;QAC7CG,SAAS;YACP,gBAAgB;QAClB;QACAvC;IACF;IAEA,IAAI,CAACqC,SAASG,EAAE,EAAE;QAChB,MAAM,IAAI1B,MAAM,CAAC,0BAA0B,EAAEuB,SAASI,MAAM,CAAC,CAAC,EAAEJ,SAASK,UAAU,EAAE;IACvF;IAEA,OAAOL,SAASM,IAAI;AACtB"}