@fluentui-react-native/storybook-desktop-runtime 0.2.1 → 0.3.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.
- package/.cache/tsconfig.tsbuildinfo +1 -1
- package/CHANGELOG.md +39 -0
- package/README.md +10 -0
- package/config/metro.cjs +6 -1
- package/jest.config.cjs +1 -0
- package/lib/DesktopStoryRoot.d.ts.map +1 -1
- package/lib/DesktopStoryRoot.js +3 -2
- package/lib/DesktopStoryRoot.js.map +1 -1
- package/lib/StorybookTheme.d.ts +0 -1
- package/lib/StorybookTheme.d.ts.map +1 -1
- package/lib/StorybookTheme.js +29 -27
- package/lib/StorybookTheme.js.map +1 -1
- package/lib/StorybookTheme.test.d.ts +2 -0
- package/lib/StorybookTheme.test.d.ts.map +1 -0
- package/lib/StorybookTheme.test.js +111 -0
- package/lib/StorybookTheme.test.js.map +1 -0
- package/lib/StorybookUI.win32.js +3 -3
- package/lib/StorybookUI.win32.js.map +1 -1
- package/lib/ThemedStorybookUI.d.ts +3 -0
- package/lib/ThemedStorybookUI.d.ts.map +1 -0
- package/lib/ThemedStorybookUI.js +8 -0
- package/lib/ThemedStorybookUI.js.map +1 -0
- package/lib/Win32CalloutPortal.d.ts.map +1 -1
- package/lib/Win32CalloutPortal.js +3 -2
- package/lib/Win32CalloutPortal.js.map +1 -1
- package/lib/createDesktopStorybookApp.d.ts.map +1 -1
- package/lib/createDesktopStorybookApp.js +4 -14
- package/lib/createDesktopStorybookApp.js.map +1 -1
- package/lib/createDesktopStorybookPreview.d.ts.map +1 -1
- package/lib/createDesktopStorybookPreview.js +1 -2
- package/lib/createDesktopStorybookPreview.js.map +1 -1
- package/lib/useStorybookTheme.d.ts +3 -0
- package/lib/useStorybookTheme.d.ts.map +1 -0
- package/lib/useStorybookTheme.js +105 -0
- package/lib/useStorybookTheme.js.map +1 -0
- package/lib/useStorybookTheme.test.d.ts +2 -0
- package/lib/useStorybookTheme.test.d.ts.map +1 -0
- package/lib/useStorybookTheme.test.js +43 -0
- package/lib/useStorybookTheme.test.js.map +1 -0
- package/package.json +13 -6
- package/src/DesktopStoryRoot.tsx +3 -2
- package/src/StorybookTheme.test.tsx +133 -0
- package/src/StorybookTheme.tsx +91 -78
- package/src/StorybookUI.win32.tsx +3 -3
- package/src/ThemedStorybookUI.tsx +9 -0
- package/src/Win32CalloutPortal.tsx +4 -3
- package/src/createDesktopStorybookApp.tsx +6 -25
- package/src/createDesktopStorybookPreview.tsx +5 -8
- package/src/useStorybookTheme.test.tsx +63 -0
- package/src/useStorybookTheme.ts +112 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { StyleSheet
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { Callout } from '@fluentui-react-native/callout';
|
|
5
5
|
import type { CalloutProps, DirectionalHint } from '@fluentui-react-native/callout';
|
|
6
|
+
import { RootInputBoundary } from '@fluentui-react-native/design';
|
|
6
7
|
import { useTheme } from '@storybook/react-native-theming';
|
|
7
8
|
|
|
8
9
|
type Win32CalloutPortalProps = React.PropsWithChildren<{
|
|
@@ -55,13 +56,13 @@ export function Win32CalloutPortal({
|
|
|
55
56
|
target={target}
|
|
56
57
|
testID={testID}
|
|
57
58
|
>
|
|
58
|
-
<
|
|
59
|
+
<RootInputBoundary
|
|
59
60
|
collapsable={false}
|
|
60
61
|
style={[styles.content, { backgroundColor: theme.background.content, height, width }]}
|
|
61
62
|
testID={`${testID}-content`}
|
|
62
63
|
>
|
|
63
64
|
{children}
|
|
64
|
-
</
|
|
65
|
+
</RootInputBoundary>
|
|
65
66
|
</Callout>
|
|
66
67
|
);
|
|
67
68
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import type * as React from 'react';
|
|
2
|
-
import { StyleSheet, View as NativeView } from 'react-native';
|
|
3
1
|
import type { View } from '@storybook/react-native';
|
|
4
2
|
|
|
5
3
|
import { DesktopDriverBridge } from './DesktopDriverBridge';
|
|
6
|
-
import { DesktopStorybookConfigProvider
|
|
4
|
+
import { DesktopStorybookConfigProvider } from './DesktopStorybookConfig';
|
|
7
5
|
import type { DesktopStorybookRuntimeInstance } from './DesktopStorybookConfig';
|
|
8
6
|
import { StorybookThemeHost } from './StorybookTheme';
|
|
9
|
-
import {
|
|
7
|
+
import { ThemedStorybookUI } from './ThemedStorybookUI';
|
|
10
8
|
|
|
11
9
|
export type DesktopStorybookOptions = {
|
|
12
10
|
enableWebsockets?: boolean;
|
|
@@ -35,7 +33,7 @@ export function createDesktopStorybookApp(
|
|
|
35
33
|
enableWebsockets,
|
|
36
34
|
host,
|
|
37
35
|
port: port ?? runtimeInstance?.storybookPort ?? 7007,
|
|
38
|
-
CustomUIComponent:
|
|
36
|
+
CustomUIComponent: ThemedStorybookUI,
|
|
39
37
|
storage,
|
|
40
38
|
});
|
|
41
39
|
|
|
@@ -43,30 +41,13 @@ export function createDesktopStorybookApp(
|
|
|
43
41
|
const resolvedTestIDPrefix = runtimeInstance?.testIDPrefix ?? testIDPrefix;
|
|
44
42
|
return (
|
|
45
43
|
<DesktopStorybookConfigProvider runtimeInstance={runtimeInstance} testIDPrefix={resolvedTestIDPrefix}>
|
|
46
|
-
<
|
|
44
|
+
<StorybookThemeHost>
|
|
47
45
|
<DesktopDriverBridge />
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
</StorybookThemeHost>
|
|
51
|
-
</DesktopStorybookAppRoot>
|
|
46
|
+
<StorybookUI />
|
|
47
|
+
</StorybookThemeHost>
|
|
52
48
|
</DesktopStorybookConfigProvider>
|
|
53
49
|
);
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
return DesktopStorybookApp;
|
|
57
53
|
}
|
|
58
|
-
|
|
59
|
-
function DesktopStorybookAppRoot({ children }: React.PropsWithChildren) {
|
|
60
|
-
const testID = useDesktopStorybookTestID('app-root');
|
|
61
|
-
return (
|
|
62
|
-
<NativeView style={styles.root} testID={testID}>
|
|
63
|
-
{children}
|
|
64
|
-
</NativeView>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const styles = StyleSheet.create({
|
|
69
|
-
root: {
|
|
70
|
-
flex: 1,
|
|
71
|
-
},
|
|
72
|
-
});
|
|
@@ -2,19 +2,16 @@ import { StyleSheet, View } from 'react-native';
|
|
|
2
2
|
import type { Preview } from '@storybook/react-native';
|
|
3
3
|
|
|
4
4
|
import { DesktopStoryRoot } from './DesktopStoryRoot';
|
|
5
|
-
import { StorybookThemeProvider } from './StorybookTheme';
|
|
6
5
|
|
|
7
6
|
export function createDesktopStorybookPreview(): Preview {
|
|
8
7
|
return {
|
|
9
8
|
decorators: [
|
|
10
9
|
(Story, context) => (
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
</DesktopStoryRoot>
|
|
17
|
-
</StorybookThemeProvider>
|
|
10
|
+
<DesktopStoryRoot storyId={context.id}>
|
|
11
|
+
<View style={styles.story}>
|
|
12
|
+
<Story />
|
|
13
|
+
</View>
|
|
14
|
+
</DesktopStoryRoot>
|
|
18
15
|
),
|
|
19
16
|
],
|
|
20
17
|
parameters: {},
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { PlatformColor } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { act, render } from '@testing-library/react-native';
|
|
4
|
+
import { FlexThemeReference, ThemedRoot, useThemeState } from '@fluentui-react-native/design';
|
|
5
|
+
import type { ThemeState } from '@fluentui-react-native/design';
|
|
6
|
+
import type { Theme } from '@storybook/react-native-theming';
|
|
7
|
+
|
|
8
|
+
import { useStorybookTheme } from './useStorybookTheme';
|
|
9
|
+
|
|
10
|
+
function Probe({ themes, states }: { themes: Theme[]; states?: ThemeState[] }) {
|
|
11
|
+
themes.push(useStorybookTheme());
|
|
12
|
+
const state = useThemeState();
|
|
13
|
+
states?.push(state);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('Storybook theme palette', () => {
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
jest.restoreAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('derives colors from the current tokens and updates after source invalidation', async () => {
|
|
23
|
+
const themes: Theme[] = [];
|
|
24
|
+
const states: ThemeState[] = [];
|
|
25
|
+
const source = new FlexThemeReference({
|
|
26
|
+
base: { color: { surfaceNeutralFar: '#123456', foregroundNeutralPrimary: '#abcdef' } },
|
|
27
|
+
});
|
|
28
|
+
const scene = await render(
|
|
29
|
+
<ThemedRoot theme={source}>
|
|
30
|
+
<Probe themes={themes} states={states} />
|
|
31
|
+
</ThemedRoot>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(themes[0].background.app).toBe('#123456');
|
|
35
|
+
expect(themes[0].color.defaultText).toBe('#abcdef');
|
|
36
|
+
await scene.rerender(
|
|
37
|
+
<ThemedRoot theme={source}>
|
|
38
|
+
<Probe themes={themes} states={states} />
|
|
39
|
+
</ThemedRoot>,
|
|
40
|
+
);
|
|
41
|
+
expect(themes[themes.length - 1]).toBe(themes[0]);
|
|
42
|
+
|
|
43
|
+
await act(() => source.update({ color: { surfaceNeutralFar: '#654321' } }));
|
|
44
|
+
expect(themes[themes.length - 1].background.app).toBe('#654321');
|
|
45
|
+
expect(themes[themes.length - 1]).not.toBe(themes[0]);
|
|
46
|
+
expect(themes[themes.length - 1].appBorderColor).toBe(states[states.length - 1].tokens.color.strokeNeutralSubtle);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('warns and substitutes a contrasting theme color for native opaque colors', async () => {
|
|
50
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation();
|
|
51
|
+
const themes: Theme[] = [];
|
|
52
|
+
const states: ThemeState[] = [];
|
|
53
|
+
const source = new FlexThemeReference({ base: { color: { foregroundBrandPrimary: PlatformColor('labelColor') } } });
|
|
54
|
+
await render(
|
|
55
|
+
<ThemedRoot theme={source}>
|
|
56
|
+
<Probe themes={themes} states={states} />
|
|
57
|
+
</ThemedRoot>,
|
|
58
|
+
);
|
|
59
|
+
expect(themes[0].color.primary).toBe(states[0].tokens.color.fixedBlack);
|
|
60
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('foregroundBrandPrimary'));
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { useThemeState } from '@fluentui-react-native/design';
|
|
4
|
+
import type { SemanticColorTokenValues, ThemeState } from '@fluentui-react-native/design';
|
|
5
|
+
import { darkTheme, theme as lightTheme } from '@storybook/react-native-theming';
|
|
6
|
+
import type { Theme } from '@storybook/react-native-theming';
|
|
7
|
+
|
|
8
|
+
export function useStorybookTheme(): Theme {
|
|
9
|
+
const state = useThemeState();
|
|
10
|
+
const { theme, nativeColors } = React.useMemo(() => storybookThemeFromThemeState(state), [state]);
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
if (nativeColors.length > 0) {
|
|
13
|
+
console.warn(
|
|
14
|
+
`Storybook chrome cannot resolve native system colors; using fixed contrasting theme colors for: ${nativeColors.join(', ')}.`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}, [nativeColors]);
|
|
18
|
+
return theme;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function storybookThemeFromThemeState({ appearance, highContrast, tokens }: ThemeState) {
|
|
22
|
+
const base = highContrast || appearance.colorScheme === 'dark' ? darkTheme : lightTheme;
|
|
23
|
+
const foreground = base.base === 'dark' ? 'fixedWhite' : 'fixedBlack';
|
|
24
|
+
const background = base.base === 'dark' ? 'fixedBlack' : 'fixedWhite';
|
|
25
|
+
const nativeColors = new Set<keyof SemanticColorTokenValues>();
|
|
26
|
+
const color = (name: keyof SemanticColorTokenValues, role: 'foreground' | 'background' = 'foreground'): string => {
|
|
27
|
+
const value = tokens.color[name];
|
|
28
|
+
// Storybook also passes these colors to JS color utilities, not just native styles.
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
nativeColors.add(name);
|
|
33
|
+
const fallbackName = role === 'foreground' ? foreground : background;
|
|
34
|
+
const fallback = tokens.color[fallbackName];
|
|
35
|
+
if (typeof fallback !== 'string') {
|
|
36
|
+
throw new Error(`Storybook chrome requires a literal "${fallbackName}" fallback for the "${name}" theme token.`);
|
|
37
|
+
}
|
|
38
|
+
return fallback;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const theme: Theme = {
|
|
42
|
+
...base,
|
|
43
|
+
color: {
|
|
44
|
+
...base.color,
|
|
45
|
+
primary: color('foregroundBrandPrimary'),
|
|
46
|
+
secondary: color('backgroundBrandHeavy'),
|
|
47
|
+
secondaryLighter: color('backgroundBrandSoft', 'background'),
|
|
48
|
+
tertiary: color('foregroundSuccessPrimary'),
|
|
49
|
+
ancillary: color('foregroundBrandPrimary'),
|
|
50
|
+
orange: color('foregroundWarningPrimary'),
|
|
51
|
+
gold: color('foregroundWarningPrimary'),
|
|
52
|
+
green: color('foregroundSuccessPrimary'),
|
|
53
|
+
seafoam: color('foregroundSuccessPrimary'),
|
|
54
|
+
purple: color('foregroundBrandPrimary'),
|
|
55
|
+
ultraviolet: color('foregroundBrandPrimary'),
|
|
56
|
+
lightest: color('foregroundBrandOnloud', 'background'),
|
|
57
|
+
lighter: color('surfaceNeutralNear', 'background'),
|
|
58
|
+
light: color('backgroundNeutralSubtle', 'background'),
|
|
59
|
+
mediumlight: color('strokeNeutralSubtle'),
|
|
60
|
+
medium: color('strokeNeutralSoft'),
|
|
61
|
+
mediumdark: color('foregroundNeutralSecondary'),
|
|
62
|
+
dark: color('foregroundNeutralPrimary'),
|
|
63
|
+
darker: color('foregroundNeutralPrimary'),
|
|
64
|
+
darkest: color('foregroundNeutralPrimary'),
|
|
65
|
+
border: color('strokeNeutralSoft'),
|
|
66
|
+
positive: color('foregroundSuccessPrimary'),
|
|
67
|
+
negative: color('foregroundDangerPrimary'),
|
|
68
|
+
warning: color('foregroundWarningPrimary'),
|
|
69
|
+
critical: color('foregroundDangerPrimary'),
|
|
70
|
+
defaultText: color('foregroundNeutralPrimary'),
|
|
71
|
+
inverseText: color('foregroundNeutralOnloud', 'background'),
|
|
72
|
+
positiveText: color('foregroundSuccessPrimary'),
|
|
73
|
+
negativeText: color('foregroundDangerPrimary'),
|
|
74
|
+
warningText: color('foregroundWarningPrimary'),
|
|
75
|
+
},
|
|
76
|
+
background: {
|
|
77
|
+
...base.background,
|
|
78
|
+
app: color('surfaceNeutralFar', 'background'),
|
|
79
|
+
bar: color('backgroundNeutralSubtle', 'background'),
|
|
80
|
+
content: color('surfaceNeutralNear', 'background'),
|
|
81
|
+
preview: color('surfaceNeutralFar', 'background'),
|
|
82
|
+
hoverable: color('backgroundNeutralSoft', 'background'),
|
|
83
|
+
positive: color('backgroundSuccessSubtle', 'background'),
|
|
84
|
+
negative: color('backgroundDangerSubtle', 'background'),
|
|
85
|
+
warning: color('backgroundWarningSubtle', 'background'),
|
|
86
|
+
critical: color('backgroundDangerSubtle', 'background'),
|
|
87
|
+
},
|
|
88
|
+
textMutedColor: color('foregroundNeutralTertiary'),
|
|
89
|
+
input: {
|
|
90
|
+
...base.input,
|
|
91
|
+
border: color('strokeNeutralSoft'),
|
|
92
|
+
background: color('backgroundNeutralLoud', 'background'),
|
|
93
|
+
color: color('foregroundNeutralPrimary'),
|
|
94
|
+
},
|
|
95
|
+
button: {
|
|
96
|
+
...base.button,
|
|
97
|
+
background: color('backgroundNeutralLoud', 'background'),
|
|
98
|
+
border: color('strokeNeutralSoft'),
|
|
99
|
+
},
|
|
100
|
+
boolean: {
|
|
101
|
+
...base.boolean,
|
|
102
|
+
background: color('backgroundNeutralSoft', 'background'),
|
|
103
|
+
selectedBackground: color('backgroundBrandHeavy'),
|
|
104
|
+
},
|
|
105
|
+
appBorderColor: color('strokeNeutralSubtle'),
|
|
106
|
+
barTextColor: color('foregroundNeutralPrimary'),
|
|
107
|
+
barHoverColor: color('foregroundBrandPrimary'),
|
|
108
|
+
barSelectedColor: color('foregroundBrandPrimary'),
|
|
109
|
+
barBg: color('backgroundNeutralSubtle', 'background'),
|
|
110
|
+
};
|
|
111
|
+
return { theme, nativeColors: [...nativeColors] };
|
|
112
|
+
}
|