✨(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:
@@ -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 });
|
|
||||||
34
packages/react/.storybook/manager.tsx
Normal file
34
packages/react/.storybook/manager.tsx
Normal 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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,7 +12,17 @@
|
|||||||
pre * {
|
pre * {
|
||||||
font-family: ui-monospace,Menlo,Monaco,"Roboto Mono","Oxygen Mono","Ubuntu Monospace","Source Code Pro","Droid Sans Mono","Courier New",monospace;
|
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>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
window.global = window;
|
window.global = window;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
76
packages/react/.storybook/preview.tsx
Normal file
76
packages/react/.storybook/preview.tsx
Normal 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;
|
||||||
61
packages/react/.storybook/themes.ts
Normal file
61
packages/react/.storybook/themes.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -7,16 +7,6 @@
|
|||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
<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">
|
<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="0.28125" stop-color="#5894E1"/>
|
||||||
<stop offset="1" stop-color="#377FDB"/>
|
<stop offset="1" stop-color="#377FDB"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
Reference in New Issue
Block a user