Skip to content

Commit a7242c2

Browse files
fix: storybook dark mode and component compatibility (#7209)
1 parent 7f219c0 commit a7242c2

5 files changed

Lines changed: 202 additions & 2 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React, { useState, useEffect } from 'react'
2+
import { DocsContainer as BaseContainer } from '@storybook/addon-docs/blocks'
3+
import { create } from 'storybook/theming'
4+
import { addons } from 'storybook/preview-api'
5+
6+
const darkTheme = create({
7+
base: 'dark',
8+
appBg: '#15192b',
9+
appContentBg: '#161d30',
10+
barBg: '#15192b',
11+
colorPrimary: '#6837fc',
12+
colorSecondary: '#6837fc',
13+
textColor: '#e0e3e9',
14+
textMutedColor: '#9da4ae',
15+
})
16+
17+
const lightTheme = create({
18+
base: 'light',
19+
colorPrimary: '#6837fc',
20+
colorSecondary: '#6837fc',
21+
})
22+
23+
function getInitialTheme(context) {
24+
try {
25+
return context?.store?.globals?.globals?.theme === 'dark'
26+
} catch {
27+
return window.matchMedia('(prefers-color-scheme: dark)').matches
28+
}
29+
}
30+
31+
export const DocsContainer = ({ children, context, ...props }) => {
32+
const [isDark, setIsDark] = useState(() => getInitialTheme(context))
33+
34+
useEffect(() => {
35+
const channel = addons.getChannel()
36+
const handleGlobalsUpdate = ({ globals }) => {
37+
if (globals?.theme !== undefined) {
38+
setIsDark(globals.theme === 'dark')
39+
}
40+
}
41+
channel.on('globalsUpdated', handleGlobalsUpdate)
42+
return () => channel.off('globalsUpdated', handleGlobalsUpdate)
43+
}, [])
44+
45+
return (
46+
<BaseContainer {...props} context={context} theme={isDark ? darkTheme : lightTheme}>
47+
{children}
48+
</BaseContainer>
49+
)
50+
}

frontend/.storybook/docs-theme.scss

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,66 @@
7474
border-color: var(--color-border-default, rgba(255, 255, 255, 0.16));
7575
}
7676

77-
// Canvas (story preview) background
77+
// Canvas wrapper — emotion-styled div with no stable class, child of .sb-anchor
78+
.sb-anchor > div {
79+
background-color: var(--color-surface-subtle, #15192b) !important;
80+
border-color: var(--color-border-default, rgba(255, 255, 255, 0.16)) !important;
81+
}
82+
83+
// Args/controls table
84+
.docblock-argstable {
85+
background-color: var(--color-surface-subtle, #15192b) !important;
86+
border-color: var(--color-border-default, rgba(255, 255, 255, 0.16)) !important;
87+
}
88+
89+
.docblock-argstable th,
90+
.docblock-argstable td {
91+
background-color: var(--color-surface-subtle, #15192b) !important;
92+
border-color: var(--color-border-default, rgba(255, 255, 255, 0.16)) !important;
93+
color: var(--color-text-default, #e0e3e9) !important;
94+
}
95+
96+
// Argstable description text, type badges, default values
97+
.docblock-argstable td span,
98+
.docblock-argstable td p,
99+
.docblock-argstable td code,
100+
.docblock-argstable td label {
101+
color: var(--color-text-secondary, #9da4ae) !important;
102+
}
103+
104+
// Prop names (first column)
105+
.docblock-argstable td:first-child span {
106+
color: var(--color-text-default, #e0e3e9) !important;
107+
}
108+
109+
// Control values, radio labels, object inspector text
110+
.docblock-argstable [class*='css-'] {
111+
color: var(--color-text-default, #e0e3e9) !important;
112+
}
113+
114+
// Control buttons (Set object, Set string, Set boolean, etc.)
115+
.docblock-argstable button {
116+
background-color: var(--color-surface-muted, #161d30) !important;
117+
color: var(--color-text-default, #e0e3e9) !important;
118+
border-color: var(--color-border-default, rgba(255, 255, 255, 0.16)) !important;
119+
}
120+
121+
// Control inputs (text fields, textareas, number inputs)
122+
.docblock-argstable input,
123+
.docblock-argstable textarea,
124+
.docblock-argstable select {
125+
background-color: var(--color-surface-muted, #161d30) !important;
126+
color: var(--color-text-default, #e0e3e9) !important;
127+
border-color: var(--color-border-default, rgba(255, 255, 255, 0.16)) !important;
128+
}
129+
78130
.docs-story {
79-
background-color: var(--color-surface-default, #101628);
131+
background-color: var(--color-surface-subtle, #15192b) !important;
132+
}
133+
134+
// Canvas toolbar (zoom, open in new tab icons)
135+
.sbdocs-preview [role='toolbar'] {
136+
background-color: var(--color-surface-subtle, #15192b) !important;
80137
}
81138

82139
// Table borders

frontend/.storybook/main.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ const config = {
1717
name: '@storybook/react-webpack5',
1818
options: {},
1919
},
20+
typescript: {
21+
reactDocgen: 'react-docgen-typescript',
22+
reactDocgenTypescriptOptions: {
23+
shouldExtractLiteralValuesFromEnum: true,
24+
shouldRemoveUndefinedFromOptional: true,
25+
propFilter: (prop) =>
26+
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
27+
},
28+
},
2029
swc: () => ({
2130
jsc: {
2231
transform: {
@@ -57,6 +66,21 @@ const config = {
5766
],
5867
})
5968

69+
// Stub modules that cause circular dependency crashes in Storybook.
70+
// common/utils/utils → account-store → constants creates a webpack
71+
// initialisation error. Ionic/stencil also triggers the same chain.
72+
config.resolve.alias = {
73+
...config.resolve.alias,
74+
[path.resolve(__dirname, '../common/utils/utils')]: path.resolve(
75+
__dirname,
76+
'stubs/utils.js',
77+
),
78+
'@stencil/core/internal/client': false,
79+
'@stencil/core': false,
80+
'@ionic/react': false,
81+
'ionicons/icons': false,
82+
}
83+
6084
config.plugins = config.plugins || []
6185
config.plugins.push(
6286
new webpack.DefinePlugin({

frontend/.storybook/preview.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,53 @@
11
import '../web/styles/styles.scss'
22
import './docs-theme.scss'
33
import { allModes } from './modes'
4+
import { DocsContainer } from './DocsContainer'
5+
6+
// TODO: Remove these globals once legacy .js components (Input.js, etc.) are
7+
// migrated to TypeScript with proper imports. These replicate what
8+
// web/project/libs.js and web/project/project-components.js set at boot.
9+
// Utils is stubbed via webpack alias in main.js to avoid circular deps.
10+
import React from 'react'
11+
import PropTypes from 'prop-types'
12+
import Utils from 'common/utils/utils'
13+
import ReactSelect, { components as selectComponents } from 'react-select'
14+
import Tooltip from '../web/components/Tooltip'
15+
16+
window.React = React
17+
window.propTypes = PropTypes
18+
window.OptionalString = PropTypes.string
19+
window.OptionalFunc = PropTypes.func
20+
window.OptionalBool = PropTypes.bool
21+
window.OptionalNumber = PropTypes.number
22+
window.OptionalObject = PropTypes.object
23+
window.OptionalArray = PropTypes.array
24+
window.OptionalNode = PropTypes.node
25+
window.OptionalElement = PropTypes.element
26+
window.RequiredString = PropTypes.string.isRequired
27+
window.RequiredFunc = PropTypes.func.isRequired
28+
window.RequiredBool = PropTypes.bool.isRequired
29+
window.RequiredNumber = PropTypes.number.isRequired
30+
window.RequiredObject = PropTypes.object.isRequired
31+
window.RequiredNode = PropTypes.node.isRequired
32+
window.Any = PropTypes.any
33+
window.oneOf = PropTypes.oneOf
34+
window.oneOfType = PropTypes.oneOfType
35+
window.Utils = Utils
36+
// Wrap ReactSelect to match the real app's global.Select (project-components.js)
37+
// which adds className="react-select" and classNamePrefix="react-select"
38+
// so _react-select.scss dark mode selectors work.
39+
global.Select = (props) =>
40+
React.createElement(
41+
'div',
42+
{ className: props.className, onClick: (e) => e.stopPropagation() },
43+
React.createElement(ReactSelect, {
44+
...props,
45+
className: `react-select ${props.size || ''}`,
46+
classNamePrefix: 'react-select',
47+
components: { ...props.components },
48+
}),
49+
)
50+
global.Tooltip = Tooltip
451

552
/** @type { import('storybook').Preview } */
653
const preview = {
@@ -45,6 +92,9 @@ const preview = {
4592
},
4693
},
4794
backgrounds: { disable: true },
95+
docs: {
96+
container: DocsContainer,
97+
},
4898
chromatic: {
4999
modes: {
50100
light: allModes.light,

frontend/.storybook/stubs/utils.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Storybook stub for common/utils/utils.
2+
// The real Utils has circular dependencies (utils → account-store → constants)
3+
// that crash webpack in Storybook.
4+
// Only legacy .js components (Input.js) depend on this global.
5+
// New components should NOT import Utils — use dedicated utilities instead.
6+
// TODO: Remove once legacy .js files are migrated to TypeScript with imports.
7+
8+
const Utils = {
9+
getFlagsmithHasFeature: () => false,
10+
getFlagsmithValue: () => '',
11+
getPlansPermission: () => true,
12+
keys: {
13+
isEscape: (e) => e.key === 'Escape' || e.keyCode === 27,
14+
},
15+
safeParseEventValue: (e) =>
16+
e?.target?.value !== undefined ? e.target.value : e,
17+
}
18+
19+
export default Utils

0 commit comments

Comments
 (0)