@@ -15,41 +15,22 @@ import {
15
15
} from '@guardian/source/foundations' ;
16
16
import { Decorator } from '@storybook/react' ;
17
17
import { storybookPaletteDeclarations as paletteDeclarations } from '../mocks/paletteDeclarations' ;
18
+ import type { ReactNode } from 'react' ;
18
19
19
20
interface Orientation {
20
21
orientation ?: 'horizontal' | 'vertical' ;
21
22
}
22
23
23
- const headerCss = css `
24
- ${ textSansBold20 } ;
25
- text- align: center;
26
- padding: ${ space [ 2 ] } px;
27
- ` ;
28
-
29
- const styles = css `
30
- dis play: grid;
31
- max- width: 100%;
32
- ` ;
24
+ /**
25
+ * The `splitTheme` decorator displays a story simultaneously in both light and
26
+ * dark mode.
27
+ */
28
+ type ColourScheme = 'light' | 'dark' ;
33
29
34
- const FormatHeading = ( { format } : { format : ArticleFormat } ) => (
35
- < h3
36
- css = { css `
37
- ${ textSans17 } ;
38
- text- align: center;
39
- padding: ${ space [ 1 ] } px;
40
- opacity: 0.75;
41
- ` }
42
- >
43
- { [
44
- `Display: ${ ArticleDisplay [ format . display ] } ` ,
45
- `Design: ${ ArticleDesign [ format . design ] } ` ,
46
- `Theme: ${ Pillar [ format . theme ] || ArticleSpecial [ format . theme ] } ` ,
47
- ]
48
- . map ( ( line ) => line . replaceAll ( ' ' , ' ' ) ) // non-breaking spaces
49
- . join ( ', ' ) }
50
- </ h3 >
51
- ) ;
52
- // ----- Decorators ----- //
30
+ /**
31
+ * The second argument to a Storybook decorator is the story's context.
32
+ */
33
+ type Context = Parameters < Decorator > [ 1 ] ;
53
34
54
35
/** A list of the most typical formats */
55
36
export const defaultFormats = [
@@ -126,9 +107,197 @@ export const defaultFormats = [
126
107
] as const satisfies readonly ArticleFormat [ ] ;
127
108
128
109
/**
129
- * Creates storybook decorator used to render a component twice
130
- * Once in light mode, once in dark mode
131
- * Using a split screen
110
+ * Derives the background colour for a story based on the colour scheme set by
111
+ * the `splitTheme` decorator and the parameters passed to the story. If
112
+ * background colours are set via the `colourSchemeBackground` parameter, these
113
+ * are used, otherwise defaults from {@linkcode sourcePalette} are used.
114
+ *
115
+ * @param colourScheme Light or dark mode.
116
+ * @param context A story's context, containing the story parameters.
117
+ * @returns A CSS `color` value.
118
+ */
119
+ const backgroundColour = (
120
+ colourScheme : ColourScheme ,
121
+ context : Context ,
122
+ ) : string =>
123
+ colourScheme === 'light'
124
+ ? context . parameters . colourSchemeBackground ?. light ??
125
+ sourcePalette . neutral [ 100 ]
126
+ : context . parameters . colourSchemeBackground ?. dark ??
127
+ sourcePalette . neutral [ 0 ] ;
128
+
129
+ /**
130
+ * Derives the text colour for a story based on the colour scheme set by the
131
+ * `splitTheme` decorator and the parameters passed to the story. If text
132
+ * colours are set via the `colourSchemeTextColour` parameter, these are used,
133
+ * otherwise defaults from {@linkcode sourcePalette} are used.
134
+ *
135
+ * @param colourScheme Light or dark mode.
136
+ * @param context A story's context, containing the story parameters.
137
+ * @returns A CSS `color` value.
138
+ */
139
+ const textColour = ( colourScheme : ColourScheme , context : Context ) : string =>
140
+ colourScheme === 'light'
141
+ ? context . parameters . colourSchemeTextColour ?. light ??
142
+ sourcePalette . neutral [ 0 ]
143
+ : context . parameters . colourSchemeTextColour ?. dark ??
144
+ sourcePalette . neutral [ 100 ] ;
145
+
146
+ /**
147
+ * Describes the theme being used to render the Story, 'light' or 'dark'.
148
+ */
149
+ const ThemeHeading = ( { colourScheme } : { colourScheme : ColourScheme } ) => (
150
+ < h2
151
+ css = { [
152
+ textSansBold20 ,
153
+ {
154
+ textAlign : 'center' ,
155
+ padding : space [ 2 ] ,
156
+ } ,
157
+ ] }
158
+ >
159
+ { colourScheme === 'light' ? 'Light Theme ☀️' : 'Dark Theme 🌙' }
160
+ </ h2 >
161
+ ) ;
162
+
163
+ type FormatHeadingProps = {
164
+ colourScheme : ColourScheme ;
165
+ format : ArticleFormat ;
166
+ } ;
167
+
168
+ /**
169
+ * Describes the {@linkcode ArticleFormat} being used to render the story.
170
+ */
171
+ const FormatHeading = ( { format, colourScheme } : FormatHeadingProps ) => (
172
+ < h3
173
+ css = { [
174
+ textSans17 ,
175
+ {
176
+ textAlign : 'center' ,
177
+ padding : space [ 1 ] ,
178
+ color :
179
+ colourScheme === 'light'
180
+ ? sourcePalette . neutral [ 20 ]
181
+ : sourcePalette . neutral [ 73 ] ,
182
+ } ,
183
+ ] }
184
+ >
185
+ { [
186
+ `Display: ${ ArticleDisplay [ format . display ] } ` ,
187
+ `Design: ${ ArticleDesign [ format . design ] } ` ,
188
+ `Theme: ${ Pillar [ format . theme ] || ArticleSpecial [ format . theme ] } ` ,
189
+ ]
190
+ . map ( ( line ) => line . replaceAll ( ' ' , ' ' ) ) // non-breaking spaces
191
+ . join ( ', ' ) }
192
+ </ h3 >
193
+ ) ;
194
+
195
+ type PaletteProps = {
196
+ format : ArticleFormat ;
197
+ colourScheme : ColourScheme ;
198
+ context : Context ;
199
+ children : ReactNode ;
200
+ } ;
201
+
202
+ /**
203
+ * Generates the palette colours using the given {@linkcode ArticleFormat} and
204
+ * {@linkcode ColourScheme} and makes them available to the Story.
205
+ *
206
+ * For more information on how the palette works see
207
+ * {@linkcode paletteDeclarations}.
208
+ */
209
+ const Palette = ( { format, colourScheme, context, children } : PaletteProps ) => (
210
+ < div
211
+ data-color-scheme = { colourScheme }
212
+ css = { [
213
+ css ( paletteDeclarations ( format , colourScheme ) ) ,
214
+ {
215
+ backgroundColor : backgroundColour ( colourScheme , context ) ,
216
+ color : textColour ( colourScheme , context ) ,
217
+ } ,
218
+ ] }
219
+ >
220
+ { children }
221
+ </ div >
222
+ ) ;
223
+
224
+ type ThemeProps = {
225
+ formats : ArticleFormat [ ] ;
226
+ Story : Parameters < Decorator > [ 0 ] ;
227
+ context : Context ;
228
+ colourScheme : ColourScheme ;
229
+ } ;
230
+
231
+ /**
232
+ * Renders a story one or more times, based on the list of
233
+ * {@linkcode ArticleFormat}s passed, and in a particular
234
+ * {@linkcode ColourScheme}.
235
+ *
236
+ * For example, if a single format is passed and the colour scheme is 'light',
237
+ * it will render the story once in light mode. If three formats are passed and
238
+ * the colours scheme is 'dark', it will render the story three times, once for
239
+ * each format, and all three will be in dark mode.
240
+ *
241
+ * Also sets default background and text colours supplied via the
242
+ * `colourSchemeBackground` and `colourSchemeTextColour` parameters from the
243
+ * story, or provides defaults when these are not supplied.
244
+ */
245
+ const Theme = ( { formats, Story, context, colourScheme } : ThemeProps ) => (
246
+ < div
247
+ css = { {
248
+ color :
249
+ colourScheme === 'light'
250
+ ? sourcePalette . neutral [ 0 ]
251
+ : sourcePalette . neutral [ 100 ] ,
252
+ backgroundColor :
253
+ colourScheme === 'light'
254
+ ? sourcePalette . neutral [ 100 ]
255
+ : sourcePalette . neutral [ 0 ] ,
256
+ } }
257
+ >
258
+ < ThemeHeading colourScheme = { colourScheme } />
259
+ { formats . map ( ( format ) => (
260
+ < >
261
+ < FormatHeading format = { format } colourScheme = { colourScheme } />
262
+ < Palette
263
+ colourScheme = { colourScheme }
264
+ context = { context }
265
+ format = { format }
266
+ >
267
+ < Story
268
+ args = { {
269
+ ...context . args ,
270
+ format,
271
+ } }
272
+ />
273
+ </ Palette >
274
+ </ >
275
+ ) ) }
276
+ </ div >
277
+ ) ;
278
+
279
+ /**
280
+ * Creates a Storybook decorator used to render a story in both light and dark
281
+ * mode simultaneously; either vertically, light above dark, or horizontally,
282
+ * light on the left and dark on the right.
283
+ *
284
+ * If multiple {@linkcode ArticleFormat}s are passed, it will render the story
285
+ * in both light and dark mode for each of these. For example, if three formats
286
+ * are passed then it will render the story six times, three times in light
287
+ * mode, once for each format, and three times in dark mode, once for each
288
+ * format.
289
+ *
290
+ * The returned "splitTheme" decorator was historically used directly in story
291
+ * files. This approach is now deprecated in favour of the "global colour
292
+ * scheme" decorator and toolbar item, which use the "splitTheme" decorator in
293
+ * turn. The "global colour scheme" can be set in Storybook via the toolbar, and
294
+ * in Chromatic via "modes".
295
+ *
296
+ * @param formats A list of formats to render the story in. If none are passed
297
+ * then the {@linkcode defaultFormats} are used.
298
+ * @param orientation Whether to render light and dark mode side-by-side
299
+ * vertically or horizontally. The default is `horizontal`.
300
+ * @returns A decorator that can be used with Storybook.
132
301
*/
133
302
export const splitTheme =
134
303
(
@@ -137,64 +306,25 @@ export const splitTheme =
137
306
) : Decorator =>
138
307
( Story , context ) => (
139
308
< div
140
- css = { styles }
141
- style = { {
309
+ css = { {
310
+ display : 'grid' ,
311
+ maxWidth : '100%' ,
142
312
gridTemplateColumns :
143
313
orientation === 'horizontal' ? '1fr 1fr' : '1fr' ,
144
314
} }
145
315
>
146
- < div
147
- data-color-scheme = "light"
148
- style = { {
149
- backgroundColor :
150
- context . parameters . colourSchemeBackground ?. light ??
151
- sourcePalette . neutral [ 100 ] ,
152
- color :
153
- context . parameters . colourSchemeTextColour ?. light ??
154
- sourcePalette . neutral [ 0 ] ,
155
- } }
156
- css = { css ( paletteDeclarations ( defaultFormats [ 0 ] , 'light' ) ) }
157
- >
158
- < h2 css = { headerCss } > Light Theme ☀️</ h2 >
159
- { formats . map ( ( format ) => (
160
- < div css = { css ( paletteDeclarations ( format , 'light' ) ) } >
161
- < FormatHeading format = { format } />
162
- < Story
163
- args = { {
164
- ...context . args ,
165
- format,
166
- theme : 'light' ,
167
- } }
168
- />
169
- </ div >
170
- ) ) }
171
- </ div >
172
- < div
173
- data-color-scheme = "dark"
174
- style = { {
175
- backgroundColor :
176
- context . parameters . colourSchemeBackground ?. dark ??
177
- sourcePalette . neutral [ 0 ] ,
178
- color :
179
- context . parameters . colourSchemeTextColour ?. dark ??
180
- sourcePalette . neutral [ 100 ] ,
181
- } }
182
- css = { css ( paletteDeclarations ( defaultFormats [ 0 ] , 'dark' ) ) }
183
- >
184
- < h2 css = { headerCss } > Dark Theme 🌙</ h2 >
185
- { formats . map ( ( format ) => (
186
- < div css = { css ( paletteDeclarations ( format , 'dark' ) ) } >
187
- < FormatHeading format = { format } />
188
- < Story
189
- args = { {
190
- ...context . args ,
191
- format,
192
- theme : 'dark' ,
193
- } }
194
- />
195
- </ div >
196
- ) ) }
197
- </ div >
316
+ < Theme
317
+ colourScheme = "light"
318
+ formats = { formats }
319
+ Story = { Story }
320
+ context = { context }
321
+ />
322
+ < Theme
323
+ colourScheme = "dark"
324
+ formats = { formats }
325
+ Story = { Story }
326
+ context = { context }
327
+ />
198
328
</ div >
199
329
) ;
200
330
0 commit comments