(react) add themes switching in Storybook

We want to be able to visualize easily themes directly inside
Storybook. This was not a trivial task as there is no centralized
logic to handle Doc / Stories / Manager at the same time.
This commit is contained in:
Nathan Vasse
2023-09-26 11:40:40 +02:00
committed by NathanVss
parent 8b1dbd2f25
commit b94abbd6b3
7 changed files with 182 additions and 97 deletions

View File

@@ -1,40 +0,0 @@
import { addons } from "@storybook/manager-api";
import { create } from "@storybook/theming";
import { defaultTokens } from "@openfun/cunningham-tokens";
const COLORS = defaultTokens.theme.colors;
const theme = create({
base: "light",
brandUrl: "https://github.com/openfun/cunningham",
brandImage: "logo-cunningham.svg",
brandTitle: "Cunningham",
brandTarget: "_self",
//
colorPrimary: COLORS["primary-400"],
colorSecondary: COLORS["primary-400"],
// UI
appBg: COLORS["greyscale-100"],
appContentBg: COLORS["greyscale-000"],
appBorderColor: COLORS["greyscale-300"],
appBorderRadius: 4,
// Text colors
textColor: COLORS["greyscale-900"],
textInverseColor: COLORS["greyscale-000"],
// Toolbar default and active colors
barTextColor: COLORS["greyscale-500"],
barSelectedColor: COLORS["greyscale-900"],
barBg: COLORS["greyscale-000"],
// Form colors
inputBg: COLORS["greyscale-000"],
inputBorder: COLORS["greyscale-300"],
inputTextColor: COLORS["greyscale-800"],
inputBorderRadius: 2,
});
addons.setConfig({ theme });

View File

@@ -0,0 +1,34 @@
import { addons, types, useStorybookApi } from '@storybook/manager-api';
import { getThemeFromGlobals, themes } from './themes';
import React, { useEffect } from 'react';
import { useGlobals } from '@storybook/api';
addons.setConfig({ theme: themes.default });
/**
* This add-on is just here to apply the theme to the Storybook manager ( the top-most frame
* containing sidebar, toolbar, etc ) when the theme is switched.
*
* The reason why we needed to add this add-on is that add-ons are the only place from where you can
* dynamically change the current theme of the manager.
*/
addons.register('theme-synchronizer', () => {
addons.add('theme-synchronizer/main', {
title: 'Theme synchronizer',
//👇 Sets the type of UI element in Storybook
type: types.TOOL,
//👇 Shows the Toolbar UI element if either the Canvas or Docs tab is active
match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)),
render: ({ active }) => {
const api = useStorybookApi();
const [globals, updateGlobals] = useGlobals();
const theme = getThemeFromGlobals(globals);
useEffect(() => {
api.setOptions({
theme: themes[theme]
})
}, [theme]);
return null;
},
});
});

View File

@@ -12,7 +12,17 @@
pre * {
font-family: ui-monospace,Menlo,Monaco,"Roboto Mono","Oxygen Mono","Ubuntu Monospace","Source Code Pro","Droid Sans Mono","Courier New",monospace;
}
.cunningham-theme--dark {
.docblock-source {
background-color: var(--c--theme--colors--greyscale-100);
}
.prismjs {
background-color: var(--c--theme--colors--greyscale-100) !important;
}
}
</style>
<script>
window.global = window;
</script>
</script>

View File

@@ -1,46 +0,0 @@
import "../src/icons.scss";
import "../src/index.scss";
import "../src/fonts.scss";
import { Preview } from "@storybook/react";
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
options: {
storySort: (a, b) => {
const roots = ["Getting Started", "Components"];
const gettingStartedOrder = [
"Installation",
"Customization",
"Colors",
"Spacings",
"Typography",
];
const aParts = a.title.split("/");
const bParts = b.title.split("/");
if (aParts[0] !== bParts[0]) {
return roots.indexOf(aParts[0]) - roots.indexOf(bParts[0]);
}
if (aParts[1] !== bParts[1]) {
if (aParts[0] === "Getting Started") {
return (
gettingStartedOrder.indexOf(aParts[1]) -
gettingStartedOrder.indexOf(bParts[1])
);
}
return aParts[1].localeCompare(bParts[1]);
}
return 0;
},
},
},
};
export default preview;

View File

@@ -0,0 +1,76 @@
import '../src/icons.scss';
import '../src/index.scss';
import '../src/fonts.scss';
import { Preview } from '@storybook/react';
import { DocsContainer } from '@storybook/blocks';
import { CunninghamProvider } from ':/components/Provider';
import { BACKGROUND_COLOR_TO_THEME, getThemeFromGlobals, themes } from './themes';
export const DocsWithTheme = (props, context) => {
const theme = getThemeFromGlobals(props.context.store.globals.globals);
return <CunninghamProvider theme={theme}>
<DocsContainer {...props} theme={themes[theme]} />
</CunninghamProvider>;
};
const preview: Preview = {
decorators: [
(Story, context) => (
<CunninghamProvider theme={getThemeFromGlobals(context.globals)}>
<Story />
</CunninghamProvider>
),
],
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
backgrounds: {
default: null,
values: Object.entries(BACKGROUND_COLOR_TO_THEME).map(value => ({
name: value[1],
value: value[0],
})),
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
docs: {
container: DocsWithTheme,
},
options: {
storySort: (a, b) => {
const roots = ['Getting Started', 'Components'];
const gettingStartedOrder = [
'Installation',
'First steps',
'Customization',
'Theming',
'Colors',
'Spacings',
'Typography',
];
const aParts = a.title.split('/');
const bParts = b.title.split('/');
if (aParts[0] !== bParts[0]) {
return roots.indexOf(aParts[0]) - roots.indexOf(bParts[0]);
}
if (aParts[1] !== bParts[1]) {
if (aParts[0] === 'Getting Started') {
return (
gettingStartedOrder.indexOf(aParts[1]) -
gettingStartedOrder.indexOf(bParts[1])
);
}
return aParts[1].localeCompare(bParts[1]);
}
return 0;
},
},
},
};
export default preview;

View File

@@ -0,0 +1,61 @@
import { create } from '@storybook/theming';
import { tokens } from '../src/cunningham-tokens';
const buildTheme = (colors: typeof tokens.themes.default.theme.colors & any) => {
return {
brandUrl: 'https://github.com/openfun/cunningham',
brandImage: 'logo-cunningham.svg',
brandTitle: 'Cunningham',
brandTarget: '_self',
//
colorPrimary: colors['primary-400'],
colorSecondary: colors['primary-400'],
// UI
appBg: colors['greyscale-100'],
appContentBg: colors['greyscale-000'],
appBorderColor: colors['greyscale-300'],
appBorderRadius: 4,
// Text colors
textColor: colors['greyscale-900'],
textInverseColor: colors['greyscale-000'],
// Toolbar default and active colors
barTextColor: colors['greyscale-500'],
barSelectedColor: colors['greyscale-900'],
barBg: colors['greyscale-000'],
// Form colors
inputBg: colors['greyscale-000'],
inputBorder: colors['greyscale-300'],
inputTextColor: colors['greyscale-800'],
inputBorderRadius: 2,
};
};
export const themes = {
default: create({
base: 'light',
...buildTheme(tokens.themes.default.theme.colors),
}),
dark: create({
base: 'dark',
...buildTheme(tokens.themes.dark.theme.colors),
}),
};
export enum Themes {
dark = 'dark',
default = 'default'
}
export const BACKGROUND_COLOR_TO_THEME = {
'#0C1A2B': Themes.dark,
};
export const getThemeFromGlobals = (globals: any): string => {
const color = BACKGROUND_COLOR_TO_THEME[globals.backgrounds?.value];
return color ?? Themes.default;
};

View File

@@ -7,16 +7,6 @@
</g>
</g>
<defs>
<filter id="filter0_d_16_5113" x="-112.85" y="-113.135" width="722.099" height="318.964" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="58.2902"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_16_5113"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_16_5113" result="shape"/>
</filter>
<linearGradient id="paint0_linear_16_5113" x1="248.5" y1="0" x2="248.5" y2="90" gradientUnits="userSpaceOnUse">
<stop offset="0.28125" stop-color="#5894E1"/>
<stop offset="1" stop-color="#377FDB"/>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB