mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-08 11:11:53 +08:00
Merge branch 'next' into patch-1
This commit is contained in:
commit
e500d1bb83
@ -1,3 +1,8 @@
|
||||
## 8.3.6
|
||||
|
||||
- CLI: Install Svelte CSF v5 in Svelte5 projects - [#29323](https://github.com/storybookjs/storybook/pull/29323), thanks @shilman!
|
||||
- Svelte: Add v5 stories to CLI templates - [#29382](https://github.com/storybookjs/storybook/pull/29382), thanks @JReinhold!
|
||||
|
||||
## 8.3.5
|
||||
|
||||
- CLI: Update the React Native init to include v8 dependencies - [#29273](https://github.com/storybookjs/storybook/pull/29273), thanks @dannyhw!
|
||||
|
@ -86,7 +86,7 @@ View [Component Encyclopedia](https://storybook.js.org/showcase) to see how lead
|
||||
|
||||
Use [storybook.new](https://storybook.new) to quickly create an example project in Stackblitz.
|
||||
|
||||
Storybook comes with a lot of [addons](https://storybook.js.org/docs/react/configure/storybook-addons) for component design, documentation, testing, interactivity, and so on. Storybook's API makes it possible to configure and extend in various ways. It has even been extended to support React Native, Android, iOS, and Flutter development for mobile.
|
||||
Storybook comes with a lot of [addons](https://storybook.js.org/docs/configure/user-interface/storybook-addons) for component design, documentation, testing, interactivity, and so on. Storybook's API makes it possible to configure and extend in various ways. It has even been extended to support React Native, Android, iOS, and Flutter development for mobile.
|
||||
|
||||
### Community
|
||||
|
||||
@ -97,10 +97,10 @@ For additional help, share your issue in [the repo's GitHub Discussions](https:/
|
||||
### Supported Frameworks
|
||||
|
||||
| Renderer | Demo | |
|
||||
|----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [React](code/renderers/react) | [](https://next--630511d655df72125520f051.chromatic.com/) | [](code/renderers/react) |
|
||||
| [Angular](code/frameworks/angular/) | [](https://next--6322ce6af69825592bbb28fc.chromatic.com/) | [](code/frameworks/angular/) |
|
||||
| [Vue 3](code/renderers/vue3) | [](https://next--630513346a8e284ae244d415.chromatic.com/) | [](code/renderers/vue3/) |
|
||||
| [Vue 3](code/renderers/vue3) | [](https://next--630513346a8e284ae244d415.chromatic.com/) | [](code/renderers/vue3/) |
|
||||
| [Web components](code/renderers/web-components) | [](https://next--638db5bf49adfdfe8cf545e0.chromatic.com/) | [](code/renderers/web-components) |
|
||||
| [React Native](https://github.com/storybookjs/react-native) | [](/) | [](https://github.com/storybookjs/react-native) |
|
||||
| [HTML](code/renderers/html) | [](https://next--63dd39a158cf6fc05199b4bb.chromatic.com/) | [](code/renderers/html) |
|
||||
@ -132,7 +132,7 @@ For additional help, share your issue in [the repo's GitHub Discussions](https:/
|
||||
| [storysource](code/addons/storysource/) | View the code of your stories within the Storybook UI |
|
||||
| [viewport](code/addons/viewport/) | Change display sizes and layouts for responsive components using Storybook |
|
||||
|
||||
See [Addon / Framework Support Table](https://storybook.js.org/docs/react/api/frameworks-feature-support)
|
||||
See [Addon / Framework Support Table](https://storybook.js.org/docs/configure/integration/frameworks-feature-support)
|
||||
|
||||
To continue improving your experience, we have to eventually deprecate or remove certain addons in favor of new and better tools.
|
||||
|
||||
@ -237,7 +237,6 @@ By making a recurring donation, you can support us and our work. \[[Become a bac
|
||||
|
||||
<a href="https://opencollective.com/storybook"><img src="https://opencollective.com/storybook/tiers/backers.svg?limit=80&button=false&avatarHeight=46&width=750"></a>
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/storybookjs/storybook/blob/main/LICENSE)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
This Storybook addon can be helpful to make your UI components more accessible.
|
||||
|
||||
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
|
||||
[Framework Support](https://storybook.js.org/docs/configure/integration/frameworks-feature-support)
|
||||
|
||||

|
||||
|
||||
|
@ -2,19 +2,19 @@
|
||||
|
||||
Storybook Addon Actions can be used to display data received by event handlers in [Storybook](https://storybook.js.org).
|
||||
|
||||
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
|
||||
[Framework Support](https://storybook.js.org/docs/configure/integration/frameworks-feature-support)
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
Actions is part of [essentials](https://storybook.js.org/docs/react/essentials) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run:
|
||||
Actions is part of [essentials](https://storybook.js.org/docs/essentials) 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-actions
|
||||
```
|
||||
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/react/configure/#configure-your-storybook-project):
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/configure#configure-your-storybook-project):
|
||||
|
||||
```js
|
||||
export default {
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
Storybook Addon Backgrounds can be used to change background colors inside the preview in [Storybook](https://storybook.js.org).
|
||||
|
||||
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
|
||||
[Framework Support](https://storybook.js.org/docs/configure/integration/frameworks-feature-support)
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
Backgrounds is part of [essentials](https://storybook.js.org/docs/react/essentials) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run:
|
||||
Backgrounds is part of [essentials](https://storybook.js.org/docs/essentials) 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-backgrounds
|
||||
@ -16,7 +16,7 @@ npm i -D @storybook/addon-backgrounds
|
||||
|
||||
## Configuration
|
||||
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/react/configure/#configure-your-storybook-project):
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/configure#configure-your-storybook-project):
|
||||
|
||||
```js
|
||||
export default {
|
||||
|
@ -2,19 +2,19 @@
|
||||
|
||||
[Storybook](https://storybook.js.org) Controls gives you a graphical UI to interact with a component's arguments dynamically, without needing to code. It creates an addon panel next to your component examples ("stories"), so you can edit them live.
|
||||
|
||||
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
|
||||
[Framework Support](https://storybook.js.org/docs/configure/integration/frameworks-feature-support)
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
Controls is part of [essentials](https://storybook.js.org/docs/react/essentials) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run:
|
||||
Controls is part of [essentials](https://storybook.js.org/docs/essentials) 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-controls
|
||||
```
|
||||
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/react/configure/#configure-your-storybook-project):
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/configure#configure-your-storybook-project):
|
||||
|
||||
```js
|
||||
export default {
|
||||
@ -123,7 +123,7 @@ Reflow.argTypes = {
|
||||
|
||||
There are a few known cases where controls can't be auto-generated:
|
||||
|
||||
- You're using a framework for which automatic generation [isn't supported](https://storybook.js.org/docs/react/api/frameworks-feature-support)
|
||||
- You're using a framework for which automatic generation [isn't supported](https://storybook.js.org/docs/configure/integration/frameworks-feature-support)
|
||||
- You're trying to generate controls for a component defined in an external library
|
||||
|
||||
With a little manual work you can still use controls in such cases. Consider the following example:
|
||||
@ -172,7 +172,7 @@ CustomControls.argTypes = {
|
||||
};
|
||||
```
|
||||
|
||||
Like [story parameters](https://storybook.js.org/docs/react/writing-stories/parameters), `args` and `argTypes` annotations are hierarchically merged, so story-level annotations overwrite component-level annotations.
|
||||
Like [story parameters](https://storybook.js.org/docs/writing-stories/parameters), `args` and `argTypes` annotations are hierarchically merged, so story-level annotations overwrite component-level annotations.
|
||||
|
||||
### How do controls work with MDX?
|
||||
|
||||
|
@ -76,7 +76,7 @@ For more information on `MDX`, see the [`MDX` reference](https://github.com/stor
|
||||
|
||||
Storybook Docs supports all view layers that Storybook supports except for React Native (currently). There are some framework-specific features as well, such as props tables and inline story rendering. The following page captures the current state of support:
|
||||
|
||||
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
|
||||
[Framework Support](https://storybook.js.org/docs/configure/integration/frameworks-feature-support)
|
||||
|
||||
**Note:** `#` = WIP support
|
||||
|
||||
@ -139,11 +139,11 @@ export default {
|
||||
|
||||
`csfPluginOptions` is an object for configuring `@storybook/csf-plugin`. When set to `null` it tells docs not to run the `csf-plugin` at all, which can be used as an optimization, or if you're already using `csf-plugin` in your `main.js`.
|
||||
|
||||
> With the release of version 7.0, it is no longer possible to import `.md` files directly into Storybook using the `transcludeMarkdown` [option](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#importing-plain-markdown-files-with-transcludemarkdown-has-changed). Instead, we recommend using the [`Markdown`](https://storybook.js.org/docs/react/api/doc-block-markdown) Doc Block for importing Markdown files into your Storybook documentation.
|
||||
> With the release of version 7.0, it is no longer possible to import `.md` files directly into Storybook using the `transcludeMarkdown` [option](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#importing-plain-markdown-files-with-transcludemarkdown-has-changed). Instead, we recommend using the [`Markdown`](https://storybook.js.org/docs/api/doc-blocks/doc-block-markdown) Doc Block for importing Markdown files into your Storybook documentation.
|
||||
|
||||
## TypeScript configuration
|
||||
|
||||
As of SB6 [TypeScript is zero-config](https://storybook.js.org/docs/react/configure/typescript) and should work with SB Docs out of the box. For advanced configuration options, refer to the [Props documentation](https://github.com/storybookjs/storybook/tree/next/code/addons/docs/docs/props-tables.md).
|
||||
As of SB6 [TypeScript is zero-config](https://storybook.js.org/docs/configure/integration/typescript) and should work with SB Docs out of the box. For advanced configuration options, refer to the [Props documentation](https://github.com/storybookjs/storybook/tree/next/code/addons/docs/docs/props-tables.md).
|
||||
|
||||
## More resources
|
||||
|
||||
|
@ -34,7 +34,7 @@ However, `DocsPage` brings the following improvements:
|
||||
|
||||
Storybook uses `component` to extract the component's description and props, and will rely on it further in future releases. We encourage you to add it to existing stories and use it in all new stories.
|
||||
|
||||
Here's how to set the component in [Component Story Format (CSF)](https://storybook.js.org/docs/react/api/csf):
|
||||
Here's how to set the component in [Component Story Format (CSF)](https://storybook.js.org/docs/api/csf):
|
||||
|
||||
```js
|
||||
import { Badge } from './Badge';
|
||||
|
@ -149,7 +149,7 @@ You can also use the rest of the MDX features in conjunction with embedding. Tha
|
||||
|
||||
## Decorators and parameters
|
||||
|
||||
To add [decorators](https://storybook.js.org/docs/react/writing-stories/decorators) and [parameters](https://storybook.js.org/docs/react/writing-stories/parameters) in MDX:
|
||||
To add [decorators](https://storybook.js.org/docs/writing-stories/decorators) and [parameters](https://storybook.js.org/docs/writing-stories/parameters) in MDX:
|
||||
|
||||
```md
|
||||
<Meta
|
||||
|
@ -106,7 +106,7 @@ The input is the story function and the story context (id, parameters, args, etc
|
||||
|
||||
## Dynamic source rendering
|
||||
|
||||
With the release of Storybook 6.0, we've improved how stories are rendered in the [`Source` doc block](https://storybook.js.org/docs/react/api/doc-block-source). One of such improvements is the `dynamic` source type, which renders a snippet based on the output the story function.
|
||||
With the release of Storybook 6.0, we've improved how stories are rendered in the [`Source` doc block](https://storybook.js.org/docs/api/doc-blocks/doc-block-source). One of such improvements is the `dynamic` source type, which renders a snippet based on the output the story function.
|
||||
|
||||
This dynamic rendering is framework-specific, meaning it needs a custom implementation for each framework.
|
||||
|
||||
|
@ -59,7 +59,7 @@ Starting in SB 6.0, the `ArgsTable` block has built-in `Controls` (formerly know
|
||||
|
||||
<br/>
|
||||
|
||||
These controls are implemented to appear automatically in the props table when your story accepts [Storybook Args](https://storybook.js.org/docs/react/api/csf#args-story-inputs) as its input. This is done slightly differently depending on whether you're using `DocsPage` or `MDX`.
|
||||
These controls are implemented to appear automatically in the props table when your story accepts [Storybook Args](https://storybook.js.org/docs/api/csf#args-story-inputs) as its input. This is done slightly differently depending on whether you're using `DocsPage` or `MDX`.
|
||||
|
||||
**DocsPage.** In [DocsPage](./docspage.md), simply write your story to consume args and the auto-generated props table will display controls in the right-most column:
|
||||
|
||||
@ -200,7 +200,7 @@ Here are the possible customizations for the rest of the prop table:
|
||||
| `table.type.detail` | A longer version of the type (if it's a complex type) |
|
||||
| `table.defaultValue.summary` | A short version of the default value |
|
||||
| `table.defaultValue.detail` | A longer version of the default value (if it's a complex value) |
|
||||
| `control` | See [`addon-controls` README](https://storybook.js.org/docs/essentials/controls#configuration) |
|
||||
| `control` | See [`addon-controls` README](https://storybook.js.org/docs/essentials/controls#configuration) |
|
||||
|
||||
## Reporting a bug
|
||||
|
||||
|
@ -282,7 +282,7 @@ These two methods are complementary. The former is useful for story-specific, an
|
||||
|
||||
What happens if you want to add some wrapper for your MDX page, or add some other kind of React context?
|
||||
|
||||
When you're writing stories you can do this by adding a [decorator](https://storybook.js.org/docs/react/writing-stories/decorators), but when you're adding arbitrary JSX to your MDX documentation outside of a `<Story>` block, decorators no longer apply, and you need to use the `docs.container` parameter.
|
||||
When you're writing stories you can do this by adding a [decorator](https://storybook.js.org/docs/writing-stories/decorators), but when you're adding arbitrary JSX to your MDX documentation outside of a `<Story>` block, decorators no longer apply, and you need to use the `docs.container` parameter.
|
||||
|
||||
The closest Docs equivalent of a decorator is the `container`, a wrapper element that is rendered around the page that is being rendered. Here's an example of adding a solid red border around the page. It uses Storybook's default page container (that sets up various contexts and other magic) and then inserts its own logic between that container and the contents of the page:
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
## Storybook theming
|
||||
|
||||
Storybook theming is the **recommended way** to theme your docs. Docs uses the same theme system as [Storybook UI](https://storybook.js.org/docs/react/configure/theming), but is themed independently from the main UI.
|
||||
Storybook theming is the **recommended way** to theme your docs. Docs uses the same theme system as [Storybook UI](https://storybook.js.org/docs/configure/user-interface/theming), but is themed independently from the main UI.
|
||||
|
||||
Supposing you have a Storybook theme defined for the main UI in `.storybook/manager.js`:
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Storybook Essentials is a curated collection of addons to bring out the best of Storybook.
|
||||
|
||||
Each addon is documented and maintained by the core team and will be upgraded alongside Storybook as the platform evolves. We will also do our best to maintain [framework support](https://storybook.js.org/docs/react/api/frameworks-feature-support) for all of the officially supported frameworks.
|
||||
Each addon is documented and maintained by the core team and will be upgraded alongside Storybook as the platform evolves. We will also do our best to maintain [framework support](https://storybook.js.org/docs/configure/integration/frameworks-feature-support) for all of the officially supported frameworks.
|
||||
|
||||
## Contents
|
||||
|
||||
|
@ -8,7 +8,7 @@ Use it to call attention to particular parts of the story. Or use it to enhance
|
||||
|
||||
## Usage
|
||||
|
||||
This addon requires Storybook 6.5 or later. Highlight is part of [essentials](https://storybook.js.org/docs/react/essentials) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run the following command:
|
||||
This addon requires Storybook 6.5 or later. Highlight is part of [essentials](https://storybook.js.org/docs/essentials) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run the following command:
|
||||
|
||||
yarn:
|
||||
|
||||
|
@ -30,6 +30,7 @@ export function createAnsiToHtmlFilter(theme: StorybookTheme) {
|
||||
return new Filter({
|
||||
fg: theme.color.defaultText,
|
||||
bg: theme.background.content,
|
||||
escapeXML: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Storybook addon for inspecting Jest unit test results.
|
||||
|
||||
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
|
||||
[Framework Support](https://storybook.js.org/docs/configure/integration/frameworks-feature-support)
|
||||
|
||||
[](http://storybooks-official.netlify.com/?selectedKind=Addons%7Cjest&selectedStory=withTests&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybook%2Ftests%2Fpanel)
|
||||
|
||||
@ -20,7 +20,7 @@ Or if you're using yarn as a package manager:
|
||||
|
||||
## Configuration
|
||||
|
||||
Register the addon in your [`.storybook/main.js`](https://storybook.js.org/docs/react/configure/#configure-your-storybook-project):
|
||||
Register the addon in your [`.storybook/main.js`](https://storybook.js.org/docs/configure#configure-your-storybook-project):
|
||||
|
||||
```js
|
||||
export default {
|
||||
@ -84,7 +84,7 @@ Assuming that you have already created a test file for your component (e.g., `My
|
||||
|
||||
### Story-level
|
||||
|
||||
In your story file, add a [decorator](https://storybook.js.org/docs/react/writing-stories/decorators) to your story's default export to display the results:
|
||||
In your story file, add a [decorator](https://storybook.js.org/docs/writing-stories/decorators) to your story's default export to display the results:
|
||||
|
||||
```js
|
||||
// MyComponent.stories.js|jsx
|
||||
@ -99,7 +99,7 @@ export default {
|
||||
};
|
||||
```
|
||||
|
||||
You can also add multiple tests results within your story by including the `jest` [parameter](https://storybook.js.org/docs/react/writing-stories/parameters), for example:
|
||||
You can also add multiple tests results within your story by including the `jest` [parameter](https://storybook.js.org/docs/writing-stories/parameters), for example:
|
||||
|
||||
```js
|
||||
// MyComponent.stories.js|jsx
|
||||
@ -130,7 +130,7 @@ Default.parameters = {
|
||||
### Global level
|
||||
|
||||
To avoid importing the results of the tests in each story, you can update
|
||||
your [`.storybook/preview.js`](https://storybook.js.org/docs/react/configure/#configure-story-rendering) and include a decorator allowing you to display the results only for the stories that have the `jest` parameter defined:
|
||||
your [`.storybook/preview.js`](https://storybook.js.org/docs/configure#configure-story-rendering) and include a decorator allowing you to display the results only for the stories that have the `jest` parameter defined:
|
||||
|
||||
```js
|
||||
// .storybook/preview.js
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
The Storybook Links addon can be used to create links that navigate between stories in [Storybook](https://storybook.js.org).
|
||||
|
||||
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
|
||||
[Framework Support](https://storybook.js.org/docs/configure/integration/frameworks-feature-support)
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
@ -12,7 +12,7 @@ Storybook addon for inspecting layouts and visualizing the box model.
|
||||
|
||||
## Usage
|
||||
|
||||
This addon requires Storybook 6.3 or later. Measure is part of [essentials](https://storybook.js.org/docs/react/essentials) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run:
|
||||
This addon requires Storybook 6.3 or later. Measure is part of [essentials](https://storybook.js.org/docs/essentials) 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-measure
|
||||
|
@ -6,13 +6,13 @@ Storybook Addon Outline can be used for visually debugging CSS layout and alignm
|
||||
|
||||
## Usage
|
||||
|
||||
Requires Storybook 6.1 or later. Outline is part of [essentials](https://storybook.js.org/docs/react/essentials) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run:
|
||||
Requires Storybook 6.1 or later. Outline is part of [essentials](https://storybook.js.org/docs/essentials) 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-outline
|
||||
```
|
||||
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/react/configure/#configure-your-storybook-project):
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/configure#configure-your-storybook-project):
|
||||
|
||||
```js
|
||||
export default {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
This addon is used to show stories source in the addon panel.
|
||||
|
||||
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
|
||||
[Framework Support](https://storybook.js.org/docs/configure/integration/frameworks-feature-support)
|
||||
|
||||

|
||||
|
||||
@ -56,7 +56,7 @@ To customize the `source-loader`, pass `loaderOptions`. Valid configurations are
|
||||
|
||||
## Theming
|
||||
|
||||
Storysource will automatically use the light or dark syntax theme based on your storybook theme. See [Theming Storybook](https://storybook.js.org/docs/react/configure/theming) for more information.
|
||||
Storysource will automatically use the light or dark syntax theme based on your storybook theme. See [Theming Storybook](https://storybook.js.org/docs/configure/user-interface/theming) for more information.
|
||||
|
||||

|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { UnsupportedViewportDimensionError } from 'storybook/internal/preview-errors';
|
||||
|
||||
import { page } from '@vitest/browser/context';
|
||||
|
||||
import { MINIMAL_VIEWPORTS } from '../../../viewport/src/defaults';
|
||||
import type { ViewportMap, ViewportStyles } from '../../../viewport/src/types';
|
||||
|
||||
@ -50,6 +48,10 @@ const parseDimension = (value: string, dimension: 'width' | 'height') => {
|
||||
export const setViewport = async (viewportsParam: ViewportsParam = {} as ViewportsParam) => {
|
||||
const defaultViewport = viewportsParam.defaultViewport;
|
||||
|
||||
const { page } = await import('@vitest/browser/context').catch(() => ({
|
||||
page: null,
|
||||
}));
|
||||
|
||||
if (!page || !globalThis.__vitest_browser__) {
|
||||
return;
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ Requires Storybook 7.0 or later. If you need to add it to your Storybook, you ca
|
||||
npm i -D @storybook/addon-themes
|
||||
```
|
||||
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/react/configure/#configure-your-storybook-project):
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/configure#configure-your-storybook-project):
|
||||
|
||||
```js
|
||||
export default {
|
||||
|
@ -6,19 +6,19 @@ The Toolbars addon controls global story rendering options from [Storybook's](ht
|
||||
- set your components' internationalization (i18n) locale
|
||||
- configure just about anything in Storybook that makes use of a global variable
|
||||
|
||||
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
|
||||
[Framework Support](https://storybook.js.org/docs/configure/integration/frameworks-feature-support)
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
Toolbars is part of [essentials](https://storybook.js.org/docs/react/essentials) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run:
|
||||
Toolbars is part of [essentials](https://storybook.js.org/docs/essentials) 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-toolbars
|
||||
```
|
||||
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/react/configure/#configure-your-storybook-project):
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/configure#configure-your-storybook-project):
|
||||
|
||||
```js
|
||||
export default {
|
||||
|
@ -2,19 +2,19 @@
|
||||
|
||||
Storybook Viewport Addon allows your stories to be displayed in different sizes and layouts in [Storybook](https://storybook.js.org). This helps build responsive components inside of Storybook.
|
||||
|
||||
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
|
||||
[Framework Support](https://storybook.js.org/docs/configure/integration/frameworks-feature-support)
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
Viewport is part of [essentials](https://storybook.js.org/docs/react/essentials) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run:
|
||||
Viewport is part of [essentials](https://storybook.js.org/docs/essentials) 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-viewport
|
||||
```
|
||||
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/react/configure/#configure-your-storybook-project):
|
||||
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/configure#configure-your-storybook-project):
|
||||
|
||||
```js
|
||||
export default {
|
||||
|
@ -2,12 +2,13 @@ import { global } from '@storybook/global';
|
||||
|
||||
import { addons } from '@storybook/core/manager-api';
|
||||
|
||||
const TAG_FILTERS = 'tag-filters';
|
||||
const STATIC_FILTER = 'static-filter';
|
||||
|
||||
addons.register(STATIC_FILTER, (api) => {
|
||||
addons.register(TAG_FILTERS, (api) => {
|
||||
// FIXME: this ensures the filter is applied after the first render
|
||||
// to avoid a strange race condition in Webkit only.
|
||||
const excludeTags = Object.entries(global.TAGS_OPTIONS ?? {}).reduce(
|
||||
const staticExcludeTags = Object.entries(global.TAGS_OPTIONS ?? {}).reduce(
|
||||
(acc, entry) => {
|
||||
const [tag, option] = entry;
|
||||
if ((option as any).excludeFromSidebar) {
|
||||
@ -23,7 +24,7 @@ addons.register(STATIC_FILTER, (api) => {
|
||||
return (
|
||||
// we can filter out the primary story, but we still want to show autodocs
|
||||
(tags.includes('dev') || item.type === 'docs') &&
|
||||
tags.filter((tag) => excludeTags[tag]).length === 0
|
||||
tags.filter((tag) => staticExcludeTags[tag]).length === 0
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { babelPrint } from '@storybook/core/babel';
|
||||
@ -1080,7 +1081,6 @@ describe('ConfigFile', () => {
|
||||
const config = loadConfig(source).parse();
|
||||
config.setImport('path', 'path');
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const parsed = babelPrint(config._ast);
|
||||
|
||||
expect(parsed).toMatchInlineSnapshot(`
|
||||
@ -1099,7 +1099,6 @@ describe('ConfigFile', () => {
|
||||
const config = loadConfig(source).parse();
|
||||
config.setImport('path', 'path');
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const parsed = babelPrint(config._ast);
|
||||
|
||||
expect(parsed).toMatchInlineSnapshot(`
|
||||
@ -1118,7 +1117,6 @@ describe('ConfigFile', () => {
|
||||
const config = loadConfig(source).parse();
|
||||
config.setImport(['dirname'], 'path');
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const parsed = babelPrint(config._ast);
|
||||
|
||||
expect(parsed).toMatchInlineSnapshot(`
|
||||
@ -1139,7 +1137,6 @@ describe('ConfigFile', () => {
|
||||
const config = loadConfig(source).parse();
|
||||
config.setImport(['dirname'], 'path');
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const parsed = babelPrint(config._ast);
|
||||
|
||||
expect(parsed).toMatchInlineSnapshot(`
|
||||
@ -1161,7 +1158,6 @@ describe('ConfigFile', () => {
|
||||
const config = loadConfig(source).parse();
|
||||
config.setRequireImport('path', 'path');
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const parsed = babelPrint(config._ast);
|
||||
|
||||
expect(parsed).toMatchInlineSnapshot(`
|
||||
@ -1181,7 +1177,6 @@ describe('ConfigFile', () => {
|
||||
const config = loadConfig(source).parse();
|
||||
config.setRequireImport('path', 'path');
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const parsed = babelPrint(config._ast);
|
||||
|
||||
expect(parsed).toMatchInlineSnapshot(`
|
||||
@ -1200,7 +1195,6 @@ describe('ConfigFile', () => {
|
||||
const config = loadConfig(source).parse();
|
||||
config.setRequireImport(['dirname'], 'path');
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const parsed = babelPrint(config._ast);
|
||||
|
||||
expect(parsed).toMatchInlineSnapshot(`
|
||||
@ -1224,7 +1218,6 @@ describe('ConfigFile', () => {
|
||||
const config = loadConfig(source).parse();
|
||||
config.setRequireImport(['dirname', 'basename'], 'path');
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const parsed = babelPrint(config._ast);
|
||||
|
||||
expect(parsed).toMatchInlineSnapshot(`
|
||||
@ -1308,4 +1301,31 @@ describe('ConfigFile', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
it("export { X } with X is import { X } from 'another-file'", () => {
|
||||
const source = dedent`
|
||||
import type { StorybookConfig } from '@storybook/react-webpack5';
|
||||
import { path } from 'path';
|
||||
|
||||
export { path };
|
||||
|
||||
const config: StorybookConfig = {
|
||||
addons: [
|
||||
'foo',
|
||||
{ name: 'bar', options: {} },
|
||||
],
|
||||
"otherField": [
|
||||
"foo",
|
||||
{ "name": 'bar', options: {} },
|
||||
],
|
||||
}
|
||||
export default config;
|
||||
`;
|
||||
const config = loadConfig(source).parse();
|
||||
|
||||
expect(config._exportDecls['path']).toBe(undefined);
|
||||
expect(config._exports['path']).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -102,6 +102,7 @@ const _findVarDeclarator = (
|
||||
): t.VariableDeclarator | null | undefined => {
|
||||
let declarator: t.VariableDeclarator | null | undefined = null;
|
||||
let declarations: t.VariableDeclarator[] | null = null;
|
||||
|
||||
program.body.find((node: t.Node) => {
|
||||
if (t.isVariableDeclaration(node)) {
|
||||
declarations = node.declarations;
|
||||
@ -248,9 +249,13 @@ export class ConfigFile {
|
||||
) {
|
||||
const { name: localName } = spec.local;
|
||||
const { name: exportName } = spec.exported;
|
||||
|
||||
const decl = _findVarDeclarator(localName, parent as t.Program) as any;
|
||||
self._exports[exportName] = decl.init;
|
||||
self._exportDecls[exportName] = decl;
|
||||
// decl can be empty in case X from `import { X } from ....` because it is not handled in _findVarDeclarator
|
||||
if (decl) {
|
||||
self._exports[exportName] = decl.init;
|
||||
self._exportDecls[exportName] = decl;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -159,9 +159,10 @@ export interface SubAPI {
|
||||
* Set the query parameters for the current URL & navigates.
|
||||
*
|
||||
* @param {QueryParams} input - An object containing the query parameters to set.
|
||||
* @param {NavigateOptions} options - Options for the navigation.
|
||||
* @returns {void}
|
||||
*/
|
||||
applyQueryParams: (input: QueryParams) => void;
|
||||
applyQueryParams: (input: QueryParams, options?: NavigateOptions) => void;
|
||||
}
|
||||
|
||||
export const init: ModuleFn<SubAPI, SubState> = (moduleArgs) => {
|
||||
@ -206,10 +207,10 @@ export const init: ModuleFn<SubAPI, SubState> = (moduleArgs) => {
|
||||
provider.channel?.emit(UPDATE_QUERY_PARAMS, update);
|
||||
}
|
||||
},
|
||||
applyQueryParams(input) {
|
||||
applyQueryParams(input, options) {
|
||||
const { path, queryParams } = api.getUrlState();
|
||||
|
||||
navigateTo(path, { ...queryParams, ...input } as any);
|
||||
navigateTo(path, { ...queryParams, ...input } as any, options);
|
||||
api.setQueryParams(input);
|
||||
},
|
||||
navigateUrl(url, options) {
|
||||
|
@ -7,6 +7,7 @@ import { styled, useTheme } from '@storybook/core/theming';
|
||||
import { global } from '@storybook/global';
|
||||
import {
|
||||
AlertIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
DocumentIcon,
|
||||
GlobeIcon,
|
||||
@ -216,7 +217,7 @@ export const RefIndicator = React.memo(
|
||||
<TooltipLinkList
|
||||
// @ts-expect-error (non strict)
|
||||
links={Object.entries(ref.versions).map(([id, href]) => ({
|
||||
icon: href === ref.url ? 'check' : undefined,
|
||||
icon: href === ref.url ? <CheckIcon /> : undefined,
|
||||
id,
|
||||
title: id,
|
||||
href,
|
||||
|
@ -47,11 +47,7 @@ const baseProps = {
|
||||
|
||||
export const Simple: StoryFn = () => <Search {...baseProps}>{() => null}</Search>;
|
||||
|
||||
export const SimpleWithCreateButton: StoryFn = () => (
|
||||
<Search {...baseProps} showCreateStoryButton={true}>
|
||||
{() => null}
|
||||
</Search>
|
||||
);
|
||||
export const SimpleWithCreateButton: StoryFn = () => <Search {...baseProps}>{() => null}</Search>;
|
||||
|
||||
export const FilledIn: StoryFn = () => (
|
||||
<Search {...baseProps} initialQuery="Search query">
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import React, { type ReactNode, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { IconButton, TooltipNote, WithTooltip } from '@storybook/core/components';
|
||||
import { IconButton } from '@storybook/core/components';
|
||||
import { styled } from '@storybook/core/theming';
|
||||
import { global } from '@storybook/global';
|
||||
import { CloseIcon, PlusIcon, SearchIcon } from '@storybook/icons';
|
||||
import { CloseIcon, SearchIcon } from '@storybook/icons';
|
||||
|
||||
import { shortcutToHumanString, useStorybookApi } from '@storybook/core/manager-api';
|
||||
|
||||
@ -15,7 +15,6 @@ import Fuse from 'fuse.js';
|
||||
import { getGroupStatus, getHighestStatus } from '../../utils/status';
|
||||
import { scrollIntoView, searchItem } from '../../utils/tree';
|
||||
import { useLayout } from '../layout/LayoutProvider';
|
||||
import { CreateNewStoryFileModal } from './CreateNewStoryFileModal';
|
||||
import { DEFAULT_REF_ID } from './Sidebar';
|
||||
import type {
|
||||
CombinedDataset,
|
||||
@ -54,10 +53,6 @@ const SearchBar = styled.div({
|
||||
columnGap: 6,
|
||||
});
|
||||
|
||||
const TooltipNoteWrapper = styled(TooltipNote)({
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
const ScreenReaderLabel = styled.label({
|
||||
position: 'absolute',
|
||||
left: -10000,
|
||||
@ -67,49 +62,47 @@ const ScreenReaderLabel = styled.label({
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
const CreateNewStoryButton = styled(IconButton)(({ theme }) => ({
|
||||
color: theme.color.mediumdark,
|
||||
const SearchField = styled.div(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 2,
|
||||
flexGrow: 1,
|
||||
height: 32,
|
||||
width: '100%',
|
||||
boxShadow: `${theme.button.border} 0 0 0 1px inset`,
|
||||
borderRadius: theme.appBorderRadius + 2,
|
||||
|
||||
'&:has(input:focus), &:has(input:active)': {
|
||||
boxShadow: `${theme.color.secondary} 0 0 0 1px inset`,
|
||||
background: theme.background.app,
|
||||
},
|
||||
}));
|
||||
|
||||
const SearchIconWrapper = styled.div(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 8,
|
||||
zIndex: 1,
|
||||
pointerEvents: 'none',
|
||||
const IconWrapper = styled.div(({ theme, onClick }) => ({
|
||||
cursor: onClick ? 'pointer' : 'default',
|
||||
flex: '0 0 28px',
|
||||
height: '100%',
|
||||
pointerEvents: onClick ? 'auto' : 'none',
|
||||
color: theme.textMutedColor,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
}));
|
||||
|
||||
const SearchField = styled.div({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
const Input = styled.input(({ theme }) => ({
|
||||
appearance: 'none',
|
||||
height: 28,
|
||||
paddingLeft: 28,
|
||||
paddingRight: 28,
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
border: 0,
|
||||
boxShadow: `${theme.button.border} 0 0 0 1px inset`,
|
||||
background: 'transparent',
|
||||
borderRadius: 4,
|
||||
fontSize: `${theme.typography.size.s1 + 1}px`,
|
||||
fontFamily: 'inherit',
|
||||
transition: 'all 150ms',
|
||||
color: theme.color.defaultText,
|
||||
width: '100%',
|
||||
outline: 0,
|
||||
|
||||
'&:focus, &:active': {
|
||||
outline: 0,
|
||||
borderColor: theme.color.secondary,
|
||||
background: theme.background.app,
|
||||
},
|
||||
'&::placeholder': {
|
||||
color: theme.textMutedColor,
|
||||
opacity: 1,
|
||||
@ -133,11 +126,9 @@ const Input = styled.input(({ theme }) => ({
|
||||
}));
|
||||
|
||||
const FocusKey = styled.code(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
right: 9,
|
||||
margin: 5,
|
||||
marginTop: 6,
|
||||
height: 16,
|
||||
zIndex: 1,
|
||||
lineHeight: '16px',
|
||||
textAlign: 'center',
|
||||
fontSize: '11px',
|
||||
@ -147,50 +138,43 @@ const FocusKey = styled.code(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
flexShrink: 0,
|
||||
}));
|
||||
|
||||
const FocusKeyCmd = styled.span({
|
||||
fontSize: '14px',
|
||||
});
|
||||
|
||||
const ClearIcon = styled.div(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 8,
|
||||
zIndex: 1,
|
||||
color: theme.textMutedColor,
|
||||
cursor: 'pointer',
|
||||
const Actions = styled.div({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
}));
|
||||
gap: 2,
|
||||
});
|
||||
|
||||
const FocusContainer = styled.div({ outline: 0 });
|
||||
|
||||
const isDevelopment = global.CONFIG_TYPE === 'DEVELOPMENT';
|
||||
const isRendererReact = global.STORYBOOK_RENDERER === 'react';
|
||||
|
||||
export const Search = React.memo<{
|
||||
children: SearchChildrenFn;
|
||||
dataset: CombinedDataset;
|
||||
enableShortcuts?: boolean;
|
||||
getLastViewed: () => Selection[];
|
||||
initialQuery?: string;
|
||||
showCreateStoryButton?: boolean;
|
||||
searchBarContent?: ReactNode;
|
||||
searchFieldContent?: ReactNode;
|
||||
}>(function Search({
|
||||
children,
|
||||
dataset,
|
||||
enableShortcuts = true,
|
||||
getLastViewed,
|
||||
initialQuery = '',
|
||||
showCreateStoryButton = isDevelopment && isRendererReact,
|
||||
searchBarContent,
|
||||
searchFieldContent,
|
||||
}) {
|
||||
const api = useStorybookApi();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputPlaceholder, setPlaceholder] = useState('Find components');
|
||||
const [allComponents, showAllComponents] = useState(false);
|
||||
const searchShortcut = api ? shortcutToHumanString(api.getShortcutKeys().search) : '/';
|
||||
const [isFileSearchModalOpen, setIsFileSearchModalOpen] = useState(false);
|
||||
|
||||
const makeFuse = useCallback(() => {
|
||||
const list = dataset.entries.reduce<SearchItem[]>((acc, [refId, { index, status }]) => {
|
||||
@ -406,9 +390,9 @@ export const Search = React.memo<{
|
||||
{...getRootProps({ refKey: '' }, { suppressRefError: true })}
|
||||
className="search-field"
|
||||
>
|
||||
<SearchIconWrapper>
|
||||
<IconWrapper>
|
||||
<SearchIcon />
|
||||
</SearchIconWrapper>
|
||||
</IconWrapper>
|
||||
<Input {...inputProps} />
|
||||
{!isMobile && enableShortcuts && !isOpen && (
|
||||
<FocusKey>
|
||||
@ -421,34 +405,16 @@ export const Search = React.memo<{
|
||||
)}
|
||||
</FocusKey>
|
||||
)}
|
||||
{isOpen && (
|
||||
<ClearIcon onClick={() => clearSelection()}>
|
||||
<CloseIcon />
|
||||
</ClearIcon>
|
||||
)}
|
||||
<Actions>
|
||||
{isOpen && (
|
||||
<IconButton onClick={() => clearSelection()}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
{searchFieldContent}
|
||||
</Actions>
|
||||
</SearchField>
|
||||
{showCreateStoryButton && (
|
||||
<>
|
||||
<WithTooltip
|
||||
trigger="hover"
|
||||
hasChrome={false}
|
||||
tooltip={<TooltipNoteWrapper note="Create a new story" />}
|
||||
>
|
||||
<CreateNewStoryButton
|
||||
onClick={() => {
|
||||
setIsFileSearchModalOpen(true);
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<PlusIcon />
|
||||
</CreateNewStoryButton>
|
||||
</WithTooltip>
|
||||
<CreateNewStoryFileModal
|
||||
open={isFileSearchModalOpen}
|
||||
onOpenChange={setIsFileSearchModalOpen}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{searchBarContent}
|
||||
</SearchBar>
|
||||
<FocusContainer tabIndex={0} id="storybook-explorer-menu">
|
||||
{children({
|
||||
|
@ -43,6 +43,14 @@ const managerContext: any = {
|
||||
),
|
||||
selectStory: fn().mockName('api::selectStory'),
|
||||
experimental_setFilter: fn().mockName('api::experimental_setFilter'),
|
||||
getDocsUrl: () => 'https://storybook.js.org/docs/',
|
||||
getUrlState: () => ({
|
||||
queryParams: {},
|
||||
path: '',
|
||||
viewMode: 'story',
|
||||
url: 'http://localhost:6006/',
|
||||
}),
|
||||
applyQueryParams: fn().mockName('api::applyQueryParams'),
|
||||
},
|
||||
};
|
||||
|
||||
@ -56,6 +64,20 @@ const meta = {
|
||||
menu,
|
||||
extra: [] as Addon_SidebarTopType[],
|
||||
index: index,
|
||||
indexJson: {
|
||||
entries: {
|
||||
// force the tags filter menu to show in production
|
||||
['dummy--dummyId']: {
|
||||
id: 'dummy--dummyId',
|
||||
name: 'Dummy story',
|
||||
title: 'dummy',
|
||||
importPath: './dummy.stories.js',
|
||||
type: 'story',
|
||||
tags: ['A', 'B', 'C', 'dev'],
|
||||
},
|
||||
},
|
||||
v: 6,
|
||||
},
|
||||
storyId,
|
||||
refId: DEFAULT_REF_ID,
|
||||
refs: {},
|
||||
|
@ -1,18 +1,28 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { ScrollArea, Spaced } from '@storybook/core/components';
|
||||
import {
|
||||
IconButton,
|
||||
ScrollArea,
|
||||
Spaced,
|
||||
TooltipNote,
|
||||
WithTooltip,
|
||||
} from '@storybook/core/components';
|
||||
import { styled } from '@storybook/core/theming';
|
||||
import type { API_LoadedRefData, Addon_SidebarTopType } from '@storybook/core/types';
|
||||
import type { API_LoadedRefData, Addon_SidebarTopType, StoryIndex } from '@storybook/core/types';
|
||||
import { global } from '@storybook/global';
|
||||
import { PlusIcon } from '@storybook/icons';
|
||||
|
||||
import type { State } from '@storybook/core/manager-api';
|
||||
import { type State, useStorybookApi } from '@storybook/core/manager-api';
|
||||
|
||||
import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants';
|
||||
import { CreateNewStoryFileModal } from './CreateNewStoryFileModal';
|
||||
import { Explorer } from './Explorer';
|
||||
import type { HeadingProps } from './Heading';
|
||||
import { Heading } from './Heading';
|
||||
import { Search } from './Search';
|
||||
import { SearchResults } from './SearchResults';
|
||||
import { SidebarBottom } from './SidebarBottom';
|
||||
import { TagsFilter } from './TagsFilter';
|
||||
import type { CombinedDataset, Selection } from './types';
|
||||
import { useLastViewed } from './useLastViewed';
|
||||
|
||||
@ -57,6 +67,17 @@ const Bottom = styled.div(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const TooltipNoteWrapper = styled(TooltipNote)({
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
const CreateNewStoryButton = styled(IconButton)(({ theme }) => ({
|
||||
color: theme.color.mediumdark,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: theme.appBorderRadius + 2,
|
||||
}));
|
||||
|
||||
const Swap = React.memo(function Swap({
|
||||
children,
|
||||
condition,
|
||||
@ -99,6 +120,9 @@ const useCombination = (
|
||||
return useMemo(() => ({ hash, entries: Object.entries(hash) }), [hash]);
|
||||
};
|
||||
|
||||
const isDevelopment = global.CONFIG_TYPE === 'DEVELOPMENT';
|
||||
const isRendererReact = global.STORYBOOK_RENDERER === 'react';
|
||||
|
||||
export interface SidebarProps extends API_LoadedRefData {
|
||||
refs: State['refs'];
|
||||
status: State['status'];
|
||||
@ -110,13 +134,14 @@ export interface SidebarProps extends API_LoadedRefData {
|
||||
enableShortcuts?: boolean;
|
||||
onMenuClick?: HeadingProps['onMenuClick'];
|
||||
showCreateStoryButton?: boolean;
|
||||
indexJson?: StoryIndex;
|
||||
}
|
||||
|
||||
export const Sidebar = React.memo(function Sidebar({
|
||||
// @ts-expect-error (non strict)
|
||||
storyId = null,
|
||||
refId = DEFAULT_REF_ID,
|
||||
index,
|
||||
indexJson,
|
||||
indexError,
|
||||
status,
|
||||
previewInitialized,
|
||||
@ -126,13 +151,15 @@ export const Sidebar = React.memo(function Sidebar({
|
||||
enableShortcuts = true,
|
||||
refs = {},
|
||||
onMenuClick,
|
||||
showCreateStoryButton,
|
||||
showCreateStoryButton = isDevelopment && isRendererReact,
|
||||
}: SidebarProps) {
|
||||
const [isFileSearchModalOpen, setIsFileSearchModalOpen] = useState(false);
|
||||
// @ts-expect-error (non strict)
|
||||
const selected: Selection = useMemo(() => storyId && { storyId, refId }, [storyId, refId]);
|
||||
const dataset = useCombination(index, indexError, previewInitialized, status, refs);
|
||||
const isLoading = !index && !indexError;
|
||||
const lastViewedProps = useLastViewed(selected);
|
||||
const api = useStorybookApi();
|
||||
|
||||
return (
|
||||
<Container className="container sidebar-container">
|
||||
@ -150,7 +177,35 @@ export const Sidebar = React.memo(function Sidebar({
|
||||
<Search
|
||||
dataset={dataset}
|
||||
enableShortcuts={enableShortcuts}
|
||||
showCreateStoryButton={showCreateStoryButton}
|
||||
searchBarContent={
|
||||
showCreateStoryButton && (
|
||||
<>
|
||||
<WithTooltip
|
||||
trigger="hover"
|
||||
hasChrome={false}
|
||||
tooltip={<TooltipNoteWrapper note="Create a new story" />}
|
||||
>
|
||||
<CreateNewStoryButton
|
||||
onClick={() => {
|
||||
setIsFileSearchModalOpen(true);
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<PlusIcon />
|
||||
</CreateNewStoryButton>
|
||||
</WithTooltip>
|
||||
<CreateNewStoryFileModal
|
||||
open={isFileSearchModalOpen}
|
||||
onOpenChange={setIsFileSearchModalOpen}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
searchFieldContent={
|
||||
indexJson && (
|
||||
<TagsFilter api={api} indexJson={indexJson} isDevelopment={isDevelopment} />
|
||||
)
|
||||
}
|
||||
{...lastViewedProps}
|
||||
>
|
||||
{({
|
||||
|
@ -0,0 +1,75 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { findByRole, fn } from '@storybook/test';
|
||||
|
||||
import { TagsFilter } from './TagsFilter';
|
||||
|
||||
const meta = {
|
||||
component: TagsFilter,
|
||||
tags: ['haha'],
|
||||
args: {
|
||||
api: {
|
||||
experimental_setFilter: fn(),
|
||||
getDocsUrl: () => 'https://storybook.js.org/docs/',
|
||||
getUrlState: () => ({
|
||||
queryParams: {},
|
||||
path: '',
|
||||
viewMode: 'story',
|
||||
url: 'http://localhost:6006/',
|
||||
}),
|
||||
applyQueryParams: fn().mockName('api::applyQueryParams'),
|
||||
} as any,
|
||||
isDevelopment: true,
|
||||
},
|
||||
} satisfies Meta<typeof TagsFilter>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Closed: Story = {
|
||||
args: {
|
||||
indexJson: {
|
||||
v: 6,
|
||||
entries: {
|
||||
'c1-s1': { tags: ['A', 'B', 'C', 'dev'] } as any,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ClosedWithSelection: Story = {
|
||||
args: {
|
||||
...Closed.args,
|
||||
initialSelectedTags: ['A', 'B'],
|
||||
},
|
||||
};
|
||||
|
||||
export const Open: Story = {
|
||||
...Closed,
|
||||
play: async ({ canvasElement }) => {
|
||||
const button = await findByRole(canvasElement, 'button');
|
||||
await button.click();
|
||||
},
|
||||
};
|
||||
|
||||
export const OpenWithSelection: Story = {
|
||||
...ClosedWithSelection,
|
||||
play: Open.play,
|
||||
};
|
||||
|
||||
export const OpenEmpty: Story = {
|
||||
args: {
|
||||
indexJson: {
|
||||
v: 6,
|
||||
entries: {},
|
||||
},
|
||||
},
|
||||
play: Open.play,
|
||||
};
|
||||
|
||||
export const EmptyProduction: Story = {
|
||||
args: {
|
||||
...OpenEmpty.args,
|
||||
isDevelopment: false,
|
||||
},
|
||||
};
|
130
code/core/src/manager/components/sidebar/TagsFilter.tsx
Normal file
130
code/core/src/manager/components/sidebar/TagsFilter.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Badge, IconButton, WithTooltip } from '@storybook/core/components';
|
||||
import { styled } from '@storybook/core/theming';
|
||||
import { FilterIcon } from '@storybook/icons';
|
||||
import type { StoryIndex, Tag } from '@storybook/types';
|
||||
|
||||
import type { API } from '@storybook/core/manager-api';
|
||||
|
||||
import { TagsFilterPanel } from './TagsFilterPanel';
|
||||
|
||||
const TAGS_FILTER = 'tags-filter';
|
||||
|
||||
const BUILT_IN_TAGS_HIDE = new Set([
|
||||
'dev',
|
||||
'docs-only',
|
||||
'test-only',
|
||||
'autodocs',
|
||||
'test',
|
||||
'attached-mdx',
|
||||
'unattached-mdx',
|
||||
]);
|
||||
|
||||
const Wrapper = styled.div({
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
const TagSelected = styled(Badge)(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
top: 7,
|
||||
right: 7,
|
||||
transform: 'translate(50%, -50%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 3,
|
||||
height: 6,
|
||||
minWidth: 6,
|
||||
lineHeight: 'px',
|
||||
boxShadow: `${theme.barSelectedColor} 0 0 0 1px inset`,
|
||||
fontSize: theme.typography.size.s1 - 1,
|
||||
background: theme.color.secondary,
|
||||
color: theme.color.lightest,
|
||||
}));
|
||||
|
||||
export interface TagsFilterProps {
|
||||
api: API;
|
||||
indexJson: StoryIndex;
|
||||
initialSelectedTags?: Tag[];
|
||||
isDevelopment: boolean;
|
||||
}
|
||||
|
||||
export const TagsFilter = ({
|
||||
api,
|
||||
indexJson,
|
||||
initialSelectedTags = [],
|
||||
isDevelopment,
|
||||
}: TagsFilterProps) => {
|
||||
const [selectedTags, setSelectedTags] = useState(initialSelectedTags);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const tagsActive = selectedTags.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
api.experimental_setFilter(TAGS_FILTER, (item) => {
|
||||
if (selectedTags.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return selectedTags.some((tag) => item.tags?.includes(tag));
|
||||
});
|
||||
}, [api, selectedTags]);
|
||||
|
||||
const allTags = Object.values(indexJson.entries).reduce((acc, entry) => {
|
||||
entry.tags?.forEach((tag: Tag) => {
|
||||
if (!BUILT_IN_TAGS_HIDE.has(tag)) {
|
||||
acc.add(tag);
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, new Set<Tag>());
|
||||
|
||||
const toggleTag = useCallback(
|
||||
(tag: string) => {
|
||||
if (selectedTags.includes(tag)) {
|
||||
setSelectedTags(selectedTags.filter((t) => t !== tag));
|
||||
} else {
|
||||
setSelectedTags([...selectedTags, tag]);
|
||||
}
|
||||
},
|
||||
[selectedTags, setSelectedTags]
|
||||
);
|
||||
|
||||
const handleToggleExpand = useCallback(
|
||||
(event: React.SyntheticEvent<Element, Event>): void => {
|
||||
event.preventDefault();
|
||||
setExpanded(!expanded);
|
||||
},
|
||||
[expanded, setExpanded]
|
||||
);
|
||||
|
||||
// Hide the entire UI if there are no tags and it's a built Storybook
|
||||
if (allTags.size === 0 && !isDevelopment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WithTooltip
|
||||
placement="bottom"
|
||||
trigger="click"
|
||||
onVisibleChange={setExpanded}
|
||||
tooltip={() => (
|
||||
<TagsFilterPanel
|
||||
api={api}
|
||||
allTags={Array.from(allTags).toSorted()}
|
||||
selectedTags={selectedTags}
|
||||
toggleTag={toggleTag}
|
||||
isDevelopment={isDevelopment}
|
||||
/>
|
||||
)}
|
||||
closeOnOutsideClick
|
||||
>
|
||||
<Wrapper>
|
||||
<IconButton key="tags" title="Tag filters" active={tagsActive} onClick={handleToggleExpand}>
|
||||
<FilterIcon />
|
||||
</IconButton>
|
||||
{selectedTags.length > 0 && <TagSelected />}
|
||||
</Wrapper>
|
||||
</WithTooltip>
|
||||
);
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
import { TagsFilterPanel } from './TagsFilterPanel';
|
||||
|
||||
const meta = {
|
||||
component: TagsFilterPanel,
|
||||
args: {
|
||||
toggleTag: fn(),
|
||||
api: {
|
||||
getDocsUrl: () => 'https://storybook.js.org/docs/',
|
||||
} as any,
|
||||
isDevelopment: true,
|
||||
},
|
||||
tags: ['hoho'],
|
||||
} satisfies Meta<typeof TagsFilterPanel>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
allTags: [],
|
||||
selectedTags: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const BuiltInTagsOnly: Story = {
|
||||
args: {
|
||||
allTags: ['play-fn'],
|
||||
selectedTags: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const BuiltInTagsOnlyProduction: Story = {
|
||||
args: {
|
||||
...BuiltInTagsOnly.args,
|
||||
isDevelopment: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
allTags: ['tag1', 'tag2', 'tag3'],
|
||||
selectedTags: ['tag1', 'tag3'],
|
||||
},
|
||||
};
|
||||
|
||||
export const BuiltInTags: Story = {
|
||||
args: {
|
||||
allTags: [...Default.args.allTags, 'play-fn'],
|
||||
selectedTags: ['tag1', 'tag3'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ExtraBuiltInTagsSelected: Story = {
|
||||
args: {
|
||||
...BuiltInTags.args,
|
||||
selectedTags: ['tag1', 'tag3', 'autodocs', 'play-fn'],
|
||||
},
|
||||
};
|
70
code/core/src/manager/components/sidebar/TagsFilterPanel.tsx
Normal file
70
code/core/src/manager/components/sidebar/TagsFilterPanel.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
|
||||
import { TooltipLinkList } from '@storybook/core/components';
|
||||
import { styled, useTheme } from '@storybook/core/theming';
|
||||
import { ShareAltIcon } from '@storybook/icons';
|
||||
import type { Tag } from '@storybook/types';
|
||||
|
||||
import type { API } from '@storybook/core/manager-api';
|
||||
|
||||
const BUILT_IN_TAGS_SHOW = new Set(['play-fn']);
|
||||
|
||||
const Wrapper = styled.div({
|
||||
minWidth: 180,
|
||||
maxWidth: 220,
|
||||
});
|
||||
|
||||
interface TagsFilterPanelProps {
|
||||
api: API;
|
||||
allTags: Tag[];
|
||||
selectedTags: Tag[];
|
||||
toggleTag: (tag: Tag) => void;
|
||||
isDevelopment: boolean;
|
||||
}
|
||||
|
||||
export const TagsFilterPanel = ({
|
||||
api,
|
||||
allTags,
|
||||
selectedTags,
|
||||
toggleTag,
|
||||
isDevelopment,
|
||||
}: TagsFilterPanelProps) => {
|
||||
const theme = useTheme();
|
||||
const userTags = allTags.filter((tag) => !BUILT_IN_TAGS_SHOW.has(tag));
|
||||
const docsUrl = api.getDocsUrl({ subpath: 'writing-stories/tags#filtering-by-custom-tags' });
|
||||
const items = allTags.map((tag) => {
|
||||
const checked = selectedTags.includes(tag);
|
||||
const id = `tag-${tag}`;
|
||||
return {
|
||||
id,
|
||||
title: tag,
|
||||
right: <input type="checkbox" id={id} name={id} value={tag} checked={checked} />,
|
||||
onClick: () => toggleTag(tag),
|
||||
};
|
||||
}) as any[];
|
||||
|
||||
if (allTags.length === 0) {
|
||||
items.push({
|
||||
id: 'no-tags',
|
||||
title: 'There are no tags. Use tags to organize and filter your Storybook.',
|
||||
isIndented: false,
|
||||
});
|
||||
}
|
||||
if (userTags.length === 0 && isDevelopment) {
|
||||
items.push({
|
||||
id: 'tags-docs',
|
||||
title: 'Learn how to add tags',
|
||||
icon: <ShareAltIcon />,
|
||||
href: docsUrl,
|
||||
style: {
|
||||
borderTop: `4px solid ${theme.appBorderColor}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<TooltipLinkList links={items} />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
@ -23,6 +23,10 @@ const Sidebar = React.memo(function Sideber({ onMenuClick }: SidebarProps) {
|
||||
storyId,
|
||||
refId,
|
||||
layout: { showToolbar },
|
||||
// FIXME: This is the actual `index.json` index where the `index` below
|
||||
// is actually the stories hash. We should fix this up and make it consistent.
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
internal_index,
|
||||
index,
|
||||
status,
|
||||
indexError,
|
||||
@ -50,6 +54,7 @@ const Sidebar = React.memo(function Sideber({ onMenuClick }: SidebarProps) {
|
||||
return {
|
||||
title: name,
|
||||
url,
|
||||
indexJson: internal_index,
|
||||
index,
|
||||
indexError,
|
||||
status,
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
Storybook collects completely anonymous telemetry data about general usage. Participation in this program is optional and it’s easy to opt-out.
|
||||
|
||||
For more information visit: [storybook.js.org/telemetry](https://storybook.js.org/telemetry)
|
||||
For more information visit: [storybook.js.org/telemetry](https://storybook.js.org/docs/configure/telemetry)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Storybook Webpack preset for HTML
|
||||
|
||||
This package is a [preset](https://storybook.js.org/docs/html/addons/writing-presets#presets-api) that configures Storybook's webpack settings for handling HTML.
|
||||
This package is a [preset](https://storybook.js.org/docs/addons/writing-presets?renderer=html) that configures Storybook's webpack settings for handling HTML.
|
||||
It's an internal package that's not intended to be used directly by users.
|
||||
|
||||
- More info on [Storybook for HTML](https://storybook.js.org/docs/html/get-started)
|
||||
- More info on [Storybook for HTML](https://storybook.js.org/docs/get-started/install?renderer=html)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Storybook Webpack preset for Preact
|
||||
|
||||
This package is a [preset](https://storybook.js.org/docs/preact/addons/writing-presets#presets-api) that configures Storybook's webpack settings for handling Preact.
|
||||
This package is a [preset](https://storybook.js.org/docs/addons/writing-presets?renderer=preact) that configures Storybook's webpack settings for handling Preact.
|
||||
It's an internal package that's not intended to be used directly by users.
|
||||
|
||||
- More info on [Storybook for Preact](https://storybook.js.org/docs/preact/get-started)
|
||||
- More info on [Storybook for Preact](https://storybook.js.org/docs/get-started/install?renderer=preact)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Storybook Webpack preset for React
|
||||
|
||||
This package is a [preset](https://storybook.js.org/docs/react/addons/writing-presets#presets-api) that configures Storybook's webpack settings for handling React.
|
||||
This package is a [preset](https://storybook.js.org/docs/addons/writing-presets?renderer=react) that configures Storybook's webpack settings for handling React.
|
||||
It's an internal package that's not intended to be used directly by users.
|
||||
|
||||
- More info on [Storybook for React](https://storybook.js.org/docs/react/get-started)
|
||||
- More info on [Storybook for React](https://storybook.js.org/docs/get-started/install?renderer=react)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Storybook Webpack preset for Server
|
||||
|
||||
This package is a [preset](https://storybook.js.org/docs/react/addons/writing-presets#presets-api) that configures Storybook's webpack settings for handling React.
|
||||
This package is a [preset](https://storybook.js.org/docs/addons/writing-presets) that configures Storybook's webpack settings for handling React.
|
||||
It's an internal package that's not intended to be used directly by users.
|
||||
|
||||
- More info on [Storybook for Server](https://github.com/storybookjs/storybook/tree/next/code/frameworks/server-webpack5)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Storybook Webpack preset for Svelte
|
||||
|
||||
This package is a [preset](https://storybook.js.org/docs/svelte/addons/writing-presets#presets-api) that configures Storybook's webpack settings for handling Svelte.
|
||||
This package is a [preset](https://storybook.js.org/docs/addons/writing-presets?renderer=svelte) that configures Storybook's webpack settings for handling Svelte.
|
||||
It's an internal package that's not intended to be used directly by users.
|
||||
|
||||
- More info on [Storybook for Svelte](https://storybook.js.org/docs/svelte/get-started)
|
||||
- More info on [Storybook for Svelte](https://storybook.js.org/docs/get-started/install?renderer=svelte)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Storybook Webpack preset for Vue3
|
||||
|
||||
This package is a [preset](https://storybook.js.org/docs/addons/writing-presets#presets-api) that configures Storybook's webpack settings for handling Vue 3.
|
||||
This package is a [preset](https://storybook.js.org/docs/addons/writing-presets?renderer=vue) that configures Storybook's webpack settings for handling Vue 3.
|
||||
It's an internal package that's not intended to be used directly by users.
|
||||
|
||||
- More info on [Storybook for Vue3](https://storybook.js.org/docs/get-started/why-storybook)
|
||||
- More info on [Storybook for Vue3](https://storybook.js.org/docs/get-started/install?renderer=vue)
|
||||
|
BIN
docs/_assets/writing-stories/custom-tag-filter.png
Normal file
BIN
docs/_assets/writing-stories/custom-tag-filter.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 127 KiB |
@ -51,6 +51,21 @@ To remove a tag from a story, prefix it with `!`. For example:
|
||||
|
||||
Tags can be removed for all stories in your project (in `.storybook/preview.js|ts`), all stories for a component (in the CSF file meta), or a single story (as above).
|
||||
|
||||
## Filtering by custom tags
|
||||
|
||||
Custom tags enable a flexible layer of categorization on top of Storybook's sidebar hierarchy. In the example above, we created an `experimental` tag to indicate that a story is not yet stable.
|
||||
|
||||
You can create custom tags for any purpose. Sample uses might include:
|
||||
- Status, such as `experimental`, `new`, `stable`, or `deprecated`
|
||||
- User persona, such as `admin`, `user`, or `developer`
|
||||
- Component/code ownership
|
||||
|
||||
Custom tags are useful because they show up as filters in Storybook's sidebar. Selecting a tag in the filter causes the sidebar to only show stories with that tag. Selecting multiple tags shows stories that contain any of those tags.
|
||||
|
||||

|
||||
|
||||
Filtering by tags is a powerful way to focus on a subset of stories, especially in large Storybook projects. You can also narrow your stories by tag and then search within that subset.
|
||||
|
||||
## Recipes
|
||||
|
||||
### Docs-only stories
|
||||
|
Loading…
x
Reference in New Issue
Block a user