@fluentui-react-native/storybook-desktop-runtime 0.2.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 (67) hide show
  1. package/.cache/tsconfig.tsbuildinfo +1 -0
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +21 -0
  4. package/config/metro.cjs +125 -0
  5. package/config/react-native-safe-area-context.cjs +22 -0
  6. package/lib/DesktopDriverBridge.d.ts +7 -0
  7. package/lib/DesktopDriverBridge.d.ts.map +1 -0
  8. package/lib/DesktopDriverBridge.js +52 -0
  9. package/lib/DesktopDriverBridge.js.map +1 -0
  10. package/lib/DesktopStoryRoot.d.ts +8 -0
  11. package/lib/DesktopStoryRoot.d.ts.map +1 -0
  12. package/lib/DesktopStoryRoot.js +57 -0
  13. package/lib/DesktopStoryRoot.js.map +1 -0
  14. package/lib/DesktopStorybookConfig.d.ts +28 -0
  15. package/lib/DesktopStorybookConfig.d.ts.map +1 -0
  16. package/lib/DesktopStorybookConfig.js +23 -0
  17. package/lib/DesktopStorybookConfig.js.map +1 -0
  18. package/lib/StorybookTheme.d.ts +4 -0
  19. package/lib/StorybookTheme.d.ts.map +1 -0
  20. package/lib/StorybookTheme.js +79 -0
  21. package/lib/StorybookTheme.js.map +1 -0
  22. package/lib/StorybookUI.d.ts +2 -0
  23. package/lib/StorybookUI.d.ts.map +1 -0
  24. package/lib/StorybookUI.js +2 -0
  25. package/lib/StorybookUI.js.map +1 -0
  26. package/lib/StorybookUI.win32.d.ts +3 -0
  27. package/lib/StorybookUI.win32.d.ts.map +1 -0
  28. package/lib/StorybookUI.win32.js +171 -0
  29. package/lib/StorybookUI.win32.js.map +1 -0
  30. package/lib/Win32AddonsPanel.d.ts +9 -0
  31. package/lib/Win32AddonsPanel.d.ts.map +1 -0
  32. package/lib/Win32AddonsPanel.js +72 -0
  33. package/lib/Win32AddonsPanel.js.map +1 -0
  34. package/lib/Win32CalloutPortal.d.ts +16 -0
  35. package/lib/Win32CalloutPortal.d.ts.map +1 -0
  36. package/lib/Win32CalloutPortal.js +17 -0
  37. package/lib/Win32CalloutPortal.js.map +1 -0
  38. package/lib/Win32ResizeHandle.d.ts +8 -0
  39. package/lib/Win32ResizeHandle.d.ts.map +1 -0
  40. package/lib/Win32ResizeHandle.js +46 -0
  41. package/lib/Win32ResizeHandle.js.map +1 -0
  42. package/lib/createDesktopStorybookApp.d.ts +9 -0
  43. package/lib/createDesktopStorybookApp.d.ts.map +1 -0
  44. package/lib/createDesktopStorybookApp.js +38 -0
  45. package/lib/createDesktopStorybookApp.js.map +1 -0
  46. package/lib/createDesktopStorybookPreview.d.ts +3 -0
  47. package/lib/createDesktopStorybookPreview.d.ts.map +1 -0
  48. package/lib/createDesktopStorybookPreview.js +21 -0
  49. package/lib/createDesktopStorybookPreview.js.map +1 -0
  50. package/lib/index.d.ts +7 -0
  51. package/lib/index.d.ts.map +1 -0
  52. package/lib/index.js +5 -0
  53. package/lib/index.js.map +1 -0
  54. package/package.json +106 -0
  55. package/src/DesktopDriverBridge.tsx +66 -0
  56. package/src/DesktopStoryRoot.tsx +88 -0
  57. package/src/DesktopStorybookConfig.tsx +58 -0
  58. package/src/StorybookTheme.tsx +114 -0
  59. package/src/StorybookUI.ts +1 -0
  60. package/src/StorybookUI.win32.tsx +330 -0
  61. package/src/Win32AddonsPanel.tsx +135 -0
  62. package/src/Win32CalloutPortal.tsx +73 -0
  63. package/src/Win32ResizeHandle.tsx +67 -0
  64. package/src/createDesktopStorybookApp.tsx +72 -0
  65. package/src/createDesktopStorybookPreview.tsx +31 -0
  66. package/src/index.ts +12 -0
  67. package/tsconfig.json +24 -0
package/package.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "@fluentui-react-native/storybook-desktop-runtime",
3
+ "version": "0.2.0",
4
+ "description": "React Native runtime and Metro services for desktop Storybook applications",
5
+ "license": "MIT",
6
+ "author": "",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/microsoft/fluentui-react-native.git",
10
+ "directory": "packages/agentic/storybook-desktop-runtime"
11
+ },
12
+ "type": "module",
13
+ "main": "lib/index.js",
14
+ "module": "lib/index.js",
15
+ "types": "lib/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/index.d.ts",
19
+ "react-native": "./src/index.ts",
20
+ "import": "./lib/index.js",
21
+ "default": "./src/index.ts"
22
+ },
23
+ "./metro": "./config/metro.cjs",
24
+ "./package.json": "./package.json"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -b",
28
+ "clean": "fluentui-scripts clean",
29
+ "format": "fluentui-scripts format",
30
+ "lint": "fluentui-scripts lint"
31
+ },
32
+ "dependencies": {
33
+ "@fluentui-react-native/callout": "0.29.4",
34
+ "@fluentui-react-native/default-theme": "0.27.11",
35
+ "@fluentui-react-native/design": "0.4.0",
36
+ "@rnx-kit/metro-config": "^2.2.4",
37
+ "@rnx-kit/metro-resolver-symlinks": "^0.3.1",
38
+ "@storybook/react-native": "^10.4.7",
39
+ "@storybook/react-native-theming": "^10.4.7",
40
+ "@storybook/react-native-ui-common": "^10.4.7",
41
+ "@storybook/react-native-ui-lite": "^10.4.7",
42
+ "oxc-resolver": "^11.24.0",
43
+ "storybook": "^10.4.0"
44
+ },
45
+ "devDependencies": {
46
+ "@babel/core": "^7.20.0",
47
+ "@fluentui-react-native/scripts": "0.1.0",
48
+ "@office-iss/react-native-win32": "^0.81.0",
49
+ "@react-native/babel-preset": "^0.81.0",
50
+ "@types/react": "~19.1.4",
51
+ "react": "19.1.4",
52
+ "react-native": "^0.81.6",
53
+ "react-native-macos": "^0.81.0",
54
+ "react-native-windows": "^0.81.0"
55
+ },
56
+ "peerDependencies": {
57
+ "@office-iss/react-native-win32": "^0.81.0",
58
+ "@types/react": "~19.1.4",
59
+ "react": "19.1.4",
60
+ "react-native": "^0.81.6",
61
+ "react-native-macos": "^0.81.0",
62
+ "react-native-windows": "^0.81.0"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "@office-iss/react-native-win32": {
66
+ "optional": true
67
+ },
68
+ "@types/react": {
69
+ "optional": true
70
+ },
71
+ "react-native-macos": {
72
+ "optional": true
73
+ },
74
+ "react-native-windows": {
75
+ "optional": true
76
+ }
77
+ },
78
+ "furn": {
79
+ "knip": {
80
+ "ignoreDependencies": [
81
+ "@fluentui-react-native/callout"
82
+ ]
83
+ }
84
+ },
85
+ "rnx-kit": {
86
+ "kitType": "library",
87
+ "alignDeps": {
88
+ "requirements": {
89
+ "development": [
90
+ "react-native@0.81"
91
+ ],
92
+ "production": [
93
+ "react-native@0.81"
94
+ ]
95
+ },
96
+ "capabilities": [
97
+ "babel-preset-react-native",
98
+ "core",
99
+ "core-macos",
100
+ "core-win32",
101
+ "core-windows"
102
+ ]
103
+ },
104
+ "extends": "@fluentui-react-native/scripts/kit-config"
105
+ }
106
+ }
@@ -0,0 +1,66 @@
1
+ import * as React from 'react';
2
+ import { addons } from 'storybook/preview-api';
3
+
4
+ import { useDesktopStorybookConfig } from './DesktopStorybookConfig';
5
+
6
+ export const desktopPrepareStoryEvent = 'furn:desktop:prepare-story';
7
+ export const desktopRequestHelloEvent = 'furn:desktop:request-hello';
8
+ export const desktopRuntimeHelloEvent = 'furn:desktop:hello';
9
+ export const desktopStoryErrorEvent = 'furn:desktop:story-error';
10
+ export const desktopStoryReadyEvent = 'furn:desktop:story-ready';
11
+
12
+ type PrepareStoryPayload = {
13
+ requestId: string;
14
+ runId: string;
15
+ storyId: string;
16
+ };
17
+
18
+ export function DesktopDriverBridge(): null {
19
+ const { prepareStory, runtimeInstance } = useDesktopStorybookConfig();
20
+
21
+ React.useEffect(() => {
22
+ if (
23
+ !runtimeInstance?.bridgeNonce ||
24
+ !runtimeInstance.endpoint ||
25
+ !runtimeInstance.instanceId ||
26
+ !runtimeInstance.platformManifestDigest ||
27
+ !runtimeInstance.targetId
28
+ ) {
29
+ return undefined;
30
+ }
31
+ const channel = addons.getChannel();
32
+ const emitHello = () => {
33
+ channel.emit(desktopRuntimeHelloEvent, {
34
+ endpoint: runtimeInstance.endpoint,
35
+ instanceId: runtimeInstance.instanceId,
36
+ nonce: runtimeInstance.bridgeNonce,
37
+ platformManifestDigest: runtimeInstance.platformManifestDigest,
38
+ targetId: runtimeInstance.targetId,
39
+ version: 1,
40
+ });
41
+ };
42
+ const onPrepare = (payload: PrepareStoryPayload) => {
43
+ if (isPrepareStoryPayload(payload)) {
44
+ prepareStory(payload);
45
+ }
46
+ };
47
+
48
+ channel.on(desktopPrepareStoryEvent, onPrepare);
49
+ channel.on(desktopRequestHelloEvent, emitHello);
50
+ emitHello();
51
+ return () => {
52
+ channel.off(desktopPrepareStoryEvent, onPrepare);
53
+ channel.off(desktopRequestHelloEvent, emitHello);
54
+ };
55
+ }, [prepareStory, runtimeInstance]);
56
+
57
+ return null;
58
+ }
59
+
60
+ function isPrepareStoryPayload(value: unknown): value is PrepareStoryPayload {
61
+ if (!value || typeof value !== 'object') {
62
+ return false;
63
+ }
64
+ const payload = value as Record<string, unknown>;
65
+ return typeof payload.requestId === 'string' && typeof payload.runId === 'string' && typeof payload.storyId === 'string';
66
+ }
@@ -0,0 +1,88 @@
1
+ import * as React from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import { addons } from 'storybook/preview-api';
4
+
5
+ import { desktopStoryErrorEvent, desktopStoryReadyEvent } from './DesktopDriverBridge';
6
+ import { useDesktopStorybookConfig, useDesktopStorybookTestID } from './DesktopStorybookConfig';
7
+
8
+ type DesktopStoryRootProps = {
9
+ children: React.ReactNode;
10
+ storyId: string;
11
+ };
12
+
13
+ export function DesktopStoryRoot({ children, storyId }: DesktopStoryRootProps) {
14
+ const { runtimeInstance, selection } = useDesktopStorybookConfig();
15
+ const testID = useDesktopStorybookTestID('story-root');
16
+ const activeSelection = selection?.storyId === storyId ? selection : undefined;
17
+
18
+ React.useEffect(() => {
19
+ if (!activeSelection || !runtimeInstance?.portablePlanDigest) {
20
+ return;
21
+ }
22
+ addons.getChannel().emit(desktopStoryReadyEvent, {
23
+ portablePlanDigest: runtimeInstance.portablePlanDigest,
24
+ previewGeneration: activeSelection.previewGeneration,
25
+ requestId: activeSelection.requestId,
26
+ runId: activeSelection.runId,
27
+ storyId,
28
+ });
29
+ }, [activeSelection, runtimeInstance?.portablePlanDigest, storyId]);
30
+
31
+ const marker = JSON.stringify({
32
+ previewGeneration: activeSelection?.previewGeneration ?? 0,
33
+ runId: activeSelection?.runId,
34
+ storyId,
35
+ });
36
+ const key = activeSelection ? `${activeSelection.runId}:${activeSelection.previewGeneration}` : storyId;
37
+
38
+ return (
39
+ <StoryRenderErrorBoundary
40
+ key={key}
41
+ onError={(message) => {
42
+ if (activeSelection) {
43
+ addons.getChannel().emit(desktopStoryErrorEvent, {
44
+ message,
45
+ requestId: activeSelection.requestId,
46
+ runId: activeSelection.runId,
47
+ storyId,
48
+ });
49
+ }
50
+ }}
51
+ >
52
+ <View accessibilityLabel={marker} style={styles.root} testID={testID}>
53
+ {children}
54
+ </View>
55
+ </StoryRenderErrorBoundary>
56
+ );
57
+ }
58
+
59
+ type StoryRenderErrorBoundaryProps = {
60
+ children: React.ReactNode;
61
+ onError(message: string): void;
62
+ };
63
+
64
+ type StoryRenderErrorBoundaryState = {
65
+ error?: Error;
66
+ };
67
+
68
+ class StoryRenderErrorBoundary extends React.Component<StoryRenderErrorBoundaryProps, StoryRenderErrorBoundaryState> {
69
+ override state: StoryRenderErrorBoundaryState = {};
70
+
71
+ static getDerivedStateFromError(error: Error): StoryRenderErrorBoundaryState {
72
+ return { error };
73
+ }
74
+
75
+ override componentDidCatch(error: Error): void {
76
+ this.props.onError(error.message);
77
+ }
78
+
79
+ override render(): React.ReactNode {
80
+ return this.state.error ? null : this.props.children;
81
+ }
82
+ }
83
+
84
+ const styles = StyleSheet.create({
85
+ root: {
86
+ flex: 1,
87
+ },
88
+ });
@@ -0,0 +1,58 @@
1
+ import * as React from 'react';
2
+
3
+ export type DesktopRuntimeSelection = {
4
+ previewGeneration: number;
5
+ requestId: string;
6
+ runId: string;
7
+ storyId: string;
8
+ };
9
+
10
+ export type DesktopStorybookRuntimeInstance = {
11
+ bridgeNonce?: string;
12
+ endpoint?: 'macos' | 'windows' | 'win32';
13
+ instanceId?: string;
14
+ platformManifestDigest?: string;
15
+ portablePlanDigest?: string;
16
+ storybookPort?: number;
17
+ targetId?: string;
18
+ testIDPrefix?: string;
19
+ };
20
+
21
+ type DesktopStorybookConfig = {
22
+ prepareStory(selection: Omit<DesktopRuntimeSelection, 'previewGeneration'>): void;
23
+ runtimeInstance?: DesktopStorybookRuntimeInstance;
24
+ selection?: DesktopRuntimeSelection;
25
+ testIDPrefix: string;
26
+ };
27
+
28
+ const defaultConfig: DesktopStorybookConfig = {
29
+ prepareStory: () => undefined,
30
+ testIDPrefix: 'storybook-desktop',
31
+ };
32
+
33
+ const DesktopStorybookConfigContext = React.createContext<DesktopStorybookConfig>(defaultConfig);
34
+
35
+ export function DesktopStorybookConfigProvider({
36
+ children,
37
+ runtimeInstance,
38
+ testIDPrefix,
39
+ }: React.PropsWithChildren<Pick<DesktopStorybookConfig, 'runtimeInstance' | 'testIDPrefix'>>) {
40
+ const [selection, setSelection] = React.useState<DesktopRuntimeSelection>();
41
+ const prepareStory = React.useCallback((next: Omit<DesktopRuntimeSelection, 'previewGeneration'>) => {
42
+ setSelection((current) => ({ ...next, previewGeneration: (current?.previewGeneration ?? 0) + 1 }));
43
+ }, []);
44
+ const value = React.useMemo(
45
+ () => ({ prepareStory, runtimeInstance, selection, testIDPrefix }),
46
+ [prepareStory, runtimeInstance, selection, testIDPrefix],
47
+ );
48
+ return <DesktopStorybookConfigContext.Provider value={value}>{children}</DesktopStorybookConfigContext.Provider>;
49
+ }
50
+
51
+ export function useDesktopStorybookConfig(): DesktopStorybookConfig {
52
+ return React.useContext(DesktopStorybookConfigContext);
53
+ }
54
+
55
+ export function useDesktopStorybookTestID(suffix: string): string {
56
+ const { testIDPrefix } = useDesktopStorybookConfig();
57
+ return `${testIDPrefix}-${suffix}`;
58
+ }
@@ -0,0 +1,114 @@
1
+ import * as React from 'react';
2
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
3
+
4
+ import { createDefaultTheme } from '@fluentui-react-native/default-theme';
5
+ import { ThemeProvider } from '@fluentui-react-native/design/theming';
6
+ import type { ThemeReference } from '@fluentui-react-native/design/theming';
7
+
8
+ import { useDesktopStorybookTestID } from './DesktopStorybookConfig';
9
+
10
+ type ThemeChoice = {
11
+ label: string;
12
+ theme?: ThemeReference;
13
+ };
14
+
15
+ const themeChoiceNames = ['none', 'light', 'dark', 'highContrast'] as const;
16
+ type ThemeChoiceName = (typeof themeChoiceNames)[number];
17
+
18
+ const themeChoices: Record<ThemeChoiceName, ThemeChoice> = {
19
+ none: { label: 'No theme' },
20
+ light: { label: 'Light', theme: createDefaultTheme({ appearance: 'light' }) },
21
+ dark: { label: 'Dark', theme: createDefaultTheme({ appearance: 'dark' }) },
22
+ highContrast: { label: 'High contrast', theme: createDefaultTheme({ appearance: 'highContrast' }) },
23
+ };
24
+
25
+ const StorybookThemeContext = React.createContext<ThemeReference | undefined>(undefined);
26
+
27
+ export function StorybookThemeHost({ children }: React.PropsWithChildren) {
28
+ const [selectedName, setSelectedName] = React.useState<ThemeChoiceName>('none');
29
+ const selectedTheme = themeChoices[selectedName].theme;
30
+ const toolbarTestID = useDesktopStorybookTestID('theme-toolbar');
31
+ const optionTestID = useDesktopStorybookTestID('theme');
32
+
33
+ return (
34
+ <StorybookThemeContext.Provider value={selectedTheme}>
35
+ <View style={styles.root}>
36
+ <View accessibilityRole="toolbar" style={styles.header} testID={toolbarTestID}>
37
+ <Text style={styles.label}>Theme</Text>
38
+ {themeChoiceNames.map((name) => {
39
+ const choice = themeChoices[name];
40
+ const selected = name === selectedName;
41
+ return (
42
+ <Pressable
43
+ accessibilityRole="button"
44
+ accessibilityState={{ selected }}
45
+ key={name}
46
+ onPress={() => setSelectedName(name)}
47
+ style={({ pressed }) => [styles.option, selected && styles.selectedOption, pressed && styles.pressedOption]}
48
+ testID={`${optionTestID}-${name}`}
49
+ >
50
+ <Text style={[styles.optionText, selected && styles.selectedOptionText]}>{choice.label}</Text>
51
+ </Pressable>
52
+ );
53
+ })}
54
+ </View>
55
+ <View style={styles.storybook}>{children}</View>
56
+ </View>
57
+ </StorybookThemeContext.Provider>
58
+ );
59
+ }
60
+
61
+ export function StorybookThemeProvider({ children }: React.PropsWithChildren) {
62
+ const theme = React.useContext(StorybookThemeContext);
63
+ return theme ? <ThemeProvider theme={theme}>{children}</ThemeProvider> : children;
64
+ }
65
+
66
+ const styles = StyleSheet.create({
67
+ root: {
68
+ flex: 1,
69
+ },
70
+ header: {
71
+ alignItems: 'center',
72
+ backgroundColor: '#f5f5f5',
73
+ borderBottomColor: '#d1d1d1',
74
+ borderBottomWidth: 1,
75
+ flexDirection: 'row',
76
+ flexWrap: 'wrap',
77
+ gap: 8,
78
+ minHeight: 44,
79
+ paddingHorizontal: 12,
80
+ paddingVertical: 6,
81
+ },
82
+ label: {
83
+ color: '#242424',
84
+ fontSize: 14,
85
+ fontWeight: '600',
86
+ marginEnd: 4,
87
+ },
88
+ option: {
89
+ backgroundColor: '#ffffff',
90
+ borderColor: '#b3b3b3',
91
+ borderRadius: 4,
92
+ borderWidth: 1,
93
+ paddingHorizontal: 10,
94
+ paddingVertical: 5,
95
+ },
96
+ selectedOption: {
97
+ backgroundColor: '#0f6cbd',
98
+ borderColor: '#0f6cbd',
99
+ },
100
+ pressedOption: {
101
+ opacity: 0.75,
102
+ },
103
+ optionText: {
104
+ color: '#242424',
105
+ fontSize: 12,
106
+ },
107
+ selectedOptionText: {
108
+ color: '#ffffff',
109
+ fontWeight: '600',
110
+ },
111
+ storybook: {
112
+ flex: 1,
113
+ },
114
+ });
@@ -0,0 +1 @@
1
+ export { LiteUI as StorybookUIComponent } from '@storybook/react-native-ui-lite';