Skip to content

Commit

Permalink
Merge pull request #23524 from storybookjs/addon-themes
Browse files Browse the repository at this point in the history
Addon: Create @storybook/addon-themes
  • Loading branch information
Shaun Evening committed Jul 25, 2023
2 parents d4e6098 + dec3296 commit aa09ec1
Show file tree
Hide file tree
Showing 42 changed files with 1,558 additions and 24 deletions.
15 changes: 14 additions & 1 deletion code/addons/essentials/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@
"require": "./dist/outline/manager.js",
"import": "./dist/outline/manager.mjs"
},
"./themes/manager": {
"types": "./dist/themes/manager.d.ts",
"require": "./dist/themes/manager.js",
"import": "./dist/themes/manager.mjs"
},
"./themes/preview": {
"types": "./dist/themes/preview.d.ts",
"require": "./dist/themes/preview.js",
"import": "./dist/themes/preview.mjs"
},
"./toolbars/manager": {
"types": "./dist/toolbars/manager.d.ts",
"require": "./dist/toolbars/manager.js",
Expand Down Expand Up @@ -126,6 +136,7 @@
"@storybook/addon-highlight": "7.2.0-alpha.0",
"@storybook/addon-measure": "7.2.0-alpha.0",
"@storybook/addon-outline": "7.2.0-alpha.0",
"@storybook/addon-themes": "7.2.0-alpha.0",
"@storybook/addon-toolbars": "7.2.0-alpha.0",
"@storybook/addon-viewport": "7.2.0-alpha.0",
"@storybook/core-common": "7.2.0-alpha.0",
Expand Down Expand Up @@ -162,7 +173,9 @@
"./src/outline/preview.ts",
"./src/outline/manager.ts",
"./src/toolbars/manager.ts",
"./src/viewport/manager.ts"
"./src/viewport/manager.ts",
"./src/themes/manager.ts",
"./src/themes/preview.ts"
],
"platform": "node"
},
Expand Down
14 changes: 9 additions & 5 deletions code/addons/essentials/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import { logger } from '@storybook/node-logger';
import { serverRequire } from '@storybook/core-common';

interface PresetOptions {
configDir: string;
docs?: boolean;
controls?: boolean;
actions?: boolean;
backgrounds?: boolean;
viewport?: boolean;
toolbars?: boolean;
configDir: string;
controls?: boolean;
docs?: boolean;
measure?: boolean;
outline?: boolean;
themes?: boolean;
toolbars?: boolean;
viewport?: boolean;
}

const requireMain = (configDir: string) => {
Expand All @@ -37,6 +38,8 @@ export function addons(options: PresetOptions) {
};

const main = requireMain(options.configDir);

// NOTE: The order of these addons is important.
return [
'docs',
'controls',
Expand All @@ -47,6 +50,7 @@ export function addons(options: PresetOptions) {
'measure',
'outline',
'highlight',
'themes',
]
.filter((key) => (options as any)[key] !== false)
.filter((addon) => !checkInstalled(addon, main))
Expand Down
1 change: 1 addition & 0 deletions code/addons/essentials/src/themes/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '@storybook/addon-themes/manager';
2 changes: 2 additions & 0 deletions code/addons/essentials/src/themes/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// eslint-disable-next-line import/export
export * from '@storybook/addon-themes/preview';
72 changes: 72 additions & 0 deletions code/addons/themes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# `@storybook/addon-themes

Storybook Addon Themes can be used which between multiple themes for components inside the preview in [Storybook](https://storybook.js.org).

![React Storybook Screenshot](https://user-images.githubusercontent.com/42671/98158421-dada2300-1ea8-11eb-8619-af1e7018e1ec.png)

## Usage

Requires Storybook 7.0 or later. Themes is part of [essentials](https://storybook.js.org/docs/react/essentials/introduction) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run:

```sh
npm i -D @storybook/addon-themes
```

Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/react/configure/overview#configure-your-storybook-project):

```js
export default {
addons: ['@storybook/addon-themes'],
};
```

### 👇 Tool specific configuration

For tool-specific setup, check out the recipes below

- [`@emotion/styled`](https://github.com/storybookjs/storybook/tree/next/code/addons/themes/docs/getting-started/emotion.md)
- [`@mui/material`](https://github.com/storybookjs/storybook/tree/next/code/addons/themes/docs/getting-started/material-ui.md)
- [`bootstrap`](https://github.com/storybookjs/storybook/tree/next/code/addons/themes/docs/getting-started/bootstrap.md)
- [`styled-components`](https://github.com/storybookjs/storybook/tree/next/code/addons/themes/docs/getting-started/styled-components.md)
- [`tailwind`](https://github.com/storybookjs/storybook/tree/next/code/addons/themes/docs/getting-started/tailwind.md)
- [`vuetify@3.x`](https://github.com/storybookjs/storybook/blob/next/code/addons/themes/docs/api.md#writing-a-custom-decorator)

Don't see your favorite tool listed? Don't worry! That doesn't mean this addon isn't for you. Check out the ["Writing a custom decorator"](https://github.com/storybookjs/storybook/blob/next/code/addons/themes/docs/api.md#writing-a-custom-decorator) section of the [api reference](https://github.com/storybookjs/storybook/blob/next/code/addons/themes/docs/api.md).

### ❗️ Overriding theme

If you want to override your theme for a particular component or story, you can use the `themes.themeOverride` parameter.

```js
import React from 'react';
import { Button } from './Button';

export default {
title: 'Example/Button',
component: Button,
parameters: {
themes: {
themeOverride: 'light', // component level override
},
},
};

export const Primary = {
args: {
primary: true,
label: 'Button',
},
};

export const PrimaryDark = {
args: {
primary: true,
label: 'Button',
},
parameters: {
themes: {
themeOverride: 'dark', // Story level override
},
},
};
```
206 changes: 206 additions & 0 deletions code/addons/themes/docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# API

## Decorators

### `withThemeFromJSXProvider`

Takes your provider component, global styles, and theme(s)to wrap your stories in.

```js
import { withThemeFromJSXProvider } from '@storybook/addon-styling';

export const decorators = [
withThemeFromJSXProvider({
themes: {
light: lightTheme,
dark: darkTheme,
},
defaultTheme: 'light',
Provider: ThemeProvider,
GlobalStyles: CssBaseline,
}),
];
```

Available options:

| option | type | required? | Description |
| ------------ | --------------------- | :-------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| themes | `Record<string, any>` | | An object of theme configurations where the key is the name of the theme and the value is the theme object. If multiple themes are provided, a toolbar item will be added to switch between themes. |
| defaultTheme | `string` | | The name of the default theme to use |
| Provider | | | The JSX component to provide themes |
| GlobalStyles | | | A JSX component containing global css styles. |

### `withThemeByClassName`

Takes your theme class names to apply your parent element to enable your theme(s).

```js
import { withThemeByClassName } from '@storybook/addon-styling';

export const decorators = [
withThemeByClassName({
themes: {
light: 'light-theme',
dark: 'dark-theme',
},
defaultTheme: 'light',
}),
];
```

Available options:

| option | type | required? | Description |
| -------------- | ------------------------ | :-------: | --------------------------------------------------------------------------------------------------------------- |
| themes | `Record<string, string>` || An object of theme configurations where the key is the name of the theme and the value is the theme class name. |
| defaultTheme | `string` || The name of the default theme to use |
| parentSelector | `string` | | The selector for the parent element that you want to apply your theme class to. Defaults to "html" |

### `withThemeByDataAttribute`

Takes your theme names and data attribute to apply your parent element to enable your theme(s).

```js
import { withThemeByDataAttribute } from '@storybook/addon-styling';

export const decorators = [
withThemeByDataAttribute({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light',
attributeName: 'data-bs-theme',
}),
];
```

available options:

| option | type | required? | Description |
| -------------- | ------------------------ | :-------: | ------------------------------------------------------------------------------------------------------------------- |
| themes | `Record<string, string>` || An object of theme configurations where the key is the name of the theme and the value is the data attribute value. |
| defaultTheme | `string` || The name of the default theme to use |
| parentSelector | `string` | | The selector for the parent element that you want to apply your theme class to. Defaults to "html" |
| attributeName | `string` | | The name of the data attribute to set on the parent element for your theme(s). Defaults to "data-theme" |

## Writing a custom decorator

If none of these decorators work for your library there is still hope. We've provided a collection of helper functions to get access to the theme toggling state so that you can create a decorator of your own.

### `pluckThemeFromContext`

Pulls the selected theme from storybook's global state.

```js
import { DecoratorHelpers } from '@storybook/addon-styling';
const { pluckThemeFromContext } = DecoratorHelpers;

export const myCustomDecorator =
({ themes, defaultState, ...rest }) =>
(storyFn, context) => {
const selectedTheme = pluckThemeFromContext(context);

// Snipped
};
```

### `useThemeParameters`

Returns the theme parameters for this addon.

```js
import { DecoratorHelpers } from '@storybook/addon-styling';
const { useThemeParameters } = DecoratorHelpers;

export const myCustomDecorator =
({ themes, defaultState, ...rest }) =>
(storyFn, context) => {
const { themeOverride } = useThemeParameters();

// Snipped
};
```

### `initializeThemeState`

Used to register the themes and defaultTheme with the addon state.

```js
import { DecoratorHelpers } from '@storybook/addon-styling';
const { initializeThemeState } = DecoratorHelpers;

export const myCustomDecorator = ({ themes, defaultState, ...rest }) => {
initializeThemeState(Object.keys(themes), defaultTheme);

return (storyFn, context) => {
// Snipped
};
};
```

### Putting it all together

Let's use Vuetify as an example. Vuetify uses it's own global state to know which theme to render. To build a custom decorator to accommodate this method we'll need to do the following

```js
// .storybook/withVeutifyTheme.decorator.js

import { DecoratorHelpers } from '@storybook/addon-styling';
import { useTheme } from 'vuetify';

const { initializeThemeState, pluckThemeFromContext, useThemeParameters } = DecoratorHelpers;

export const withVuetifyTheme = ({ themes, defaultTheme }) => {
initializeThemeState(Object.keys(themes), defaultTheme);

return (story, context) => {
const selectedTheme = pluckThemeFromContext(context);
const { themeOverride } = useThemeParameters();

const selected = themeOverride || selectedTheme || defaultTheme;

return {
components: { story },
setup() {
const theme = useTheme();

theme.global.name.value = selected;

return {
theme,
};
},
template: `<v-app><story /></v-app>`,
};
};
};
```

This can then be provided to Storybook in `.storybook/preview.js`:

```js
// .storybook/preview.js

import { setup } from '@storybook/vue3';
import { registerPlugins } from '../src/plugins';
import { withVuetifyTheme } from './withVuetifyTheme.decorator';

setup((app) => {
registerPlugins(app);
});

/* snipped for brevity */

export const decorators = [
withVuetifyTheme({
themes: {
light: 'light',
dark: 'dark',
customTheme: 'myCustomTheme',
},
defaultTheme: 'customTheme', // The key of your default theme
}),
];
```
Loading

0 comments on commit aa09ec1

Please sign in to comment.