Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addon: Create @storybook/addon-themes #23524

Merged
merged 22 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions 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
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';
1 change: 1 addition & 0 deletions code/addons/essentials/src/themes/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is not the screenshot you want to use?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, Joao is going to help me out with an optimized video like we have for the other addons


## 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 `theming.themeOverride` parameter.

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

export default {
title: 'Example/Button',
component: Button,
parameters: {
theming: {
ShaunEvening marked this conversation as resolved.
Show resolved Hide resolved
themeOverride: 'light', // component level override
},
},
};

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

export const PrimaryDark = {
args: {
primary: true,
label: 'Button',
},
parameters: {
theming: {
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