diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 3317e750e0c5..a57524a04ebb 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -1,4 +1,5 @@
34e364a0ca1d93555d36a7367d78e8e229493de8
c0896915fb7fb9a8dd416b9aebca17abd909d1c1
a41c227037e7e7249b8b376f838f4f8bcc3e3e59
-13c46e6c0b7f3dd8cf4ba42d1cfd6714f4777d54
\ No newline at end of file
+13c46e6c0b7f3dd8cf4ba42d1cfd6714f4777d54
+0a4522a3f84773f39daec4820c49b8a92e9f9d11
\ No newline at end of file
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
deleted file mode 100644
index 1a1c6b59a4a2..000000000000
--- a/.github/workflows/scorecards.yml
+++ /dev/null
@@ -1,72 +0,0 @@
-# This workflow uses actions that are not certified by GitHub. They are provided
-# by a third-party and are governed by separate terms of service, privacy
-# policy, and support documentation.
-
-name: Scorecards supply-chain security
-on:
- # For Branch-Protection check. Only the default branch is supported. See
- # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
- branch_protection_rule:
- # To guarantee Maintained check is occasionally updated. See
- # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
- schedule:
- - cron: '31 16 * * 5'
- push:
- branches: [ "next" ]
-
-# Declare default permissions as read only.
-permissions: read-all
-
-jobs:
- analysis:
- name: Scorecards analysis
- runs-on: ubuntu-latest
- permissions:
- # Needed to upload the results to code-scanning dashboard.
- security-events: write
- # Needed to publish results and get a badge (see publish_results below).
- id-token: write
- # Uncomment the permissions below if installing in a private repository.
- # contents: read
- # actions: read
-
- steps:
- - name: "Checkout code"
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
- with:
- persist-credentials: false
-
- - name: "Run analysis"
- uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.6
- with:
- results_file: results.sarif
- results_format: sarif
- # (Optional) Read-only PAT token. Uncomment the `repo_token` line below if:
- # - you want to enable the Branch-Protection check on a *public* repository, or
- # - you are installing Scorecards on a *private* repository
- # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
- # repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
-
- # Public repositories:
- # - Publish results to OpenSSF REST API for easy access by consumers
- # - Allows the repository to include the Scorecard badge.
- # - See https://github.com/ossf/scorecard-action#publishing-results.
- # For private repositories:
- # - `publish_results` will always be set to `false`, regardless
- # of the value entered here.
- publish_results: true
-
- # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
- # format to the repository Actions tab.
- - name: "Upload artifact"
- uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0
- with:
- name: SARIF file
- path: results.sarif
- retention-days: 5
-
- # Upload the results to GitHub's code scanning dashboard.
- - name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@807578363a7869ca324a79039e6db9c843e0e100 # v2.1.27
- with:
- sarif_file: results.sarif
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 444fe7ae0bea..bdf5324fe23d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 8.2.7
+
+- CPC: Fix type usage in renderers - [#28745](https://github.com/storybookjs/storybook/pull/28745), thanks @ndelangen!
+- Core: Introduce run over play in portable stories, and revert back play changes of 8.2 - [#28764](https://github.com/storybookjs/storybook/pull/28764), thanks @kasperpeulen!
+
## 8.2.6
- CPC: Fix missing exports for addon-kit - [#28691](https://github.com/storybookjs/storybook/pull/28691), thanks @ndelangen!
diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md
index a5ab53b9e199..6180d4046fb2 100644
--- a/CHANGELOG.prerelease.md
+++ b/CHANGELOG.prerelease.md
@@ -1,3 +1,12 @@
+## 8.3.0-alpha.4
+
+- CSF: Allow overridding globals at the story level - [#26654](https://github.com/storybookjs/storybook/pull/26654), thanks @tmeasday!
+- Core: Introduce run over play in portable stories, and revert back play changes of 8.2 - [#28764](https://github.com/storybookjs/storybook/pull/28764), thanks @kasperpeulen!
+- Core: Split Storybook CLI - [#28519](https://github.com/storybookjs/storybook/pull/28519), thanks @kasperpeulen!
+- Fix: Invariant failed: Expected package.json#version to be defined in the "undefined" package - [#28752](https://github.com/storybookjs/storybook/pull/28752), thanks @abcdmku!
+- Next.js: Make RSC portable-stories compatible - [#28756](https://github.com/storybookjs/storybook/pull/28756), thanks @valentinpalkovic!
+- UI: Fix collapse/expand all functionality - [#28582](https://github.com/storybookjs/storybook/pull/28582), thanks @filipemelo2002!
+
## 8.3.0-alpha.3
- Angular: Fix Angular template error for props with a circular reference - [#28498](https://github.com/storybookjs/storybook/pull/28498), thanks @Marklb!
diff --git a/MIGRATION.md b/MIGRATION.md
index 3dd1311dd620..dcb75d359224 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -1632,7 +1632,7 @@ export const Primary = {
## From version 6.5.x to 7.0.0
-A number of these changes can be made automatically by the Storybook CLI. To take advantage of these "automigrations", run `npx storybook@latest upgrade --prerelease` or `pnpx dlx storybook@latest upgrade --prerelease`.
+A number of these changes can be made automatically by the Storybook CLI. To take advantage of these "automigrations", run `npx storybook@7 upgrade` or `pnpx dlx storybook@7 upgrade`.
### 7.0 breaking changes
diff --git a/code/.storybook/main.ts b/code/.storybook/main.ts
index 7c4f74c8c2ef..6a1f51f62f7c 100644
--- a/code/.storybook/main.ts
+++ b/code/.storybook/main.ts
@@ -68,6 +68,10 @@ const config: StorybookConfig = {
directory: '../addons/toolbars/template/stories',
titlePrefix: 'addons/toolbars',
},
+ {
+ directory: '../addons/themes/template/stories',
+ titlePrefix: 'addons/themes',
+ },
{
directory: '../addons/onboarding/src',
titlePrefix: 'addons/onboarding',
@@ -83,6 +87,7 @@ const config: StorybookConfig = {
],
addons: [
'@storybook/addon-links',
+ '@storybook/addon-themes',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-storysource',
@@ -119,7 +124,6 @@ const config: StorybookConfig = {
},
features: {
viewportStoryGlobals: true,
- themesStoryGlobals: true,
backgroundsStoryGlobals: true,
},
viteFinal: (viteConfig, { configType }) =>
diff --git a/code/.storybook/preview.tsx b/code/.storybook/preview.tsx
index c80bdcea2937..548f29473efc 100644
--- a/code/.storybook/preview.tsx
+++ b/code/.storybook/preview.tsx
@@ -12,16 +12,17 @@ import {
} from 'storybook/internal/theming';
import { useArgs, DocsContext as DocsContextProps } from 'storybook/internal/preview-api';
import type { PreviewWeb } from 'storybook/internal/preview-api';
-import type { ReactRenderer } from '@storybook/react';
+import type { ReactRenderer, Decorator } from '@storybook/react';
import type { Channel } from 'storybook/internal/channels';
import { DocsContext } from '@storybook/blocks';
+import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport';
import { DocsPageWrapper } from '../lib/blocks/src/components';
const { document } = global;
-const ThemeBlock = styled.div<{ side: 'left' | 'right' }>(
+const ThemeBlock = styled.div<{ side: 'left' | 'right'; layout: string }>(
{
position: 'absolute',
top: 0,
@@ -31,8 +32,10 @@ const ThemeBlock = styled.div<{ side: 'left' | 'right' }>(
height: '100vh',
bottom: 0,
overflow: 'auto',
- padding: 10,
},
+ ({ layout }) => ({
+ padding: layout === 'fullscreen' ? 0 : '1rem',
+ }),
({ theme }) => ({
background: theme.background.content,
color: theme.color.defaultText,
@@ -49,14 +52,17 @@ const ThemeBlock = styled.div<{ side: 'left' | 'right' }>(
}
);
-const ThemeStack = styled.div(
+const ThemeStack = styled.div<{ layout: string }>(
{
position: 'relative',
- minHeight: 'calc(50vh - 15px)',
+ flex: 1,
},
({ theme }) => ({
background: theme.background.content,
color: theme.color.defaultText,
+ }),
+ ({ layout }) => ({
+ padding: layout === 'fullscreen' ? 0 : '1rem',
})
);
@@ -80,6 +86,25 @@ const PlayFnNotice = styled.div(
})
);
+const StackContainer = ({ children, layout }) => (
+
+
+ {layout === 'fullscreen' ? null : (
+
+ )}
+ {children}
+
+);
+
const ThemedSetRoot = () => {
const theme = useTheme();
@@ -159,10 +184,20 @@ export const decorators = [
/**
* This decorator renders the stories side-by-side, stacked or default based on the theme switcher in the toolbar
*/
- (StoryFn, { globals, parameters, playFunction, args }) => {
- const defaultTheme =
- isChromatic() && !playFunction && args.autoplay !== true ? 'stacked' : 'light';
- const theme = globals.theme || parameters.theme || defaultTheme;
+ (StoryFn, { globals, playFunction, args, storyGlobals, parameters }) => {
+ let theme = globals.sb_theme;
+ let showPlayFnNotice = false;
+
+ // this makes the decorator be out of 'phase' with the actually selected theme in the toolbar
+ // but this is acceptable, I guess
+ // we need to ensure only a single rendering in chromatic
+ // a more 'correct' approach would be to set a specific theme global on every story that has a playFunction
+ if (playFunction && args.autoplay !== false && !(theme === 'light' || theme === 'dark')) {
+ theme = 'light';
+ showPlayFnNotice = true;
+ } else if (isChromatic() && !storyGlobals.sb_theme && !playFunction) {
+ theme = 'stacked';
+ }
switch (theme) {
case 'side-by-side': {
@@ -172,12 +207,12 @@ export const decorators = [
-
+
-
+
@@ -190,16 +225,18 @@ export const decorators = [
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
);
}
@@ -209,7 +246,7 @@ export const decorators = [
- {!parameters.theme && isChromatic() && playFunction && (
+ {showPlayFnNotice && (
<>
@@ -233,7 +270,7 @@ export const decorators = [
*
* If parameters.withRawArg is not set, this decorator will do nothing
*/
- (StoryFn, { parameters, args, hooks }) => {
+ (StoryFn, { parameters, args }) => {
const [, updateArgs] = useArgs();
if (!parameters.withRawArg) {
return ;
@@ -246,6 +283,7 @@ export const decorators = [
...args,
onChange: (newValue) => {
updateArgs({ [parameters.withRawArg]: newValue });
+ // @ts-expect-error onChange is not a valid arg
args.onChange?.(newValue);
},
}}
@@ -257,7 +295,7 @@ export const decorators = [
>
);
},
-];
+] satisfies Decorator[];
export const parameters = {
options: {
@@ -295,4 +333,22 @@ export const parameters = {
'slategray',
],
},
+ viewport: {
+ options: MINIMAL_VIEWPORTS,
+ },
+ themes: {
+ disable: true,
+ },
+ backgrounds: {
+ options: {
+ light: { name: 'light', value: '#edecec' },
+ dark: { name: 'dark', value: '#262424' },
+ blue: { name: 'blue', value: '#1b1a2c' },
+ },
+ grid: {
+ cellSize: 15,
+ cellAmount: 10,
+ opacity: 0.4,
+ },
+ },
};
diff --git a/code/addons/backgrounds/package.json b/code/addons/backgrounds/package.json
index 7f5ec2166e1d..abde37a1235c 100644
--- a/code/addons/backgrounds/package.json
+++ b/code/addons/backgrounds/package.json
@@ -80,7 +80,7 @@
"./src/manager.tsx"
],
"previewEntries": [
- "./src/preview.tsx"
+ "./src/preview.ts"
]
},
"gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16",
diff --git a/code/addons/backgrounds/src/components/Tool.tsx b/code/addons/backgrounds/src/components/Tool.tsx
new file mode 100644
index 000000000000..e214780714e4
--- /dev/null
+++ b/code/addons/backgrounds/src/components/Tool.tsx
@@ -0,0 +1,145 @@
+import React, { useState, memo, Fragment, useCallback } from 'react';
+
+import { useGlobals, useParameter } from 'storybook/internal/manager-api';
+import { IconButton, WithTooltip, TooltipLinkList } from 'storybook/internal/components';
+
+import { CircleIcon, GridIcon, PhotoIcon, RefreshIcon } from '@storybook/icons';
+import { PARAM_KEY as KEY } from '../constants';
+import type { Background, BackgroundMap, Config, GlobalStateUpdate } from '../types';
+
+type Link = Parameters['0']['links'][0];
+
+const emptyBackgroundMap: BackgroundMap = {};
+
+export const BackgroundTool = memo(function BackgroundSelector() {
+ const config = useParameter(KEY);
+ const [globals, updateGlobals, storyGlobals] = useGlobals();
+ const [isTooltipVisible, setIsTooltipVisible] = useState(false);
+
+ const { options = emptyBackgroundMap, disable = true } = config || {};
+ if (disable) {
+ return null;
+ }
+
+ const data = globals[KEY] || {};
+ const backgroundName: string = data.value;
+ const isGridActive = data.grid || false;
+
+ const item = options[backgroundName];
+ const isLocked = !!storyGlobals?.[KEY];
+ const length = Object.keys(options).length;
+
+ return (
+
+ );
+});
+
+interface PureProps {
+ length: number;
+ backgroundMap: BackgroundMap;
+ item: Background | undefined;
+ updateGlobals: ReturnType['1'];
+ backgroundName: string | undefined;
+ setIsTooltipVisible: React.Dispatch>;
+ isLocked: boolean;
+ isGridActive: boolean;
+ isTooltipVisible: boolean;
+}
+
+const Pure = memo(function PureTool(props: PureProps) {
+ const {
+ item,
+ length,
+ updateGlobals,
+ setIsTooltipVisible,
+ backgroundMap,
+ backgroundName,
+ isLocked,
+ isGridActive: isGrid,
+ isTooltipVisible,
+ } = props;
+
+ const update = useCallback(
+ (input: GlobalStateUpdate) => {
+ updateGlobals({
+ [KEY]: input,
+ });
+ },
+ [updateGlobals]
+ );
+
+ return (
+
+ update({ value: backgroundName, grid: !isGrid })}
+ >
+
+
+
+ {length > 0 ? (
+ {
+ return (
+ ,
+ onClick: () => {
+ update({ value: undefined, grid: isGrid });
+ onHide();
+ },
+ },
+ ]
+ : []),
+ ...Object.entries(backgroundMap).map(([k, value]) => ({
+ id: k,
+ title: value.name,
+ icon: ,
+ active: k === backgroundName,
+ onClick: () => {
+ update({ value: k, grid: isGrid });
+ onHide();
+ },
+ })),
+ ]}
+ />
+ );
+ }}
+ onVisibleChange={setIsTooltipVisible}
+ >
+
+
+
+
+ ) : null}
+
+ );
+});
diff --git a/code/addons/backgrounds/src/decorator.ts b/code/addons/backgrounds/src/decorator.ts
new file mode 100644
index 000000000000..bdee2f2ea19b
--- /dev/null
+++ b/code/addons/backgrounds/src/decorator.ts
@@ -0,0 +1,96 @@
+import { useEffect } from 'storybook/internal/preview-api';
+import type {
+ Renderer,
+ PartialStoryFn as StoryFunction,
+ StoryContext,
+} from 'storybook/internal/types';
+
+import { PARAM_KEY as KEY } from './constants';
+import { clearStyles, addBackgroundStyle, isReduceMotionEnabled, addGridStyle } from './utils';
+import type { Config, GridConfig } from './types';
+
+const defaultGrid: GridConfig = {
+ cellSize: 100,
+ cellAmount: 10,
+ opacity: 0.8,
+};
+
+const BG_SELECTOR_BASE = `addon-backgrounds`;
+const GRID_SELECTOR_BASE = 'addon-backgrounds-grid';
+
+const transitionStyle = isReduceMotionEnabled() ? '' : 'transition: background-color 0.3s;';
+
+export const withBackgroundAndGrid = (
+ StoryFn: StoryFunction,
+ context: StoryContext
+) => {
+ const { globals, parameters, viewMode, id } = context;
+ const { options = {}, disable, grid = defaultGrid } = (parameters[KEY] || {}) as Config;
+ const data = globals[KEY] || {};
+ const backgroundName: string | undefined = data.value;
+
+ const item = backgroundName ? options[backgroundName] : undefined;
+ const value = item?.value || 'transparent';
+
+ const showGrid = data.grid || false;
+ const shownBackground = !!item && !disable;
+
+ const backgroundSelector = viewMode === 'docs' ? `#anchor--${id} .docs-story` : '.sb-show-main';
+ const gridSelector = viewMode === 'docs' ? `#anchor--${id} .docs-story` : '.sb-show-main';
+
+ const isLayoutPadded = parameters.layout === undefined || parameters.layout === 'padded';
+ const defaultOffset = viewMode === 'docs' ? 20 : isLayoutPadded ? 16 : 0;
+ const { cellAmount, cellSize, opacity, offsetX = defaultOffset, offsetY = defaultOffset } = grid;
+
+ const backgroundSelectorId =
+ viewMode === 'docs' ? `${BG_SELECTOR_BASE}-docs-${id}` : `${BG_SELECTOR_BASE}-color`;
+ const backgroundTarget = viewMode === 'docs' ? id : null;
+
+ useEffect(() => {
+ const backgroundStyles = `
+ ${backgroundSelector} {
+ background: ${value} !important;
+ ${transitionStyle}
+ }`;
+
+ if (!shownBackground) {
+ clearStyles(backgroundSelectorId);
+ return;
+ }
+
+ addBackgroundStyle(backgroundSelectorId, backgroundStyles, backgroundTarget);
+ }, [backgroundSelector, backgroundSelectorId, backgroundTarget, shownBackground, value]);
+
+ const gridSelectorId =
+ viewMode === 'docs' ? `${GRID_SELECTOR_BASE}-docs-${id}` : `${GRID_SELECTOR_BASE}`;
+ useEffect(() => {
+ if (!showGrid) {
+ clearStyles(gridSelectorId);
+ return;
+ }
+ const gridSize = [
+ `${cellSize * cellAmount}px ${cellSize * cellAmount}px`,
+ `${cellSize * cellAmount}px ${cellSize * cellAmount}px`,
+ `${cellSize}px ${cellSize}px`,
+ `${cellSize}px ${cellSize}px`,
+ ].join(', ');
+
+ const gridStyles = `
+ ${gridSelector} {
+ background-size: ${gridSize} !important;
+ background-position: ${offsetX}px ${offsetY}px, ${offsetX}px ${offsetY}px, ${offsetX}px ${offsetY}px, ${offsetX}px ${offsetY}px !important;
+ background-blend-mode: difference !important;
+ background-image: linear-gradient(rgba(130, 130, 130, ${opacity}) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(130, 130, 130, ${opacity}) 1px, transparent 1px),
+ linear-gradient(rgba(130, 130, 130, ${opacity / 2}) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(130, 130, 130, ${
+ opacity / 2
+ }) 1px, transparent 1px) !important;
+ }
+ `;
+
+ addGridStyle(gridSelectorId, gridStyles);
+ }, [cellAmount, cellSize, gridSelector, gridSelectorId, showGrid, offsetX, offsetY, opacity]);
+
+ return StoryFn();
+};
diff --git a/code/addons/backgrounds/src/containers/BackgroundSelector.tsx b/code/addons/backgrounds/src/legacy/BackgroundSelectorLegacy.tsx
similarity index 63%
rename from code/addons/backgrounds/src/containers/BackgroundSelector.tsx
rename to code/addons/backgrounds/src/legacy/BackgroundSelectorLegacy.tsx
index 640b3856557a..dcc8ffb8e658 100644
--- a/code/addons/backgrounds/src/containers/BackgroundSelector.tsx
+++ b/code/addons/backgrounds/src/legacy/BackgroundSelectorLegacy.tsx
@@ -1,5 +1,5 @@
-import type { FC } from 'react';
-import React, { useState, Fragment, useCallback, useMemo, memo } from 'react';
+import type { FC, ReactElement } from 'react';
+import React, { useState, useCallback, useMemo, memo } from 'react';
import memoize from 'memoizerific';
import { useParameter, useGlobals } from 'storybook/internal/manager-api';
@@ -8,14 +8,29 @@ import { IconButton, WithTooltip, TooltipLinkList } from 'storybook/internal/com
import { PhotoIcon } from '@storybook/icons';
import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants';
-import { ColorIcon } from '../components/ColorIcon';
-import type {
- BackgroundSelectorItem,
- Background,
- BackgroundsParameter,
- GlobalState,
-} from '../types';
-import { getBackgroundColorByName } from '../helpers';
+import { ColorIcon } from './ColorIcon';
+import type { Background } from '../types';
+import { getBackgroundColorByName } from './getBackgroundColorByName';
+
+export interface DeprecatedGlobalState {
+ name: string | undefined;
+ selected: string | undefined;
+}
+
+export interface BackgroundsParameter {
+ default?: string | null;
+ disable?: boolean;
+ values: Background[];
+}
+
+export interface BackgroundSelectorItem {
+ id: string;
+ title: string;
+ onClick: () => void;
+ value: string;
+ active: boolean;
+ right?: ReactElement;
+}
const createBackgroundSelectorItem = memoize(1000)(
(
@@ -62,7 +77,7 @@ const DEFAULT_BACKGROUNDS_CONFIG: BackgroundsParameter = {
values: [],
};
-export const BackgroundSelector: FC = memo(function BackgroundSelector() {
+export const BackgroundToolLegacy: FC = memo(function BackgroundSelector() {
const backgroundsConfig = useParameter(
BACKGROUNDS_PARAM_KEY,
DEFAULT_BACKGROUNDS_CONFIG
@@ -98,36 +113,34 @@ export const BackgroundSelector: FC = memo(function BackgroundSelector() {
}
return (
-
- {
- return (
- {
- if (selectedBackgroundColor !== selected) {
- onBackgroundChange(selected);
- }
- onHide();
+ {
+ return (
+ {
+ if (selectedBackgroundColor !== selected) {
+ onBackgroundChange(selected);
}
- )}
- />
- );
- }}
- onVisibleChange={setIsTooltipVisible}
+ onHide();
+ }
+ )}
+ />
+ );
+ }}
+ onVisibleChange={setIsTooltipVisible}
+ >
+
-
-
-
-
-
+
+
+
);
});
diff --git a/code/addons/backgrounds/src/components/ColorIcon.tsx b/code/addons/backgrounds/src/legacy/ColorIcon.tsx
similarity index 100%
rename from code/addons/backgrounds/src/components/ColorIcon.tsx
rename to code/addons/backgrounds/src/legacy/ColorIcon.tsx
diff --git a/code/addons/backgrounds/src/containers/GridSelector.tsx b/code/addons/backgrounds/src/legacy/GridSelectorLegacy.tsx
similarity index 93%
rename from code/addons/backgrounds/src/containers/GridSelector.tsx
rename to code/addons/backgrounds/src/legacy/GridSelectorLegacy.tsx
index 796c281c0f43..e061fd4f2311 100644
--- a/code/addons/backgrounds/src/containers/GridSelector.tsx
+++ b/code/addons/backgrounds/src/legacy/GridSelectorLegacy.tsx
@@ -7,7 +7,7 @@ import { IconButton } from 'storybook/internal/components';
import { GridIcon } from '@storybook/icons';
import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants';
-export const GridSelector: FC = memo(function GridSelector() {
+export const GridToolLegacy: FC = memo(function GridSelector() {
const [globals, updateGlobals] = useGlobals();
const { grid } = useParameter(BACKGROUNDS_PARAM_KEY, {
diff --git a/code/addons/backgrounds/src/legacy/getBackgroundColorByName.ts b/code/addons/backgrounds/src/legacy/getBackgroundColorByName.ts
new file mode 100644
index 000000000000..95b5296752c1
--- /dev/null
+++ b/code/addons/backgrounds/src/legacy/getBackgroundColorByName.ts
@@ -0,0 +1,39 @@
+import { dedent } from 'ts-dedent';
+import { logger } from 'storybook/internal/client-logger';
+import type { Background } from '../types';
+
+export const getBackgroundColorByName = (
+ currentSelectedValue: string,
+ backgrounds: Background[] = [],
+ defaultName: string | null | undefined
+): string => {
+ if (currentSelectedValue === 'transparent') {
+ return 'transparent';
+ }
+
+ if (backgrounds.find((background) => background.value === currentSelectedValue)) {
+ return currentSelectedValue;
+ }
+
+ if (currentSelectedValue) {
+ return currentSelectedValue;
+ }
+
+ const defaultBackground = backgrounds.find((background) => background.name === defaultName);
+ if (defaultBackground) {
+ return defaultBackground.value;
+ }
+
+ if (defaultName) {
+ const availableColors = backgrounds.map((background) => background.name).join(', ');
+ logger.warn(
+ dedent`
+ Backgrounds Addon: could not find the default color "${defaultName}".
+ These are the available colors for your story based on your configuration:
+ ${availableColors}.
+ `
+ );
+ }
+
+ return 'transparent';
+};
diff --git a/code/addons/backgrounds/src/decorators/withBackground.ts b/code/addons/backgrounds/src/legacy/withBackgroundLegacy.ts
similarity index 92%
rename from code/addons/backgrounds/src/decorators/withBackground.ts
rename to code/addons/backgrounds/src/legacy/withBackgroundLegacy.ts
index 1cdbf499db84..ba649a0a1d36 100644
--- a/code/addons/backgrounds/src/decorators/withBackground.ts
+++ b/code/addons/backgrounds/src/legacy/withBackgroundLegacy.ts
@@ -6,12 +6,8 @@ import type {
} from 'storybook/internal/types';
import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants';
-import {
- clearStyles,
- addBackgroundStyle,
- getBackgroundColorByName,
- isReduceMotionEnabled,
-} from '../helpers';
+import { clearStyles, addBackgroundStyle, isReduceMotionEnabled } from '../utils';
+import { getBackgroundColorByName } from './getBackgroundColorByName';
export const withBackground = (
StoryFn: StoryFunction,
diff --git a/code/addons/backgrounds/src/decorators/withGrid.ts b/code/addons/backgrounds/src/legacy/withGridLegacy.ts
similarity index 97%
rename from code/addons/backgrounds/src/decorators/withGrid.ts
rename to code/addons/backgrounds/src/legacy/withGridLegacy.ts
index 70cc47cf48bd..801a1962d926 100644
--- a/code/addons/backgrounds/src/decorators/withGrid.ts
+++ b/code/addons/backgrounds/src/legacy/withGridLegacy.ts
@@ -5,7 +5,7 @@ import type {
StoryContext,
} from 'storybook/internal/types';
-import { clearStyles, addGridStyle } from '../helpers';
+import { clearStyles, addGridStyle } from '../utils';
import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants';
export const withGrid = (StoryFn: StoryFunction, context: StoryContext) => {
diff --git a/code/addons/backgrounds/src/manager.tsx b/code/addons/backgrounds/src/manager.tsx
index d6f3b855506d..3f8ab23da58d 100644
--- a/code/addons/backgrounds/src/manager.tsx
+++ b/code/addons/backgrounds/src/manager.tsx
@@ -2,19 +2,23 @@ import React, { Fragment } from 'react';
import { addons, types } from 'storybook/internal/manager-api';
import { ADDON_ID } from './constants';
-import { BackgroundSelector } from './containers/BackgroundSelector';
-import { GridSelector } from './containers/GridSelector';
+import { BackgroundToolLegacy } from './legacy/BackgroundSelectorLegacy';
+import { GridToolLegacy } from './legacy/GridSelectorLegacy';
+import { BackgroundTool } from './components/Tool';
addons.register(ADDON_ID, () => {
addons.add(ADDON_ID, {
title: 'Backgrounds',
type: types.TOOL,
match: ({ viewMode, tabId }) => !!(viewMode && viewMode.match(/^(story|docs)$/)) && !tabId,
- render: () => (
-
-
-
-
- ),
+ render: () =>
+ FEATURES?.backgroundsStoryGlobals ? (
+
+ ) : (
+
+
+
+
+ ),
});
});
diff --git a/code/addons/backgrounds/src/preview.ts b/code/addons/backgrounds/src/preview.ts
new file mode 100644
index 000000000000..dad8dad077bb
--- /dev/null
+++ b/code/addons/backgrounds/src/preview.ts
@@ -0,0 +1,41 @@
+import type { Addon_DecoratorFunction } from 'storybook/internal/types';
+import { withBackground } from './legacy/withBackgroundLegacy';
+import { withGrid } from './legacy/withGridLegacy';
+import { PARAM_KEY as KEY } from './constants';
+import { withBackgroundAndGrid } from './decorator';
+import type { Config, GlobalState } from './types';
+
+export const decorators: Addon_DecoratorFunction[] = FEATURES?.backgroundsStoryGlobals
+ ? [withBackgroundAndGrid]
+ : [withGrid, withBackground];
+
+export const parameters = {
+ [KEY]: {
+ grid: {
+ cellSize: 20,
+ opacity: 0.5,
+ cellAmount: 5,
+ },
+ disable: false,
+ ...(FEATURES?.backgroundsStoryGlobals
+ ? {
+ options: {
+ light: { name: 'light', value: '#F8F8F8' },
+ dark: { name: 'dark', value: '#333' },
+ },
+ }
+ : {
+ // TODO: remove in 9.0
+ values: [
+ { name: 'light', value: '#F8F8F8' },
+ { name: 'dark', value: '#333333' },
+ ],
+ }),
+ } satisfies Partial,
+};
+
+const modern: Record = {
+ [KEY]: { value: undefined, grid: false },
+};
+
+export const initialGlobals = FEATURES?.backgroundsStoryGlobals ? modern : { [KEY]: null };
diff --git a/code/addons/backgrounds/src/preview.tsx b/code/addons/backgrounds/src/preview.tsx
deleted file mode 100644
index b28c5539ecff..000000000000
--- a/code/addons/backgrounds/src/preview.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { Addon_DecoratorFunction } from 'storybook/internal/types';
-import { withBackground } from './decorators/withBackground';
-import { withGrid } from './decorators/withGrid';
-import { PARAM_KEY } from './constants';
-
-export const decorators: Addon_DecoratorFunction[] = [withGrid, withBackground];
-export const parameters = {
- [PARAM_KEY]: {
- grid: {
- cellSize: 20,
- opacity: 0.5,
- cellAmount: 5,
- },
- values: [
- { name: 'light', value: '#F8F8F8' },
- { name: 'dark', value: '#333333' },
- ],
- },
-};
-
-export const initialGlobals = {
- [PARAM_KEY]: null as any,
-};
diff --git a/code/addons/backgrounds/src/types.ts b/code/addons/backgrounds/src/types.ts
new file mode 100644
index 000000000000..8f6c66b20a85
--- /dev/null
+++ b/code/addons/backgrounds/src/types.ts
@@ -0,0 +1,23 @@
+export interface Background {
+ name: string;
+ value: string;
+}
+
+export type BackgroundMap = Record;
+
+export interface GridConfig {
+ cellAmount: number;
+ cellSize: number;
+ opacity: number;
+ offsetX?: number;
+ offsetY?: number;
+}
+
+export interface Config {
+ options: BackgroundMap;
+ disable: boolean;
+ grid: GridConfig;
+}
+
+export type GlobalState = { value: string | undefined; grid: boolean };
+export type GlobalStateUpdate = Partial;
diff --git a/code/addons/backgrounds/src/types/index.ts b/code/addons/backgrounds/src/types/index.ts
deleted file mode 100644
index 1439f4cd1329..000000000000
--- a/code/addons/backgrounds/src/types/index.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import type { ReactElement } from 'react';
-
-export interface GlobalState {
- name: string | undefined;
- selected: string | undefined;
-}
-
-export interface BackgroundSelectorItem {
- id: string;
- title: string;
- onClick: () => void;
- value: string;
- active: boolean;
- right?: ReactElement;
-}
-
-export interface Background {
- name: string;
- value: string;
-}
-
-export interface BackgroundsParameter {
- default?: string | null;
- disable?: boolean;
- values: Background[];
-}
-
-export interface BackgroundsConfig {
- backgrounds: Background[] | null;
- selectedBackgroundName: string | null;
- defaultBackgroundName: string | null;
- disable: boolean;
-}
diff --git a/code/addons/backgrounds/src/typings.d.ts b/code/addons/backgrounds/src/typings.d.ts
index bfd9e55123ff..22eb9c03a481 100644
--- a/code/addons/backgrounds/src/typings.d.ts
+++ b/code/addons/backgrounds/src/typings.d.ts
@@ -1 +1 @@
-declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined;
+declare var FEATURES: import('storybook/internal/types').StorybookConfigRaw['features'];
diff --git a/code/addons/backgrounds/src/helpers/index.ts b/code/addons/backgrounds/src/utils.ts
similarity index 60%
rename from code/addons/backgrounds/src/helpers/index.ts
rename to code/addons/backgrounds/src/utils.ts
index 7670784449cd..1487e21a345d 100644
--- a/code/addons/backgrounds/src/helpers/index.ts
+++ b/code/addons/backgrounds/src/utils.ts
@@ -1,47 +1,10 @@
import { global } from '@storybook/global';
-import { dedent } from 'ts-dedent';
-
-import { logger } from 'storybook/internal/client-logger';
-
-import type { Background } from '../types';
const { document, window } = global;
export const isReduceMotionEnabled = () => {
- const prefersReduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
- return prefersReduceMotion.matches;
-};
-
-export const getBackgroundColorByName = (
- currentSelectedValue: string,
- backgrounds: Background[] = [],
- defaultName: string | null | undefined
-): string => {
- if (currentSelectedValue === 'transparent') {
- return 'transparent';
- }
-
- if (backgrounds.find((background) => background.value === currentSelectedValue)) {
- return currentSelectedValue;
- }
-
- const defaultBackground = backgrounds.find((background) => background.name === defaultName);
- if (defaultBackground) {
- return defaultBackground.value;
- }
-
- if (defaultName) {
- const availableColors = backgrounds.map((background) => background.name).join(', ');
- logger.warn(
- dedent`
- Backgrounds Addon: could not find the default color "${defaultName}".
- These are the available colors for your story based on your configuration:
- ${availableColors}.
- `
- );
- }
-
- return 'transparent';
+ const prefersReduceMotion = window?.matchMedia('(prefers-reduced-motion: reduce)');
+ return !!prefersReduceMotion?.matches;
};
export const clearStyles = (selector: string | string[]) => {
diff --git a/code/addons/backgrounds/template/stories/globals.stories.ts b/code/addons/backgrounds/template/stories/globals.stories.ts
new file mode 100644
index 000000000000..ff6d407a6bef
--- /dev/null
+++ b/code/addons/backgrounds/template/stories/globals.stories.ts
@@ -0,0 +1,98 @@
+import { global as globalThis } from '@storybook/global';
+
+export default {
+ component: globalThis.Components.Pre,
+ args: {
+ text: 'Testing the background',
+ },
+ parameters: {
+ chromatic: { disable: true },
+ backgrounds: {
+ options: {
+ red: { name: 'light', value: 'red' },
+ darker: { name: 'darker', value: '#000' },
+ },
+ },
+ },
+};
+
+export const Set = {
+ globals: {
+ backgrounds: { value: 'red' },
+ },
+};
+
+export const SetAndCustom = {
+ parameters: {
+ backgrounds: {
+ options: {
+ pink: { value: '#F99CB4', name: 'pink' },
+ },
+ },
+ },
+ globals: {
+ backgrounds: { value: 'pink' },
+ },
+};
+
+export const UnsetCustom = {
+ parameters: {
+ backgrounds: {
+ options: {
+ pink: { value: '#Ff5CB7', name: 'hot pink' },
+ },
+ },
+ },
+};
+
+export const Disabled = {
+ parameters: {
+ backgrounds: {
+ disable: true,
+ },
+ },
+};
+
+export const Grid = {
+ globals: {
+ backgrounds: { grid: true },
+ },
+};
+
+export const GridAndBackground = {
+ globals: {
+ backgrounds: { grid: true, value: 'darker' },
+ },
+};
+
+export const GridConfig = {
+ parameters: {
+ backgrounds: {
+ grid: {
+ cellSize: 100,
+ cellAmount: 10,
+ opacity: 0.8,
+ },
+ },
+ },
+ globals: {
+ backgrounds: { grid: true, value: 'light' },
+ },
+};
+
+export const GridOffset = {
+ parameters: {
+ backgrounds: {
+ grid: {
+ cellSize: 100,
+ cellAmount: 10,
+ opacity: 0.8,
+ offsetX: 50,
+ offsetY: 50,
+ },
+ },
+ },
+ globals: {
+ backgrounds: { grid: true, value: 'light' },
+ },
+};
diff --git a/code/addons/backgrounds/template/stories/grid.stories.ts b/code/addons/backgrounds/template/stories/grid.stories.ts
deleted file mode 100644
index 361c18752ebb..000000000000
--- a/code/addons/backgrounds/template/stories/grid.stories.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { global as globalThis } from '@storybook/global';
-
-export default {
- component: globalThis.Components.Button,
- args: {
- label: 'Click Me!',
- },
- parameters: {
- backgrounds: {
- grid: {
- cellSize: 10,
- cellAmount: 4,
- opacity: 0.2,
- },
- },
- chromatic: { disable: true },
- },
-};
-
-export const Basic = {
- parameters: {},
-};
-
-export const Custom = {
- parameters: {
- backgrounds: {
- grid: {
- cellSize: 100,
- cellAmount: 10,
- opacity: 0.8,
- },
- },
- },
-};
-
-// Grid should have an offset of 0 when in fullscreen
-export const Fullscreen = {
- parameters: {
- layout: 'fullscreen',
- },
-};
-
-export const Disabled = {
- parameters: {
- backgrounds: {
- grid: {
- disable: true,
- },
- },
- },
-};
diff --git a/code/addons/backgrounds/template/stories/parameters.stories.ts b/code/addons/backgrounds/template/stories/parameters.stories.ts
index e6085424b99e..16f8b13d66c5 100644
--- a/code/addons/backgrounds/template/stories/parameters.stories.ts
+++ b/code/addons/backgrounds/template/stories/parameters.stories.ts
@@ -1,5 +1,11 @@
import { global as globalThis } from '@storybook/global';
+// these stories only work with `backgroundsStoryGlobals` set to false
+// because the `default` prop is dropped and because, `values` changed to `options` and is now an object
+
+const img =
+ 'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTkwIDQ3NSIgZmlsbD0ibm9uZSI+CiAgPGcgaWQ9ImNvbXBvbmVudC1jb21wb3NpdGlvbiI+CiAgICA8bWFzayBpZD0iY29tcG9uZW50cyIgd2lkdGg9IjcxMSIgaGVpZ2h0PSI2NjciIHg9Ii01MiIgeT0iLTg2IiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHBhdGggZmlsbD0iI2ZmZiIgZD0iTTY1OC4wMiAxODEuNDAzbC00NC4zMTItLjAwMS0uMDAxIDEzMi4xODVoNDQuMzEzdjQ1LjA2NmgtNDQuMzEybC0uMDAxIDEzMi45MzktODguNjI1LS4wMDEtLjAwMSA4OC42MjZoLTQ1LjA2NnYtNDQuMzEybC00NC4zMTItLjAwMXYtNDQuMzEzaC04Ny44NzJsLS4wMDEgNDQuMzEzLTQ0LjMxMi4wMDF2NDQuMzEyaC00NS4wNjZ2LTQ0LjMxM2gtNDQuMzEzdi00NC4zMTJsLTQzLjU1OC0uMDAxLS4wMDIgNDQuMzEzaC00NS4wNjZsLjAwMS00NC4zMTMtNDMuNTYuMDAxdjQ0LjMxMkgzNi44ODh2LTQ0LjMxM2wtNDMuNTYuMDAxdjg4LjYyNWgtNDUuMDY2VjM1Ny45aDQ0LjMxNHYtNDMuNTU5aC00NC4zMTN2LTQ1LjA2Nmg0NC4zMTNWMTM3LjA5aC00NC4zMTNWOTIuMDI0bDQ0LjMxMy0uMDAxVjQuMTUyaC00NC4zMTN2LTQ1LjA2NkgzNy42NFYzLjM5OEgyMTQuMTRsLS4wMDEtNDQuMzEyaDQ1LjA2NlYzLjM5OGgyMjAuODExbC0uMDAxLTg4LjYyNWg0NS4wNjZsLjAwMSA0NC4zMTNoNDQuMzEybC4wMDEgNDQuMzEyaDQ0LjMxMmwuMDAxIDg4LjYyNWg0NC4zMTJ2ODkuMzh6TTM2Ljg4OCAzNTcuOVYyMjUuNzE2aC00My41NnY0NC4zMTJoLTQ0LjMxMnY0My41Nmw0NC4zMTMtLjAwMVYzNTcuOWg0My41NnptMCA4OC42MjV2LTg3Ljg3MmgtODcuODcydjg3Ljg3Mmg4Ny44NzJ6bTAtMjIxLjU2M1Y5Mi43NzdoLTg3Ljg3MnY0My41Nmw0NC4zMTMtLjAwMXY4OC42MjZoNDMuNTU5ek04MS4yIDQ3LjcxMVY0LjE1SDM2Ljg4OFYtNDAuMTZoLTg3Ljg3MnY0My41Nmg0NC4zMTNWNDcuNzFIODEuMnpNMzYuODg4IDQ5MC44Mzl2LTQzLjU2aC04Ny44NzJ2MTMyLjE4NWg0My41NnYtODguNjI2bDQ0LjMxMi4wMDF6bTg4LjYyNi0zOTguMDYySDgxLjIwMVY0OC40NjRILTYuNjcxdjQzLjU2SDM3LjY0djQ0LjMxMmg4Ny44NzN2LTQzLjU2em0tLjAwMSAxMzIuMTg1di00My41NmwtNDQuMzEyLjAwMVYxMzcuMDlIMzcuNjR2MTMyLjE4NGg0My41NnYtNDQuMzEyaDQ0LjMxMnptMCAyNjUuODc2di00My41NTlIODEuMjAxdi00NC4zMTNIMzcuNjR2MTMyLjE4NWg0My41NnYtNDQuMzEzaDQ0LjMxMnptLjAwMS0xNzcuMjUxdi00My41NTlIMzcuNjQydjEzMi4xODVIODEuMnYtODguNjI2aDQ0LjMxM3ptODguNjI1LTEzMi45MzhsLjAwMS00My41NTlIODEuOTU1bC0uMDAxIDQzLjU1OWg0NC4zMTN2NDQuMzEzaDQzLjU1OXYtNDQuMzEzaDQ0LjMxM3ptMC04OC42MjVWNC4xNTJIODEuOTU0djQzLjU2bDg4LjYyNS0uMDAxLjAwMSA0NC4zMTJoNDMuNTU5em0wIDE3Ny4yNXYtNDMuNTU5SDgxLjk1NHY0My41NTlIMjE0LjE0em0wLTEzMi45Mzh2LTQzLjU2bC00NC4zMTMuMDAxVjQ4LjQ2NEg4MS45NTR2NDMuNTZoNDQuMzEzdjQ0LjMxMmg4Ny44NzJ6bS00NC4zMTMgMjY1Ljg3N3YtNDMuNTZoLTQ0LjMxM2wuMDAxLTQ0LjMxMmgtNDMuNTZ2MTMyLjE4NGw0My41NTkuMDAxdi00NC4zMTNoNDQuMzEzem00NC4zMTMtNDQuMzEzdi04Ny44NzJoLTg3Ljg3MlYzNTcuOWg4Ny44NzJ6bTAgODguNjI2di00My41NmgtODcuODcydjEzMi4xODVoNDMuNTU5di04OC42MjVoNDQuMzEzem04OC42MjYtMjY1Ljg3N2wtLjAwMS00My41NTloLTg3Ljg3MnY0NC4zMTNIMTcwLjU4djQzLjU1OWg4Ny44NzJ2LTQ0LjMxM2g0NC4zMTN6bTAgMzEwLjE4OXYtNDMuNTU5SDE3MC41OHY0My41NTloNDQuMzEybC4wMDEgNDQuMzEzaDQzLjU1OXYtNDQuMzEzaDQ0LjMxM3ptMC04Ny44NzJoLTQ0LjMxM3YtNDQuMzEzSDE3MC41OHY0My41Nmg0NC4zMTJsLjAwMSA0NC4zMTJoODcuODcydi00My41NTl6bTQ0LjMxMi0yMjIuMzE3bC4wMDEtODcuODcySDIxNC44OTJ2NDMuNTZsODguNjI2LS4wMDEuMDAxIDQ0LjMxM2g0My41NTh6TTMwMi43NjQgNDcuNzExVjQuMTVoLTQ0LjMxMmwtLjAwMS00NC4zMTJoLTQzLjU1OVY5Mi4wMjRsNDMuNTYtLjAwMVY0Ny43MWg0NC4zMTJ6bS00NC4zMTMgMjIxLjU2NGwuMDAxLTQzLjU2aC00My41NTl2NDMuNTU5bDQzLjU1OC4wMDF6bTg4LjYyNyA4OC42MjVsLS4wMDEtNDMuNTU5SDIxNC44OTNWMzU3LjloMTMyLjE4NXptLTg3Ljg3My04Ny44NzJoLTQ0LjMxM3Y0My41NTloODcuODcyVjE4MS40MDJsLTQzLjU1OS4wMDF2ODguNjI1ek0zOTEuMzkgNDcuNzExVjQuMTUxaC04Ny44NzJ2NDQuMzEzaC00NC4zMTN2NDMuNTZoODcuODcyVjQ3LjcxaDQ0LjMxM3ptLTg4LjYyNSAzNTQuNTAydi00My41NmgtNDMuNTZ2NDMuNTZoNDMuNTZ6bTQ0LjMxMiAxMzIuOTM4di04Ny44NzJoLTQzLjU1OXY0NC4zMTNoLTQ0LjMxM3Y4Ny44NzJoNDMuNTZsLS4wMDEtNDQuMzEzaDQ0LjMxM3ptNDQuMzEyLTEzMi45MzhsLjAwMS00My41NmgtODcuODcydjg3Ljg3Mmg0My41NTl2LTQ0LjMxMmg0NC4zMTJ6bTAtNDQuMzEzdi04Ny44NzJoLTQ0LjMxMnYtNDQuMzEzaC00My41NTh2ODcuODcyaDQ0LjMxMVYzNTcuOWg0My41NTl6bTQ0LjMxNC0xMzIuOTM4di00My41NkgzMDMuNTE4djQzLjU2aDQ0LjMxMmwuMDAxIDQ0LjMxMmg0My41NTl2LTQ0LjMxMmg0NC4zMTN6bTAgMjIxLjU2M1YzMTQuMzQxaC00My41NTlsLS4wMDEgODguNjI1aC00NC4zMTJsLS4wMDEgNDMuNTU5aDg3Ljg3M3ptNDQuMzEyIDQ0LjMxM3YtODcuODcyaC00My41NTl2NDQuMzEzSDM0Ny44M3Y0My41NTloMTMyLjE4NXptMC0zMTAuMTg5VjEzNy4wOUgzOTEuMzlWOTIuNzc3aC00My41NTl2ODcuODcyaDEzMi4xODR6bTAtODguNjI2VjQ4LjQ2NEgzNDcuODMxdjQzLjU2aDQ0LjMxMnY0NC4zMTJoNDMuNTZWOTIuMDI0aDQ0LjMxMnptLjAwMSAxNzcuMjUxbC0uMDAxLTg3Ljg3MWgtNDMuNTU5djQ0LjMxMmgtNDQuMzEydjg3Ljg3M2w0My41NTktLjAwMXYtNDQuMzEybDQ0LjMxMy0uMDAxem00NC4zMTItMTc3LjI1VjQuMTUySDM5Mi4xNDN2NDMuNTZsODguNjI2LS4wMDF2NDQuMzEzaDQzLjU1OXptNDQuMzEzIDg4LjYyNWwuMDAxLTQzLjU1OWgtNDQuMzE0VjkyLjc3N2gtODcuODcydjQzLjU1OWg0NC4zMTN2NDQuMzEzaDg3Ljg3MnpNNTI0LjMyOCAzNTcuOWwuMDAxLTQzLjU1OWgtNDQuMzE0bC4wMDEtNDQuMzEzaC00My41NmwuMDAxIDEzMi4xODQgNDMuNTU4LjAwMVYzNTcuOWg0NC4zMTN6bTAgMjIxLjU2NFY0NDcuMjc5aC00My41NTl2NDQuMzEzaC00NC4zMTN2NDMuNTU5aDQ0LjMxM3Y0NC4zMTNoNDMuNTU5em00NC4zMTMtODguNjI2di04Ny44NzJoLTQ0LjMxM3YtNDQuMzEybC00My41Ni0uMDAxLjAwMiA4Ny44NzIgNDQuMzExLjAwMXY0NC4zMTJoNDMuNTZ6bTAtNDQzLjEyN3YtODcuODcyaC00NC4zMTN2LTQ0LjMxM2gtNDMuNTU5VjMuMzk4aDQ0LjMxMmwuMDAxIDQ0LjMxM2g0My41NTl6bTAgMTc3LjI1MXYtNDMuNTU5aC04Ny44NzJ2MTMyLjE4NGg0My41NTl2LTg4LjYyNWg0NC4zMTN6bTQ0LjMxMy0xMzIuOTM4VjQuMTUyaC00My41NnY0NC4zMTJoLTQ0LjMxM3Y4Ny44NzJoNDMuNTZWOTIuMDI0aDQ0LjMxM3ptLS4wMDEgMzk4LjgxNFYzNTguNjU0aC04Ny44NzJ2NDMuNTU5aDQ0LjMxM3Y4OC42MjVoNDMuNTU5em0wLTIyMS41NjN2LTg3Ljg3MmgtNDMuNTU5djQ0LjMxMmwtNDQuMzEyLjAwMXY4Ny44NzFoNDMuNTU5di00NC4zMTJoNDQuMzEyem00NC4zMTMgODguNjI1di00My41NTloLTQ0LjMxM3YtNDQuMzEzaC00My41NTl2NDQuMzEzaC00NC4zMTJWMzU3LjloMTMyLjE4NHptMC0xNzcuMjUxVjkyLjc3N2gtODcuODcydjg3Ljg3Mmg4Ny44NzJ6Ij4KICAgICAgPC9wYXRoPgogICAgPC9tYXNrPgogICAgPGcgbWFzaz0idXJsKCNjb21wb25lbnRzKSI+CiAgICAgIDxyZWN0IHdpZHRoPSI1OTAiIGhlaWdodD0iNDc1IiBmaWxsPSJ1cmwoI2dyYWRpZW50LWZpbGwpIiBvcGFjaXR5PSIwLjc1Ij4KICAgICAgPC9yZWN0PgogICAgPC9nPgogIDwvZz4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0id2luZG93LWNsaXAiPgogICAgICA8cGF0aCBmaWxsPSIjZmZmIiBkPSJNMCAwSDU4OFY0NTNIMHoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEgMjEpIj4KICAgICAgPC9wYXRoPgogICAgPC9jbGlwUGF0aD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQtZmlsbCIgeDE9Ijc2IiB4Mj0iNTUzLjUiIHkxPSIyNjkiIHkyPSI0MTUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI0EyNEZCRCI+CiAgICAgIDwvc3RvcD4KICAgICAgPHN0b3Agb2Zmc2V0PSIwLjQ3NyIgc3RvcC1jb2xvcj0iIzM2N0VENiI+CiAgICAgIDwvc3RvcD4KICAgICAgPHN0b3Agb2Zmc2V0PSIwLjk4MiIgc3RvcC1jb2xvcj0iI0UxMjY0RCI+CiAgICAgIDwvc3RvcD4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgPC9kZWZzPgo8L3N2Zz4K';
+
const COLORS = [
{ name: 'red', value: '#FB001D' },
{ name: 'orange', value: '#FB8118' },
@@ -10,9 +16,9 @@ const COLORS = [
];
export default {
- component: globalThis.Components.Button,
+ component: globalThis.Components.Pre,
args: {
- label: 'Click Me!',
+ text: 'Testing the background',
},
parameters: {
backgrounds: {
@@ -22,10 +28,6 @@ export default {
},
};
-export const Basic = {
- parameters: {},
-};
-
export const Default = {
parameters: {
backgrounds: {
@@ -37,7 +39,7 @@ export const Default = {
export const StorySpecific = {
parameters: {
backgrounds: {
- default: 'white',
+ default: 'pink',
values: [
{ name: 'white', value: '#F9F5F1' },
{ name: 'pink', value: '#F99CB4' },
@@ -63,9 +65,6 @@ export const Gradient = {
},
};
-const img =
- 'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTkwIDQ3NSIgZmlsbD0ibm9uZSI+CiAgPGcgaWQ9ImNvbXBvbmVudC1jb21wb3NpdGlvbiI+CiAgICA8bWFzayBpZD0iY29tcG9uZW50cyIgd2lkdGg9IjcxMSIgaGVpZ2h0PSI2NjciIHg9Ii01MiIgeT0iLTg2IiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHBhdGggZmlsbD0iI2ZmZiIgZD0iTTY1OC4wMiAxODEuNDAzbC00NC4zMTItLjAwMS0uMDAxIDEzMi4xODVoNDQuMzEzdjQ1LjA2NmgtNDQuMzEybC0uMDAxIDEzMi45MzktODguNjI1LS4wMDEtLjAwMSA4OC42MjZoLTQ1LjA2NnYtNDQuMzEybC00NC4zMTItLjAwMXYtNDQuMzEzaC04Ny44NzJsLS4wMDEgNDQuMzEzLTQ0LjMxMi4wMDF2NDQuMzEyaC00NS4wNjZ2LTQ0LjMxM2gtNDQuMzEzdi00NC4zMTJsLTQzLjU1OC0uMDAxLS4wMDIgNDQuMzEzaC00NS4wNjZsLjAwMS00NC4zMTMtNDMuNTYuMDAxdjQ0LjMxMkgzNi44ODh2LTQ0LjMxM2wtNDMuNTYuMDAxdjg4LjYyNWgtNDUuMDY2VjM1Ny45aDQ0LjMxNHYtNDMuNTU5aC00NC4zMTN2LTQ1LjA2Nmg0NC4zMTNWMTM3LjA5aC00NC4zMTNWOTIuMDI0bDQ0LjMxMy0uMDAxVjQuMTUyaC00NC4zMTN2LTQ1LjA2NkgzNy42NFYzLjM5OEgyMTQuMTRsLS4wMDEtNDQuMzEyaDQ1LjA2NlYzLjM5OGgyMjAuODExbC0uMDAxLTg4LjYyNWg0NS4wNjZsLjAwMSA0NC4zMTNoNDQuMzEybC4wMDEgNDQuMzEyaDQ0LjMxMmwuMDAxIDg4LjYyNWg0NC4zMTJ2ODkuMzh6TTM2Ljg4OCAzNTcuOVYyMjUuNzE2aC00My41NnY0NC4zMTJoLTQ0LjMxMnY0My41Nmw0NC4zMTMtLjAwMVYzNTcuOWg0My41NnptMCA4OC42MjV2LTg3Ljg3MmgtODcuODcydjg3Ljg3Mmg4Ny44NzJ6bTAtMjIxLjU2M1Y5Mi43NzdoLTg3Ljg3MnY0My41Nmw0NC4zMTMtLjAwMXY4OC42MjZoNDMuNTU5ek04MS4yIDQ3LjcxMVY0LjE1SDM2Ljg4OFYtNDAuMTZoLTg3Ljg3MnY0My41Nmg0NC4zMTNWNDcuNzFIODEuMnpNMzYuODg4IDQ5MC44Mzl2LTQzLjU2aC04Ny44NzJ2MTMyLjE4NWg0My41NnYtODguNjI2bDQ0LjMxMi4wMDF6bTg4LjYyNi0zOTguMDYySDgxLjIwMVY0OC40NjRILTYuNjcxdjQzLjU2SDM3LjY0djQ0LjMxMmg4Ny44NzN2LTQzLjU2em0tLjAwMSAxMzIuMTg1di00My41NmwtNDQuMzEyLjAwMVYxMzcuMDlIMzcuNjR2MTMyLjE4NGg0My41NnYtNDQuMzEyaDQ0LjMxMnptMCAyNjUuODc2di00My41NTlIODEuMjAxdi00NC4zMTNIMzcuNjR2MTMyLjE4NWg0My41NnYtNDQuMzEzaDQ0LjMxMnptLjAwMS0xNzcuMjUxdi00My41NTlIMzcuNjQydjEzMi4xODVIODEuMnYtODguNjI2aDQ0LjMxM3ptODguNjI1LTEzMi45MzhsLjAwMS00My41NTlIODEuOTU1bC0uMDAxIDQzLjU1OWg0NC4zMTN2NDQuMzEzaDQzLjU1OXYtNDQuMzEzaDQ0LjMxM3ptMC04OC42MjVWNC4xNTJIODEuOTU0djQzLjU2bDg4LjYyNS0uMDAxLjAwMSA0NC4zMTJoNDMuNTU5em0wIDE3Ny4yNXYtNDMuNTU5SDgxLjk1NHY0My41NTlIMjE0LjE0em0wLTEzMi45Mzh2LTQzLjU2bC00NC4zMTMuMDAxVjQ4LjQ2NEg4MS45NTR2NDMuNTZoNDQuMzEzdjQ0LjMxMmg4Ny44NzJ6bS00NC4zMTMgMjY1Ljg3N3YtNDMuNTZoLTQ0LjMxM2wuMDAxLTQ0LjMxMmgtNDMuNTZ2MTMyLjE4NGw0My41NTkuMDAxdi00NC4zMTNoNDQuMzEzem00NC4zMTMtNDQuMzEzdi04Ny44NzJoLTg3Ljg3MlYzNTcuOWg4Ny44NzJ6bTAgODguNjI2di00My41NmgtODcuODcydjEzMi4xODVoNDMuNTU5di04OC42MjVoNDQuMzEzem04OC42MjYtMjY1Ljg3N2wtLjAwMS00My41NTloLTg3Ljg3MnY0NC4zMTNIMTcwLjU4djQzLjU1OWg4Ny44NzJ2LTQ0LjMxM2g0NC4zMTN6bTAgMzEwLjE4OXYtNDMuNTU5SDE3MC41OHY0My41NTloNDQuMzEybC4wMDEgNDQuMzEzaDQzLjU1OXYtNDQuMzEzaDQ0LjMxM3ptMC04Ny44NzJoLTQ0LjMxM3YtNDQuMzEzSDE3MC41OHY0My41Nmg0NC4zMTJsLjAwMSA0NC4zMTJoODcuODcydi00My41NTl6bTQ0LjMxMi0yMjIuMzE3bC4wMDEtODcuODcySDIxNC44OTJ2NDMuNTZsODguNjI2LS4wMDEuMDAxIDQ0LjMxM2g0My41NTh6TTMwMi43NjQgNDcuNzExVjQuMTVoLTQ0LjMxMmwtLjAwMS00NC4zMTJoLTQzLjU1OVY5Mi4wMjRsNDMuNTYtLjAwMVY0Ny43MWg0NC4zMTJ6bS00NC4zMTMgMjIxLjU2NGwuMDAxLTQzLjU2aC00My41NTl2NDMuNTU5bDQzLjU1OC4wMDF6bTg4LjYyNyA4OC42MjVsLS4wMDEtNDMuNTU5SDIxNC44OTNWMzU3LjloMTMyLjE4NXptLTg3Ljg3My04Ny44NzJoLTQ0LjMxM3Y0My41NTloODcuODcyVjE4MS40MDJsLTQzLjU1OS4wMDF2ODguNjI1ek0zOTEuMzkgNDcuNzExVjQuMTUxaC04Ny44NzJ2NDQuMzEzaC00NC4zMTN2NDMuNTZoODcuODcyVjQ3LjcxaDQ0LjMxM3ptLTg4LjYyNSAzNTQuNTAydi00My41NmgtNDMuNTZ2NDMuNTZoNDMuNTZ6bTQ0LjMxMiAxMzIuOTM4di04Ny44NzJoLTQzLjU1OXY0NC4zMTNoLTQ0LjMxM3Y4Ny44NzJoNDMuNTZsLS4wMDEtNDQuMzEzaDQ0LjMxM3ptNDQuMzEyLTEzMi45MzhsLjAwMS00My41NmgtODcuODcydjg3Ljg3Mmg0My41NTl2LTQ0LjMxMmg0NC4zMTJ6bTAtNDQuMzEzdi04Ny44NzJoLTQ0LjMxMnYtNDQuMzEzaC00My41NTh2ODcuODcyaDQ0LjMxMVYzNTcuOWg0My41NTl6bTQ0LjMxNC0xMzIuOTM4di00My41NkgzMDMuNTE4djQzLjU2aDQ0LjMxMmwuMDAxIDQ0LjMxMmg0My41NTl2LTQ0LjMxMmg0NC4zMTN6bTAgMjIxLjU2M1YzMTQuMzQxaC00My41NTlsLS4wMDEgODguNjI1aC00NC4zMTJsLS4wMDEgNDMuNTU5aDg3Ljg3M3ptNDQuMzEyIDQ0LjMxM3YtODcuODcyaC00My41NTl2NDQuMzEzSDM0Ny44M3Y0My41NTloMTMyLjE4NXptMC0zMTAuMTg5VjEzNy4wOUgzOTEuMzlWOTIuNzc3aC00My41NTl2ODcuODcyaDEzMi4xODR6bTAtODguNjI2VjQ4LjQ2NEgzNDcuODMxdjQzLjU2aDQ0LjMxMnY0NC4zMTJoNDMuNTZWOTIuMDI0aDQ0LjMxMnptLjAwMSAxNzcuMjUxbC0uMDAxLTg3Ljg3MWgtNDMuNTU5djQ0LjMxMmgtNDQuMzEydjg3Ljg3M2w0My41NTktLjAwMXYtNDQuMzEybDQ0LjMxMy0uMDAxem00NC4zMTItMTc3LjI1VjQuMTUySDM5Mi4xNDN2NDMuNTZsODguNjI2LS4wMDF2NDQuMzEzaDQzLjU1OXptNDQuMzEzIDg4LjYyNWwuMDAxLTQzLjU1OWgtNDQuMzE0VjkyLjc3N2gtODcuODcydjQzLjU1OWg0NC4zMTN2NDQuMzEzaDg3Ljg3MnpNNTI0LjMyOCAzNTcuOWwuMDAxLTQzLjU1OWgtNDQuMzE0bC4wMDEtNDQuMzEzaC00My41NmwuMDAxIDEzMi4xODQgNDMuNTU4LjAwMVYzNTcuOWg0NC4zMTN6bTAgMjIxLjU2NFY0NDcuMjc5aC00My41NTl2NDQuMzEzaC00NC4zMTN2NDMuNTU5aDQ0LjMxM3Y0NC4zMTNoNDMuNTU5em00NC4zMTMtODguNjI2di04Ny44NzJoLTQ0LjMxM3YtNDQuMzEybC00My41Ni0uMDAxLjAwMiA4Ny44NzIgNDQuMzExLjAwMXY0NC4zMTJoNDMuNTZ6bTAtNDQzLjEyN3YtODcuODcyaC00NC4zMTN2LTQ0LjMxM2gtNDMuNTU5VjMuMzk4aDQ0LjMxMmwuMDAxIDQ0LjMxM2g0My41NTl6bTAgMTc3LjI1MXYtNDMuNTU5aC04Ny44NzJ2MTMyLjE4NGg0My41NTl2LTg4LjYyNWg0NC4zMTN6bTQ0LjMxMy0xMzIuOTM4VjQuMTUyaC00My41NnY0NC4zMTJoLTQ0LjMxM3Y4Ny44NzJoNDMuNTZWOTIuMDI0aDQ0LjMxM3ptLS4wMDEgMzk4LjgxNFYzNTguNjU0aC04Ny44NzJ2NDMuNTU5aDQ0LjMxM3Y4OC42MjVoNDMuNTU5em0wLTIyMS41NjN2LTg3Ljg3MmgtNDMuNTU5djQ0LjMxMmwtNDQuMzEyLjAwMXY4Ny44NzFoNDMuNTU5di00NC4zMTJoNDQuMzEyem00NC4zMTMgODguNjI1di00My41NTloLTQ0LjMxM3YtNDQuMzEzaC00My41NTl2NDQuMzEzaC00NC4zMTJWMzU3LjloMTMyLjE4NHptMC0xNzcuMjUxVjkyLjc3N2gtODcuODcydjg3Ljg3Mmg4Ny44NzJ6Ij4KICAgICAgPC9wYXRoPgogICAgPC9tYXNrPgogICAgPGcgbWFzaz0idXJsKCNjb21wb25lbnRzKSI+CiAgICAgIDxyZWN0IHdpZHRoPSI1OTAiIGhlaWdodD0iNDc1IiBmaWxsPSJ1cmwoI2dyYWRpZW50LWZpbGwpIiBvcGFjaXR5PSIwLjc1Ij4KICAgICAgPC9yZWN0PgogICAgPC9nPgogIDwvZz4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0id2luZG93LWNsaXAiPgogICAgICA8cGF0aCBmaWxsPSIjZmZmIiBkPSJNMCAwSDU4OFY0NTNIMHoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEgMjEpIj4KICAgICAgPC9wYXRoPgogICAgPC9jbGlwUGF0aD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQtZmlsbCIgeDE9Ijc2IiB4Mj0iNTUzLjUiIHkxPSIyNjkiIHkyPSI0MTUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI0EyNEZCRCI+CiAgICAgIDwvc3RvcD4KICAgICAgPHN0b3Agb2Zmc2V0PSIwLjQ3NyIgc3RvcC1jb2xvcj0iIzM2N0VENiI+CiAgICAgIDwvc3RvcD4KICAgICAgPHN0b3Agb2Zmc2V0PSIwLjk4MiIgc3RvcC1jb2xvcj0iI0UxMjY0RCI+CiAgICAgIDwvc3RvcD4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgPC9kZWZzPgo8L3N2Zz4K';
-
export const Image = {
parameters: {
backgrounds: {
diff --git a/code/addons/controls/template/stories/conditional.stories.ts b/code/addons/controls/template/stories/conditional.stories.ts
index 32f795b63f16..ed3c51d8ba8e 100644
--- a/code/addons/controls/template/stories/conditional.stories.ts
+++ b/code/addons/controls/template/stories/conditional.stories.ts
@@ -54,9 +54,9 @@ export const ToggleExpandCollapse = {
export const GlobalBased = {
argTypes: {
- ifThemeExists: { control: 'text', if: { global: 'theme' } },
- ifThemeNotExists: { control: 'text', if: { global: 'theme', exists: false } },
- ifLightTheme: { control: 'text', if: { global: 'theme', eq: 'light' } },
- ifNotLightTheme: { control: 'text', if: { global: 'theme', neq: 'light' } },
+ ifThemeExists: { control: 'text', if: { global: 'sb_theme' } },
+ ifThemeNotExists: { control: 'text', if: { global: 'sb_theme', exists: false } },
+ ifLightTheme: { control: 'text', if: { global: 'sb_theme', eq: 'light' } },
+ ifNotLightTheme: { control: 'text', if: { global: 'sb_theme', neq: 'light' } },
},
};
diff --git a/code/addons/interactions/src/components/Interaction.stories.tsx b/code/addons/interactions/src/components/Interaction.stories.tsx
index 4486c83ff664..98f579090986 100644
--- a/code/addons/interactions/src/components/Interaction.stories.tsx
+++ b/code/addons/interactions/src/components/Interaction.stories.tsx
@@ -54,10 +54,7 @@ export const Disabled: Story = {
export const Hovered: Story = {
...Done,
- parameters: {
- // Set light theme to avoid stacked theme in chromatic
- theme: 'light',
- },
+ globals: { sb_theme: 'light' },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.hover(canvas.getByRole('button'));
diff --git a/code/addons/interactions/src/components/InteractionsPanel.stories.tsx b/code/addons/interactions/src/components/InteractionsPanel.stories.tsx
index 8f0e48faab58..9105ea7f2a3a 100644
--- a/code/addons/interactions/src/components/InteractionsPanel.stories.tsx
+++ b/code/addons/interactions/src/components/InteractionsPanel.stories.tsx
@@ -33,10 +33,7 @@ const meta = {
),
],
- parameters: {
- layout: 'fullscreen',
- theme: 'light', // stacked will break interactions
- },
+ parameters: { layout: 'fullscreen' },
args: {
calls: new Map(getCalls(CallStates.DONE).map((call) => [call.id, call])),
controls: SubnavStories.args.controls,
@@ -60,35 +57,35 @@ export const Passing: Story = {
args: {
interactions: getInteractions(CallStates.DONE),
},
-};
-Passing.play = async ({ args, canvasElement }) => {
- if (isChromatic()) return;
- const canvas = within(canvasElement);
-
- await waitFor(async () => {
- await userEvent.click(canvas.getByLabelText('Go to start'));
- await expect(args.controls.start).toHaveBeenCalled();
- });
-
- await waitFor(async () => {
- await userEvent.click(canvas.getByLabelText('Go back'));
- await expect(args.controls.back).toHaveBeenCalled();
- });
-
- await waitFor(async () => {
- await userEvent.click(canvas.getByLabelText('Go forward'));
- await expect(args.controls.next).not.toHaveBeenCalled();
- });
-
- await waitFor(async () => {
- await userEvent.click(canvas.getByLabelText('Go to end'));
- await expect(args.controls.end).not.toHaveBeenCalled();
- });
-
- await waitFor(async () => {
- await userEvent.click(canvas.getByLabelText('Rerun'));
- await expect(args.controls.rerun).toHaveBeenCalled();
- });
+ play: async ({ args, canvasElement }) => {
+ if (isChromatic()) return;
+ const canvas = within(canvasElement);
+
+ await waitFor(async () => {
+ await userEvent.click(canvas.getByLabelText('Go to start'));
+ await expect(args.controls.start).toHaveBeenCalled();
+ });
+
+ await waitFor(async () => {
+ await userEvent.click(canvas.getByLabelText('Go back'));
+ await expect(args.controls.back).toHaveBeenCalled();
+ });
+
+ await waitFor(async () => {
+ await userEvent.click(canvas.getByLabelText('Go forward'));
+ await expect(args.controls.next).not.toHaveBeenCalled();
+ });
+
+ await waitFor(async () => {
+ await userEvent.click(canvas.getByLabelText('Go to end'));
+ await expect(args.controls.end).not.toHaveBeenCalled();
+ });
+
+ await waitFor(async () => {
+ await userEvent.click(canvas.getByLabelText('Rerun'));
+ await expect(args.controls.rerun).toHaveBeenCalled();
+ });
+ },
};
export const Paused: Story = {
diff --git a/code/addons/interactions/src/components/Subnav.stories.tsx b/code/addons/interactions/src/components/Subnav.stories.tsx
index 02515ea13f7a..bdaa5fae2831 100644
--- a/code/addons/interactions/src/components/Subnav.stories.tsx
+++ b/code/addons/interactions/src/components/Subnav.stories.tsx
@@ -1,10 +1,14 @@
import { action } from '@storybook/addon-actions';
import { CallStates } from '@storybook/instrumenter';
import { Subnav } from './Subnav';
+import { parameters } from '../preview';
export default {
title: 'Subnav',
component: Subnav,
+ parameters: {
+ layout: 'fullscreen',
+ },
args: {
controls: {
start: action('start'),
diff --git a/code/addons/interactions/template/stories/basics.stories.ts b/code/addons/interactions/template/stories/basics.stories.ts
index cf6e34eddf2c..459471817711 100644
--- a/code/addons/interactions/template/stories/basics.stories.ts
+++ b/code/addons/interactions/template/stories/basics.stories.ts
@@ -14,6 +14,9 @@ export default {
args: {
onSuccess: fn(),
},
+ globals: {
+ sb_theme: 'light',
+ },
};
export const Validation = {
diff --git a/code/addons/interactions/template/stories/unhandled-errors.stories.ts b/code/addons/interactions/template/stories/unhandled-errors.stories.ts
index 0daa0850584b..0f8409ad792a 100644
--- a/code/addons/interactions/template/stories/unhandled-errors.stories.ts
+++ b/code/addons/interactions/template/stories/unhandled-errors.stories.ts
@@ -5,6 +5,7 @@ export default {
component: globalThis.Components.Button,
args: {
label: 'Button',
+ // onClick: fn(), <-- this is intentionally missing to trigger an unhandled error
},
argTypes: {
onClick: { type: 'function' },
diff --git a/code/addons/themes/README.md b/code/addons/themes/README.md
index c529f8a60258..0c71eb3ce159 100644
--- a/code/addons/themes/README.md
+++ b/code/addons/themes/README.md
@@ -45,11 +45,8 @@ import { Button } from './Button';
export default {
title: 'Example/Button',
component: Button,
- parameters: {
- themes: {
- themeOverride: 'light', // component level override
- },
- },
+ // meta level override
+ globals: { theme: 'dark' },
};
export const Primary = {
@@ -64,10 +61,7 @@ export const PrimaryDark = {
primary: true,
label: 'Button',
},
- parameters: {
- themes: {
- themeOverride: 'dark', // Story level override
- },
- },
+ // story level override
+ globals: { theme: 'dark' },
};
```
diff --git a/code/addons/themes/src/constants.ts b/code/addons/themes/src/constants.ts
index 677c9499a7b4..654cc57ea893 100644
--- a/code/addons/themes/src/constants.ts
+++ b/code/addons/themes/src/constants.ts
@@ -15,6 +15,7 @@ export const DEFAULT_ADDON_STATE: ThemeAddonState = {
export interface ThemeParameters {
themeOverride?: string;
+ disable?: boolean;
}
export const DEFAULT_THEME_PARAMETERS: ThemeParameters = {};
diff --git a/code/addons/themes/src/decorators/class-name.decorator.tsx b/code/addons/themes/src/decorators/class-name.decorator.tsx
index b8ebe8c89810..4bc56202f038 100644
--- a/code/addons/themes/src/decorators/class-name.decorator.tsx
+++ b/code/addons/themes/src/decorators/class-name.decorator.tsx
@@ -14,7 +14,7 @@ const DEFAULT_ELEMENT_SELECTOR = 'html';
const classStringToArray = (classString: string) => classString.split(' ').filter(Boolean);
// TODO check with @kasperpeulen: change the types so they can be correctly inferred from context e.g. any>
-export const withThemeByClassName = ({
+export const withThemeByClassName = ({
themes,
defaultTheme,
parentSelector = DEFAULT_ELEMENT_SELECTOR,
@@ -47,7 +47,7 @@ export const withThemeByClassName = ({
if (newThemeClasses.length > 0) {
parentElement.classList.add(...newThemeClasses);
}
- }, [themeOverride, selected, parentSelector]);
+ }, [themeOverride, selected]);
return storyFn();
};
diff --git a/code/addons/themes/src/decorators/data-attribute.decorator.tsx b/code/addons/themes/src/decorators/data-attribute.decorator.tsx
index dee9988ec7d0..05ad62792e7c 100644
--- a/code/addons/themes/src/decorators/data-attribute.decorator.tsx
+++ b/code/addons/themes/src/decorators/data-attribute.decorator.tsx
@@ -31,7 +31,7 @@ export const withThemeByDataAttribute = ({
if (parentElement) {
parentElement.setAttribute(attributeName, themes[themeKey]);
}
- }, [themeOverride, selected, parentSelector, attributeName]);
+ }, [themeOverride, selected]);
return storyFn();
};
diff --git a/code/addons/themes/src/decorators/provider.decorator.tsx b/code/addons/themes/src/decorators/provider.decorator.tsx
index 09eb5824c488..f8b477810035 100644
--- a/code/addons/themes/src/decorators/provider.decorator.tsx
+++ b/code/addons/themes/src/decorators/provider.decorator.tsx
@@ -38,7 +38,7 @@ export const withThemeFromJSXProvider = ({
const pairs = Object.entries(themes);
return pairs.length === 1 ? pluckThemeFromKeyPairTuple(pairs[0]) : themes[selectedThemeName];
- }, [themes, selected, themeOverride]);
+ }, [selected, themeOverride]);
if (!Provider) {
return (
diff --git a/code/addons/themes/src/preview.tsx b/code/addons/themes/src/preview.tsx
index 28c52bdbb57a..6f4a444e7f7c 100644
--- a/code/addons/themes/src/preview.tsx
+++ b/code/addons/themes/src/preview.tsx
@@ -1,7 +1,6 @@
import type { Renderer, ProjectAnnotations } from 'storybook/internal/types';
-import { GLOBAL_KEY } from './constants';
+import { GLOBAL_KEY as KEY } from './constants';
-export const globals: ProjectAnnotations['globals'] = {
- // Required to make sure SB picks this up from URL params
- [GLOBAL_KEY]: '',
+export const initialGlobals: ProjectAnnotations['initialGlobals'] = {
+ [KEY]: '',
};
diff --git a/code/addons/themes/src/theme-switcher.tsx b/code/addons/themes/src/theme-switcher.tsx
index bbc836177ea0..d57b9ce1e5e8 100644
--- a/code/addons/themes/src/theme-switcher.tsx
+++ b/code/addons/themes/src/theme-switcher.tsx
@@ -1,4 +1,4 @@
-import React, { Fragment, useMemo } from 'react';
+import React from 'react';
import {
useAddonState,
useChannel,
@@ -17,6 +17,7 @@ import {
THEMING_EVENTS,
DEFAULT_ADDON_STATE,
DEFAULT_THEME_PARAMETERS,
+ GLOBAL_KEY as KEY,
} from './constants';
const IconButtonLabel = styled.div(({ theme }) => ({
@@ -27,11 +28,11 @@ const hasMultipleThemes = (themesList: ThemeAddonState['themesList']) => themesL
const hasTwoThemes = (themesList: ThemeAddonState['themesList']) => themesList.length === 2;
export const ThemeSwitcher = React.memo(function ThemeSwitcher() {
- const { themeOverride } = useParameter(
+ const { themeOverride, disable } = useParameter(
PARAM_KEY,
DEFAULT_THEME_PARAMETERS
) as ThemeParameters;
- const [{ theme: selected }, updateGlobals] = useGlobals();
+ const [{ theme: selected }, updateGlobals, storyGlobals] = useGlobals();
const channel = addons.getChannel();
const fromLast = channel.last(THEMING_EVENTS.REGISTER_THEMES);
@@ -45,6 +46,8 @@ export const ThemeSwitcher = React.memo(function ThemeSwitcher() {
initializeThemeState
);
+ const isLocked = KEY in storyGlobals || !!themeOverride;
+
useChannel({
[THEMING_EVENTS.REGISTER_THEMES]: ({ themes, defaultTheme }) => {
updateState((state) => ({
@@ -55,21 +58,24 @@ export const ThemeSwitcher = React.memo(function ThemeSwitcher() {
},
});
- const label = useMemo(() => {
- if (themeOverride) {
- return <>Story override>;
- }
-
- const themeName = selected || themeDefault;
+ const themeName = selected || themeDefault;
+ let label = '';
+ if (isLocked) {
+ label = 'Story override';
+ } else if (themeName) {
+ label = `${themeName} theme`;
+ }
- return themeName && <>{`${themeName} theme`}>;
- }, [themeOverride, themeDefault, selected]);
+ if (disable) {
+ return null;
+ }
if (hasTwoThemes(themesList)) {
const currentTheme = selected || themeDefault;
const alternateTheme = themesList.find((theme) => theme !== currentTheme);
return (
- {label && {label}}
+ {label ? {label} : null}
);
}
if (hasMultipleThemes(themesList)) {
return (
-
- {
- return (
- ({
- id: theme,
- title: theme,
- active: selected === theme,
- onClick: () => {
- updateGlobals({ theme });
- onHide();
- },
- }))}
- />
- );
- }}
+ {
+ return (
+ ({
+ id: theme,
+ title: theme,
+ active: selected === theme,
+ onClick: () => {
+ updateGlobals({ theme });
+ onHide();
+ },
+ }))}
+ />
+ );
+ }}
+ >
+
-
-
- {label && {label}}
-
-
-
+
+ {label && {label}}
+
+
);
}
diff --git a/code/addons/themes/template/stories/decorators.stories.ts b/code/addons/themes/template/stories/decorators.stories.ts
new file mode 100644
index 000000000000..aba8f17d0e79
--- /dev/null
+++ b/code/addons/themes/template/stories/decorators.stories.ts
@@ -0,0 +1,96 @@
+import { global as globalThis } from '@storybook/global';
+import {
+ withThemeByClassName,
+ withThemeByDataAttribute,
+ withThemeFromJSXProvider,
+} from '@storybook/addon-themes';
+import { useEffect } from 'storybook/internal/preview-api';
+
+const cleanup = () => {
+ const existing = globalThis.document.querySelector('style[data-theme-css]');
+ if (existing) {
+ existing.remove();
+ }
+};
+
+const addStyleSheetDecorator = (storyFn: any) => {
+ useEffect(() => {
+ cleanup();
+
+ const sheet = globalThis.document.createElement('style');
+ sheet.setAttribute('data-theme-css', '');
+ sheet.textContent = `
+ [data-theme="theme-a"], .theme-a {
+ background-color: white;
+ color: black;
+ }
+ [data-theme="theme-b"], .theme-b {
+ background-color: black;
+ color: white;
+ }
+ `;
+
+ globalThis.document.body.appendChild(sheet);
+
+ return cleanup;
+ });
+
+ return storyFn();
+};
+
+export default {
+ component: globalThis.Components.Pre,
+ args: {
+ text: 'Testing the themes',
+ },
+ globals: {
+ sb_theme: 'light',
+ },
+ parameters: {
+ chromatic: { disable: true },
+ themes: { disable: false },
+ },
+ decorators: [addStyleSheetDecorator],
+};
+
+export const WithThemeByClassName = {
+ globals: {},
+ decorators: [
+ withThemeByClassName({
+ defaultTheme: 'a',
+ themes: { a: 'theme-a', b: 'theme-b' },
+ parentSelector: '#storybook-root > *',
+ }),
+ ],
+};
+
+export const WithThemeByDataAttribute = {
+ globals: {},
+ decorators: [
+ withThemeByDataAttribute({
+ defaultTheme: 'a',
+ themes: { a: 'theme-a', b: 'theme-b' },
+ parentSelector: '#storybook-root > *',
+ }),
+ ],
+};
+
+export const WithThemeFromJSXProvider = {
+ globals: {},
+ decorators: [
+ withThemeFromJSXProvider({
+ defaultTheme: 'a',
+ themes: { a: { custom: 'theme-a' }, b: { custom: 'theme-b' } },
+ Provider: ({ theme, children }: any) => {
+ // this is not was a normal provider looks like obviously, but this needs to work in non-react as well
+ // the timeout is to wait for the render to complete, as it's not possible to use the useEffect hook here
+ setTimeout(() => {
+ const element = globalThis.document.querySelector('#storybook-root > *');
+ element?.classList.remove('theme-a', 'theme-b');
+ element?.classList.add(theme.custom);
+ }, 16);
+ return children;
+ },
+ }),
+ ],
+};
diff --git a/code/addons/themes/template/stories/globals.stories.ts b/code/addons/themes/template/stories/globals.stories.ts
new file mode 100644
index 000000000000..3d193d1fa71a
--- /dev/null
+++ b/code/addons/themes/template/stories/globals.stories.ts
@@ -0,0 +1,63 @@
+import { global as globalThis } from '@storybook/global';
+import { withThemeByClassName } from '@storybook/addon-themes';
+import { useEffect } from 'storybook/internal/preview-api';
+
+const cleanup = () => {
+ const existing = globalThis.document.querySelector('style[data-theme-css]');
+ if (existing) {
+ existing.remove();
+ }
+};
+
+const addStyleSheetDecorator = (storyFn: any) => {
+ useEffect(() => {
+ cleanup();
+
+ const sheet = globalThis.document.createElement('style');
+ sheet.setAttribute('data-theme-css', '');
+ sheet.textContent = `
+ [data-theme="theme-a"], .theme-a {
+ background-color: white;
+ color: black;
+ }
+ [data-theme="theme-b"], .theme-b {
+ background-color: black;
+ color: white;
+ }
+ `;
+
+ globalThis.document.body.appendChild(sheet);
+
+ return cleanup;
+ });
+
+ return storyFn();
+};
+
+export default {
+ component: globalThis.Components.Pre,
+ args: {
+ text: 'Testing the themes',
+ },
+ parameters: {
+ chromatic: { disable: true },
+ themes: { disable: false },
+ },
+ globals: {
+ sb_theme: 'light',
+ },
+ decorators: [addStyleSheetDecorator],
+};
+
+export const SetGlobal = {
+ globals: {
+ theme: 'b',
+ },
+ decorators: [
+ withThemeByClassName({
+ defaultTheme: 'a',
+ themes: { a: 'theme-a', b: 'theme-b' },
+ parentSelector: '#storybook-root > *',
+ }),
+ ],
+};
diff --git a/code/addons/themes/template/stories/parameters.stories.ts b/code/addons/themes/template/stories/parameters.stories.ts
new file mode 100644
index 000000000000..f2906696859f
--- /dev/null
+++ b/code/addons/themes/template/stories/parameters.stories.ts
@@ -0,0 +1,80 @@
+import { global as globalThis } from '@storybook/global';
+import { withThemeByClassName } from '@storybook/addon-themes';
+import { useEffect } from 'storybook/internal/preview-api';
+
+const cleanup = () => {
+ const existing = globalThis.document.querySelector('style[data-theme-css]');
+ if (existing) {
+ existing.remove();
+ }
+};
+
+const addStyleSheetDecorator = (storyFn: any) => {
+ useEffect(() => {
+ cleanup();
+
+ const sheet = globalThis.document.createElement('style');
+ sheet.setAttribute('data-theme-css', '');
+ sheet.textContent = `
+ [data-theme="theme-a"], .theme-a {
+ background-color: white;
+ color: black;
+ }
+ [data-theme="theme-b"], .theme-b {
+ background-color: black;
+ color: white;
+ }
+ `;
+
+ globalThis.document.body.appendChild(sheet);
+
+ return cleanup;
+ });
+
+ return storyFn();
+};
+
+export default {
+ component: globalThis.Components.Pre,
+ args: {
+ text: 'Testing the themes',
+ },
+ globals: {
+ sb_theme: 'light',
+ },
+ parameters: {
+ chromatic: { disable: true },
+ themes: { disable: false },
+ },
+ decorators: [addStyleSheetDecorator],
+};
+
+export const SetOverride = {
+ parameters: {
+ themes: {
+ themeOverride: 'b',
+ },
+ },
+ decorators: [
+ withThemeByClassName({
+ defaultTheme: 'a',
+ themes: { a: 'theme-a', b: 'theme-b' },
+ parentSelector: '#storybook-root > *',
+ }),
+ ],
+};
+
+export const Disabled = {
+ parameters: {
+ themes: {
+ disable: true,
+ },
+ },
+ decorators: [
+ withThemeByClassName({
+ defaultTheme: 'a',
+ themes: { a: 'theme-a', b: 'theme-b' },
+ parentSelector: '#storybook-root > *',
+ }),
+ ],
+};
diff --git a/code/addons/toolbars/src/components/ToolbarMenuButton.tsx b/code/addons/toolbars/src/components/ToolbarMenuButton.tsx
index 684c92214f3c..bc093fabfade 100644
--- a/code/addons/toolbars/src/components/ToolbarMenuButton.tsx
+++ b/code/addons/toolbars/src/components/ToolbarMenuButton.tsx
@@ -4,6 +4,7 @@ import { Icons, IconButton, type IconsProps } from 'storybook/internal/component
interface ToolbarMenuButtonProps {
active: boolean;
+ disabled?: boolean;
title: string;
icon?: IconsProps['icon'];
description: string;
@@ -16,13 +17,19 @@ interface ToolbarMenuButtonProps {
export const ToolbarMenuButton: FC = ({
active,
+ disabled,
title,
icon,
description,
onClick,
}) => {
return (
-
+ {} : onClick}
+ >
{icon && }
{title ? `\xa0${title}` : null}
diff --git a/code/addons/toolbars/src/components/ToolbarMenuList.tsx b/code/addons/toolbars/src/components/ToolbarMenuList.tsx
index 607d23ec8823..b54f0df44e59 100644
--- a/code/addons/toolbars/src/components/ToolbarMenuList.tsx
+++ b/code/addons/toolbars/src/components/ToolbarMenuList.tsx
@@ -18,11 +18,12 @@ export const ToolbarMenuList: FC = withKeyboardCycle(
description,
toolbar: { icon: _icon, items, title: _title, preventDynamicIcon, dynamicTitle },
}) => {
- const [globals, updateGlobals] = useGlobals();
+ const [globals, updateGlobals, storyGlobals] = useGlobals();
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
const currentValue = globals[id];
const hasGlobalValue = !!currentValue;
+ const isOverridden = id in storyGlobals;
let icon = _icon;
let title = _title;
@@ -42,7 +43,7 @@ export const ToolbarMenuList: FC = withKeyboardCycle(
(value: string | undefined) => {
updateGlobals({ [id]: value });
},
- [currentValue, updateGlobals]
+ [id, updateGlobals]
);
return (
@@ -64,6 +65,7 @@ export const ToolbarMenuList: FC = withKeyboardCycle(
const listItem = ToolbarMenuListItem({
...item,
currentValue,
+ disabled: isOverridden,
onClick: () => {
handleItemClick(item.value);
onHide();
@@ -77,12 +79,15 @@ export const ToolbarMenuList: FC = withKeyboardCycle(
closeOnOutsideClick
onVisibleChange={setIsTooltipVisible}
>
-
+ {
+
+ }
);
}
diff --git a/code/addons/toolbars/src/components/ToolbarMenuListItem.tsx b/code/addons/toolbars/src/components/ToolbarMenuListItem.tsx
index a56f415b2d6d..39fa7df5dace 100644
--- a/code/addons/toolbars/src/components/ToolbarMenuListItem.tsx
+++ b/code/addons/toolbars/src/components/ToolbarMenuListItem.tsx
@@ -6,6 +6,7 @@ import type { ToolbarItem } from '../types';
export type ToolbarMenuListItemProps = {
currentValue: string;
onClick: () => void;
+ disabled?: boolean;
} & ToolbarItem;
export const ToolbarMenuListItem = ({
@@ -15,6 +16,7 @@ export const ToolbarMenuListItem = ({
icon,
hideIcon,
onClick,
+ disabled,
currentValue,
}: ToolbarMenuListItemProps) => {
const Icon = icon && ;
@@ -24,7 +26,7 @@ export const ToolbarMenuListItem = ({
active: currentValue === value,
right,
title,
- icon,
+ disabled,
onClick,
};
diff --git a/code/addons/toolbars/template/stories/globals.stories.ts b/code/addons/toolbars/template/stories/globals.stories.ts
index c82a6f8264d4..dd5b52ddece0 100644
--- a/code/addons/toolbars/template/stories/globals.stories.ts
+++ b/code/addons/toolbars/template/stories/globals.stories.ts
@@ -1,5 +1,5 @@
import { global as globalThis } from '@storybook/global';
-import type { PartialStoryFn, StoryContext } from 'storybook/internal/types';
+import type { DecoratorFunction } from 'storybook/internal/types';
const greetingForLocale = (locale: string) => {
switch (locale) {
@@ -20,14 +20,33 @@ const greetingForLocale = (locale: string) => {
export default {
component: globalThis.Components.Pre,
decorators: [
- (storyFn: PartialStoryFn, { globals }: StoryContext) => {
+ (storyFn, { globals }) => {
const object = {
...globals,
caption: `Locale is '${globals.locale}', so I say: ${greetingForLocale(globals.locale)}`,
};
return storyFn({ args: { object } });
},
- ],
+ ] satisfies DecoratorFunction[],
};
export const Basic = {};
+
+export const OverrideLocale = {
+ globals: {
+ locale: 'kr',
+ },
+};
+
+export const OverrideTheme = {
+ globals: {
+ sb_theme: 'dark',
+ },
+};
+
+export const OverrideBoth = {
+ globals: {
+ locale: 'kr',
+ sb_theme: 'dark',
+ },
+};
diff --git a/code/addons/toolbars/template/stories/preview.ts b/code/addons/toolbars/template/stories/preview.ts
index c37009553346..fe2f2531df93 100644
--- a/code/addons/toolbars/template/stories/preview.ts
+++ b/code/addons/toolbars/template/stories/preview.ts
@@ -1,5 +1,5 @@
export const globalTypes = {
- theme: {
+ sb_theme: {
name: 'Theme',
description: 'Global theme for components',
toolbar: {
@@ -42,3 +42,8 @@ export const globalTypes = {
},
},
};
+
+export const initialGlobals = {
+ sb_theme: 'light',
+ locale: 'en',
+};
diff --git a/code/addons/viewport/package.json b/code/addons/viewport/package.json
index 203cccc16fda..2ef52480c45a 100644
--- a/code/addons/viewport/package.json
+++ b/code/addons/viewport/package.json
@@ -69,7 +69,6 @@
},
"bundler": {
"exportEntries": [
- "./src/models/index.ts",
"./src/index.ts"
],
"managerEntries": [
diff --git a/code/addons/viewport/src/Tool.tsx b/code/addons/viewport/src/Tool.tsx
deleted file mode 100644
index ff2bfd3d155c..000000000000
--- a/code/addons/viewport/src/Tool.tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-import type { ReactNode, FC } from 'react';
-import React, { useState, Fragment, useEffect, useRef, memo } from 'react';
-import memoize from 'memoizerific';
-
-import { styled, Global, type Theme, withTheme } from 'storybook/internal/theming';
-
-import { IconButton, WithTooltip, TooltipLinkList } from 'storybook/internal/components';
-
-import { useStorybookApi, useParameter, useGlobals } from 'storybook/internal/manager-api';
-import { GrowIcon, TransferIcon } from '@storybook/icons';
-import { registerShortcuts } from './shortcuts';
-import { PARAM_KEY } from './constants';
-import { MINIMAL_VIEWPORTS } from './defaults';
-import type { ViewportAddonParameter, ViewportMap, ViewportStyles, Styles } from './models';
-
-interface ViewportItem {
- id: string;
- title: string;
- styles: Styles;
- type: 'desktop' | 'mobile' | 'tablet' | 'other';
- default?: boolean;
-}
-
-const toList = memoize(50)((items: ViewportMap): ViewportItem[] => [
- ...baseViewports,
- ...Object.entries(items).map(([id, { name, ...rest }]) => ({ ...rest, id, title: name })),
-]);
-
-const responsiveViewport: ViewportItem = {
- id: 'reset',
- title: 'Reset viewport',
- styles: null,
- type: 'other',
-};
-
-const baseViewports: ViewportItem[] = [responsiveViewport];
-
-const toLinks = memoize(50)((
- list: ViewportItem[],
- active: LinkBase,
- updateGlobals,
- close
-): Link[] => {
- return list
- .filter((i) => i.id !== responsiveViewport.id || active.id !== i.id)
- .map((i) => {
- return {
- ...i,
- onClick: () => {
- updateGlobals({ viewport: i.id });
- close();
- },
- };
- });
-});
-
-interface LinkBase {
- id: string;
- title: string;
- right?: ReactNode;
- type: 'desktop' | 'mobile' | 'tablet' | 'other';
- styles: ViewportStyles | ((s: ViewportStyles) => ViewportStyles) | null;
-}
-
-interface Link extends LinkBase {
- onClick: () => void;
-}
-
-const flip = ({ width, height, ...styles }: ViewportStyles) => ({
- ...styles,
- height: width,
- width: height,
-});
-
-const ActiveViewportSize = styled.div(() => ({
- display: 'inline-flex',
- alignItems: 'center',
-}));
-
-const ActiveViewportLabel = styled.div(({ theme }) => ({
- display: 'inline-block',
- textDecoration: 'none',
- padding: 10,
- fontWeight: theme.typography.weight.bold,
- fontSize: theme.typography.size.s2 - 1,
- lineHeight: '1',
- height: 40,
- border: 'none',
- borderTop: '3px solid transparent',
- borderBottom: '3px solid transparent',
- background: 'transparent',
-}));
-
-const IconButtonWithLabel = styled(IconButton)(() => ({
- display: 'inline-flex',
- alignItems: 'center',
-}));
-
-const IconButtonLabel = styled.div(({ theme }) => ({
- fontSize: theme.typography.size.s2 - 1,
- marginLeft: 10,
-}));
-
-const getStyles = (
- prevStyles: ViewportStyles | undefined,
- styles: Styles,
- isRotated: boolean
-): ViewportStyles | undefined => {
- if (styles === null) {
- return undefined;
- }
-
- const result = typeof styles === 'function' ? styles(prevStyles) : styles;
- return isRotated ? flip(result) : result;
-};
-
-export const ViewportTool: FC = memo(
- withTheme(({ theme }: { theme: Theme }) => {
- const [globals, updateGlobals] = useGlobals();
-
- const {
- viewports = MINIMAL_VIEWPORTS,
- defaultOrientation,
- defaultViewport,
- disable,
- } = useParameter(PARAM_KEY, {});
-
- const list = toList(viewports);
- const api = useStorybookApi();
- const [isTooltipVisible, setIsTooltipVisible] = useState(false);
-
- if (defaultViewport && !list.find((i) => i.id === defaultViewport)) {
- console.warn(
- `Cannot find "defaultViewport" of "${defaultViewport}" in addon-viewport configs, please check the "viewports" setting in the configuration.`
- );
- }
-
- useEffect(() => {
- registerShortcuts(api, globals, updateGlobals, Object.keys(viewports));
- }, [viewports, globals, globals.viewport, updateGlobals, api]);
-
- useEffect(() => {
- const defaultRotated = defaultOrientation === 'landscape';
-
- if (
- (defaultViewport && globals.viewport !== defaultViewport) ||
- (defaultOrientation && globals.viewportRotated !== defaultRotated)
- ) {
- updateGlobals({
- viewport: defaultViewport,
- viewportRotated: defaultRotated,
- });
- }
- // NOTE: we don't want to re-run this effect when `globals` changes
- // due to https://github.com/storybookjs/storybook/issues/26334
- //
- // Also, this *will* rerun every time you change story as the parameter is briefly `undefined`.
- // This behaviour is intentional, if a bit of a happy accident in implementation.
- //
- // Ultimately this process of "locking in" a parameter value should be
- // replaced by https://github.com/storybookjs/storybook/discussions/23347
- // or something similar.
- //
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [defaultOrientation, defaultViewport, updateGlobals]);
-
- const item =
- list.find((i) => i.id === globals.viewport) ||
- list.find((i) => i.id === defaultViewport) ||
- list.find((i) => i.default) ||
- responsiveViewport;
-
- const ref = useRef();
-
- const styles = getStyles(ref.current, item.styles, globals.viewportRotated);
-
- useEffect(() => {
- ref.current = styles;
- }, [item]);
-
- if (disable || Object.entries(viewports).length === 0) {
- return null;
- }
-
- return (
-
- (
-
- )}
- closeOnOutsideClick
- onVisibleChange={setIsTooltipVisible}
- >
- {
- updateGlobals({ viewport: responsiveViewport.id });
- }}
- >
-
- {styles ? (
-
- {globals.viewportRotated ? `${item.title} (L)` : `${item.title} (P)`}
-
- ) : null}
-
-
-
- {styles ? (
-
-
-
- {styles.width.replace('px', '')}
-
- {
- updateGlobals({ viewportRotated: !globals.viewportRotated });
- }}
- >
-
-
-
- {styles.height.replace('px', '')}
-
-
- ) : null}
-
- );
- })
-);
diff --git a/code/addons/viewport/src/components/Tool.tsx b/code/addons/viewport/src/components/Tool.tsx
new file mode 100644
index 000000000000..8ee3c3bfb966
--- /dev/null
+++ b/code/addons/viewport/src/components/Tool.tsx
@@ -0,0 +1,196 @@
+import React, { useState, Fragment, useEffect, type FC, useCallback } from 'react';
+
+import { Global } from 'storybook/internal/theming';
+import { IconButton, WithTooltip, TooltipLinkList } from 'storybook/internal/components';
+import { useGlobals, type API, useParameter } from 'storybook/internal/manager-api';
+
+import { GrowIcon, RefreshIcon, TransferIcon } from '@storybook/icons';
+import { PARAM_KEY as KEY } from '../constants';
+import { registerShortcuts } from '../shortcuts';
+import {
+ IconButtonWithLabel,
+ IconButtonLabel,
+ ActiveViewportSize,
+ ActiveViewportLabel,
+ iconsMap,
+ emptyViewportMap,
+} from '../utils';
+import { responsiveViewport } from '../responsiveViewport';
+import type { Config, Viewport, ViewportMap, GlobalState, GlobalStateUpdate } from '../types';
+
+interface PureProps {
+ item: Viewport;
+ updateGlobals: ReturnType['1'];
+ setIsTooltipVisible: React.Dispatch>;
+ viewportMap: ViewportMap;
+ viewportName: any;
+ isLocked: boolean;
+ isActive: boolean;
+ isRotated: any;
+ width: string;
+ height: string;
+}
+
+type Link = Parameters['0']['links'][0];
+
+export const ViewportTool: FC<{ api: API }> = ({ api }) => {
+ const config = useParameter(KEY);
+ const [globals, updateGlobals, storyGlobals] = useGlobals();
+ const [isTooltipVisible, setIsTooltipVisible] = useState(false);
+
+ const { options = emptyViewportMap, disable } = config || {};
+ const data = globals?.[KEY] || {};
+ const viewportName: string = data.value;
+ const isRotated: boolean = data.isRotated;
+
+ const item = options[viewportName] || responsiveViewport;
+ const isActive = isTooltipVisible || item !== responsiveViewport;
+ const isLocked = KEY in storyGlobals;
+
+ const length = Object.keys(options).length;
+
+ useEffect(() => {
+ registerShortcuts(api, viewportName, updateGlobals, Object.keys(options));
+ }, [options, viewportName, updateGlobals, api]);
+
+ if (item.styles === null || !options || length < 1) {
+ return null;
+ }
+
+ if (typeof item.styles === 'function') {
+ console.warn(
+ 'Addon Viewport no longer supports dynamic styles using a function, use css calc() instead'
+ );
+ return null;
+ }
+
+ const width = isRotated ? item.styles.height : item.styles.width;
+ const height = isRotated ? item.styles.width : item.styles.height;
+
+ if (disable) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+const Pure = React.memo(function PureTool(props: PureProps) {
+ const {
+ item,
+ viewportMap,
+ viewportName,
+ isRotated,
+ updateGlobals,
+ setIsTooltipVisible,
+ isLocked,
+ isActive,
+ width,
+ height,
+ } = props;
+
+ const update = useCallback(
+ (input: GlobalStateUpdate) => updateGlobals({ [KEY]: input }),
+ [updateGlobals]
+ );
+
+ return (
+
+ (
+ 0 && item !== responsiveViewport
+ ? [
+ {
+ id: 'reset',
+ title: 'Reset viewport',
+ icon: ,
+ onClick: () => {
+ update({ value: undefined, isRotated: false });
+ onHide();
+ },
+ },
+ ]
+ : []),
+ ...Object.entries(viewportMap).map(([k, value]) => ({
+ id: k,
+ title: value.name,
+ icon: iconsMap[value.type],
+ active: k === viewportName,
+ onClick: () => {
+ update({ value: k, isRotated: false });
+ onHide();
+ },
+ })),
+ ]}
+ />
+ )}
+ closeOnOutsideClick
+ onVisibleChange={setIsTooltipVisible}
+ >
+ {
+ update({ value: undefined, isRotated: false });
+ }}
+ >
+
+ {item !== responsiveViewport ? (
+
+ {item.name} {isRotated ? `(L)` : `(P)`}
+
+ ) : null}
+
+
+
+
+
+ {item !== responsiveViewport ? (
+
+
+ {width.replace('px', '')}
+
+ {!isLocked ? (
+ {
+ update({ value: viewportName, isRotated: !isRotated });
+ }}
+ >
+
+
+ ) : (
+ '/'
+ )}
+
+ {height.replace('px', '')}
+
+
+ ) : null}
+
+ );
+});
diff --git a/code/addons/viewport/src/defaults.ts b/code/addons/viewport/src/defaults.ts
index fdc6246eb897..4190ccb7f3cc 100644
--- a/code/addons/viewport/src/defaults.ts
+++ b/code/addons/viewport/src/defaults.ts
@@ -1,4 +1,4 @@
-import type { ViewportMap } from './models';
+import type { ViewportMap } from './types';
export const INITIAL_VIEWPORTS: ViewportMap = {
iphone5: {
diff --git a/code/addons/viewport/src/index.ts b/code/addons/viewport/src/index.ts
index 79e0216d7923..fac593a2f2dd 100644
--- a/code/addons/viewport/src/index.ts
+++ b/code/addons/viewport/src/index.ts
@@ -1,2 +1,2 @@
export * from './defaults';
-export type * from './models';
+export type * from './types';
diff --git a/code/addons/viewport/src/legacy/ToolLegacy.tsx b/code/addons/viewport/src/legacy/ToolLegacy.tsx
new file mode 100644
index 000000000000..c3eb3f59f7ae
--- /dev/null
+++ b/code/addons/viewport/src/legacy/ToolLegacy.tsx
@@ -0,0 +1,243 @@
+import type { ReactNode, FC } from 'react';
+import React, { useState, Fragment, useEffect, useRef, memo } from 'react';
+import memoize from 'memoizerific';
+
+import { styled, Global } from 'storybook/internal/theming';
+
+import { IconButton, WithTooltip, TooltipLinkList } from 'storybook/internal/components';
+
+import { useStorybookApi, useParameter, useGlobals } from 'storybook/internal/manager-api';
+import { GrowIcon, TransferIcon } from '@storybook/icons';
+import { registerShortcuts } from '../shortcuts';
+import { PARAM_KEY } from '../constants';
+import { MINIMAL_VIEWPORTS } from '../defaults';
+import type { ViewportMap, ViewportStyles, Styles } from '../types';
+import type { ViewportAddonParameter } from './ViewportAddonParameter';
+
+interface ViewportItem {
+ id: string;
+ title: string;
+ styles: Styles;
+ type: 'desktop' | 'mobile' | 'tablet' | 'other';
+ default?: boolean;
+}
+
+const toList = memoize(50)((items: ViewportMap): ViewportItem[] => [
+ ...baseViewports,
+ ...Object.entries(items).map(([id, { name, ...rest }]) => ({ ...rest, id, title: name })),
+]);
+
+const responsiveViewport: ViewportItem = {
+ id: 'reset',
+ title: 'Reset viewport',
+ styles: null,
+ type: 'other',
+};
+
+const baseViewports: ViewportItem[] = [responsiveViewport];
+
+const toLinks = memoize(50)((
+ list: ViewportItem[],
+ active: LinkBase,
+ updateGlobals,
+ close
+): Link[] => {
+ return list
+ .filter((i) => i.id !== responsiveViewport.id || active.id !== i.id)
+ .map((i) => {
+ return {
+ ...i,
+ onClick: () => {
+ updateGlobals({ viewport: i.id });
+ close();
+ },
+ };
+ });
+});
+
+interface LinkBase {
+ id: string;
+ title: string;
+ right?: ReactNode;
+ type: 'desktop' | 'mobile' | 'tablet' | 'other';
+ styles: ViewportStyles | ((s: ViewportStyles) => ViewportStyles) | null;
+}
+
+interface Link extends LinkBase {
+ onClick: () => void;
+}
+
+const flip = ({ width, height, ...styles }: ViewportStyles) => ({
+ ...styles,
+ height: width,
+ width: height,
+});
+
+const ActiveViewportSize = styled.div(() => ({
+ display: 'inline-flex',
+ alignItems: 'center',
+}));
+
+const ActiveViewportLabel = styled.div(({ theme }) => ({
+ display: 'inline-block',
+ textDecoration: 'none',
+ padding: 10,
+ fontWeight: theme.typography.weight.bold,
+ fontSize: theme.typography.size.s2 - 1,
+ lineHeight: '1',
+ height: 40,
+ border: 'none',
+ borderTop: '3px solid transparent',
+ borderBottom: '3px solid transparent',
+ background: 'transparent',
+}));
+
+const IconButtonWithLabel = styled(IconButton)(() => ({
+ display: 'inline-flex',
+ alignItems: 'center',
+}));
+
+const IconButtonLabel = styled.div(({ theme }) => ({
+ fontSize: theme.typography.size.s2 - 1,
+ marginLeft: 10,
+}));
+
+const getStyles = (
+ prevStyles: ViewportStyles | undefined,
+ styles: Styles,
+ isRotated: boolean
+): ViewportStyles | undefined => {
+ if (styles === null) {
+ return undefined;
+ }
+
+ const result = typeof styles === 'function' ? styles(prevStyles) : styles;
+ return isRotated ? flip(result) : result;
+};
+
+export const ViewportToolLegacy: FC = memo(function Tool() {
+ const [globals, updateGlobals] = useGlobals();
+
+ const {
+ viewports = MINIMAL_VIEWPORTS,
+ defaultOrientation,
+ defaultViewport,
+ disable,
+ } = useParameter(PARAM_KEY, {});
+
+ const list = toList(viewports);
+ const api = useStorybookApi();
+ const [isTooltipVisible, setIsTooltipVisible] = useState(false);
+
+ if (defaultViewport && !list.find((i) => i.id === defaultViewport)) {
+ console.warn(
+ `Cannot find "defaultViewport" of "${defaultViewport}" in addon-viewport configs, please check the "viewports" setting in the configuration.`
+ );
+ }
+
+ useEffect(() => {
+ registerShortcuts(api, globals, updateGlobals, Object.keys(viewports));
+ }, [viewports, globals, globals.viewport, updateGlobals, api]);
+
+ useEffect(() => {
+ const defaultRotated = defaultOrientation === 'landscape';
+
+ if (
+ (defaultViewport && globals.viewport !== defaultViewport) ||
+ (defaultOrientation && globals.viewportRotated !== defaultRotated)
+ ) {
+ updateGlobals({
+ viewport: defaultViewport,
+ viewportRotated: defaultRotated,
+ });
+ }
+ // NOTE: we don't want to re-run this effect when `globals` changes
+ // due to https://github.com/storybookjs/storybook/issues/26334
+ //
+ // Also, this *will* rerun every time you change story as the parameter is briefly `undefined`.
+ // This behavior is intentional, if a bit of a happy accident in implementation.
+ //
+ // Ultimately this process of "locking in" a parameter value should be
+ // replaced by https://github.com/storybookjs/storybook/discussions/23347
+ // or something similar.
+ //
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [defaultOrientation, defaultViewport, updateGlobals]);
+
+ const item =
+ list.find((i) => i.id === globals.viewport) ||
+ list.find((i) => i.id === defaultViewport) ||
+ list.find((i) => i.default) ||
+ responsiveViewport;
+
+ const ref = useRef();
+
+ const styles = getStyles(ref.current, item.styles, globals.viewportRotated);
+
+ useEffect(() => {
+ ref.current = styles;
+ }, [item]);
+
+ if (disable || Object.entries(viewports).length === 0) {
+ return null;
+ }
+
+ return (
+
+ (
+
+ )}
+ closeOnOutsideClick
+ onVisibleChange={setIsTooltipVisible}
+ >
+ {
+ updateGlobals({ viewport: responsiveViewport.id });
+ }}
+ >
+
+ {styles ? (
+
+ {globals.viewportRotated ? `${item.title} (L)` : `${item.title} (P)`}
+
+ ) : null}
+
+
+
+ {styles ? (
+
+
+
+ {styles.width.replace('px', '')}
+
+ {
+ updateGlobals({ viewportRotated: !globals.viewportRotated });
+ }}
+ >
+
+
+
+ {styles.height.replace('px', '')}
+
+
+ ) : null}
+
+ );
+});
diff --git a/code/addons/viewport/src/models/ViewportAddonParameter.ts b/code/addons/viewport/src/legacy/ViewportAddonParameter.ts
similarity index 71%
rename from code/addons/viewport/src/models/ViewportAddonParameter.ts
rename to code/addons/viewport/src/legacy/ViewportAddonParameter.ts
index 875ca46fbc7d..1d68f691f90b 100644
--- a/code/addons/viewport/src/models/ViewportAddonParameter.ts
+++ b/code/addons/viewport/src/legacy/ViewportAddonParameter.ts
@@ -1,5 +1,6 @@
-import type { ViewportMap } from './Viewport';
+import type { ViewportMap } from '../types';
+// TODO: remove at 9.0
export interface ViewportAddonParameter {
disable?: boolean;
defaultOrientation?: 'portrait' | 'landscape';
diff --git a/code/addons/viewport/src/manager.tsx b/code/addons/viewport/src/manager.tsx
index ee597958c1e6..e7425aff76d3 100644
--- a/code/addons/viewport/src/manager.tsx
+++ b/code/addons/viewport/src/manager.tsx
@@ -3,13 +3,15 @@ import { addons, types } from 'storybook/internal/manager-api';
import { ADDON_ID } from './constants';
-import { ViewportTool } from './Tool';
+import { ViewportToolLegacy } from './legacy/ToolLegacy';
+import { ViewportTool } from './components/Tool';
-addons.register(ADDON_ID, () => {
+addons.register(ADDON_ID, (api) => {
addons.add(ADDON_ID, {
title: 'viewport / media-queries',
type: types.TOOL,
match: ({ viewMode, tabId }) => viewMode === 'story' && !tabId,
- render: () => ,
+ render: () =>
+ FEATURES?.viewportStoryGlobals ? : ,
});
});
diff --git a/code/addons/viewport/src/models/Viewport.ts b/code/addons/viewport/src/models/Viewport.ts
deleted file mode 100644
index 7b3c98e069c9..000000000000
--- a/code/addons/viewport/src/models/Viewport.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-export type Styles = ViewportStyles | ((s: ViewportStyles | undefined) => ViewportStyles) | null;
-
-export interface Viewport {
- name: string;
- styles: Styles;
- type: 'desktop' | 'mobile' | 'tablet' | 'other';
-}
-
-export interface ViewportStyles {
- height: string;
- width: string;
-}
-
-export interface ViewportMap {
- [key: string]: Viewport;
-}
diff --git a/code/addons/viewport/src/models/index.ts b/code/addons/viewport/src/models/index.ts
deleted file mode 100644
index 7eb7ed1a6942..000000000000
--- a/code/addons/viewport/src/models/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export type * from './Viewport';
-export type * from './ViewportAddonParameter';
diff --git a/code/addons/viewport/src/preview.ts b/code/addons/viewport/src/preview.ts
index a429414748df..a59790415d2f 100644
--- a/code/addons/viewport/src/preview.ts
+++ b/code/addons/viewport/src/preview.ts
@@ -1 +1,11 @@
-export const initialGlobals = { viewport: 'reset', viewportRotated: false };
+import { PARAM_KEY as KEY } from './constants';
+import type { GlobalState } from './types';
+
+const modern: Record = {
+ [KEY]: { value: undefined, isRotated: false },
+};
+
+// TODO: remove in 9.0
+const legacy = { viewport: 'reset', viewportRotated: false };
+
+export const initialGlobals = FEATURES?.viewportStoryGlobals ? modern : legacy;
diff --git a/code/addons/viewport/src/responsiveViewport.tsx b/code/addons/viewport/src/responsiveViewport.tsx
new file mode 100644
index 000000000000..ce108cf07d9a
--- /dev/null
+++ b/code/addons/viewport/src/responsiveViewport.tsx
@@ -0,0 +1,10 @@
+import type { Viewport } from './types';
+
+export const responsiveViewport: Viewport = {
+ name: 'Reset viewport',
+ styles: {
+ height: '100%',
+ width: '100%',
+ },
+ type: 'desktop',
+};
diff --git a/code/addons/viewport/src/shortcuts.ts b/code/addons/viewport/src/shortcuts.ts
index b8869f48327c..f53d460b81bd 100644
--- a/code/addons/viewport/src/shortcuts.ts
+++ b/code/addons/viewport/src/shortcuts.ts
@@ -21,7 +21,7 @@ const getPreviousViewport = (viewportsKeys: string[], current: string): string =
export const registerShortcuts = async (
api: API,
- globals: any,
+ viewport: any,
updateGlobals: any,
viewportsKeys: string[]
) => {
@@ -31,7 +31,7 @@ export const registerShortcuts = async (
actionName: 'previous',
action: () => {
updateGlobals({
- viewport: getPreviousViewport(viewportsKeys, globals.viewport),
+ viewport: getPreviousViewport(viewportsKeys, viewport),
});
},
});
@@ -42,7 +42,7 @@ export const registerShortcuts = async (
actionName: 'next',
action: () => {
updateGlobals({
- viewport: getNextViewport(viewportsKeys, globals.viewport),
+ viewport: getNextViewport(viewportsKeys, viewport),
});
},
});
diff --git a/code/addons/viewport/src/types.ts b/code/addons/viewport/src/types.ts
new file mode 100644
index 000000000000..33f186c2c3c1
--- /dev/null
+++ b/code/addons/viewport/src/types.ts
@@ -0,0 +1,28 @@
+// TODO: remove the function type from styles in 9.0
+export type Styles = ViewportStyles | ((s: ViewportStyles | undefined) => ViewportStyles) | null;
+
+export interface Viewport {
+ name: string;
+ styles: Styles;
+ type: 'desktop' | 'mobile' | 'tablet' | 'other';
+}
+export interface ModernViewport {
+ name: string;
+ styles: ViewportStyles;
+ type: 'desktop' | 'mobile' | 'tablet' | 'other';
+}
+
+export interface ViewportStyles {
+ height: string;
+ width: string;
+}
+
+export type ViewportMap = Record;
+
+export interface Config {
+ options: Record;
+ disable: boolean;
+}
+
+export type GlobalState = { value: string | undefined; isRotated: boolean };
+export type GlobalStateUpdate = Partial;
diff --git a/code/addons/viewport/src/typings.d.ts b/code/addons/viewport/src/typings.d.ts
new file mode 100644
index 000000000000..22eb9c03a481
--- /dev/null
+++ b/code/addons/viewport/src/typings.d.ts
@@ -0,0 +1 @@
+declare var FEATURES: import('storybook/internal/types').StorybookConfigRaw['features'];
diff --git a/code/addons/viewport/src/utils.tsx b/code/addons/viewport/src/utils.tsx
new file mode 100644
index 000000000000..be4f3cb16096
--- /dev/null
+++ b/code/addons/viewport/src/utils.tsx
@@ -0,0 +1,45 @@
+import React, { Fragment } from 'react';
+
+import { styled } from 'storybook/internal/theming';
+import { IconButton } from 'storybook/internal/components';
+
+import { BrowserIcon, MobileIcon, TabletIcon } from '@storybook/icons';
+import type { Viewport, ViewportMap } from './types';
+
+export const ActiveViewportSize = styled.div(() => ({
+ display: 'inline-flex',
+ alignItems: 'center',
+}));
+
+export const ActiveViewportLabel = styled.div(({ theme }) => ({
+ display: 'inline-block',
+ textDecoration: 'none',
+ padding: 10,
+ fontWeight: theme.typography.weight.bold,
+ fontSize: theme.typography.size.s2 - 1,
+ lineHeight: '1',
+ height: 40,
+ border: 'none',
+ borderTop: '3px solid transparent',
+ borderBottom: '3px solid transparent',
+ background: 'transparent',
+}));
+
+export const IconButtonWithLabel = styled(IconButton)(() => ({
+ display: 'inline-flex',
+ alignItems: 'center',
+}));
+
+export const IconButtonLabel = styled.div(({ theme }) => ({
+ fontSize: theme.typography.size.s2 - 1,
+ marginLeft: 10,
+}));
+
+export const iconsMap: Record = {
+ desktop: ,
+ mobile: ,
+ tablet: ,
+ other: ,
+};
+
+export const emptyViewportMap: ViewportMap = {};
diff --git a/code/addons/viewport/template/stories/globals.stories.ts b/code/addons/viewport/template/stories/globals.stories.ts
new file mode 100644
index 000000000000..621bf0b02c62
--- /dev/null
+++ b/code/addons/viewport/template/stories/globals.stories.ts
@@ -0,0 +1,61 @@
+import { global as globalThis } from '@storybook/global';
+import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport';
+
+const first = Object.keys(MINIMAL_VIEWPORTS)[0];
+
+export default {
+ component: globalThis.Components.Pre,
+ args: {
+ text: 'Testing the viewport',
+ },
+ parameters: {
+ chromatic: { disable: true },
+ },
+};
+
+export const Unset = {
+ globals: {},
+};
+
+export const Selected = {
+ globals: {
+ viewport: {
+ value: first,
+ isRotated: false,
+ },
+ },
+};
+
+export const Orientation = {
+ globals: {
+ viewport: {
+ value: first,
+ isRotated: true,
+ },
+ },
+};
+
+export const Invalid = {
+ globals: {
+ viewport: {
+ value: 'phone',
+ isRotated: false,
+ },
+ },
+};
+
+export const NoRationDefined = {
+ globals: {
+ viewport: {
+ value: first,
+ },
+ },
+};
+
+export const Disabled = {
+ parameters: {
+ viewport: {
+ disable: true,
+ },
+ },
+};
diff --git a/code/addons/viewport/template/stories/parameters.stories.ts b/code/addons/viewport/template/stories/parameters.stories.ts
index 4aba0bd2970a..f98de1eda0c4 100644
--- a/code/addons/viewport/template/stories/parameters.stories.ts
+++ b/code/addons/viewport/template/stories/parameters.stories.ts
@@ -1,6 +1,11 @@
import { global as globalThis } from '@storybook/global';
import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport';
+// these stories only work with `viewportStoryGlobals` set to false
+// because the `default` prop is dropped and because, `values` changed to `options` and is now an object
+
+const first = Object.keys(MINIMAL_VIEWPORTS)[0];
+
export default {
component: globalThis.Components.Button,
args: {
@@ -21,7 +26,7 @@ export const Basic = {
export const Selected = {
parameters: {
viewport: {
- defaultViewport: Object.keys(MINIMAL_VIEWPORTS)[0],
+ defaultViewport: first,
},
},
};
@@ -29,7 +34,7 @@ export const Selected = {
export const Orientation = {
parameters: {
viewport: {
- defaultViewport: Object.keys(MINIMAL_VIEWPORTS)[0],
+ defaultViewport: first,
defaultOrientation: 'landscape',
},
},
diff --git a/code/core/package.json b/code/core/package.json
index 0f84c7d2e674..cfac682c85a1 100644
--- a/code/core/package.json
+++ b/code/core/package.json
@@ -152,6 +152,16 @@
"import": "./dist/preview/globals.js",
"require": "./dist/preview/globals.cjs"
},
+ "./cli": {
+ "types": "./dist/cli/index.d.ts",
+ "import": "./dist/cli/index.js",
+ "require": "./dist/cli/index.cjs"
+ },
+ "./cli/bin": {
+ "types": "./dist/cli/bin/index.d.ts",
+ "import": "./dist/cli/bin/index.js",
+ "require": "./dist/cli/bin/index.cjs"
+ },
"./package.json": "./package.json"
},
"main": "dist/index.cjs",
@@ -239,6 +249,12 @@
],
"preview/globals": [
"./dist/preview/globals.d.ts"
+ ],
+ "cli": [
+ "./dist/cli/index.d.ts"
+ ],
+ "cli/bin": [
+ "./dist/cli/bin/index.d.ts"
]
}
},
@@ -262,7 +278,7 @@
"express": "^4.19.2",
"process": "^0.11.10",
"recast": "^0.23.5",
- "util": "^0.12.4",
+ "semver": "^7.6.2",
"ws": "^8.2.3"
},
"devDependencies": {
@@ -280,7 +296,7 @@
"@emotion/styled": "^11.11.0",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
- "@ndelangen/fs-extra-unified": "^1.0.3",
+ "@ndelangen/get-tarball": "^3.0.7",
"@popperjs/core": "^2.6.0",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-scroll-area": "^1.0.5",
@@ -304,10 +320,11 @@
"@types/picomatch": "^2.3.0",
"@types/prettier": "^3.0.0",
"@types/pretty-hrtime": "^1.0.0",
+ "@types/prompts": "^2.0.9",
"@types/qs": "^6",
"@types/react-syntax-highlighter": "11.0.5",
"@types/react-transition-group": "^4",
- "@types/semver": "^7.3.4",
+ "@types/semver": "^7.5.8",
"@types/ws": "^8",
"@vitest/utils": "^1.3.1",
"@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.10",
@@ -323,6 +340,7 @@
"chai": "^4.4.1",
"chalk": "^5.3.0",
"cli-table3": "^0.6.1",
+ "commander": "^6.2.1",
"comment-parser": "^1.4.1",
"compression": "^1.7.4",
"copy-to-clipboard": "^3.3.1",
@@ -330,6 +348,7 @@
"css": "^3.0.0",
"deep-object-diff": "^1.1.0",
"dequal": "^2.0.2",
+ "detect-indent": "^7.0.1",
"detect-package-manager": "^3.0.2",
"detect-port": "^1.3.0",
"diff": "^5.2.0",
@@ -347,12 +366,14 @@
"flush-promises": "^1.0.2",
"fs-extra": "^11.1.0",
"fuse.js": "^3.6.1",
+ "get-npm-tarball-url": "^2.0.3",
"glob": "^10.0.0",
"globby": "^14.0.1",
"handlebars": "^4.7.7",
"js-yaml": "^4.1.0",
"jsdoc-type-pratt-parser": "^4.0.0",
"lazy-universal-dotenv": "^4.0.0",
+ "leven": "^4.0.0",
"lodash": "^4.17.21",
"markdown-to-jsx": "^7.4.5",
"memoizerific": "^1.11.3",
@@ -378,9 +399,9 @@
"react-transition-group": "^4.4.5",
"require-from-string": "^2.0.2",
"resolve-from": "^5.0.0",
- "semver": "^7.3.7",
"slash": "^5.0.0",
"store2": "^2.14.2",
+ "strip-json-comments": "^5.0.1",
"telejson": "^7.2.0",
"tiny-invariant": "^1.3.1",
"tinyspy": "^2.2.0",
diff --git a/code/core/scripts/entries.ts b/code/core/scripts/entries.ts
index a618dac3b623..a7062abf9a6c 100644
--- a/code/core/scripts/entries.ts
+++ b/code/core/scripts/entries.ts
@@ -36,6 +36,8 @@ export const getEntries = (cwd: string) => {
define('src/manager/globals-module-info.ts', ['node'], true),
define('src/manager/globals.ts', ['node'], true),
define('src/preview/globals.ts', ['node'], true),
+ define('src/cli/index.ts', ['node'], true),
+ define('src/cli/bin/index.ts', ['node'], true),
];
};
diff --git a/code/lib/cli/src/NpmOptions.ts b/code/core/src/cli/NpmOptions.ts
similarity index 100%
rename from code/lib/cli/src/NpmOptions.ts
rename to code/core/src/cli/NpmOptions.ts
diff --git a/code/lib/cli/src/generators/ANGULAR/helpers.ts b/code/core/src/cli/angular/helpers.ts
similarity index 98%
rename from code/lib/cli/src/generators/ANGULAR/helpers.ts
rename to code/core/src/cli/angular/helpers.ts
index d4ab56b67a62..9d651fa60cae 100644
--- a/code/lib/cli/src/generators/ANGULAR/helpers.ts
+++ b/code/core/src/cli/angular/helpers.ts
@@ -2,7 +2,7 @@ import fs from 'fs';
import { join } from 'path';
import prompts from 'prompts';
import { dedent } from 'ts-dedent';
-import { MissingAngularJsonError } from 'storybook/internal/server-errors';
+import { MissingAngularJsonError } from '@storybook/core/server-errors';
import boxen from 'boxen';
import { logger } from '@storybook/core/node-logger';
diff --git a/code/core/src/cli/bin/index.ts b/code/core/src/cli/bin/index.ts
new file mode 100644
index 000000000000..512dc963924b
--- /dev/null
+++ b/code/core/src/cli/bin/index.ts
@@ -0,0 +1,143 @@
+import program from 'commander';
+import chalk from 'chalk';
+import leven from 'leven';
+import { findPackageSync } from 'fd-package-json';
+import invariant from 'tiny-invariant';
+
+import { logger } from '@storybook/core/node-logger';
+import { addToGlobalContext } from '@storybook/core/telemetry';
+import { parseList, getEnvConfig, versions } from '@storybook/core/common';
+
+import { dev } from '../dev';
+import { build } from '../build';
+
+addToGlobalContext('cliVersion', versions.storybook);
+
+const pkg = findPackageSync(__dirname);
+invariant(pkg, 'Failed to find the closest package.json file.');
+const consoleLogger = console;
+
+const command = (name: string) =>
+ program
+ .command(name)
+ .option(
+ '--disable-telemetry',
+ 'Disable sending telemetry data',
+ // default value is false, but if the user sets STORYBOOK_DISABLE_TELEMETRY, it can be true
+ process.env.STORYBOOK_DISABLE_TELEMETRY && process.env.STORYBOOK_DISABLE_TELEMETRY !== 'false'
+ )
+ .option('--debug', 'Get more logs in debug mode', false)
+ .option('--enable-crash-reports', 'Enable sending crash reports to telemetry data');
+
+command('dev')
+ .option('-p, --port ', 'Port to run Storybook', (str) => parseInt(str, 10))
+ .option('-h, --host ', 'Host to run Storybook')
+ .option('-c, --config-dir ', 'Directory where to load Storybook configurations from')
+ .option(
+ '--https',
+ 'Serve Storybook over HTTPS. Note: You must provide your own certificate information.'
+ )
+ .option(
+ '--ssl-ca ',
+ 'Provide an SSL certificate authority. (Optional with --https, required if using a self-signed certificate)',
+ parseList
+ )
+ .option('--ssl-cert ', 'Provide an SSL certificate. (Required with --https)')
+ .option('--ssl-key ', 'Provide an SSL key. (Required with --https)')
+ .option('--smoke-test', 'Exit after successful start')
+ .option('--ci', "CI mode (skip interactive prompts, don't open browser)")
+ .option('--no-open', 'Do not open Storybook automatically in the browser')
+ .option('--loglevel ', 'Control level of logging during build')
+ .option('--quiet', 'Suppress verbose build output')
+ .option('--no-version-updates', 'Suppress update check', true)
+ .option('--debug-webpack', 'Display final webpack configurations for debugging purposes')
+ .option(
+ '--webpack-stats-json [directory]',
+ 'Write Webpack stats JSON to disk (synonym for `--stats-json`)'
+ )
+ .option('--stats-json [directory]', 'Write stats JSON to disk')
+ .option(
+ '--preview-url ',
+ 'Disables the default storybook preview and lets your use your own'
+ )
+ .option('--force-build-preview', 'Build the preview iframe even if you are using --preview-url')
+ .option('--docs', 'Build a documentation-only site using addon-docs')
+ .option('--exact-port', 'Exit early if the desired port is not available')
+ .option(
+ '--initial-path [path]',
+ 'URL path to be appended when visiting Storybook for the first time'
+ )
+ .action(async (options) => {
+ logger.setLevel(program.loglevel);
+ consoleLogger.log(chalk.bold(`${pkg.name} v${pkg.version}`) + chalk.reset('\n'));
+
+ // The key is the field created in `options` variable for
+ // each command line argument. Value is the env variable.
+ getEnvConfig(options, {
+ port: 'SBCONFIG_PORT',
+ host: 'SBCONFIG_HOSTNAME',
+ staticDir: 'SBCONFIG_STATIC_DIR',
+ configDir: 'SBCONFIG_CONFIG_DIR',
+ ci: 'CI',
+ });
+
+ if (parseInt(`${options.port}`, 10)) {
+ options.port = parseInt(`${options.port}`, 10);
+ }
+
+ await dev({ ...options, packageJson: pkg }).catch(() => process.exit(1));
+ });
+
+command('build')
+ .option('-o, --output-dir ', 'Directory where to store built files')
+ .option('-c, --config-dir ', 'Directory where to load Storybook configurations from')
+ .option('--quiet', 'Suppress verbose build output')
+ .option('--loglevel ', 'Control level of logging during build')
+ .option('--debug-webpack', 'Display final webpack configurations for debugging purposes')
+ .option(
+ '--webpack-stats-json [directory]',
+ 'Write Webpack stats JSON to disk (synonym for `--stats-json`)'
+ )
+ .option('--stats-json [directory]', 'Write stats JSON to disk')
+ .option(
+ '--preview-url ',
+ 'Disables the default storybook preview and lets your use your own'
+ )
+ .option('--force-build-preview', 'Build the preview iframe even if you are using --preview-url')
+ .option('--docs', 'Build a documentation-only site using addon-docs')
+ .option('--test', 'Build stories optimized for testing purposes.')
+ .action(async (options) => {
+ process.env.NODE_ENV = process.env.NODE_ENV || 'production';
+ logger.setLevel(program.loglevel);
+ consoleLogger.log(chalk.bold(`${pkg.name} v${pkg.version}\n`));
+
+ // The key is the field created in `options` variable for
+ // each command line argument. Value is the env variable.
+ getEnvConfig(options, {
+ staticDir: 'SBCONFIG_STATIC_DIR',
+ outputDir: 'SBCONFIG_OUTPUT_DIR',
+ configDir: 'SBCONFIG_CONFIG_DIR',
+ });
+
+ await build({
+ ...options,
+ packageJson: pkg,
+ test: !!options.test || process.env.SB_TESTBUILD === 'true',
+ }).catch(() => process.exit(1));
+ });
+
+program.on('command:*', ([invalidCmd]) => {
+ consoleLogger.error(
+ ' Invalid command: %s.\n See --help for a list of available commands.',
+ invalidCmd
+ );
+ // eslint-disable-next-line no-underscore-dangle
+ const availableCommands = program.commands.map((cmd) => cmd._name);
+ const suggestion = availableCommands.find((cmd) => leven(cmd, invalidCmd) < 3);
+ if (suggestion) {
+ consoleLogger.info(`\n Did you mean ${suggestion}?`);
+ }
+ process.exit(1);
+});
+
+program.usage(' [options]').version(String(pkg.version)).parse(process.argv);
diff --git a/code/lib/cli/src/build.ts b/code/core/src/cli/build.ts
similarity index 100%
rename from code/lib/cli/src/build.ts
rename to code/core/src/cli/build.ts
diff --git a/code/lib/cli/src/detect.test.ts b/code/core/src/cli/detect.test.ts
similarity index 100%
rename from code/lib/cli/src/detect.test.ts
rename to code/core/src/cli/detect.test.ts
diff --git a/code/lib/cli/src/detect.ts b/code/core/src/cli/detect.ts
similarity index 97%
rename from code/lib/cli/src/detect.ts
rename to code/core/src/cli/detect.ts
index 440336f74bb8..83b90237aa9b 100644
--- a/code/lib/cli/src/detect.ts
+++ b/code/core/src/cli/detect.ts
@@ -1,5 +1,5 @@
import * as fs from 'fs';
-import findUp from 'find-up';
+import { findUpSync } from 'find-up';
import semver from 'semver';
import { logger } from '@storybook/core/node-logger';
@@ -110,8 +110,8 @@ export function detectFrameworkPreset(
* @returns CoreBuilder
*/
export async function detectBuilder(packageManager: JsPackageManager, projectType: ProjectType) {
- const viteConfig = findUp.sync(viteConfigFiles);
- const webpackConfig = findUp.sync(webpackConfigFiles);
+ const viteConfig = findUpSync(viteConfigFiles);
+ const webpackConfig = findUpSync(webpackConfigFiles);
const dependencies = await packageManager.getAllDependencies();
if (viteConfig || (dependencies.vite && dependencies.webpack === undefined)) {
@@ -161,7 +161,7 @@ export function isStorybookInstantiated(configDir = resolve(process.cwd(), '.sto
}
export async function detectPnp() {
- return !!findUp.sync(['.pnp.js', '.pnp.cjs']);
+ return !!findUpSync(['.pnp.js', '.pnp.cjs']);
}
export async function detectLanguage(packageManager: JsPackageManager) {
diff --git a/code/lib/cli/src/dev.ts b/code/core/src/cli/dev.ts
similarity index 100%
rename from code/lib/cli/src/dev.ts
rename to code/core/src/cli/dev.ts
diff --git a/code/lib/cli/src/dirs.ts b/code/core/src/cli/dirs.ts
similarity index 88%
rename from code/lib/cli/src/dirs.ts
rename to code/core/src/cli/dirs.ts
index 32f31f6ecadb..88515e6b6683 100644
--- a/code/lib/cli/src/dirs.ts
+++ b/code/core/src/cli/dirs.ts
@@ -7,16 +7,11 @@ import invariant from 'tiny-invariant';
import { externalFrameworks } from './project_types';
import type { SupportedRenderers } from './project_types';
import type { JsPackageManager } from '@storybook/core/common';
-import { versions } from '@storybook/core/common';
+import { temporaryDirectory, versions } from '@storybook/core/common';
import type { SupportedFrameworks } from '@storybook/core/types';
-export function getCliDir() {
- return dirname(require.resolve('storybook/package.json'));
-}
-
const resolveUsingBranchInstall = async (packageManager: JsPackageManager, request: string) => {
- const { temporaryDirectory } = await import('tempy');
- const tempDirectory = temporaryDirectory();
+ const tempDirectory = await temporaryDirectory();
const name = request as keyof typeof versions;
// FIXME: this might not be the right version for community packages
diff --git a/code/lib/cli/src/automigrate/helpers/eslintPlugin.test.ts b/code/core/src/cli/eslintPlugin.test.ts
similarity index 100%
rename from code/lib/cli/src/automigrate/helpers/eslintPlugin.test.ts
rename to code/core/src/cli/eslintPlugin.test.ts
diff --git a/code/lib/cli/src/automigrate/helpers/eslintPlugin.ts b/code/core/src/cli/eslintPlugin.ts
similarity index 97%
rename from code/lib/cli/src/automigrate/helpers/eslintPlugin.ts
rename to code/core/src/cli/eslintPlugin.ts
index 8544b64be5b0..75fcaa96bb4c 100644
--- a/code/lib/cli/src/automigrate/helpers/eslintPlugin.ts
+++ b/code/core/src/cli/eslintPlugin.ts
@@ -7,6 +7,7 @@ import chalk from 'chalk';
import { readConfig, writeConfig } from '@storybook/core/csf-tools';
import type { JsPackageManager } from '@storybook/core/common';
import { paddedLog } from '@storybook/core/common';
+import fs from 'node:fs';
export const SUPPORTED_ESLINT_EXTENSIONS = ['js', 'cjs', 'json'];
const UNSUPPORTED_ESLINT_EXTENSIONS = ['yaml', 'yml'];
@@ -14,7 +15,7 @@ const UNSUPPORTED_ESLINT_EXTENSIONS = ['yaml', 'yml'];
export const findEslintFile = () => {
const filePrefix = '.eslintrc';
const unsupportedExtension = UNSUPPORTED_ESLINT_EXTENSIONS.find((ext: string) =>
- fse.existsSync(`${filePrefix}.${ext}`)
+ fs.existsSync(`${filePrefix}.${ext}`)
);
if (unsupportedExtension) {
@@ -22,7 +23,7 @@ export const findEslintFile = () => {
}
const extension = SUPPORTED_ESLINT_EXTENSIONS.find((ext: string) =>
- fse.existsSync(`${filePrefix}.${ext}`)
+ fs.existsSync(`${filePrefix}.${ext}`)
);
return extension ? `${filePrefix}.${extension}` : null;
};
diff --git a/code/lib/cli/src/helpers.test.ts b/code/core/src/cli/helpers.test.ts
similarity index 97%
rename from code/lib/cli/src/helpers.test.ts
rename to code/core/src/cli/helpers.test.ts
index 1edbf8a0adb7..9464c8551a9f 100644
--- a/code/lib/cli/src/helpers.test.ts
+++ b/code/core/src/cli/helpers.test.ts
@@ -38,7 +38,6 @@ vi.mock('fs', async (importOriginal) => {
vi.mock('./dirs', () => ({
getRendererDir: (_: JsPackageManager, renderer: string) =>
normalizePath(`@storybook/${renderer}`),
- getCliDir: () => normalizePath('storybook'),
}));
vi.mock('fs-extra', async (importOriginal) => {
@@ -123,11 +122,12 @@ describe('Helpers', () => {
renderer: 'react',
language,
packageManager: packageManagerMock,
+ commonAssetsDir: normalizePath('create-storybook/rendererAssets/common'),
});
expect(fse.copy).toHaveBeenNthCalledWith(
1,
- normalizePath('storybook/rendererAssets/common'),
+ normalizePath('create-storybook/rendererAssets/common'),
'./stories',
expect.anything()
);
diff --git a/code/lib/cli/src/helpers.ts b/code/core/src/cli/helpers.ts
similarity index 95%
rename from code/lib/cli/src/helpers.ts
rename to code/core/src/cli/helpers.ts
index f2256e11c2b8..e0af5f76af9f 100644
--- a/code/lib/cli/src/helpers.ts
+++ b/code/core/src/cli/helpers.ts
@@ -5,9 +5,9 @@ import path, { join } from 'path';
import { coerce, satisfies } from 'semver';
import stripJsonComments from 'strip-json-comments';
-import findUp from 'find-up';
+import { findUpSync } from 'find-up';
import invariant from 'tiny-invariant';
-import { getCliDir, getRendererDir } from './dirs';
+import { getRendererDir } from './dirs';
import {
type JsPackageManager,
type PackageJson,
@@ -15,8 +15,7 @@ import {
frameworkToRenderer as CoreFrameworkToRenderer,
} from '@storybook/core/common';
import type { SupportedFrameworks, SupportedRenderers } from '@storybook/core/types';
-import { CoreBuilder } from './project_types';
-import { SupportedLanguage } from './project_types';
+import { CoreBuilder, SupportedLanguage } from './project_types';
import { versions as storybookMonorepoPackages } from '@storybook/core/common';
const logger = console;
@@ -128,7 +127,7 @@ type CopyTemplateFilesOptions = {
packageManager: JsPackageManager;
renderer: SupportedFrameworks | SupportedRenderers;
language: SupportedLanguage;
- includeCommonAssets?: boolean;
+ commonAssetsDir?: string;
destination?: string;
};
@@ -164,7 +163,7 @@ export async function copyTemplateFiles({
renderer,
language,
destination,
- includeCommonAssets = true,
+ commonAssetsDir,
}: CopyTemplateFilesOptions) {
const languageFolderMapping: Record = {
// keeping this for backwards compatibility in case community packages are using it
@@ -213,14 +212,14 @@ export async function copyTemplateFiles({
};
const destinationPath = destination ?? (await targetPath());
- if (includeCommonAssets) {
- await fse.copy(join(getCliDir(), 'rendererAssets', 'common'), destinationPath, {
+ if (commonAssetsDir) {
+ await fse.copy(commonAssetsDir, destinationPath, {
overwrite: true,
});
}
await fse.copy(await templatePath(), destinationPath, { overwrite: true });
- if (includeCommonAssets) {
+ if (commonAssetsDir) {
let rendererType = frameworkToRenderer[renderer] || 'react';
// This is only used for docs links and the docs site uses `vue` for both `vue` & `vue3` renderers
if (rendererType === 'vue3') rendererType = 'vue';
@@ -260,7 +259,7 @@ export function getStorybookVersionSpecifier(packageJson: PackageJsonWithDepsAnd
}
export async function isNxProject() {
- return findUp.sync('nx.json');
+ return findUpSync('nx.json');
}
export function coerceSemver(version: string) {
diff --git a/code/core/src/cli/index.ts b/code/core/src/cli/index.ts
new file mode 100644
index 000000000000..c99751ac2c1b
--- /dev/null
+++ b/code/core/src/cli/index.ts
@@ -0,0 +1,7 @@
+export * from './detect';
+export * from './helpers';
+export * from './angular/helpers';
+export * from './dirs';
+export * from './project_types';
+export * from './NpmOptions';
+export * from './eslintPlugin';
diff --git a/code/lib/cli/src/project_types.test.ts b/code/core/src/cli/project_types.test.ts
similarity index 100%
rename from code/lib/cli/src/project_types.test.ts
rename to code/core/src/cli/project_types.test.ts
diff --git a/code/lib/cli/src/project_types.ts b/code/core/src/cli/project_types.ts
similarity index 100%
rename from code/lib/cli/src/project_types.ts
rename to code/core/src/cli/project_types.ts
diff --git a/code/core/src/common/js-package-manager/NPMProxy.ts b/code/core/src/common/js-package-manager/NPMProxy.ts
index d4d711915925..3cdadeb17ff3 100644
--- a/code/core/src/common/js-package-manager/NPMProxy.ts
+++ b/code/core/src/common/js-package-manager/NPMProxy.ts
@@ -1,4 +1,4 @@
-import sort from 'semver/functions/sort';
+import sort from 'semver/functions/sort.js';
import { platform } from 'os';
import dedent from 'ts-dedent';
import { findUpSync } from 'find-up';
diff --git a/code/core/src/common/js-package-manager/PNPMProxy.ts b/code/core/src/common/js-package-manager/PNPMProxy.ts
index 6d3f434c87c9..652310bf87b3 100644
--- a/code/core/src/common/js-package-manager/PNPMProxy.ts
+++ b/code/core/src/common/js-package-manager/PNPMProxy.ts
@@ -3,7 +3,7 @@ import dedent from 'ts-dedent';
import { existsSync, readFileSync } from 'node:fs';
import { findUpSync } from 'find-up';
import path from 'node:path';
-import { FindPackageVersionsError } from '@storybook/core-events/server-errors';
+import { FindPackageVersionsError } from '@storybook/core/server-errors';
import { JsPackageManager } from './JsPackageManager';
import type { PackageJson } from './PackageJson';
diff --git a/code/core/src/common/js-package-manager/Yarn1Proxy.ts b/code/core/src/common/js-package-manager/Yarn1Proxy.ts
index 44a8f0ca3d45..a6387bc34176 100644
--- a/code/core/src/common/js-package-manager/Yarn1Proxy.ts
+++ b/code/core/src/common/js-package-manager/Yarn1Proxy.ts
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
import dedent from 'ts-dedent';
import { findUpSync } from 'find-up';
import path from 'node:path';
-import { FindPackageVersionsError } from '@storybook/core-events/server-errors';
+import { FindPackageVersionsError } from '@storybook/core/server-errors';
import { createLogStream } from '../utils/cli';
import { JsPackageManager } from './JsPackageManager';
diff --git a/code/core/src/common/js-package-manager/Yarn2Proxy.ts b/code/core/src/common/js-package-manager/Yarn2Proxy.ts
index 317a56b40090..05156d146130 100644
--- a/code/core/src/common/js-package-manager/Yarn2Proxy.ts
+++ b/code/core/src/common/js-package-manager/Yarn2Proxy.ts
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { PosixFS, VirtualFS, ZipOpenFS } from '@yarnpkg/fslib';
import { getLibzipSync } from '@yarnpkg/libzip';
-import { FindPackageVersionsError } from '@storybook/core-events/server-errors';
+import { FindPackageVersionsError } from '@storybook/core/server-errors';
import { createLogStream } from '../utils/cli';
import { JsPackageManager } from './JsPackageManager';
diff --git a/code/core/src/common/versions.ts b/code/core/src/common/versions.ts
index 192f2dbea6fc..9f3f0bae9b18 100644
--- a/code/core/src/common/versions.ts
+++ b/code/core/src/common/versions.ts
@@ -62,6 +62,7 @@ export default {
'@storybook/cli': '8.3.0-alpha.3',
'@storybook/codemod': '8.3.0-alpha.3',
'@storybook/core-webpack': '8.3.0-alpha.3',
+ 'create-storybook': '8.3.0-alpha.3',
'@storybook/csf-plugin': '8.3.0-alpha.3',
'@storybook/instrumenter': '8.3.0-alpha.3',
'@storybook/react-dom-shim': '8.3.0-alpha.3',
diff --git a/code/core/src/components/components/Colors/SideBySide.tsx b/code/core/src/components/brand/SideBySide.tsx
similarity index 100%
rename from code/core/src/components/components/Colors/SideBySide.tsx
rename to code/core/src/components/brand/SideBySide.tsx
diff --git a/code/core/src/components/components/brand/StorybookIcon.stories.tsx b/code/core/src/components/brand/StorybookIcon.stories.tsx
similarity index 100%
rename from code/core/src/components/components/brand/StorybookIcon.stories.tsx
rename to code/core/src/components/brand/StorybookIcon.stories.tsx
diff --git a/code/core/src/components/components/brand/StorybookIcon.tsx b/code/core/src/components/brand/StorybookIcon.tsx
similarity index 100%
rename from code/core/src/components/components/brand/StorybookIcon.tsx
rename to code/core/src/components/brand/StorybookIcon.tsx
diff --git a/code/core/src/components/components/brand/StorybookLogo.stories.tsx b/code/core/src/components/brand/StorybookLogo.stories.tsx
similarity index 100%
rename from code/core/src/components/components/brand/StorybookLogo.stories.tsx
rename to code/core/src/components/brand/StorybookLogo.stories.tsx
diff --git a/code/core/src/components/components/brand/StorybookLogo.tsx b/code/core/src/components/brand/StorybookLogo.tsx
similarity index 100%
rename from code/core/src/components/components/brand/StorybookLogo.tsx
rename to code/core/src/components/brand/StorybookLogo.tsx
diff --git a/code/core/src/components/components/Colors/colorpalette.mdx b/code/core/src/components/brand/colorpalette.mdx
similarity index 100%
rename from code/core/src/components/components/Colors/colorpalette.mdx
rename to code/core/src/components/brand/colorpalette.mdx
diff --git a/code/core/src/components/components/typography/typography.mdx b/code/core/src/components/brand/typography.mdx
similarity index 100%
rename from code/core/src/components/components/typography/typography.mdx
rename to code/core/src/components/brand/typography.mdx
diff --git a/code/core/src/components/components/Button/Button.deprecated.stories.tsx b/code/core/src/components/components/Button/Button.deprecated.stories.tsx
index 10362221ce2a..009ea9627ab5 100644
--- a/code/core/src/components/components/Button/Button.deprecated.stories.tsx
+++ b/code/core/src/components/components/Button/Button.deprecated.stories.tsx
@@ -5,7 +5,7 @@ import { Button } from './Button';
import { Form } from '../form';
const meta: Meta = {
- title: 'Button/Deprecated',
+ title: 'Button (Deprecated)',
component: Button,
tags: ['autodocs'],
};
diff --git a/code/core/src/components/components/icon/icon.stories.tsx b/code/core/src/components/components/icon/icon.stories.tsx
index 64ecbd21be18..929319b346a7 100644
--- a/code/core/src/components/components/icon/icon.stories.tsx
+++ b/code/core/src/components/components/icon/icon.stories.tsx
@@ -55,6 +55,7 @@ const Header = styled.h2`
`;
export default {
+ title: 'Icons (deprecated)',
component: Icons,
argTypes: {
color: { control: 'color' },
diff --git a/code/core/src/components/components/tabs/EmptyTabContent.stories.tsx b/code/core/src/components/components/tabs/EmptyTabContent.stories.tsx
index 2501eb5f7c0b..e03fa329532b 100644
--- a/code/core/src/components/components/tabs/EmptyTabContent.stories.tsx
+++ b/code/core/src/components/components/tabs/EmptyTabContent.stories.tsx
@@ -5,6 +5,7 @@ import { Link } from '@storybook/core/components';
import type { Meta, StoryObj } from '@storybook/react';
export default {
+ title: 'TabContentEmpty',
component: EmptyTabContent,
parameters: {
layout: 'centered',
diff --git a/code/core/src/components/components/tabs/tabs.stories.tsx b/code/core/src/components/components/tabs/tabs.stories.tsx
index f19922a91ef8..1958c3e18555 100644
--- a/code/core/src/components/components/tabs/tabs.stories.tsx
+++ b/code/core/src/components/components/tabs/tabs.stories.tsx
@@ -101,7 +101,6 @@ const content = Object.entries(panels).map(([k, v]) => (
));
export default {
- title: 'Tabs',
args: {
menuName: 'Addons',
},
@@ -178,12 +177,11 @@ const customViewports = {
export const StatefulDynamicWithOpenTooltip = {
parameters: {
viewport: {
- defaultViewport: 'sized',
viewports: customViewports,
},
- theme: 'light',
chromatic: { viewports: [380] },
},
+ globals: { sb_theme: 'light', viewport: 'sized' },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
diff --git a/code/core/src/components/index.ts b/code/core/src/components/index.ts
index febbe94778e8..ad14eb46e728 100644
--- a/code/core/src/components/index.ts
+++ b/code/core/src/components/index.ts
@@ -76,8 +76,8 @@ export { AddonPanel } from './components/addon-panel/addon-panel';
export type { IconsProps } from './components/icon/icon';
export { Icons, Symbols } from './components/icon/icon';
export { icons } from './components/icon/icon';
-export { StorybookLogo } from './components/brand/StorybookLogo';
-export { StorybookIcon } from './components/brand/StorybookIcon';
+export { StorybookLogo } from './brand/StorybookLogo';
+export { StorybookIcon } from './brand/StorybookIcon';
// Loader
export { Loader } from './components/Loader/Loader';
diff --git a/code/core/src/core-server/standalone.ts b/code/core/src/core-server/standalone.ts
index 354a1852f54d..72a98d617112 100644
--- a/code/core/src/core-server/standalone.ts
+++ b/code/core/src/core-server/standalone.ts
@@ -4,7 +4,8 @@ import { dirname } from 'node:path';
async function build(options: any = {}, frameworkOptions: any = {}) {
const { mode = 'dev' } = options;
- const packageJson = dirname(require.resolve('@storybook/core/package.json'));
+ const packageJsonDir = dirname(require.resolve('@storybook/core/package.json'));
+ const packageJson = JSON.parse(require('fs').readFileSync(`${packageJsonDir}/package.json`));
const commonOptions = {
...options,
diff --git a/code/core/src/core-server/utils/warnOnIncompatibleAddons.ts b/code/core/src/core-server/utils/warnOnIncompatibleAddons.ts
index df43ed0d7738..a921f4ba3645 100644
--- a/code/core/src/core-server/utils/warnOnIncompatibleAddons.ts
+++ b/code/core/src/core-server/utils/warnOnIncompatibleAddons.ts
@@ -2,7 +2,7 @@ import { logger } from '@storybook/core/node-logger';
import {
getIncompatibleStorybookPackages,
getIncompatiblePackagesSummary,
-} from '../../../../lib/cli/src/doctor/getIncompatibleStorybookPackages';
+} from '../../../../lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages';
export const warnOnIncompatibleAddons = async (currentStorybookVersion: string) => {
const incompatiblePackagesList = await getIncompatibleStorybookPackages({
diff --git a/code/core/src/manager-api/modules/globals.ts b/code/core/src/manager-api/modules/globals.ts
index f66f0599e3dc..cd990975ad10 100644
--- a/code/core/src/manager-api/modules/globals.ts
+++ b/code/core/src/manager-api/modules/globals.ts
@@ -1,7 +1,12 @@
import { SET_GLOBALS, UPDATE_GLOBALS, GLOBALS_UPDATED } from '@storybook/core/core-events';
import { logger } from '@storybook/core/client-logger';
import { dequal as deepEqual } from 'dequal';
-import type { SetGlobalsPayload, Globals, GlobalTypes } from '@storybook/core/types';
+import type {
+ SetGlobalsPayload,
+ Globals,
+ GlobalTypes,
+ GlobalsUpdatedPayload,
+} from '@storybook/core/types';
import type { ModuleFn } from '../lib/types';
@@ -9,23 +14,34 @@ import { getEventMetadata } from '../lib/events';
export interface SubState {
globals?: Globals;
+ userGlobals?: Globals;
+ storyGlobals?: Globals;
globalTypes?: GlobalTypes;
}
export interface SubAPI {
/**
- * Returns the current global data object.
- * @returns {Globals} The current global data object.
+ * Returns the current globals, which is the user globals overlaid with the story globals
+ * @returns {Globals} The current globals.
*/
getGlobals: () => Globals;
/**
- * Returns the current global types object.
- * @returns {GlobalTypes} The current global types object.
+ * Returns the current globals, as set by the user (a story may have override values)
+ * @returns {Globals} The current user globals.
*/
+ getUserGlobals: () => Globals /**
+ /**
+ * Returns the current globals, as set by the story
+ * @returns {Globals} The current story globals.
+ */;
+ getStoryGlobals: () => Globals /**
+ * Returns the globalTypes, as defined at the project level.
+ * @returns {GlobalTypes} The globalTypes.
+ */;
getGlobalTypes: () => GlobalTypes;
/**
- * Updates the current global data object with the provided new global data object.
- * @param {Globals} newGlobals - The new global data object to update with.
+ * Updates the current globals with the provided new globals.
+ * @param {Globals} newGlobals - The new globals to update with.
* @returns {void}
*/
updateGlobals: (newGlobals: Globals) => void;
@@ -36,6 +52,12 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) =
getGlobals() {
return store.getState().globals as Globals;
},
+ getUserGlobals() {
+ return store.getState().userGlobals as Globals;
+ },
+ getStoryGlobals() {
+ return store.getState().storyGlobals as Globals;
+ },
getGlobalTypes() {
return store.getState().globalTypes as GlobalTypes;
},
@@ -52,22 +74,45 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) =
const state: SubState = {
globals: {},
+ userGlobals: {},
+ storyGlobals: {},
globalTypes: {},
};
- const updateGlobals = (globals: Globals) => {
- const currentGlobals = store.getState()?.globals;
+ const updateGlobals = ({
+ globals,
+ storyGlobals,
+ userGlobals,
+ }: {
+ globals: Globals;
+ storyGlobals: Globals;
+ userGlobals: Globals;
+ }) => {
+ const {
+ globals: currentGlobals,
+ userGlobals: currentUserGlobals,
+ storyGlobals: currentStoryGlobals,
+ } = store.getState();
if (!deepEqual(globals, currentGlobals)) {
store.setState({ globals });
}
+ if (!deepEqual(userGlobals, currentUserGlobals)) {
+ store.setState({ userGlobals });
+ }
+ if (!deepEqual(storyGlobals, currentStoryGlobals)) {
+ store.setState({ storyGlobals });
+ }
};
provider.channel?.on(
GLOBALS_UPDATED,
- function handleGlobalsUpdated(this: any, { globals }: { globals: Globals }) {
+ function handleGlobalsUpdated(
+ this: any,
+ { globals, storyGlobals, userGlobals }: GlobalsUpdatedPayload
+ ) {
const { ref } = getEventMetadata(this, fullAPI)!;
if (!ref) {
- updateGlobals(globals);
+ updateGlobals({ globals, storyGlobals, userGlobals });
} else {
logger.warn(
'received a GLOBALS_UPDATED from a non-local ref. This is not currently supported.'
@@ -79,16 +124,18 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) =
// Emitted by the preview on initialization
provider.channel?.on(
SET_GLOBALS,
- function handleSetStories(this: any, { globals, globalTypes }: SetGlobalsPayload) {
+ function handleSetGlobals(this: any, { globals, globalTypes }: SetGlobalsPayload) {
const { ref } = getEventMetadata(this, fullAPI)!;
const currentGlobals = store.getState()?.globals;
if (!ref) {
- store.setState({ globals, globalTypes });
+ store.setState({ globals, userGlobals: globals, globalTypes });
} else if (Object.keys(globals).length > 0) {
logger.warn('received globals from a non-local ref. This is not currently supported.');
}
+ // If we have stored globals different to what the preview just inited with,
+ // we should update it to those values
if (
currentGlobals &&
Object.keys(currentGlobals).length !== 0 &&
diff --git a/code/core/src/manager-api/modules/shortcuts.ts b/code/core/src/manager-api/modules/shortcuts.ts
index 1d4708425c92..e65a7767faa9 100644
--- a/code/core/src/manager-api/modules/shortcuts.ts
+++ b/code/core/src/manager-api/modules/shortcuts.ts
@@ -1,6 +1,11 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { global } from '@storybook/global';
-import { FORCE_REMOUNT, PREVIEW_KEYDOWN } from '@storybook/core/core-events';
+import {
+ FORCE_REMOUNT,
+ PREVIEW_KEYDOWN,
+ STORIES_COLLAPSE_ALL,
+ STORIES_EXPAND_ALL,
+} from '@storybook/core/core-events';
import type { ModuleFn } from '../lib/types';
@@ -356,11 +361,11 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => {
break;
}
case 'collapseAll': {
- fullAPI.collapseAll();
+ fullAPI.emit(STORIES_COLLAPSE_ALL);
break;
}
case 'expandAll': {
- fullAPI.expandAll();
+ fullAPI.emit(STORIES_EXPAND_ALL);
break;
}
case 'remount': {
diff --git a/code/core/src/manager-api/modules/url.ts b/code/core/src/manager-api/modules/url.ts
index 2ad7a340839d..d69e194a6947 100644
--- a/code/core/src/manager-api/modules/url.ts
+++ b/code/core/src/manager-api/modules/url.ts
@@ -234,9 +234,9 @@ export const init: ModuleFn = (moduleArgs) => {
}
});
- provider.channel?.on(GLOBALS_UPDATED, ({ globals, initialGlobals }: any) => {
+ provider.channel?.on(GLOBALS_UPDATED, ({ userGlobals, initialGlobals }: any) => {
const { path, queryParams } = api.getUrlState();
- const globalsString = buildArgsParam(initialGlobals, globals);
+ const globalsString = buildArgsParam(initialGlobals, userGlobals);
navigateTo(path, { ...queryParams, globals: globalsString }, { replace: true });
api.setQueryParams({ globals: globalsString });
});
diff --git a/code/core/src/manager-api/root.tsx b/code/core/src/manager-api/root.tsx
index e77e2eef9bc1..8d23d1d8fcd2 100644
--- a/code/core/src/manager-api/root.tsx
+++ b/code/core/src/manager-api/root.tsx
@@ -28,6 +28,7 @@ import type {
API_StoryEntry,
Parameters,
StoryId,
+ Globals,
} from '@storybook/core/types';
import {
@@ -498,9 +499,14 @@ export function useArgs(): [Args, (newArgs: Args) => void, (argNames?: string[])
return [args!, updateArgs, resetArgs, initialArgs!];
}
-export function useGlobals(): [Args, (newGlobals: Args) => void] {
+export function useGlobals(): [
+ globals: Globals,
+ updateGlobals: (newGlobals: Globals) => void,
+ storyGlobals: Globals,
+ userGlobals: Globals,
+] {
const api = useStorybookApi();
- return [api.getGlobals(), api.updateGlobals];
+ return [api.getGlobals(), api.updateGlobals, api.getStoryGlobals(), api.getUserGlobals()];
}
export function useGlobalTypes(): ArgTypes {
diff --git a/code/core/src/manager-api/tests/globals.test.ts b/code/core/src/manager-api/tests/globals.test.ts
index 48ad8b854179..52f3fa379472 100644
--- a/code/core/src/manager-api/tests/globals.test.ts
+++ b/code/core/src/manager-api/tests/globals.test.ts
@@ -14,6 +14,7 @@ import { init as initModule } from '../modules/globals';
import type { ModuleArgs } from '../lib/types';
import { getEventMetadata as _getEventData } from '../lib/events';
+import type { GlobalsUpdatedPayload, SetGlobalsPayload } from '@storybook/core/types';
const getEventMetadata = vi.mocked(_getEventData, true);
const logger = vi.mocked(_logger, true);
@@ -41,6 +42,8 @@ describe('globals API', () => {
const { state } = initModule({ store, provider: { channel } } as unknown as ModuleArgs);
expect(state).toEqual({
+ userGlobals: {},
+ storyGlobals: {},
globals: {},
globalTypes: {},
});
@@ -58,11 +61,45 @@ describe('globals API', () => {
channel.emit(SET_GLOBALS, {
globals: { a: 'b' },
globalTypes: { a: { type: { name: 'string' } } },
+ } satisfies SetGlobalsPayload);
+ expect(store.getState()).toEqual({
+ userGlobals: { a: 'b' },
+ storyGlobals: {},
+ globals: { a: 'b' },
+ globalTypes: { a: { type: { name: 'string' } } },
});
+ });
+
+ it('emits UPDATE_GLOBALS if retains a globals value different to what recieves on SET_GLOBALS', () => {
+ const channel = new EventEmitter();
+ const listener = vi.fn();
+ channel.on(UPDATE_GLOBALS, listener);
+
+ const store = createMockStore();
+ const { state } = initModule({
+ store,
+ provider: { channel },
+ } as unknown as ModuleArgs);
+ store.setState({
+ ...state,
+ globals: { a: 'c' },
+ });
+
+ channel.emit(SET_GLOBALS, {
+ globals: { a: 'b' },
+ globalTypes: { a: { type: { name: 'string' } } },
+ } satisfies SetGlobalsPayload);
expect(store.getState()).toEqual({
+ userGlobals: { a: 'b' },
+ storyGlobals: {},
globals: { a: 'b' },
globalTypes: { a: { type: { name: 'string' } } },
});
+
+ expect(listener).toHaveBeenCalledWith({
+ globals: { a: 'c' },
+ options: { target: 'storybook-preview-iframe' },
+ });
});
it('ignores SET_STORIES from other refs', () => {
@@ -78,7 +115,12 @@ describe('globals API', () => {
getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'ref' } } as any);
channel.emit(SET_STORIES, { globals: { a: 'b' } });
- expect(store.getState()).toEqual({ globals: {}, globalTypes: {} });
+ expect(store.getState()).toEqual({
+ userGlobals: {},
+ storyGlobals: {},
+ globals: {},
+ globalTypes: {},
+ });
});
it('ignores SET_GLOBALS from other refs', () => {
@@ -96,8 +138,13 @@ describe('globals API', () => {
channel.emit(SET_GLOBALS, {
globals: { a: 'b' },
globalTypes: { a: { type: { name: 'string' } } },
+ } satisfies SetGlobalsPayload);
+ expect(store.getState()).toEqual({
+ userGlobals: {},
+ storyGlobals: {},
+ globals: {},
+ globalTypes: {},
});
- expect(store.getState()).toEqual({ globals: {}, globalTypes: {} });
});
it('updates the state when the preview emits GLOBALS_UPDATED', () => {
@@ -111,15 +158,45 @@ describe('globals API', () => {
} as unknown as ModuleArgs);
store.setState(state);
- channel.emit(GLOBALS_UPDATED, { globals: { a: 'b' } });
- expect(store.getState()).toEqual({ globals: { a: 'b' }, globalTypes: {} });
+ channel.emit(GLOBALS_UPDATED, {
+ initialGlobals: { a: 'b' },
+ userGlobals: { a: 'b' },
+ storyGlobals: {},
+ globals: { a: 'b' },
+ } satisfies GlobalsUpdatedPayload);
+ expect(store.getState()).toEqual({
+ userGlobals: { a: 'b' },
+ storyGlobals: {},
+ globals: { a: 'b' },
+ globalTypes: {},
+ });
- channel.emit(GLOBALS_UPDATED, { globals: { a: 'c' } });
- expect(store.getState()).toEqual({ globals: { a: 'c' }, globalTypes: {} });
+ channel.emit(GLOBALS_UPDATED, {
+ initialGlobals: { a: 'b' },
+ userGlobals: { a: 'c' },
+ storyGlobals: {},
+ globals: { a: 'c' },
+ } satisfies GlobalsUpdatedPayload);
+ expect(store.getState()).toEqual({
+ userGlobals: { a: 'c' },
+ storyGlobals: {},
+ globals: { a: 'c' },
+ globalTypes: {},
+ });
- // SHOULD NOT merge global args
- channel.emit(GLOBALS_UPDATED, { globals: { d: 'e' } });
- expect(store.getState()).toEqual({ globals: { d: 'e' }, globalTypes: {} });
+ // SHOULD NOT merge globals
+ channel.emit(GLOBALS_UPDATED, {
+ initialGlobals: { a: 'b' },
+ userGlobals: { d: 'e' },
+ storyGlobals: {},
+ globals: { d: 'e' },
+ } satisfies GlobalsUpdatedPayload);
+ expect(store.getState()).toEqual({
+ userGlobals: { d: 'e' },
+ storyGlobals: {},
+ globals: { d: 'e' },
+ globalTypes: {},
+ });
});
it('ignores GLOBALS_UPDATED from other refs', () => {
@@ -135,8 +212,18 @@ describe('globals API', () => {
getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'ref' } } as any);
logger.warn.mockClear();
- channel.emit(GLOBALS_UPDATED, { globals: { a: 'b' } });
- expect(store.getState()).toEqual({ globals: {}, globalTypes: {} });
+ channel.emit(GLOBALS_UPDATED, {
+ initialGlobals: { a: 'b' },
+ userGlobals: { a: 'b' },
+ storyGlobals: {},
+ globals: { a: 'b' },
+ } satisfies GlobalsUpdatedPayload);
+ expect(store.getState()).toEqual({
+ userGlobals: {},
+ storyGlobals: {},
+ globals: {},
+ globalTypes: {},
+ });
expect(logger.warn).toHaveBeenCalled();
});
diff --git a/code/core/src/manager-api/tests/url.test.js b/code/core/src/manager-api/tests/url.test.js
index 53946d7ab6b9..a4a9de83ba75 100644
--- a/code/core/src/manager-api/tests/url.test.js
+++ b/code/core/src/manager-api/tests/url.test.js
@@ -178,7 +178,12 @@ describe('initModule', () => {
const channel = new EventEmitter();
initURL({ store, provider: { channel }, state: { location: {} }, navigate, fullAPI });
- channel.emit(GLOBALS_UPDATED, { globals: { a: 2 }, initialGlobals: { a: 1, b: 1 } });
+ channel.emit(GLOBALS_UPDATED, {
+ userGlobals: { a: 2 },
+ storyGlobals: {},
+ globals: { a: 2 },
+ initialGlobals: { a: 1, b: 1 },
+ });
expect(navigate).toHaveBeenCalledWith(
'/story/test--story&globals=a:2;b:!undefined',
expect.objectContaining({ replace: true })
@@ -200,7 +205,7 @@ describe('initModule', () => {
}),
});
- channel.emit(GLOBALS_UPDATED, { globals: { g: 2 } });
+ channel.emit(GLOBALS_UPDATED, { userGlobals: { g: 2 }, storyGlobals: {}, globals: { g: 2 } });
expect(navigate).toHaveBeenCalledWith(
'/story/test--story&full=1&globals=g:2',
expect.objectContaining({ replace: true })
diff --git a/code/core/src/manager/components/layout/Layout.stories.tsx b/code/core/src/manager/components/layout/Layout.stories.tsx
index 53f8e54b0d15..8282d8434587 100644
--- a/code/core/src/manager/components/layout/Layout.stories.tsx
+++ b/code/core/src/manager/components/layout/Layout.stories.tsx
@@ -63,10 +63,8 @@ const meta = {
setManagerLayoutState: fn(),
hasTab: false,
},
- parameters: {
- theme: 'light',
- layout: 'fullscreen',
- },
+ globals: { sb_theme: 'light' },
+ parameters: { layout: 'fullscreen' },
decorators: [
MobileNavigationStoriesMeta.decorators[0] as any,
(storyFn) => (
@@ -97,7 +95,7 @@ type Story = StoryObj;
export const Desktop: Story = {};
export const Dark: Story = {
- parameters: { theme: 'dark' },
+ globals: { sb_theme: 'dark' },
};
export const DesktopHorizontal: Story = {
args: {
@@ -127,10 +125,7 @@ export const Mobile = {
};
export const MobileDark = {
...Mobile,
- parameters: {
- ...Mobile.parameters,
- theme: 'dark',
- },
+ globals: { sb_theme: 'dark' },
};
export const MobileDocs = {
diff --git a/code/core/src/manager/components/mobile/about/MobileAbout.stories.tsx b/code/core/src/manager/components/mobile/about/MobileAbout.stories.tsx
index 465db469cde9..e552171cca4b 100644
--- a/code/core/src/manager/components/mobile/about/MobileAbout.stories.tsx
+++ b/code/core/src/manager/components/mobile/about/MobileAbout.stories.tsx
@@ -19,9 +19,9 @@ const OpenAboutHelper = ({ children }: { children: any }) => {
const meta = {
component: MobileAbout,
title: 'Mobile/About',
+ globals: { sb_theme: 'light' },
parameters: {
layout: 'fullscreen',
- theme: 'light',
viewport: {
defaultViewport: 'mobile1',
},
@@ -56,7 +56,7 @@ type Story = StoryObj;
export const Default: Story = {};
export const Dark: Story = {
- parameters: { theme: 'dark' },
+ globals: { sb_theme: 'dark' },
};
export const Closed: Story = {
diff --git a/code/core/src/manager/components/mobile/navigation/MobileNavigation.stories.tsx b/code/core/src/manager/components/mobile/navigation/MobileNavigation.stories.tsx
index b67bf15a3e62..552556add58c 100644
--- a/code/core/src/manager/components/mobile/navigation/MobileNavigation.stories.tsx
+++ b/code/core/src/manager/components/mobile/navigation/MobileNavigation.stories.tsx
@@ -69,7 +69,6 @@ const meta = {
],
parameters: {
layout: 'fullscreen',
- theme: 'light',
viewport: {
defaultViewport: 'mobile1',
},
@@ -86,9 +85,12 @@ export default meta;
type Story = StoryObj;
-export const Default: Story = {};
+export const Default: Story = {
+ globals: { sb_theme: 'light' },
+};
export const Dark: Story = {
- parameters: { theme: 'dark' },
+ globals: { sb_theme: 'dark' },
+ parameters: { chromatic: { disable: true } },
};
export const LongStoryName: Story = {
diff --git a/code/core/src/manager/components/preview/Iframe.stories.tsx b/code/core/src/manager/components/preview/Iframe.stories.tsx
index 8f542b029270..84367478c4ec 100644
--- a/code/core/src/manager/components/preview/Iframe.stories.tsx
+++ b/code/core/src/manager/components/preview/Iframe.stories.tsx
@@ -20,9 +20,9 @@ export default {
},
},
},
- theme: 'light',
chromatic: { viewports: [700] },
},
+ globals: { sb_theme: 'light' },
};
const style: CSSProperties = {
@@ -50,7 +50,7 @@ export const WorkingDocs = () => (
active
id="iframe"
title="Missing"
- src="/iframe.html?id=components-colorpalette--docs"
+ src="/iframe.html?id=brand-colorpalette--docs"
allowFullScreen
style={style}
scale={1.0}
@@ -60,17 +60,23 @@ WorkingStory.parameters = {
chromatic: { disable: true },
};
-export const MissingStory = () => (
-
-);
+export const MissingStory = {
+ render: () => (
+
+ ),
+ parameters: {
+ // Raise the threshold to ignore monospace font inconsistencies
+ chromatic: { diffThreshold: 0.65 },
+ },
+};
export const PreparingStory = () => (