diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e13bedee71..32d7a9325c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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! diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 4381e3ec634f..9f0afe1f6501 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,14 @@ +## 8.4.0-alpha.8 + +- Addon-Test: Support for `@vitest/browser` v2.1.2 - [#29407](https://github.com/storybookjs/storybook/pull/29407), thanks @strozw! +- ConfigFile: Fix `export { X }` parsing - [#29344](https://github.com/storybookjs/storybook/pull/29344), thanks @vctqs1! +- Core: Fix building Storybook deleting project root files - [#29371](https://github.com/storybookjs/storybook/pull/29371), thanks @JReinhold! +- Interactions: Escape xml of interactions errors - [#29414](https://github.com/storybookjs/storybook/pull/29414), thanks @kasperpeulen! +- Svelte: Add v5 stories to CLI templates - [#29382](https://github.com/storybookjs/storybook/pull/29382), thanks @JReinhold! +- Test: Remove unused `util` dependency - [#29310](https://github.com/storybookjs/storybook/pull/29310), thanks @JReinhold! +- UI: Fix RefIndicator to use CheckIcon instead of string - [#29209](https://github.com/storybookjs/storybook/pull/29209), thanks @JSMike! +- UI: Simple tag filtering - [#29333](https://github.com/storybookjs/storybook/pull/29333), thanks @shilman! + ## 8.4.0-alpha.7 - CLI: Install Svelte CSF v5 in Svelte5 projects - [#29323](https://github.com/storybookjs/storybook/pull/29323), thanks @shilman! diff --git a/code/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch b/code/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch deleted file mode 100644 index 212dfcc7d0ea..000000000000 --- a/code/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch +++ /dev/null @@ -1,97 +0,0 @@ -diff --git a/package.json b/package.json -index 195dac9ee7d42fdb76bb22dc37580fa0bffd4680..980ad42f41a06023f9f7e370fd382c9217c24be5 100644 ---- a/package.json -+++ b/package.json -@@ -55,7 +55,7 @@ - "contributors:generate": "all-contributors generate" - }, - "peerDependencies": { -- "svelte": "^3 || ^4" -+ "svelte": "^3 || ^4 || ^5" - }, - "dependencies": { - "@testing-library/dom": "^9.3.1" -diff --git a/src/pure.js b/src/pure.js -index 6d4943412448c9f310f007ca7dab9d04cef90d0d..d62f4aebeb1b23ccc3c3d82aadd67075c6507c0e 100644 ---- a/src/pure.js -+++ b/src/pure.js -@@ -3,7 +3,7 @@ import { - getQueriesForElement, - prettyDOM - } from '@testing-library/dom' --import { tick } from 'svelte' -+import { tick, mount, unmount } from 'svelte' - - const containerCache = new Set() - const componentCache = new Set() -@@ -54,40 +54,34 @@ const render = ( - return { props: options } - } - -- let component = new ComponentConstructor({ -+ let component = mount(ComponentConstructor, { - target, -- ...checkProps(options) -+ ...checkProps(options), -+ ondestroy: () => componentCache.delete(component) - }) - - containerCache.add({ container, target, component }) - componentCache.add(component) - -- component.$$.on_destroy.push(() => { -- componentCache.delete(component) -- }) -- - return { - container, - component, - debug: (el = container) => console.log(prettyDOM(el)), - rerender: (options) => { -- if (componentCache.has(component)) component.$destroy() -+ if (componentCache.has(component)) unmount(component) - - // eslint-disable-next-line no-new - component = new ComponentConstructor({ - target, -- ...checkProps(options) -+ ...checkProps(options), -+ ondestroy: () => componentCache.delete(component) - }) - - containerCache.add({ container, target, component }) - componentCache.add(component) -- -- component.$$.on_destroy.push(() => { -- componentCache.delete(component) -- }) - }, - unmount: () => { -- if (componentCache.has(component)) component.$destroy() -+ if (componentCache.has(component)) unmount(component) - }, - ...getQueriesForElement(container, queries) - } -@@ -96,7 +90,7 @@ const render = ( - const cleanupAtContainer = (cached) => { - const { target, component } = cached - -- if (componentCache.has(component)) component.$destroy() -+ if (componentCache.has(component)) unmount(component) - - if (target.parentNode === document.body) { - document.body.removeChild(target) -@@ -109,9 +103,10 @@ const cleanup = () => { - Array.from(containerCache.keys()).forEach(cleanupAtContainer) - } - --const act = async (fn) => { -- if (fn) { -- await fn() -+const act = (fn) => { -+ const value = fn && fn() -+ if (value !== undefined && typeof value.then === 'function') { -+ return value.then(() => tick()) - } - return tick() - } diff --git a/code/addons/actions/ADVANCED.md b/code/addons/actions/ADVANCED.md index 7cca56342dee..d71320209288 100644 --- a/code/addons/actions/ADVANCED.md +++ b/code/addons/actions/ADVANCED.md @@ -1,6 +1,6 @@ ## Advanced/Legacy Actions usage -For basic usage, see the [documentation](https://storybook.js.org/docs/react/essentials/actions). +For basic usage, see the [documentation](https://storybook.js.org/docs/essentials/actions). This document describes the pre-6.0 usage of the addon, and as such is no longer recommended (although it will be supported until at least 7.0). diff --git a/code/addons/actions/README.md b/code/addons/actions/README.md index 80d2bd5d746c..a24f0e7052a2 100644 --- a/code/addons/actions/README.md +++ b/code/addons/actions/README.md @@ -24,4 +24,4 @@ export default { ## Usage -The basic usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/actions). For legacy usage, see the [advanced README](./ADVANCED.md). +The basic usage is documented in the [documentation](https://storybook.js.org/docs/essentials/actions). For legacy usage, see the [advanced README](./ADVANCED.md). diff --git a/code/addons/backgrounds/README.md b/code/addons/backgrounds/README.md index 481ba54bcd2f..a720847d4bf0 100644 --- a/code/addons/backgrounds/README.md +++ b/code/addons/backgrounds/README.md @@ -26,4 +26,4 @@ export default { ## Usage -The usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/backgrounds). +The usage is documented in the [documentation](https://storybook.js.org/docs/essentials/backgrounds). diff --git a/code/addons/controls/README.md b/code/addons/controls/README.md index 192a112e07fe..b3c097e53eeb 100644 --- a/code/addons/controls/README.md +++ b/code/addons/controls/README.md @@ -24,7 +24,7 @@ export default { ## Usage -The usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/controls). +The usage is documented in the [documentation](https://storybook.js.org/docs/essentials/controls). ## FAQs @@ -92,7 +92,7 @@ export const Reflow = () => { }; ``` -And again, as above, this can be rewritten using [fully custom args](https://storybook.js.org/docs/react/essentials/controls#fully-custom-args): +And again, as above, this can be rewritten using [fully custom args](https://storybook.js.org/docs/essentials/controls#fully-custom-args): ```jsx export const Reflow = ({ count, label, ...args }) => ( @@ -147,7 +147,7 @@ Basic.args = { }; ``` -The `argTypes` annotation (which can also be applied to individual stories if needed), gives Storybook the hints it needs to generate controls in these unsupported cases. See [control annotations](https://storybook.js.org/docs/react/essentials/controls#annotation) for a full list of control types. +The `argTypes` annotation (which can also be applied to individual stories if needed), gives Storybook the hints it needs to generate controls in these unsupported cases. See [control annotations](https://storybook.js.org/docs/essentials/controls#annotation) for a full list of control types. It's also possible that your Storybook is misconfigured. If you think this might be the case, please search through Storybook's [Github issues](https://github.com/storybookjs/storybook/issues), and file a new issue if you don't find one that matches your use case. diff --git a/code/addons/docs/docs/props-tables.md b/code/addons/docs/docs/props-tables.md index 7a44a6c653ae..6cb5029b00a6 100644 --- a/code/addons/docs/docs/props-tables.md +++ b/code/addons/docs/docs/props-tables.md @@ -82,7 +82,7 @@ export const WithControls = (args) => ; ``` -For a very detailed walkthrough of how to write stories that use controls, read the [documentation](https://storybook.js.org/docs/react/essentials/controls). +For a very detailed walkthrough of how to write stories that use controls, read the [documentation](https://storybook.js.org/docs/essentials/controls). ## Customization @@ -187,7 +187,7 @@ This would render a row with a modified description, a type display with a dropd > - `type: 'number'` is shorthand for `type: { name: 'number' }` > - `control: 'radio'` is shorthand for `control: { type: 'radio' }` -Controls customization has an entire section in the [documentation](https://storybook.js.org/docs/react/essentials/controls#configuration). +Controls customization has an entire section in the [documentation](https://storybook.js.org/docs/essentials/controls#configuration). Here are the possible customizations for the rest of the prop table: @@ -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/react/essentials/controls#configuration) | +| `control` | See [`addon-controls` README](https://storybook.js.org/docs/essentials/controls#configuration) | ## Reporting a bug diff --git a/code/addons/interactions/src/utils.ts b/code/addons/interactions/src/utils.ts index d80d9f4cdbee..cfdf6724f49d 100644 --- a/code/addons/interactions/src/utils.ts +++ b/code/addons/interactions/src/utils.ts @@ -30,6 +30,7 @@ export function createAnsiToHtmlFilter(theme: StorybookTheme) { return new Filter({ fg: theme.color.defaultText, bg: theme.background.content, + escapeXML: true, }); } diff --git a/code/addons/test/src/vitest-plugin/viewports.ts b/code/addons/test/src/vitest-plugin/viewports.ts index c779eb3a6633..3fa6b446ddcb 100644 --- a/code/addons/test/src/vitest-plugin/viewports.ts +++ b/code/addons/test/src/vitest-plugin/viewports.ts @@ -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; } diff --git a/code/addons/toolbars/README.md b/code/addons/toolbars/README.md index dcb9b9c4deef..2e37dc6c6168 100644 --- a/code/addons/toolbars/README.md +++ b/code/addons/toolbars/README.md @@ -28,7 +28,7 @@ export default { ## Usage -The usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/toolbars-and-globals). +The usage is documented in the [documentation](https://storybook.js.org/docs/essentials/toolbars-and-globals). ## FAQs @@ -40,6 +40,6 @@ The primary difference between the two packages is that `addon-toolbars` makes u - **Standardization**. Args are built into Storybook in 6.x. Since `addon-toolbars` is based on args, you don't need to learn any addon-specific APIs to use it. -- **Ergonomics**. Global args are easy to consume [in stories](https://storybook.js.org/docs/react/essentials/toolbars-and-globals#consuming-globals-from-within-a-story), in [Storybook Docs](https://github.com/storybookjs/storybook/tree/next/code/addons/docs), or even in other addons. +- **Ergonomics**. Global args are easy to consume [in stories](https://storybook.js.org/docs/essentials/toolbars-and-globals#consuming-globals-from-within-a-story), in [Storybook Docs](https://github.com/storybookjs/storybook/tree/next/code/addons/docs), or even in other addons. * **Framework compatibility**. Args are completely framework-independent, so `addon-toolbars` is compatible with React, Vue 3, Angular, etc. out of the box with no framework logic needed in the addon. diff --git a/code/addons/viewport/README.md b/code/addons/viewport/README.md index 7975688745fd..6444f180db9f 100644 --- a/code/addons/viewport/README.md +++ b/code/addons/viewport/README.md @@ -26,4 +26,4 @@ You should now be able to see the viewport addon icon in the toolbar at the top ## Usage -The usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/viewport). +The usage is documented in the [documentation](https://storybook.js.org/docs/essentials/viewport). diff --git a/code/core/package.json b/code/core/package.json index a68f7f8a0d13..fe494dcc815f 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -340,7 +340,6 @@ "boxen": "^7.1.1", "browser-dtector": "^3.4.0", "camelcase": "^8.0.0", - "chai": "^4.4.1", "cli-table3": "^0.6.1", "commander": "^12.1.0", "comment-parser": "^1.4.1", diff --git a/code/core/src/cli/helpers.test.ts b/code/core/src/cli/helpers.test.ts index 4615f0cfdd84..03e85ccc941d 100644 --- a/code/core/src/cli/helpers.test.ts +++ b/code/core/src/cli/helpers.test.ts @@ -75,6 +75,47 @@ describe('Helpers', () => { vi.clearAllMocks(); }); + describe('getVersionSafe', () => { + describe('installed', () => { + it.each([ + ['3.0.0', '3.0.0'], + ['5.0.0-next.0', '5.0.0-next.0'], + [ + '4.2.19::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsvelte%2F-%2Fsvelte-4.2.19.tgz', + '4.2.19', + ], + ])('svelte %s => %s', async (svelteVersion, expectedAddonSpecifier) => { + const packageManager = { + getInstalledVersion: async (pkg: string) => + pkg === 'svelte' ? svelteVersion : undefined, + getAllDependencies: async () => ({ svelte: `^${svelteVersion}` }), + } as any as JsPackageManager; + await expect(helpers.getVersionSafe(packageManager, 'svelte')).resolves.toBe( + expectedAddonSpecifier + ); + }); + }); + + describe('uninstalled', () => { + it.each([ + ['^3', '3.0.0'], + ['^5.0.0-next.0', '5.0.0-next.0'], + [ + '4.2.19::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsvelte%2F-%2Fsvelte-4.2.19.tgz', + '4.2.19', + ], + ])('svelte %s => %s', async (svelteSpecifier, expectedAddonSpecifier) => { + const packageManager = { + getInstalledVersion: async (pkg: string) => undefined, + getAllDependencies: async () => ({ svelte: svelteSpecifier }), + } as any as JsPackageManager; + await expect(helpers.getVersionSafe(packageManager, 'svelte')).resolves.toBe( + expectedAddonSpecifier + ); + }); + }); + }); + describe('copyTemplate', () => { it(`should copy template files when directory is present`, () => { const csfDirectory = /template-csf\/$/; diff --git a/code/core/src/cli/helpers.ts b/code/core/src/cli/helpers.ts index 38cf6151294f..47df29aba89e 100644 --- a/code/core/src/cli/helpers.ts +++ b/code/core/src/cli/helpers.ts @@ -13,7 +13,7 @@ import type { SupportedFrameworks, SupportedRenderers } from '@storybook/core/ty import { findUpSync } from 'find-up'; import picocolors from 'picocolors'; -import { coerce, satisfies } from 'semver'; +import { coerce, major, satisfies } from 'semver'; import stripJsonComments from 'strip-json-comments'; import invariant from 'tiny-invariant'; @@ -173,6 +173,26 @@ export const frameworkToDefaultBuilder: Record< 'vue3-rsbuild': CommunityBuilder.Rsbuild, }; +/** + * Return the installed version of a package, or the coerced version specifier from package.json if + * it's a dependency but not installed (e.g. in a fresh project) + */ +export async function getVersionSafe(packageManager: JsPackageManager, packageName: string) { + try { + let version = await packageManager.getInstalledVersion(packageName); + if (!version) { + const deps = await packageManager.getAllDependencies(); + const versionSpecifier = deps[packageName]; + version = versionSpecifier ?? ''; + } + const coerced = coerce(version, { includePrerelease: true }); + return coerced?.toString(); + } catch (err) { + // fall back to no version + } + return undefined; +} + export async function copyTemplateFiles({ packageManager, renderer, @@ -180,13 +200,26 @@ export async function copyTemplateFiles({ destination, commonAssetsDir, }: CopyTemplateFilesOptions) { - const languageFolderMapping: Record = { + let languageFolderMapping: Record = { // keeping this for backwards compatibility in case community packages are using it typescript: 'ts', [SupportedLanguage.JAVASCRIPT]: 'js', [SupportedLanguage.TYPESCRIPT_3_8]: 'ts-3-8', [SupportedLanguage.TYPESCRIPT_4_9]: 'ts-4-9', }; + // FIXME: remove after 9.0 + if (renderer === 'svelte') { + const svelteVersion = await getVersionSafe(packageManager, 'svelte'); + if (svelteVersion && major(svelteVersion) >= 5) { + languageFolderMapping = { + // keeping this for backwards compatibility in case community packages are using it + typescript: 'ts', + [SupportedLanguage.JAVASCRIPT]: 'svelte-5-js', + [SupportedLanguage.TYPESCRIPT_3_8]: 'svelte-5-ts-3-8', + [SupportedLanguage.TYPESCRIPT_4_9]: 'svelte-5-ts-4-9', + }; + } + } const templatePath = async () => { const baseDir = await getRendererDir(packageManager, renderer); const assetsDir = join(baseDir, 'template', 'cli'); diff --git a/code/core/src/core-server/build-static.ts b/code/core/src/core-server/build-static.ts index 8d8d5f9cce60..ab0d979acaf5 100644 --- a/code/core/src/core-server/build-static.ts +++ b/code/core/src/core-server/build-static.ts @@ -47,22 +47,8 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption if (options.outputDir === '/') { throw new Error("Won't remove directory '/'. Check your outputDir!"); } - - try { - const outputDirFiles = await readdir(options.outputDir); - for (const file of outputDirFiles) { - await rm(file, { recursive: true, force: true }); - } - } catch { - await mkdir(options.outputDir, { recursive: true }); - } - - if (!existsSync(options.outputDir)) { - await mkdir(options.outputDir, { recursive: true }); - } else if ((await readdir(options.outputDir)).length > 0) { - await rm(options.outputDir, { recursive: true, force: true }); - await mkdir(options.outputDir, { recursive: true }); - } + await rm(options.outputDir, { recursive: true, force: true }).catch(() => {}); + await mkdir(options.outputDir, { recursive: true }); const config = await loadMainConfig(options); const { framework } = config; diff --git a/code/core/src/core-server/presets/common-manager.ts b/code/core/src/core-server/presets/common-manager.ts index 07b11bbaa501..0bdf7cde0031 100644 --- a/code/core/src/core-server/presets/common-manager.ts +++ b/code/core/src/core-server/presets/common-manager.ts @@ -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 ); }); }); diff --git a/code/core/src/csf-tools/ConfigFile.test.ts b/code/core/src/csf-tools/ConfigFile.test.ts index fe841c49f488..e3409d5eb681 100644 --- a/code/core/src/csf-tools/ConfigFile.test.ts +++ b/code/core/src/csf-tools/ConfigFile.test.ts @@ -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); + }); + }); }); diff --git a/code/core/src/csf-tools/ConfigFile.ts b/code/core/src/csf-tools/ConfigFile.ts index 5cb0d28234eb..dc9f973d5ad6 100644 --- a/code/core/src/csf-tools/ConfigFile.ts +++ b/code/core/src/csf-tools/ConfigFile.ts @@ -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 { diff --git a/code/core/src/manager-api/modules/url.ts b/code/core/src/manager-api/modules/url.ts index 40971e9dea42..1b08ba7358d4 100644 --- a/code/core/src/manager-api/modules/url.ts +++ b/code/core/src/manager-api/modules/url.ts @@ -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 = (moduleArgs) => { @@ -206,10 +207,10 @@ export const init: ModuleFn = (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) { diff --git a/code/core/src/manager/components/sidebar/RefIndicator.tsx b/code/core/src/manager/components/sidebar/RefIndicator.tsx index 7c49c95105cd..e34f28d744d6 100644 --- a/code/core/src/manager/components/sidebar/RefIndicator.tsx +++ b/code/core/src/manager/components/sidebar/RefIndicator.tsx @@ -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( ({ - icon: href === ref.url ? 'check' : undefined, + icon: href === ref.url ? : undefined, id, title: id, href, diff --git a/code/core/src/manager/components/sidebar/Search.stories.tsx b/code/core/src/manager/components/sidebar/Search.stories.tsx index 5c70863735e0..1638f06b4dea 100644 --- a/code/core/src/manager/components/sidebar/Search.stories.tsx +++ b/code/core/src/manager/components/sidebar/Search.stories.tsx @@ -47,11 +47,7 @@ const baseProps = { export const Simple: StoryFn = () => {() => null}; -export const SimpleWithCreateButton: StoryFn = () => ( - - {() => null} - -); +export const SimpleWithCreateButton: StoryFn = () => {() => null}; export const FilledIn: StoryFn = () => ( diff --git a/code/core/src/manager/components/sidebar/Search.tsx b/code/core/src/manager/components/sidebar/Search.tsx index d09863a28aab..16a7ba0f151e 100644 --- a/code/core/src/manager/components/sidebar/Search.tsx +++ b/code/core/src/manager/components/sidebar/Search.tsx @@ -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(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((acc, [refId, { index, status }]) => { @@ -406,9 +390,9 @@ export const Search = React.memo<{ {...getRootProps({ refKey: '' }, { suppressRefError: true })} className="search-field" > - + - + {!isMobile && enableShortcuts && !isOpen && ( @@ -421,34 +405,16 @@ export const Search = React.memo<{ )} )} - {isOpen && ( - clearSelection()}> - - - )} + + {isOpen && ( + clearSelection()}> + + + )} + {searchFieldContent} + - {showCreateStoryButton && ( - <> - } - > - { - setIsFileSearchModalOpen(true); - }} - variant="outline" - > - - - - - - )} + {searchBarContent} {children({ diff --git a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx index 972606b79598..000a734b042f 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx @@ -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: {}, diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx index e937012c43e6..81e2f0598d2a 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.tsx @@ -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 ( @@ -150,7 +177,35 @@ export const Sidebar = React.memo(function Sidebar({ + } + > + { + setIsFileSearchModalOpen(true); + }} + variant="outline" + > + + + + + + ) + } + searchFieldContent={ + indexJson && ( + + ) + } {...lastViewedProps} > {({ diff --git a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx new file mode 100644 index 000000000000..4050986a91f1 --- /dev/null +++ b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx @@ -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; + +export default meta; + +type Story = StoryObj; + +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, + }, +}; diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx new file mode 100644 index 000000000000..02e71aa741e5 --- /dev/null +++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx @@ -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()); + + 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): 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 ( + ( + + )} + closeOnOutsideClick + > + + + + + {selectedTags.length > 0 && } + + + ); +}; diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx new file mode 100644 index 000000000000..999c5f3fdb04 --- /dev/null +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx @@ -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; + +export default meta; + +type Story = StoryObj; + +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'], + }, +}; diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx new file mode 100644 index 000000000000..45f1b5c85aaf --- /dev/null +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx @@ -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: , + 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: , + href: docsUrl, + style: { + borderTop: `4px solid ${theme.appBorderColor}`, + }, + }); + } + + return ( + + + + ); +}; diff --git a/code/core/src/manager/container/Sidebar.tsx b/code/core/src/manager/container/Sidebar.tsx index bd405706068a..bc05d1713b59 100755 --- a/code/core/src/manager/container/Sidebar.tsx +++ b/code/core/src/manager/container/Sidebar.tsx @@ -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, diff --git a/code/core/src/preview-api/modules/store/inferArgTypes.ts b/code/core/src/preview-api/modules/store/inferArgTypes.ts index 007b4971a173..35e840fedcd8 100644 --- a/code/core/src/preview-api/modules/store/inferArgTypes.ts +++ b/code/core/src/preview-api/modules/store/inferArgTypes.ts @@ -25,8 +25,8 @@ const inferType = (value: any, name: string, visited: Set): SBType => { We've detected a cycle in arg '${name}'. Args should be JSON-serializable. Consider using the mapping feature or fully custom args: - - Mapping: https://storybook.js.org/docs/react/writing-stories/args#mapping-to-complex-arg-values - - Custom args: https://storybook.js.org/docs/react/essentials/controls#fully-custom-args + - Mapping: https://storybook.js.org/docs/writing-stories/args#mapping-to-complex-arg-values + - Custom args: https://storybook.js.org/docs/essentials/controls#fully-custom-args `); return { name: 'other', value: 'cyclic object' }; } diff --git a/code/frameworks/svelte-vite/package.json b/code/frameworks/svelte-vite/package.json index 0fa571b8cb0f..bc84fb925c87 100644 --- a/code/frameworks/svelte-vite/package.json +++ b/code/frameworks/svelte-vite/package.json @@ -57,7 +57,7 @@ "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.1", "@types/node": "^22.0.0", - "svelte": "^5.0.0-next.65", + "svelte": "^5.0.0-next.268", "typescript": "^5.3.2", "vite": "^4.0.0" }, diff --git a/code/lib/blocks/src/blocks/Canvas.stories.tsx b/code/lib/blocks/src/blocks/Canvas.stories.tsx index 4a7e25f6706d..987e28678a71 100644 --- a/code/lib/blocks/src/blocks/Canvas.stories.tsx +++ b/code/lib/blocks/src/blocks/Canvas.stories.tsx @@ -93,10 +93,7 @@ export const PropAdditionalActions: Story = { { title: 'Go to documentation', onClick: () => { - window.open( - 'https://storybook.js.org/docs/react/essentials/controls#annotation', - '_blank' - ); + window.open('https://storybook.js.org/docs/essentials/controls#annotation', '_blank'); }, }, ], diff --git a/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx b/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx index 36e75aa81d36..064a115dd410 100644 --- a/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx +++ b/code/lib/blocks/src/components/ArgsTable/ArgControl.tsx @@ -71,11 +71,7 @@ export const ArgControl: FC = ({ row, arg, updateArgs, isHovere if (!control || control.disable) { const canBeSetup = control?.disable !== true && row?.type?.name !== 'function'; return isHovered && canBeSetup ? ( - + Setup controls ) : ( diff --git a/code/lib/blocks/src/examples/CanvasParameters.stories.tsx b/code/lib/blocks/src/examples/CanvasParameters.stories.tsx index aa9affefbfec..4d4433747494 100644 --- a/code/lib/blocks/src/examples/CanvasParameters.stories.tsx +++ b/code/lib/blocks/src/examples/CanvasParameters.stories.tsx @@ -30,10 +30,7 @@ export const AdditionalActions: Story = { { title: 'Go to documentation', onClick: () => { - window.open( - 'https://storybook.js.org/docs/react/essentials/controls#annotation', - '_blank' - ); + window.open('https://storybook.js.org/docs/essentials/controls#annotation', '_blank'); }, }, ], diff --git a/code/lib/create-storybook/src/generators/SVELTE/index.ts b/code/lib/create-storybook/src/generators/SVELTE/index.ts index 21661f5b02b9..9d00099437fb 100644 --- a/code/lib/create-storybook/src/generators/SVELTE/index.ts +++ b/code/lib/create-storybook/src/generators/SVELTE/index.ts @@ -1,33 +1,21 @@ +import { getVersionSafe } from 'storybook/internal/cli'; import type { JsPackageManager } from 'storybook/internal/common'; -import { coerce, major } from 'semver'; +import { major } from 'semver'; import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; -const versionHelper = (svelteMajor?: number) => { - if (svelteMajor === 4) { - return '4'; - } - // TODO: update when addon-svelte-csf v5 is released - if (svelteMajor === 5) { - return '^5.0.0-next.0'; - } - return ''; -}; - export const getAddonSvelteCsfVersion = async (packageManager: JsPackageManager) => { - const svelteVersion = await packageManager.getInstalledVersion('svelte'); + const svelteVersion = await getVersionSafe(packageManager, 'svelte'); try { - if (svelteVersion) { - return versionHelper(major(coerce(svelteVersion) || '')); - } else { - const deps = await packageManager.getAllDependencies(); - const svelteSpecifier = deps['svelte']; - const coerced = coerce(svelteSpecifier); - if (coerced?.version) { - return versionHelper(major(coerced.version)); - } + const svelteMajor = major(svelteVersion ?? ''); + if (svelteMajor === 4) { + return '4'; + } + // TODO: update when addon-svelte-csf v5 is released + if (svelteMajor === 5) { + return '^5.0.0-next.0'; } } catch { // fallback to latest version diff --git a/code/lib/instrumenter/package.json b/code/lib/instrumenter/package.json index 04d6624131ca..5cb7091d63cc 100644 --- a/code/lib/instrumenter/package.json +++ b/code/lib/instrumenter/package.json @@ -44,8 +44,7 @@ }, "dependencies": { "@storybook/global": "^5.0.0", - "@vitest/utils": "^2.0.5", - "util": "^0.12.4" + "@vitest/utils": "^2.0.5" }, "devDependencies": { "typescript": "^5.3.2" @@ -68,6 +67,9 @@ "@vitest/expect", "@vitest/spy", "@vitest/utils" + ], + "externals": [ + "util" ] }, "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16" diff --git a/code/lib/test/package.json b/code/lib/test/package.json index e144837404fb..a66703d9bdb3 100644 --- a/code/lib/test/package.json +++ b/code/lib/test/package.json @@ -51,8 +51,7 @@ "@testing-library/jest-dom": "6.5.0", "@testing-library/user-event": "14.5.2", "@vitest/expect": "2.0.5", - "@vitest/spy": "2.0.5", - "util": "^0.12.4" + "@vitest/spy": "2.0.5" }, "devDependencies": { "chai": "^5.1.1", @@ -79,6 +78,9 @@ "@vitest/expect", "@vitest/spy", "@vitest/utils" + ], + "externals": [ + "util" ] }, "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16" diff --git a/code/lib/test/src/testing-library.ts b/code/lib/test/src/testing-library.ts index 1aa814dafdce..4ff01e47eacf 100644 --- a/code/lib/test/src/testing-library.ts +++ b/code/lib/test/src/testing-library.ts @@ -28,7 +28,7 @@ testingLibrary.screen = new Proxy(testingLibrary.screen, { get(target, prop, receiver) { once.warn(dedent` You are using Testing Library's \`screen\` object. Use \`within(canvasElement)\` instead. - More info: https://storybook.js.org/docs/react/essentials/interactions + More info: https://storybook.js.org/docs/essentials/interactions `); return Reflect.get(target, prop, receiver); }, diff --git a/code/package.json b/code/package.json index c0aa1f651051..7128ed7202e7 100644 --- a/code/package.json +++ b/code/package.json @@ -215,7 +215,7 @@ "serve-static": "^1.14.1", "slash": "^5.0.0", "storybook": "workspace:^", - "svelte": "^5.0.0-next.65", + "svelte": "^5.0.0-next.268", "ts-dedent": "^2.0.0", "typescript": "^5.4.3", "util": "^0.12.4", @@ -293,5 +293,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.4.0-alpha.8" } diff --git a/code/renderers/svelte/package.json b/code/renderers/svelte/package.json index db0cd01b454b..b5180a46c612 100644 --- a/code/renderers/svelte/package.json +++ b/code/renderers/svelte/package.json @@ -66,10 +66,10 @@ }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.2", - "@testing-library/svelte": "patch:@testing-library/svelte@npm%3A4.1.0#~/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch", + "@testing-library/svelte": "^5.2.3", "expect-type": "^0.15.0", - "svelte": "^5.0.0-next.65", - "svelte-check": "^3.6.4", + "svelte": "^5.0.0-next.268", + "svelte-check": "^4.0.5", "typescript": "^5.3.2" }, "peerDependencies": { diff --git a/code/renderers/svelte/src/__test__/composeStories/Button.stories.ts b/code/renderers/svelte/src/__test__/composeStories/Button.stories.ts index 94558abf996e..92ade46f29a8 100644 --- a/code/renderers/svelte/src/__test__/composeStories/Button.stories.ts +++ b/code/renderers/svelte/src/__test__/composeStories/Button.stories.ts @@ -53,7 +53,11 @@ const getCaptionForLocale = (locale: string) => { } }; -export const CSF2StoryWithLocale: CSF2Story = (args, { globals }) => ({ +// @ts-expect-error -- incompatibility with Svelte 5 types and CSF +export const CSF2StoryWithLocale: CSF2Story = ( + args, + { globals } +) => ({ Component: StoryWithLocaleComponent, props: { ...args, @@ -113,10 +117,11 @@ export const CSF3ButtonWithRender: StoryObj = { }), }; -export const CSF3InputFieldFilled: StoryObj = { - render: () => ({ - Component: InputFilledStoryComponent, - }), +export const CSF3InputFieldFilled: StoryObj = { + render: () => + ({ + Component: InputFilledStoryComponent, + }) as any, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Step label', async () => { @@ -128,10 +133,10 @@ export const CSF3InputFieldFilled: StoryObj = { }; const mockFn = fn(); -export const LoaderStory: StoryObj = { +export const LoaderStory: StoryObj = { args: { mockFn, - }, + } as any, loaders: [ async () => { mockFn.mockReturnValueOnce('mockFn return value'); @@ -140,13 +145,14 @@ export const LoaderStory: StoryObj = { }; }, ], - render: (args, { loaded }) => ({ - Component: LoaderStoryComponent, - props: { - ...args, - loaded, - }, - }), + render: (args, { loaded }) => + ({ + Component: LoaderStoryComponent, + props: { + ...args, + loaded, + }, + }) as any, play: async () => { expect(mockFn).toHaveBeenCalledWith('render'); }, diff --git a/code/renderers/svelte/src/__test__/composeStories/__snapshots__/portable-stories.test.ts.snap b/code/renderers/svelte/src/__test__/composeStories/__snapshots__/portable-stories.test.ts.snap index 6042c29439c9..7141b7af1311 100644 --- a/code/renderers/svelte/src/__test__/composeStories/__snapshots__/portable-stories.test.ts.snap +++ b/code/renderers/svelte/src/__test__/composeStories/__snapshots__/portable-stories.test.ts.snap @@ -3,6 +3,8 @@ exports[`Renders CSF2Secondary story 1`] = `
+ + - - - + +
@@ -22,10 +23,15 @@ exports[`Renders CSF2Secondary story 1`] = ` exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = `
+ + + +
+ - +
- - - - - + + + +
@@ -50,6 +55,8 @@ exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = ` exports[`Renders CSF3Button story 1`] = `
+ + - - - + +
@@ -69,6 +75,8 @@ exports[`Renders CSF3Button story 1`] = ` exports[`Renders CSF3ButtonWithRender story 1`] = `
+ +

- - - + +
@@ -97,14 +104,15 @@ exports[`Renders CSF3ButtonWithRender story 1`] = ` exports[`Renders CSF3InputFieldFilled story 1`] = `
+ + - - - + +
@@ -113,6 +121,8 @@ exports[`Renders CSF3InputFieldFilled story 1`] = ` exports[`Renders CSF3Primary story 1`] = `
+ + - - - + +
@@ -132,6 +141,8 @@ exports[`Renders CSF3Primary story 1`] = ` exports[`Renders LoaderStory story 1`] = `
+ +
- - - + +
@@ -156,10 +166,15 @@ exports[`Renders LoaderStory story 1`] = ` exports[`Renders NewStory story 1`] = `
+ + + +
+ - +
- - - - - + + + +
diff --git a/code/renderers/svelte/template/cli/svelte-5-js/Button.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-js/Button.stories.svelte new file mode 100644 index 000000000000..4c8c7cce632a --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-js/Button.stories.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-js/Button.svelte b/code/renderers/svelte/template/cli/svelte-5-js/Button.svelte new file mode 100644 index 000000000000..b2b820ea4971 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-js/Button.svelte @@ -0,0 +1,26 @@ + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-js/Header.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-js/Header.stories.svelte new file mode 100644 index 000000000000..0130c115acf6 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-js/Header.stories.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-js/Header.svelte b/code/renderers/svelte/template/cli/svelte-5-js/Header.svelte new file mode 100644 index 000000000000..dba3b7880f49 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-js/Header.svelte @@ -0,0 +1,47 @@ + + +
+
+
+ + + + + + + +

Acme

+
+
+ {#if user} + + Welcome, {user.name}! + +
+
+
diff --git a/code/renderers/svelte/template/cli/svelte-5-js/Page.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-js/Page.stories.svelte new file mode 100644 index 000000000000..aa7372c58ef4 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-js/Page.stories.svelte @@ -0,0 +1,30 @@ + + + { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await waitFor(() => expect(loginButton).not.toBeInTheDocument()); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }} + /> + + diff --git a/code/renderers/svelte/template/cli/svelte-5-js/Page.svelte b/code/renderers/svelte/template/cli/svelte-5-js/Page.svelte new file mode 100644 index 000000000000..92a95c00c5c5 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-js/Page.svelte @@ -0,0 +1,70 @@ + + +
+
(user = { name: 'Jane Doe' })} + onLogout={() => (user = null)} + onCreateAccount={() => (user = { name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a + + component-driven + + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page data + in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at + + Storybook tutorials + + . Read more in the + docs + . +

+
+ Tip + Adjust the width of the canvas with the + + + + + + Viewports addon in the toolbar +
+
+
diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.stories.svelte new file mode 100644 index 000000000000..4c8c7cce632a --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.stories.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.svelte new file mode 100644 index 000000000000..b31f5bffe4a5 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Button.svelte @@ -0,0 +1,29 @@ + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.stories.svelte new file mode 100644 index 000000000000..0130c115acf6 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.stories.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.svelte new file mode 100644 index 000000000000..14e890c79e98 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Header.svelte @@ -0,0 +1,45 @@ + + +
+
+
+ + + + + + + +

Acme

+
+
+ {#if user} + + Welcome, {user.name}! + +
+
+
diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.stories.svelte new file mode 100644 index 000000000000..ed850d83718e --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.stories.svelte @@ -0,0 +1,30 @@ + + + { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await waitFor(() => expect(loginButton).not.toBeInTheDocument()); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }} +/> + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.svelte new file mode 100644 index 000000000000..c4c069a5a50b --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-3-8/Page.svelte @@ -0,0 +1,70 @@ + + +
+
(user = { name: 'Jane Doe' })} + onLogout={() => (user = undefined)} + onCreateAccount={() => (user = { name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a + + component-driven + + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page data + in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at + + Storybook tutorials + + . Read more in the + docs + . +

+
+ Tip + Adjust the width of the canvas with the + + + + + + Viewports addon in the toolbar +
+
+
diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.stories.svelte new file mode 100644 index 000000000000..4c8c7cce632a --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.stories.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.svelte new file mode 100644 index 000000000000..b31f5bffe4a5 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Button.svelte @@ -0,0 +1,29 @@ + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.stories.svelte new file mode 100644 index 000000000000..0130c115acf6 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.stories.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.svelte new file mode 100644 index 000000000000..14e890c79e98 --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Header.svelte @@ -0,0 +1,45 @@ + + +
+
+
+ + + + + + + +

Acme

+
+
+ {#if user} + + Welcome, {user.name}! + +
+
+
diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.stories.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.stories.svelte new file mode 100644 index 000000000000..ed850d83718e --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.stories.svelte @@ -0,0 +1,30 @@ + + + { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await waitFor(() => expect(loginButton).not.toBeInTheDocument()); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }} +/> + + diff --git a/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.svelte b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.svelte new file mode 100644 index 000000000000..c4c069a5a50b --- /dev/null +++ b/code/renderers/svelte/template/cli/svelte-5-ts-4-9/Page.svelte @@ -0,0 +1,70 @@ + + +
+
(user = { name: 'Jane Doe' })} + onLogout={() => (user = undefined)} + onCreateAccount={() => (user = { name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a + + component-driven + + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page data + in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at + + Storybook tutorials + + . Read more in the + docs + . +

+
+ Tip + Adjust the width of the canvas with the + + + + + + Viewports addon in the toolbar +
+
+
diff --git a/code/renderers/svelte/vitest.config.ts b/code/renderers/svelte/vitest.config.ts index 58fc647e8f30..f80ba91df63f 100644 --- a/code/renderers/svelte/vitest.config.ts +++ b/code/renderers/svelte/vitest.config.ts @@ -5,9 +5,12 @@ import { vitestCommonConfig } from '../../vitest.workspace'; export default defineConfig( mergeConfig(vitestCommonConfig, { plugins: [ - import('@sveltejs/vite-plugin-svelte').then(({ svelte, vitePreprocess }) => - svelte({ preprocess: vitePreprocess() }) - ), + import('@sveltejs/vite-plugin-svelte').then(({ svelte }) => svelte()), + // @ts-expect-error -- types don't match our TS module resolution setting + import('@testing-library/svelte/vite').then(({ svelteTesting }) => svelteTesting()), ], + test: { + environment: 'happy-dom', + }, }) ); diff --git a/code/yarn.lock b/code/yarn.lock index 875c0263c054..61e3e672ce05 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6052,7 +6052,6 @@ __metadata: browser-assert: "npm:^1.2.1" browser-dtector: "npm:^3.4.0" camelcase: "npm:^8.0.0" - chai: "npm:^4.4.1" cli-table3: "npm:^0.6.1" commander: "npm:^12.1.0" comment-parser: "npm:^1.4.1" @@ -6345,7 +6344,6 @@ __metadata: "@storybook/global": "npm:^5.0.0" "@vitest/utils": "npm:^2.0.5" typescript: "npm:^5.3.2" - util: "npm:^0.12.4" peerDependencies: storybook: "workspace:^" languageName: unknown @@ -6910,7 +6908,7 @@ __metadata: serve-static: "npm:^1.14.1" slash: "npm:^5.0.0" storybook: "workspace:^" - svelte: "npm:^5.0.0-next.65" + svelte: "npm:^5.0.0-next.268" ts-dedent: "npm:^2.0.0" typescript: "npm:^5.4.3" util: "npm:^0.12.4" @@ -7000,7 +6998,7 @@ __metadata: "@sveltejs/vite-plugin-svelte": "npm:^3.0.1" "@types/node": "npm:^22.0.0" magic-string: "npm:^0.30.0" - svelte: "npm:^5.0.0-next.65" + svelte: "npm:^5.0.0-next.268" svelte-preprocess: "npm:^5.1.1" sveltedoc-parser: "npm:^4.2.1" ts-dedent: "npm:^2.2.0" @@ -7041,10 +7039,10 @@ __metadata: "@storybook/preview-api": "workspace:^" "@storybook/theming": "workspace:^" "@sveltejs/vite-plugin-svelte": "npm:^3.0.2" - "@testing-library/svelte": "patch:@testing-library/svelte@npm%3A4.1.0#~/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch" + "@testing-library/svelte": "npm:^5.2.3" expect-type: "npm:^0.15.0" - svelte: "npm:^5.0.0-next.65" - svelte-check: "npm:^3.6.4" + svelte: "npm:^5.0.0-next.268" + svelte-check: "npm:^4.0.5" sveltedoc-parser: "npm:^4.2.1" ts-dedent: "npm:^2.0.0" type-fest: "npm:~2.19" @@ -7098,7 +7096,6 @@ __metadata: ts-dedent: "npm:^2.2.0" type-fest: "npm:~2.19" typescript: "npm:^5.3.2" - util: "npm:^0.12.4" peerDependencies: storybook: "workspace:^" languageName: unknown @@ -7323,7 +7320,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:10.4.0, @testing-library/dom@npm:^10.4.0": +"@testing-library/dom@npm:10.4.0, @testing-library/dom@npm:^10.0.0, @testing-library/dom@npm:^10.4.0": version: 10.4.0 resolution: "@testing-library/dom@npm:10.4.0" dependencies: @@ -7339,7 +7336,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:^9.0.0, @testing-library/dom@npm:^9.3.1, @testing-library/dom@npm:^9.3.3": +"@testing-library/dom@npm:^9.0.0, @testing-library/dom@npm:^9.3.3": version: 9.3.4 resolution: "@testing-library/dom@npm:9.3.4" dependencies: @@ -7404,25 +7401,21 @@ __metadata: languageName: node linkType: hard -"@testing-library/svelte@npm:4.1.0": - version: 4.1.0 - resolution: "@testing-library/svelte@npm:4.1.0" +"@testing-library/svelte@npm:^5.2.3": + version: 5.2.3 + resolution: "@testing-library/svelte@npm:5.2.3" dependencies: - "@testing-library/dom": "npm:^9.3.1" + "@testing-library/dom": "npm:^10.0.0" peerDependencies: - svelte: ^3 || ^4 - checksum: 10c0/4335d8be01bd1e6475062be218577fa1d1b24e9dc97c33db523c5af6b044b97625f65a58bb5f73225064c5a2bf9ae9696948a2bcd2d82c1c25423014d635dce2 - languageName: node - linkType: hard - -"@testing-library/svelte@patch:@testing-library/svelte@npm%3A4.1.0#~/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch": - version: 4.1.0 - resolution: "@testing-library/svelte@patch:@testing-library/svelte@npm%3A4.1.0#~/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch::version=4.1.0&hash=490043" - dependencies: - "@testing-library/dom": "npm:^9.3.1" - peerDependencies: - svelte: ^3 || ^4 - checksum: 10c0/95586fa05bb536fb538d01731a705121d71797a77ab7a8e1f255909e50dfe4fa09f3a6678a60b8a075332dd45940c0fa37d002d2f6c201400295fa3840c88821 + svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 + vite: "*" + vitest: "*" + peerDependenciesMeta: + vite: + optional: true + vitest: + optional: true + checksum: 10c0/a83d662ee7a0ce901598bd985b8d6afde72c7aa37f22447078bd38c7ec9df6b3fb15464d3f171726479a65f0e562732526686b6a33d6b2c7fd34edb6e7b706a9 languageName: node linkType: hard @@ -9582,7 +9575,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.0, acorn@npm:^8.10.0, acorn@npm:^8.11.2, acorn@npm:^8.11.3, acorn@npm:^8.12.1, acorn@npm:^8.4.1, acorn@npm:^8.6.0, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": +"acorn@npm:^8.0.0, acorn@npm:^8.10.0, acorn@npm:^8.11.2, acorn@npm:^8.12.1, acorn@npm:^8.4.1, acorn@npm:^8.6.0, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": version: 8.12.1 resolution: "acorn@npm:8.12.1" bin: @@ -9911,6 +9904,13 @@ __metadata: languageName: node linkType: hard +"aria-query@npm:^5.3.1": + version: 5.3.2 + resolution: "aria-query@npm:5.3.2" + checksum: 10c0/003c7e3e2cff5540bf7a7893775fc614de82b0c5dde8ae823d47b7a28a9d4da1f7ed85f340bdb93d5649caa927755f0e31ecc7ab63edfdfc00c8ef07e505e03e + languageName: node + linkType: hard + "arr-diff@npm:^4.0.0": version: 4.0.0 resolution: "arr-diff@npm:4.0.0" @@ -10115,13 +10115,6 @@ __metadata: languageName: node linkType: hard -"assertion-error@npm:^1.1.0": - version: 1.1.0 - resolution: "assertion-error@npm:1.1.0" - checksum: 10c0/25456b2aa333250f01143968e02e4884a34588a8538fbbf65c91a637f1dbfb8069249133cd2f4e530f10f624d206a664e7df30207830b659e9f5298b00a4099b - languageName: node - linkType: hard - "assertion-error@npm:^2.0.1": version: 2.0.1 resolution: "assertion-error@npm:2.0.1" @@ -10313,12 +10306,10 @@ __metadata: languageName: node linkType: hard -"axobject-query@npm:^4.0.0": - version: 4.0.0 - resolution: "axobject-query@npm:4.0.0" - dependencies: - dequal: "npm:^2.0.3" - checksum: 10c0/4d756b5c2ff099f1c7f99e55a5de9b2066cb2a13a3170185ff34bfec2d7bcab81eb820a4e7340d35c251341b61ebee6e705b7ce64db78224df1df5a4d68448fe +"axobject-query@npm:^4.1.0": + version: 4.1.0 + resolution: "axobject-query@npm:4.1.0" + checksum: 10c0/c470e4f95008f232eadd755b018cb55f16c03ccf39c027b941cd8820ac6b68707ce5d7368a46756db4256fbc91bb4ead368f84f7fb034b2b7932f082f6dc0775 languageName: node linkType: hard @@ -11422,21 +11413,6 @@ __metadata: languageName: node linkType: hard -"chai@npm:^4.4.1": - version: 4.5.0 - resolution: "chai@npm:4.5.0" - dependencies: - assertion-error: "npm:^1.1.0" - check-error: "npm:^1.0.3" - deep-eql: "npm:^4.1.3" - get-func-name: "npm:^2.0.2" - loupe: "npm:^2.3.6" - pathval: "npm:^1.1.1" - type-detect: "npm:^4.1.0" - checksum: 10c0/b8cb596bd1aece1aec659e41a6e479290c7d9bee5b3ad63d2898ad230064e5b47889a3bc367b20100a0853b62e026e2dc514acf25a3c9385f936aa3614d4ab4d - languageName: node - linkType: hard - "chai@npm:^5.1.1": version: 5.1.1 resolution: "chai@npm:5.1.1" @@ -11553,15 +11529,6 @@ __metadata: languageName: node linkType: hard -"check-error@npm:^1.0.3": - version: 1.0.3 - resolution: "check-error@npm:1.0.3" - dependencies: - get-func-name: "npm:^2.0.2" - checksum: 10c0/94aa37a7315c0e8a83d0112b5bfb5a8624f7f0f81057c73e4707729cdd8077166c6aefb3d8e2b92c63ee130d4a2ff94bad46d547e12f3238cc1d78342a973841 - languageName: node - linkType: hard - "check-error@npm:^2.1.1": version: 2.1.1 resolution: "check-error@npm:2.1.1" @@ -11576,7 +11543,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.0.0, chokidar@npm:^3.4.1, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": +"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.0.0, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -11595,6 +11562,15 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^4.0.1": + version: 4.0.1 + resolution: "chokidar@npm:4.0.1" + dependencies: + readdirp: "npm:^4.0.1" + checksum: 10c0/4bb7a3adc304059810bb6c420c43261a15bb44f610d77c35547addc84faa0374265c3adc67f25d06f363d9a4571962b02679268c40de07676d260de1986efea9 + languageName: node + linkType: hard + "chownr@npm:^1.1.1": version: 1.1.4 resolution: "chownr@npm:1.1.4" @@ -12832,15 +12808,6 @@ __metadata: languageName: node linkType: hard -"deep-eql@npm:^4.1.3": - version: 4.1.3 - resolution: "deep-eql@npm:4.1.3" - dependencies: - type-detect: "npm:^4.0.0" - checksum: 10c0/ff34e8605d8253e1bf9fe48056e02c6f347b81d9b5df1c6650a1b0f6f847b4a86453b16dc226b34f853ef14b626e85d04e081b022e20b00cd7d54f079ce9bbdd - languageName: node - linkType: hard - "deep-eql@npm:^5.0.1": version: 5.0.2 resolution: "deep-eql@npm:5.0.2" @@ -14702,13 +14669,13 @@ __metadata: languageName: node linkType: hard -"esrap@npm:^1.2.1": - version: 1.2.1 - resolution: "esrap@npm:1.2.1" +"esrap@npm:^1.2.2": + version: 1.2.2 + resolution: "esrap@npm:1.2.2" dependencies: "@jridgewell/sourcemap-codec": "npm:^1.4.15" "@types/estree": "npm:^1.0.1" - checksum: 10c0/28d6e36adcf4342a844a938a736132269c33e9db6bbefc98c6af5ed06c14899afcc85391e7ce4824ce5066877fa10b0ed5c5007592cbc58012be95f13c66467f + checksum: 10c0/a3a0b665c034f604a162b910346309c64c42635c5d2e8704a27afcdf4e6d4c529e05475d1875d6b3e0d550f8470986116414097230ab3a7c565b85091ca5e177 languageName: node linkType: hard @@ -15104,7 +15071,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:3.3.2, fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.7, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.1, fast-glob@npm:^3.3.2": +"fast-glob@npm:3.3.2, fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.1, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -15212,6 +15179,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.2.0": + version: 6.4.2 + resolution: "fdir@npm:6.4.2" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/34829886f34a3ca4170eca7c7180ec4de51a3abb4d380344063c0ae2e289b11d2ba8b724afee974598c83027fea363ff598caf2b51bc4e6b1e0d8b80cc530573 + languageName: node + linkType: hard + "fetch-retry@npm:^6.0.0": version: 6.0.0 resolution: "fetch-retry@npm:6.0.0" @@ -15988,7 +15967,7 @@ __metadata: languageName: node linkType: hard -"get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": +"get-func-name@npm:^2.0.1": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" checksum: 10c0/89830fd07623fa73429a711b9daecdb304386d237c71268007f788f113505ef1d4cc2d0b9680e072c5082490aec9df5d7758bf5ac6f1c37062855e8e3dc0b9df @@ -19255,15 +19234,6 @@ __metadata: languageName: node linkType: hard -"loupe@npm:^2.3.6": - version: 2.3.7 - resolution: "loupe@npm:2.3.7" - dependencies: - get-func-name: "npm:^2.0.1" - checksum: 10c0/71a781c8fc21527b99ed1062043f1f2bb30bdaf54fa4cf92463427e1718bc6567af2988300bc243c1f276e4f0876f29e3cbf7b58106fdc186915687456ce5bf4 - languageName: node - linkType: hard - "loupe@npm:^3.1.0, loupe@npm:^3.1.1": version: 3.1.1 resolution: "loupe@npm:3.1.1" @@ -22519,13 +22489,6 @@ __metadata: languageName: node linkType: hard -"pathval@npm:^1.1.1": - version: 1.1.1 - resolution: "pathval@npm:1.1.1" - checksum: 10c0/f63e1bc1b33593cdf094ed6ff5c49c1c0dc5dc20a646ca9725cc7fe7cd9995002d51d5685b9b2ec6814342935748b711bafa840f84c0bb04e38ff40a335c94dc - languageName: node - linkType: hard - "pathval@npm:^2.0.0": version: 2.0.0 resolution: "pathval@npm:2.0.0" @@ -24104,6 +24067,13 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:^4.0.1": + version: 4.0.2 + resolution: "readdirp@npm:4.0.2" + checksum: 10c0/a16ecd8ef3286dcd90648c3b103e3826db2b766cdb4a988752c43a83f683d01c7059158d623cbcd8bdfb39e65d302d285be2d208e7d9f34d022d912b929217dd + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -26612,23 +26582,21 @@ __metadata: languageName: node linkType: hard -"svelte-check@npm:^3.6.4": - version: 3.6.4 - resolution: "svelte-check@npm:3.6.4" +"svelte-check@npm:^4.0.5": + version: 4.0.5 + resolution: "svelte-check@npm:4.0.5" dependencies: - "@jridgewell/trace-mapping": "npm:^0.3.17" - chokidar: "npm:^3.4.1" - fast-glob: "npm:^3.2.7" - import-fresh: "npm:^3.2.1" + "@jridgewell/trace-mapping": "npm:^0.3.25" + chokidar: "npm:^4.0.1" + fdir: "npm:^6.2.0" picocolors: "npm:^1.0.0" sade: "npm:^1.7.4" - svelte-preprocess: "npm:^5.1.0" - typescript: "npm:^5.0.3" peerDependencies: - svelte: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ">=5.0.0" bin: svelte-check: bin/svelte-check - checksum: 10c0/acbcc04c8c6ab7baee7ccf36ca134dcabe49fae103aa92661e7f80e01216623363fb794fec9a3f794f7003d55629373567ff485925dc33272f48cea63e7b2452 + checksum: 10c0/192ee83f83169408b5f0b819440349f53e256db868d59fdd2422e831ef581891f5f257632dd3e632b12518ca307e1f99ff97455f56c19c3d2a5ee7be6391a181 languageName: node linkType: hard @@ -26670,7 +26638,7 @@ __metadata: languageName: node linkType: hard -"svelte-preprocess@npm:^5.1.0, svelte-preprocess@npm:^5.1.1": +"svelte-preprocess@npm:^5.1.1": version: 5.1.1 resolution: "svelte-preprocess@npm:5.1.1" dependencies: @@ -26737,24 +26705,24 @@ __metadata: languageName: node linkType: hard -"svelte@npm:^5.0.0-next.65": - version: 5.0.0-next.65 - resolution: "svelte@npm:5.0.0-next.65" +"svelte@npm:^5.0.0-next.268": + version: 5.0.0-next.268 + resolution: "svelte@npm:5.0.0-next.268" dependencies: - "@ampproject/remapping": "npm:^2.2.1" - "@jridgewell/sourcemap-codec": "npm:^1.4.15" + "@ampproject/remapping": "npm:^2.3.0" + "@jridgewell/sourcemap-codec": "npm:^1.5.0" "@types/estree": "npm:^1.0.5" - acorn: "npm:^8.11.3" + acorn: "npm:^8.12.1" acorn-typescript: "npm:^1.4.13" - aria-query: "npm:^5.3.0" - axobject-query: "npm:^4.0.0" + aria-query: "npm:^5.3.1" + axobject-query: "npm:^4.1.0" esm-env: "npm:^1.0.0" - esrap: "npm:^1.2.1" + esrap: "npm:^1.2.2" is-reference: "npm:^3.0.2" locate-character: "npm:^3.0.0" - magic-string: "npm:^0.30.5" + magic-string: "npm:^0.30.11" zimmerframe: "npm:^1.1.2" - checksum: 10c0/6a686847f887d2871eabce4888916cba6aec5bae924a76fd01f4098db1c0053d4e5d6434070d0a048eac75eaddd4fd40e3fae625a0253464f7baa6b0f147f209 + checksum: 10c0/74a954cffe2a70259a1d1d2a834e9615d3f393429ac8cc1e15bfdc66b8bbe5dc449a8289370631b29023bca51aa451d1906f570b3761de4c235ea731913ee1b2 languageName: node linkType: hard @@ -27442,13 +27410,6 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:^4.0.0, type-detect@npm:^4.1.0": - version: 4.1.0 - resolution: "type-detect@npm:4.1.0" - checksum: 10c0/df8157ca3f5d311edc22885abc134e18ff8ffbc93d6a9848af5b682730ca6a5a44499259750197250479c5331a8a75b5537529df5ec410622041650a7f293e2a - languageName: node - linkType: hard - "type-fest@npm:~2.19": version: 2.19.0 resolution: "type-fest@npm:2.19.0" @@ -27542,7 +27503,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.0.3, typescript@npm:^5.3.2, typescript@npm:^5.4.3": +"typescript@npm:^5.3.2, typescript@npm:^5.4.3": version: 5.4.3 resolution: "typescript@npm:5.4.3" bin: @@ -27572,7 +27533,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.0.3#optional!builtin, typescript@patch:typescript@npm%3A^5.3.2#optional!builtin, typescript@patch:typescript@npm%3A^5.4.3#optional!builtin": +"typescript@patch:typescript@npm%3A^5.3.2#optional!builtin, typescript@patch:typescript@npm%3A^5.4.3#optional!builtin": version: 5.4.3 resolution: "typescript@patch:typescript@npm%3A5.4.3#optional!builtin::version=5.4.3&hash=5adc0c" bin: diff --git a/docs/_assets/writing-stories/custom-tag-filter.png b/docs/_assets/writing-stories/custom-tag-filter.png new file mode 100644 index 000000000000..3579f994da12 Binary files /dev/null and b/docs/_assets/writing-stories/custom-tag-filter.png differ diff --git a/docs/versions/next.json b/docs/versions/next.json index b7cf99de3ecf..5419ab744ce7 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.4.0-alpha.7","info":{"plain":"- CLI: Install Svelte CSF v5 in Svelte5 projects - [#29323](https://github.com/storybookjs/storybook/pull/29323), thanks @shilman!\n- Manager: Add tags property to ComponentEntry objects - [#29343](https://github.com/storybookjs/storybook/pull/29343), thanks @Sidnioulz!"}} +{"version":"8.4.0-alpha.8","info":{"plain":"- Addon-Test: Support for `@vitest/browser` v2.1.2 - [#29407](https://github.com/storybookjs/storybook/pull/29407), thanks @strozw!\n- ConfigFile: Fix `export { X }` parsing - [#29344](https://github.com/storybookjs/storybook/pull/29344), thanks @vctqs1!\n- Core: Fix building Storybook deleting project root files - [#29371](https://github.com/storybookjs/storybook/pull/29371), thanks @JReinhold!\n- Interactions: Escape xml of interactions errors - [#29414](https://github.com/storybookjs/storybook/pull/29414), thanks @kasperpeulen!\n- Svelte: Add v5 stories to CLI templates - [#29382](https://github.com/storybookjs/storybook/pull/29382), thanks @JReinhold!\n- Test: Remove unused `util` dependency - [#29310](https://github.com/storybookjs/storybook/pull/29310), thanks @JReinhold!\n- UI: Fix RefIndicator to use CheckIcon instead of string - [#29209](https://github.com/storybookjs/storybook/pull/29209), thanks @JSMike!\n- UI: Simple tag filtering - [#29333](https://github.com/storybookjs/storybook/pull/29333), thanks @shilman!"}} diff --git a/docs/writing-stories/tags.mdx b/docs/writing-stories/tags.mdx index 25459a84ba59..350e168c532d 100644 --- a/docs/writing-stories/tags.mdx +++ b/docs/writing-stories/tags.mdx @@ -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 custom tag](../_assets/writing-stories/custom-tag-filter.png) + +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