diff --git a/CHANGELOG.md b/CHANGELOG.md index e39ebdce39e5..43fc923f19bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 9.1.7 + +- Dependencies: Update `vite-plugin-storybook-nextjs` to 2.0.7 - [#32331](https://github.com/storybookjs/storybook/pull/32331), thanks @k35o! +- React: Preserve `@ts-expect-error` in preview - [#32442](https://github.com/storybookjs/storybook/pull/32442), thanks @mrginglymus! +- Telemetry: Queue error reporting & filter browser-extention - [#32499](https://github.com/storybookjs/storybook/pull/32499), thanks @ndelangen! + ## 9.1.6 - CLI: Capture the version specifier used in `create-storybook` - [#32344](https://github.com/storybookjs/storybook/pull/32344), thanks @shilman! diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 71c11c5e9db8..2aba95c8b4ad 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,11 @@ +## 10.0.0-beta.6 + +- Core: Add "open in editor" feature - [#32452](https://github.com/storybookjs/storybook/pull/32452), thanks @yannbf! +- Dev: Improve the browser opening experience - [#32488](https://github.com/storybookjs/storybook/pull/32488), thanks @ndelangen! +- Maintenance: Remove globalization for dropped entrypoints - [#32491](https://github.com/storybookjs/storybook/pull/32491), thanks @ndelangen! +- Telemetry: Queue error reporting & filter browser-extention - [#32499](https://github.com/storybookjs/storybook/pull/32499), thanks @ndelangen! +- Upgrade: Packages `open` - [#32484](https://github.com/storybookjs/storybook/pull/32484), thanks @ndelangen! + ## 10.0.0-beta.5 - Dependencies: Update `vite-plugin-storybook-nextjs` to 2.0.7 - [#32331](https://github.com/storybookjs/storybook/pull/32331), thanks @k35o! diff --git a/code/addons/a11y/package.json b/code/addons/a11y/package.json index 9268100c378e..d56b29290e59 100644 --- a/code/addons/a11y/package.json +++ b/code/addons/a11y/package.json @@ -64,7 +64,7 @@ }, "devDependencies": { "@radix-ui/react-tabs": "1.0.4", - "@storybook/icons": "^1.4.0", + "@storybook/icons": "^1.6.0", "@testing-library/react": "^14.0.0", "execa": "^9.5.2", "react": "^18.2.0", diff --git a/code/addons/docs/package.json b/code/addons/docs/package.json index 44a0cf70c0b5..2324b92306ac 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -85,7 +85,7 @@ "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "workspace:*", - "@storybook/icons": "^1.4.0", + "@storybook/icons": "^1.6.0", "@storybook/react-dom-shim": "workspace:*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", diff --git a/code/addons/jest/package.json b/code/addons/jest/package.json index 43fdfe203341..7858fdb5e845 100644 --- a/code/addons/jest/package.json +++ b/code/addons/jest/package.json @@ -54,7 +54,7 @@ "upath": "^2.0.1" }, "devDependencies": { - "@storybook/icons": "^1.4.0", + "@storybook/icons": "^1.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-resize-detector": "^7.1.2", diff --git a/code/addons/onboarding/package.json b/code/addons/onboarding/package.json index 4f099998382d..6f256fcf9f12 100644 --- a/code/addons/onboarding/package.json +++ b/code/addons/onboarding/package.json @@ -52,7 +52,7 @@ }, "devDependencies": { "@neoconfetti/react": "^1.0.0", - "@storybook/icons": "^1.4.0", + "@storybook/icons": "^1.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-joyride": "^2.8.2", diff --git a/code/addons/pseudo-states/package.json b/code/addons/pseudo-states/package.json index bd501d323ce2..91b036471279 100644 --- a/code/addons/pseudo-states/package.json +++ b/code/addons/pseudo-states/package.json @@ -55,7 +55,7 @@ "prep": "jiti ../../../scripts/build/build-package.ts" }, "devDependencies": { - "@storybook/icons": "^1.4.0", + "@storybook/icons": "^1.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.8.3" diff --git a/code/addons/themes/package.json b/code/addons/themes/package.json index 816969362ed3..b66763152772 100644 --- a/code/addons/themes/package.json +++ b/code/addons/themes/package.json @@ -61,7 +61,7 @@ "ts-dedent": "^2.0.0" }, "devDependencies": { - "@storybook/icons": "^1.4.0", + "@storybook/icons": "^1.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.8.3" diff --git a/code/addons/vitest/package.json b/code/addons/vitest/package.json index 5f1aa2788723..bd1e95381645 100644 --- a/code/addons/vitest/package.json +++ b/code/addons/vitest/package.json @@ -73,7 +73,7 @@ }, "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.4.0", + "@storybook/icons": "^1.6.0", "prompts": "^2.4.0", "ts-dedent": "^2.2.0" }, diff --git a/code/addons/vitest/src/components/TestProviderRender.stories.tsx b/code/addons/vitest/src/components/TestProviderRender.stories.tsx index fa5456ce69c1..b5c4919ad5a2 100644 --- a/code/addons/vitest/src/components/TestProviderRender.stories.tsx +++ b/code/addons/vitest/src/components/TestProviderRender.stories.tsx @@ -316,6 +316,7 @@ export const InSidebarContextMenu: Story = { importPath: './path/to/story', prepared: true, parent: 'parent-id', + exportName: 'ExampleStory', depth: 1, }, }, diff --git a/code/builders/builder-webpack5/src/index.ts b/code/builders/builder-webpack5/src/index.ts index 1897e0d8c972..202d69563179 100644 --- a/code/builders/builder-webpack5/src/index.ts +++ b/code/builders/builder-webpack5/src/index.ts @@ -195,6 +195,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({ immutable: true, }) ); + router.use(compilation); router.use(webpackHotMiddleware(compiler, { log: false })); diff --git a/code/core/assets/server/openBrowser.applescript b/code/core/assets/server/openBrowser.applescript new file mode 100644 index 000000000000..5b1390749e3a --- /dev/null +++ b/code/core/assets/server/openBrowser.applescript @@ -0,0 +1,94 @@ +(* +Copyright (c) 2015-present, Facebook, Inc. +This source code is licensed under the MIT license found in the +LICENSE file in the root directory of this source tree. +*) + +property targetTab: null +property targetTabIndex: -1 +property targetWindow: null +property theProgram: "Google Chrome" + +on run argv + set theURL to item 1 of argv + set matchURL to item 2 of argv + + -- Allow requested program to be optional, + -- default to Google Chrome + if (count of argv) > 2 then + set theProgram to item 3 of argv + end if + + using terms from application "Google Chrome" + tell application theProgram + + if (count every window) = 0 then + make new window + end if + + -- 1: Looking for tab running debugger + -- then, Reload debugging tab if found + -- then return + set found to my lookupTabWithUrl(matchURL) + if found then + set targetWindow's active tab index to targetTabIndex + tell targetTab to reload + tell targetWindow to activate + set index of targetWindow to 1 + return + end if + + -- 2: Looking for Empty tab + -- In case debugging tab was not found + -- We try to find an empty tab instead + set found to my lookupTabWithUrl("chrome://newtab/") + if found then + set targetWindow's active tab index to targetTabIndex + set URL of targetTab to theURL + tell targetWindow to activate + return + end if + + -- 3: Create new tab + -- both debugging and empty tab were not found + -- make a new tab with url + tell window 1 + activate + make new tab with properties {URL:theURL} + end tell + end tell + end using terms from +end run + +-- Function: +-- Lookup tab with given url +-- if found, store tab, index, and window in properties +-- (properties were declared on top of file) +on lookupTabWithUrl(lookupUrl) + using terms from application "Google Chrome" + tell application theProgram + -- Find a tab with the given url + set found to false + set theTabIndex to -1 + repeat with theWindow in every window + set theTabIndex to 0 + repeat with theTab in every tab of theWindow + set theTabIndex to theTabIndex + 1 + if (theTab's URL as string) contains lookupUrl then + -- assign tab, tab index, and window to properties + set targetTab to theTab + set targetTabIndex to theTabIndex + set targetWindow to theWindow + set found to true + exit repeat + end if + end repeat + + if found then + exit repeat + end if + end repeat + end tell + end using terms from + return found +end lookupTabWithUrl \ No newline at end of file diff --git a/code/core/package.json b/code/core/package.json index 92f9a5da4761..37827577c9c3 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -228,13 +228,12 @@ }, "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.4.0", + "@storybook/icons": "^1.6.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/spy": "3.2.4", - "better-opn": "^3.0.2", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", "recast": "^0.23.5", "semver": "^7.6.2", @@ -318,13 +317,14 @@ "jiti": "^2.4.2", "js-yaml": "^4.1.0", "jsdoc-type-pratt-parser": "^4.0.0", + "launch-editor": "^2.11.1", "lazy-universal-dotenv": "^4.0.0", "leven": "^4.0.0", "memfs": "^4.11.1", "memoizerific": "^1.11.3", "nanoid": "^4.0.2", "npmlog": "^7.0.0", - "open": "^8.4.0", + "open": "^10.2.0", "p-limit": "^6.2.0", "package-manager-detector": "^1.1.0", "picocolors": "^1.1.0", @@ -335,6 +335,7 @@ "prettier": "^3.5.3", "pretty-hrtime": "^1.0.3", "prompts": "^2.4.0", + "qrcode.react": "^4.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", diff --git a/code/core/src/builder-manager/utils/framework.ts b/code/core/src/builder-manager/utils/framework.ts index 8362e423813f..d980b1c5d65f 100644 --- a/code/core/src/builder-manager/utils/framework.ts +++ b/code/core/src/builder-manager/utils/framework.ts @@ -55,5 +55,9 @@ export const buildFrameworkGlobalsFromOptions = async (options: Options) => { globals.STORYBOOK_FRAMEWORK = framework; } + if (options.networkAddress) { + globals.STORYBOOK_NETWORK_ADDRESS = options.networkAddress; + } + return globals; }; diff --git a/code/core/src/component-testing/components/InteractionsPanel.stories.tsx b/code/core/src/component-testing/components/InteractionsPanel.stories.tsx index 83a5adf660f4..46b7f983b07d 100644 --- a/code/core/src/component-testing/components/InteractionsPanel.stories.tsx +++ b/code/core/src/component-testing/components/InteractionsPanel.stories.tsx @@ -30,6 +30,11 @@ const managerContext: any = { api: { getDocsUrl: fn().mockName('api::getDocsUrl'), emit: fn().mockName('api::emit'), + getData: fn() + .mockName('api::getData') + .mockImplementation(() => ({ + importPath: 'core/src/component-testing/components/InteractionsPanel.stories.tsx', + })), }, }; diff --git a/code/core/src/component-testing/components/InteractionsPanel.tsx b/code/core/src/component-testing/components/InteractionsPanel.tsx index 7e2d4f4bffb5..5d0b822ae03c 100644 --- a/code/core/src/component-testing/components/InteractionsPanel.tsx +++ b/code/core/src/component-testing/components/InteractionsPanel.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { transparentize } from 'polished'; +import type { API } from 'storybook/manager-api'; import { styled } from 'storybook/theming'; import { type Call, type CallStates, type ControlStates } from '../../instrumenter/types'; @@ -44,6 +45,9 @@ interface InteractionsPanelProps { onScrollToEnd?: () => void; hasResultMismatch?: boolean; browserTestStatus?: CallStates; + importPath?: string; + canOpenInEditor?: boolean; + api: API; } const Container = styled.div(({ theme }) => ({ @@ -104,6 +108,9 @@ export const InteractionsPanel: React.FC = React.memo( endRef, hasResultMismatch, browserTestStatus, + importPath, + canOpenInEditor, + api, }) { const filter = useAnsiToHtmlFilter(); const hasRealInteractions = interactions.some((i) => i.id !== INTERNAL_RENDER_CALL_ID); @@ -120,6 +127,9 @@ export const InteractionsPanel: React.FC = React.memo( status={status} storyFileName={fileName} onScrollToEnd={onScrollToEnd} + importPath={importPath} + canOpenInEditor={canOpenInEditor} + api={api} />
{interactions.map((call) => ( diff --git a/code/core/src/component-testing/components/Panel.tsx b/code/core/src/component-testing/components/Panel.tsx index 16df7a552a67..04e8782903cc 100644 --- a/code/core/src/component-testing/components/Panel.tsx +++ b/code/core/src/component-testing/components/Panel.tsx @@ -17,6 +17,8 @@ import { useAddonState, useChannel, useParameter, + useStorybookApi, + useStorybookState, } from 'storybook/manager-api'; import { @@ -191,6 +193,12 @@ export const Panel = memo<{ refId?: string; storyId: string; storyUrl: string }> }); // shared state + const state = useStorybookState(); + const api = useStorybookApi(); + const data = api.getData(state.storyId, state.refId); + const importPath = data?.importPath as string | undefined; + const canOpenInEditor = global.CONFIG_TYPE === 'DEVELOPMENT' && !state.refId; + const [panelState, set] = useAddonState(ADDON_ID, { status: 'rendering' as PlayStatus, controlStates: INITIAL_CONTROL_STATES, @@ -406,6 +414,9 @@ export const Panel = memo<{ refId?: string; storyId: string; storyUrl: string }> // @ts-expect-error TODO endRef={endRef} onScrollToEnd={scrollTarget && scrollToTarget} + importPath={importPath} + canOpenInEditor={canOpenInEditor} + api={api} /> ); diff --git a/code/core/src/component-testing/components/Subnav.stories.tsx b/code/core/src/component-testing/components/Subnav.stories.tsx index b6af565de7c8..dd5dd6151125 100644 --- a/code/core/src/component-testing/components/Subnav.stories.tsx +++ b/code/core/src/component-testing/components/Subnav.stories.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + import { action } from 'storybook/actions'; import { Subnav } from './Subnav'; @@ -132,3 +134,18 @@ export const Detached = { }, }, }; + +export const WithOpenInEditorLink = { + args: { + status: 'completed', + controlStates: { + detached: true, + start: false, + back: false, + goto: false, + next: false, + end: false, + }, + canOpenInEditor: true, + }, +}; diff --git a/code/core/src/component-testing/components/Subnav.tsx b/code/core/src/component-testing/components/Subnav.tsx index 6fbbd9d69114..875d2a4d6eba 100644 --- a/code/core/src/component-testing/components/Subnav.tsx +++ b/code/core/src/component-testing/components/Subnav.tsx @@ -19,6 +19,7 @@ import { SyncIcon, } from '@storybook/icons'; +import { type API } from 'storybook/manager-api'; import { styled, useTheme } from 'storybook/theming'; import { type ControlStates } from '../../instrumenter/types'; @@ -47,6 +48,9 @@ interface SubnavProps { status: PlayStatus; storyFileName?: string; onScrollToEnd?: () => void; + importPath?: string; + canOpenInEditor?: boolean; + api: API; } const StyledButton = styled(Button)(({ theme }) => ({ @@ -73,8 +77,10 @@ const StyledSeparator = styled(Separator)({ marginTop: 0, }); -const StyledLocation = styled(P)(({ theme }) => ({ - color: theme.textMutedColor, +const StyledLocation = styled(P)<{ isText?: boolean }>(({ theme, isText }) => ({ + color: isText ? theme.textMutedColor : theme.color.secondary, + cursor: isText ? 'default' : 'pointer', + fontWeight: isText ? theme.typography.weight.regular : theme.typography.weight.bold, justifyContent: 'flex-end', textAlign: 'right', whiteSpace: 'nowrap', @@ -119,6 +125,9 @@ export const Subnav: React.FC = ({ status, storyFileName, onScrollToEnd, + importPath, + canOpenInEditor, + api, }) => { const buttonText = status === 'errored' ? 'Scroll to error' : 'Scroll to end'; const theme = useTheme(); @@ -182,9 +191,28 @@ export const Subnav: React.FC = ({ - {storyFileName && ( + {(importPath || storyFileName) && ( - {storyFileName} + {canOpenInEditor ? ( + } + > + { + api.openInEditor({ + file: importPath as string, + }); + }} + > + {storyFileName} + + + ) : ( + {storyFileName} + )} )} diff --git a/code/core/src/components/components/tooltip/WithTooltip.tsx b/code/core/src/components/components/tooltip/WithTooltip.tsx index 8276bcd5e43a..20252273e6b8 100644 --- a/code/core/src/components/components/tooltip/WithTooltip.tsx +++ b/code/core/src/components/components/tooltip/WithTooltip.tsx @@ -177,7 +177,12 @@ const WithToolTipState = ({ useEffect(() => { const hide = () => onVisibilityChange(false); - document.addEventListener('keydown', hide, false); + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + hide(); + } + }; + document.addEventListener('keydown', handleKeyDown, false); // Find all iframes on the screen and bind to clicks inside them (waiting until the iframe is ready) const iframes: HTMLIFrameElement[] = Array.from(document.getElementsByTagName('iframe')); @@ -211,7 +216,7 @@ const WithToolTipState = ({ }); return () => { - document.removeEventListener('keydown', hide); + document.removeEventListener('keydown', handleKeyDown); unbinders.forEach((unbind) => { unbind(); }); diff --git a/code/core/src/core-events/data/open-in-editor.ts b/code/core/src/core-events/data/open-in-editor.ts new file mode 100644 index 000000000000..3e4df8202c7c --- /dev/null +++ b/code/core/src/core-events/data/open-in-editor.ts @@ -0,0 +1,8 @@ +export type OpenInEditorRequestPayload = { file: string; line?: number; column?: number }; + +export type OpenInEditorResponsePayload = { + file: string; + line?: number; + column?: number; + error: string | null; +}; diff --git a/code/core/src/core-events/index.ts b/code/core/src/core-events/index.ts index d0ff9cca7d54..3ab4b36fb3a1 100644 --- a/code/core/src/core-events/index.ts +++ b/code/core/src/core-events/index.ts @@ -86,6 +86,9 @@ enum events { ARGTYPES_INFO_RESPONSE = 'argtypesInfoResponse', CREATE_NEW_STORYFILE_REQUEST = 'createNewStoryfileRequest', CREATE_NEW_STORYFILE_RESPONSE = 'createNewStoryfileResponse', + // Open a file in the code editor + OPEN_IN_EDITOR_REQUEST = 'openInEditorRequest', + OPEN_IN_EDITOR_RESPONSE = 'openInEditorResponse', } // Enables: `import Events from ...` @@ -151,6 +154,8 @@ export const { SAVE_STORY_RESPONSE, ARGTYPES_INFO_REQUEST, ARGTYPES_INFO_RESPONSE, + OPEN_IN_EDITOR_REQUEST, + OPEN_IN_EDITOR_RESPONSE, } = events; export * from './data/create-new-story'; @@ -160,3 +165,4 @@ export * from './data/request-response'; export * from './data/save-story'; export * from './data/whats-new'; export * from './data/phases'; +export * from './data/open-in-editor'; diff --git a/code/core/src/core-server/dev-server.ts b/code/core/src/core-server/dev-server.ts index f79251d221a3..375917558ad9 100644 --- a/code/core/src/core-server/dev-server.ts +++ b/code/core/src/core-server/dev-server.ts @@ -16,7 +16,7 @@ import { getServerChannel } from './utils/get-server-channel'; import { getAccessControlMiddleware } from './utils/getAccessControlMiddleware'; import { getStoryIndexGenerator } from './utils/getStoryIndexGenerator'; import { getMiddleware } from './utils/middleware'; -import { openInBrowser } from './utils/open-in-browser'; +import { openInBrowser } from './utils/open-browser/open-in-browser'; import { getServerAddresses } from './utils/server-address'; import { getServer } from './utils/server-init'; import { useStatics } from './utils/server-statics'; @@ -55,6 +55,9 @@ export async function storybookDevServer(options: Options) { const proto = options.https ? 'https' : 'http'; const { address, networkAddress } = getServerAddresses(port, host, proto, initialPath); + // Expose addresses on options for the manager builder to surface in globals, important for QR code link sharing + options.networkAddress = networkAddress; + if (!core?.builder) { throw new MissingBuilderError(); } @@ -121,7 +124,9 @@ export async function storybookDevServer(options: Options) { await Promise.all([initializedStoryIndexGenerator, listening]).then(async ([indexGenerator]) => { if (indexGenerator && !options.ci && !options.smokeTest && options.open) { const url = host ? networkAddress : address; - openInBrowser(options.previewOnly ? `${url}iframe.html?navigator=true` : url); + openInBrowser(options.previewOnly ? `${url}iframe.html?navigator=true` : url).catch(() => { + // the browser window could not be opened, this is non-critical, we just ignore the error + }); } }); if (indexError) { diff --git a/code/core/src/core-server/presets/common-preset.ts b/code/core/src/core-server/presets/common-preset.ts index d95626ee7e4a..fe09e8647688 100644 --- a/code/core/src/core-server/presets/common-preset.ts +++ b/code/core/src/core-server/presets/common-preset.ts @@ -32,6 +32,7 @@ import { dedent } from 'ts-dedent'; import { resolvePackageDir } from '../../shared/utils/module'; import { initCreateNewStoryChannel } from '../server-channel/create-new-story-channel'; import { initFileSearchChannel } from '../server-channel/file-search-channel'; +import { initOpenInEditorChannel } from '../server-channel/open-in-editor-channel'; import { defaultFavicon, defaultStaticDirs } from '../utils/constants'; import { initializeSaveStory } from '../utils/save-story/save-story'; import { parseStaticDir } from '../utils/server-statics'; @@ -256,6 +257,7 @@ export const experimental_serverChannel = async ( initFileSearchChannel(channel, options, coreOptions); initCreateNewStoryChannel(channel, options, coreOptions); + initOpenInEditorChannel(channel, options, coreOptions); return channel; }; diff --git a/code/core/src/core-server/server-channel/open-in-editor-channel.ts b/code/core/src/core-server/server-channel/open-in-editor-channel.ts new file mode 100644 index 000000000000..3f90477c5bad --- /dev/null +++ b/code/core/src/core-server/server-channel/open-in-editor-channel.ts @@ -0,0 +1,65 @@ +import type { Channel } from 'storybook/internal/channels'; +import type { + OpenInEditorRequestPayload, + OpenInEditorResponsePayload, +} from 'storybook/internal/core-events'; +import { OPEN_IN_EDITOR_REQUEST, OPEN_IN_EDITOR_RESPONSE } from 'storybook/internal/core-events'; +import { telemetry } from 'storybook/internal/telemetry'; +import type { CoreConfig, Options, StoryIndex } from 'storybook/internal/types'; + +import launch from 'launch-editor'; + +export async function initOpenInEditorChannel( + channel: Channel, + _options: Options, + coreOptions: CoreConfig +) { + channel.on(OPEN_IN_EDITOR_REQUEST, async (payload: OpenInEditorRequestPayload) => { + const sendTelemetry = (data: { success: boolean; error?: string }) => { + if (!coreOptions.disableTelemetry) { + telemetry('open-in-editor', data); + } + }; + try { + const { file: targetFile, line, column } = payload; + + if (!targetFile) { + throw new Error('No file was provided to open'); + } + + const location = + typeof line === 'number' + ? `${targetFile}:${line}${typeof column === 'number' ? `:${column}` : ''}` + : targetFile; + + await new Promise((resolve, reject) => { + launch(location, undefined, (_fileName: string, errorMessage: string | null) => { + if (errorMessage) { + reject(new Error(errorMessage)); + } else { + resolve(); + } + }); + }); + + channel.emit(OPEN_IN_EDITOR_RESPONSE, { + file: targetFile!, + line, + column, + error: null, + } satisfies OpenInEditorResponsePayload); + + sendTelemetry({ success: true }); + } catch (e: any) { + const error = e?.message || 'Failed to open in editor'; + channel.emit(OPEN_IN_EDITOR_RESPONSE, { + error, + ...payload, + } satisfies OpenInEditorResponsePayload); + + sendTelemetry({ success: false, error }); + } + }); + + return channel; +} diff --git a/code/core/src/core-server/typings.d.ts b/code/core/src/core-server/typings.d.ts index 3f3cb6881312..e20ddb71c073 100644 --- a/code/core/src/core-server/typings.d.ts +++ b/code/core/src/core-server/typings.d.ts @@ -1,7 +1,5 @@ declare module 'lazy-universal-dotenv'; declare module 'pnp-webpack-plugin'; -declare module 'better-opn'; -declare module 'open'; declare module '@aw-web-design/x-default-browser'; declare module '@discoveryjs/json-ext'; declare module 'watchpack'; diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts index f366f1e8ac74..27fa366eba4e 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts @@ -84,6 +84,7 @@ describe('StoryIndexGenerator', () => { "entries": { "a--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -146,6 +147,7 @@ describe('StoryIndexGenerator', () => { }, "f--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "f--story-one", "importPath": "./src/F.story.ts", "name": "Story One", @@ -192,6 +194,7 @@ describe('StoryIndexGenerator', () => { }, "stories--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "stories--story-one", "importPath": "./src/stories.ts", "name": "Story One", @@ -225,6 +228,7 @@ describe('StoryIndexGenerator', () => { "entries": { "componentpath-extension--story-one": { "componentPath": "./src/componentPath/component.js", + "exportName": "StoryOne", "id": "componentpath-extension--story-one", "importPath": "./src/componentPath/extension.stories.js", "name": "Story One", @@ -237,6 +241,7 @@ describe('StoryIndexGenerator', () => { }, "componentpath-noextension--story-one": { "componentPath": "./src/componentPath/component.js", + "exportName": "StoryOne", "id": "componentpath-noextension--story-one", "importPath": "./src/componentPath/noExtension.stories.js", "name": "Story One", @@ -249,6 +254,7 @@ describe('StoryIndexGenerator', () => { }, "componentpath-package--story-one": { "componentPath": "component-package", + "exportName": "StoryOne", "id": "componentpath-package--story-one", "importPath": "./src/componentPath/package.stories.js", "name": "Story One", @@ -261,6 +267,7 @@ describe('StoryIndexGenerator', () => { }, "nested-button--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "nested-button--story-one", "importPath": "./src/nested/Button.stories.ts", "name": "Story One", @@ -274,6 +281,7 @@ describe('StoryIndexGenerator', () => { }, "second-nested-g--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "second-nested-g--story-one", "importPath": "./src/second-nested/G.stories.ts", "name": "Story One", @@ -306,6 +314,7 @@ describe('StoryIndexGenerator', () => { "entries": { "a--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -333,6 +342,7 @@ describe('StoryIndexGenerator', () => { }, "b--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "b--story-one", "importPath": "./src/B.stories.ts", "name": "Story One", @@ -346,6 +356,7 @@ describe('StoryIndexGenerator', () => { }, "componentpath-extension--story-one": { "componentPath": "./src/componentPath/component.js", + "exportName": "StoryOne", "id": "componentpath-extension--story-one", "importPath": "./src/componentPath/extension.stories.js", "name": "Story One", @@ -358,6 +369,7 @@ describe('StoryIndexGenerator', () => { }, "componentpath-noextension--story-one": { "componentPath": "./src/componentPath/component.js", + "exportName": "StoryOne", "id": "componentpath-noextension--story-one", "importPath": "./src/componentPath/noExtension.stories.js", "name": "Story One", @@ -370,6 +382,7 @@ describe('StoryIndexGenerator', () => { }, "componentpath-package--story-one": { "componentPath": "component-package", + "exportName": "StoryOne", "id": "componentpath-package--story-one", "importPath": "./src/componentPath/package.stories.js", "name": "Story One", @@ -395,6 +408,7 @@ describe('StoryIndexGenerator', () => { }, "d--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "d--story-one", "importPath": "./src/D.stories.jsx", "name": "Story One", @@ -408,6 +422,7 @@ describe('StoryIndexGenerator', () => { }, "example-button--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "example-button--story-one", "importPath": "./src/Button.stories.ts", "name": "Story One", @@ -421,6 +436,7 @@ describe('StoryIndexGenerator', () => { }, "first-nested-deeply-f--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "first-nested-deeply-f--story-one", "importPath": "./src/first-nested/deeply/F.stories.js", "name": "Story One", @@ -433,6 +449,7 @@ describe('StoryIndexGenerator', () => { }, "first-nested-deeply-features--with-csf-1": { "componentPath": undefined, + "exportName": "WithCSF1", "id": "first-nested-deeply-features--with-csf-1", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With CSF 1", @@ -445,6 +462,7 @@ describe('StoryIndexGenerator', () => { }, "first-nested-deeply-features--with-play": { "componentPath": undefined, + "exportName": "WithPlay", "id": "first-nested-deeply-features--with-play", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With Play", @@ -458,6 +476,7 @@ describe('StoryIndexGenerator', () => { }, "first-nested-deeply-features--with-render": { "componentPath": undefined, + "exportName": "WithRender", "id": "first-nested-deeply-features--with-render", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With Render", @@ -470,6 +489,7 @@ describe('StoryIndexGenerator', () => { }, "first-nested-deeply-features--with-story-fn": { "componentPath": undefined, + "exportName": "WithStoryFn", "id": "first-nested-deeply-features--with-story-fn", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With Story Fn", @@ -482,6 +502,7 @@ describe('StoryIndexGenerator', () => { }, "first-nested-deeply-features--with-test": { "componentPath": undefined, + "exportName": "WithTest", "id": "first-nested-deeply-features--with-test", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With Test", @@ -508,6 +529,7 @@ describe('StoryIndexGenerator', () => { }, "h--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "h--story-one", "importPath": "./src/H.stories.mjs", "name": "Story One", @@ -521,6 +543,7 @@ describe('StoryIndexGenerator', () => { }, "nested-button--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "nested-button--story-one", "importPath": "./src/nested/Button.stories.ts", "name": "Story One", @@ -534,6 +557,7 @@ describe('StoryIndexGenerator', () => { }, "second-nested-g--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "second-nested-g--story-one", "importPath": "./src/second-nested/G.stories.ts", "name": "Story One", @@ -586,6 +610,7 @@ describe('StoryIndexGenerator', () => { "entries": { "a--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -613,6 +638,7 @@ describe('StoryIndexGenerator', () => { }, "b--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "b--story-one", "importPath": "./src/B.stories.ts", "name": "Story One", @@ -626,6 +652,7 @@ describe('StoryIndexGenerator', () => { }, "componentpath-extension--story-one": { "componentPath": "./src/componentPath/component.js", + "exportName": "StoryOne", "id": "componentpath-extension--story-one", "importPath": "./src/componentPath/extension.stories.js", "name": "Story One", @@ -638,6 +665,7 @@ describe('StoryIndexGenerator', () => { }, "componentpath-noextension--story-one": { "componentPath": "./src/componentPath/component.js", + "exportName": "StoryOne", "id": "componentpath-noextension--story-one", "importPath": "./src/componentPath/noExtension.stories.js", "name": "Story One", @@ -650,6 +678,7 @@ describe('StoryIndexGenerator', () => { }, "componentpath-package--story-one": { "componentPath": "component-package", + "exportName": "StoryOne", "id": "componentpath-package--story-one", "importPath": "./src/componentPath/package.stories.js", "name": "Story One", @@ -675,6 +704,7 @@ describe('StoryIndexGenerator', () => { }, "d--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "d--story-one", "importPath": "./src/D.stories.jsx", "name": "Story One", @@ -688,6 +718,7 @@ describe('StoryIndexGenerator', () => { }, "example-button--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "example-button--story-one", "importPath": "./src/Button.stories.ts", "name": "Story One", @@ -701,6 +732,7 @@ describe('StoryIndexGenerator', () => { }, "first-nested-deeply-f--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "first-nested-deeply-f--story-one", "importPath": "./src/first-nested/deeply/F.stories.js", "name": "Story One", @@ -713,6 +745,7 @@ describe('StoryIndexGenerator', () => { }, "first-nested-deeply-features--with-csf-1": { "componentPath": undefined, + "exportName": "WithCSF1", "id": "first-nested-deeply-features--with-csf-1", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With CSF 1", @@ -725,6 +758,7 @@ describe('StoryIndexGenerator', () => { }, "first-nested-deeply-features--with-play": { "componentPath": undefined, + "exportName": "WithPlay", "id": "first-nested-deeply-features--with-play", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With Play", @@ -738,6 +772,7 @@ describe('StoryIndexGenerator', () => { }, "first-nested-deeply-features--with-render": { "componentPath": undefined, + "exportName": "WithRender", "id": "first-nested-deeply-features--with-render", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With Render", @@ -750,6 +785,7 @@ describe('StoryIndexGenerator', () => { }, "first-nested-deeply-features--with-story-fn": { "componentPath": undefined, + "exportName": "WithStoryFn", "id": "first-nested-deeply-features--with-story-fn", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With Story Fn", @@ -762,6 +798,7 @@ describe('StoryIndexGenerator', () => { }, "first-nested-deeply-features--with-test": { "componentPath": undefined, + "exportName": "WithTest", "id": "first-nested-deeply-features--with-test", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With Test", @@ -788,6 +825,7 @@ describe('StoryIndexGenerator', () => { }, "h--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "h--story-one", "importPath": "./src/H.stories.mjs", "name": "Story One", @@ -801,6 +839,7 @@ describe('StoryIndexGenerator', () => { }, "nested-button--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "nested-button--story-one", "importPath": "./src/nested/Button.stories.ts", "name": "Story One", @@ -814,6 +853,7 @@ describe('StoryIndexGenerator', () => { }, "second-nested-g--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "second-nested-g--story-one", "importPath": "./src/second-nested/G.stories.ts", "name": "Story One", @@ -1033,6 +1073,7 @@ describe('StoryIndexGenerator', () => { }, "b--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "b--story-one", "importPath": "./src/B.stories.ts", "name": "Story One", @@ -1098,6 +1139,7 @@ describe('StoryIndexGenerator', () => { }, "b--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "b--story-one", "importPath": "./src/B.stories.ts", "name": "Story One", @@ -1155,6 +1197,7 @@ describe('StoryIndexGenerator', () => { }, "a--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -1212,6 +1255,7 @@ describe('StoryIndexGenerator', () => { }, "a--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -1261,6 +1305,7 @@ describe('StoryIndexGenerator', () => { }, "duplicate-a--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "duplicate-a--story-one", "importPath": "./duplicate/A.stories.js", "name": "Story One", @@ -1274,6 +1319,7 @@ describe('StoryIndexGenerator', () => { }, "duplicate-a--story-two": { "componentPath": undefined, + "exportName": "StoryTwo", "id": "duplicate-a--story-two", "importPath": "./duplicate/SecondA.stories.js", "name": "Story Two", @@ -1338,6 +1384,7 @@ describe('StoryIndexGenerator', () => { }, "my-component-a--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "my-component-a--story-one", "importPath": "./docs-id-generation/A.stories.jsx", "name": "Story One", @@ -1401,6 +1448,7 @@ describe('StoryIndexGenerator', () => { }, "a--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -1552,6 +1600,7 @@ describe('StoryIndexGenerator', () => { }, "a--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -1640,6 +1689,7 @@ describe('StoryIndexGenerator', () => { "entries": { "a--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -1667,6 +1717,7 @@ describe('StoryIndexGenerator', () => { }, "b--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "b--story-one", "importPath": "./src/B.stories.ts", "name": "Story One", @@ -1736,6 +1787,7 @@ describe('StoryIndexGenerator', () => { }, "my-component-b--story-one": { "componentPath": undefined, + "exportName": "StoryOne", "id": "my-component-b--story-one", "importPath": "./docs-id-generation/B.stories.jsx", "name": "Story One", diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.ts b/code/core/src/core-server/utils/StoryIndexGenerator.ts index 9a4be8f30d97..d66996b3dbcd 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.ts @@ -451,6 +451,7 @@ export class StoryIndexGenerator { importPath, componentPath, tags, + ...(input.exportName ? { exportName: input.exportName } : {}), }; }); diff --git a/code/core/src/core-server/utils/__tests__/index-extraction.test.ts b/code/core/src/core-server/utils/__tests__/index-extraction.test.ts index 41cfdf116fcd..4c6bea472f1f 100644 --- a/code/core/src/core-server/utils/__tests__/index-extraction.test.ts +++ b/code/core/src/core-server/utils/__tests__/index-extraction.test.ts @@ -63,6 +63,7 @@ describe('story extraction', () => { "entries": [ { "componentPath": undefined, + "exportName": "StoryOne", "extra": { "metaId": "a", "stats": {}, @@ -78,6 +79,7 @@ describe('story extraction', () => { }, { "componentPath": undefined, + "exportName": "StoryOne", "extra": { "metaId": "custom-id", "stats": {}, @@ -125,6 +127,7 @@ describe('story extraction', () => { "entries": [ { "componentPath": undefined, + "exportName": "StoryOne", "extra": { "metaId": undefined, "stats": {}, @@ -174,6 +177,7 @@ describe('story extraction', () => { "entries": [ { "componentPath": undefined, + "exportName": "StoryOne", "extra": { "metaId": "a", "stats": {}, @@ -225,6 +229,7 @@ describe('story extraction', () => { "entries": [ { "componentPath": undefined, + "exportName": "StoryOne", "extra": { "metaId": "a", "stats": {}, @@ -294,6 +299,7 @@ describe('story extraction', () => { "entries": [ { "componentPath": undefined, + "exportName": "StoryOne", "extra": { "metaId": undefined, "stats": {}, @@ -309,6 +315,7 @@ describe('story extraction', () => { }, { "componentPath": undefined, + "exportName": "StoryTwo", "extra": { "metaId": undefined, "stats": {}, @@ -324,6 +331,7 @@ describe('story extraction', () => { }, { "componentPath": undefined, + "exportName": "StoryThree", "extra": { "metaId": "custom-meta-id", "stats": {}, @@ -372,6 +380,7 @@ describe('story extraction', () => { "entries": [ { "componentPath": undefined, + "exportName": "StoryOne", "extra": { "metaId": undefined, "stats": {}, @@ -437,6 +446,7 @@ describe('docs entries from story extraction', () => { }, { "componentPath": undefined, + "exportName": "StoryOne", "extra": { "metaId": undefined, "stats": {}, diff --git a/code/core/src/core-server/utils/open-browser/open-in-browser.ts b/code/core/src/core-server/utils/open-browser/open-in-browser.ts new file mode 100644 index 000000000000..8e1475c8d22e --- /dev/null +++ b/code/core/src/core-server/utils/open-browser/open-in-browser.ts @@ -0,0 +1,33 @@ +import { logger } from 'storybook/internal/node-logger'; + +import open from 'open'; +import { dedent } from 'ts-dedent'; + +import { openBrowser } from './opener'; + +export async function openInBrowser(address: string) { + let errorOccured = false; + + try { + await openBrowser(address); + } catch (error) { + errorOccured = true; + } + + try { + if (errorOccured) { + await open(address); + errorOccured = false; + } + } catch (error) { + errorOccured = true; + } + + if (errorOccured) { + logger.error(dedent` + Could not open ${address} inside a browser. If you're running this command inside a + docker container or on a CI, you need to pass the '--ci' flag to prevent opening a + browser by default. + `); + } +} diff --git a/code/core/src/core-server/utils/open-browser/opener.ts b/code/core/src/core-server/utils/open-browser/opener.ts new file mode 100644 index 000000000000..365c547a6eb0 --- /dev/null +++ b/code/core/src/core-server/utils/open-browser/opener.ts @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the LICENSE file in the root + * directory of this source tree. + */ +import { execSync } from 'node:child_process'; +import { join } from 'node:path'; + +import spawn from 'cross-spawn'; +import open, { type App } from 'open'; +import picocolors from 'picocolors'; + +import { resolvePackageDir } from '../../../common'; + +// https://github.com/sindresorhus/open#app +const OSX_CHROME = 'google chrome'; + +const Actions = Object.freeze({ + NONE: 0, + BROWSER: 1, + SCRIPT: 2, +}); + +function getBrowserEnv() { + // Attempt to honor this environment variable. + // It is specific to the operating system. + // See https://github.com/sindresorhus/open#app for documentation. + const value = process.env.BROWSER; + const args = process.env.BROWSER_ARGS ? process.env.BROWSER_ARGS.split(' ') : []; + let action; + if (!value) { + // Default. + action = Actions.BROWSER; + } else if (value.toLowerCase().endsWith('.js')) { + action = Actions.SCRIPT; + } else if (value.toLowerCase() === 'none') { + action = Actions.NONE; + } else { + action = Actions.BROWSER; + } + return { action, value, args }; +} + +function executeNodeScript(scriptPath: string, url: string) { + const extraArgs = process.argv.slice(2); + const child = spawn(process.execPath, [scriptPath, ...extraArgs, url], { + stdio: 'inherit', + }); + child.on('close', (code) => { + if (code !== 0) { + console.log(); + console.log(picocolors.red('The script specified as BROWSER environment variable failed.')); + console.log(`${picocolors.cyan(scriptPath)} exited with code ${code}.`); + console.log(); + return; + } + }); + return true; +} + +function startBrowserProcess( + browser: App | readonly App[] | undefined, + url: string, + args: string[] +) { + // If we're on OS X, the user hasn't specifically + // requested a different browser, we can try opening + // Chrome with AppleScript. This lets us reuse an + // existing tab when possible instead of creating a new one. + const shouldTryOpenChromiumWithAppleScript = + process.platform === 'darwin' && (typeof browser !== 'string' || browser === OSX_CHROME); + + if (shouldTryOpenChromiumWithAppleScript) { + // Will use the first open browser found from list + const supportedChromiumBrowsers = [ + 'Google Chrome Canary', + 'Google Chrome Dev', + 'Google Chrome Beta', + 'Google Chrome', + 'Microsoft Edge', + 'Brave Browser', + 'Arc', + 'Vivaldi', + 'Chromium', + ]; + + for (const chromiumBrowser of supportedChromiumBrowsers) { + try { + // Try our best to reuse existing tab + // on OSX Chromium-based browser with AppleScript + execSync(`ps cax | grep "${chromiumBrowser}"`); + const pathToApplescript = join( + resolvePackageDir('storybook'), + 'assets', + 'server', + 'openBrowser.applescript' + ); + + const command = `osascript "${pathToApplescript}" \"` + .concat(encodeURI(url), '" "') + .concat( + process.env.OPEN_MATCH_HOST_ONLY === 'true' + ? encodeURI(new URL(url).origin) + : encodeURI(url), + '" "' + ) + .concat(chromiumBrowser, '"'); + + execSync(command, { + cwd: __dirname, + }); + + return true; + } catch (err) { + // Ignore errors. + } + } + } + + // Another special case: on OS X, check if BROWSER has been set to "open". + // In this case, instead of passing `open` to `opn` (which won't work), + // just ignore it (thus ensuring the intended behavior, i.e. opening the system browser): + // https://github.com/facebook/create-react-app/pull/1690#issuecomment-283518768 + // @ts-expect-error - browser is a string + if (process.platform === 'darwin' && browser === 'open') { + browser = undefined; + } + + // If there are arguments, they must be passed as array with the browser + if (typeof browser === 'string' && args.length > 0) { + // @ts-expect-error - browser is a string + browser = [browser].concat(args); + } + + // Fallback to open + // (It will always open new tab) + try { + const options = { app: browser, wait: false, url: true }; + open(url, options).catch(() => {}); // Prevent `unhandledRejection` error. + return true; + } catch (err) { + return false; + } +} + +/** + * Reads the BROWSER environment variable and decides what to do with it. Returns true if it opened + * a browser or ran a node.js script, otherwise false. + */ +export function openBrowser(url: string) { + const { action, value, args } = getBrowserEnv(); + switch (action) { + case Actions.NONE: { + // Special case: BROWSER="none" will prevent opening completely. + return false; + } + case Actions.SCRIPT: { + if (!value) { + throw new Error('BROWSER environment variable is not set.'); + } + return executeNodeScript(value, url); + } + case Actions.BROWSER: { + return startBrowserProcess(value as App | readonly App[] | undefined, url, args); + } + default: { + throw new Error('Not implemented.'); + } + } +} diff --git a/code/core/src/core-server/utils/open-in-browser.ts b/code/core/src/core-server/utils/open-in-browser.ts deleted file mode 100644 index 2a8789171f49..000000000000 --- a/code/core/src/core-server/utils/open-in-browser.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { logger } from 'storybook/internal/node-logger'; - -import getDefaultBrowser from '@aw-web-design/x-default-browser'; -import betterOpn from 'better-opn'; -// betterOpn alias used because also loading open -import open from 'open'; -import { dedent } from 'ts-dedent'; - -export function openInBrowser(address: string) { - const browserEnvVar = process.env.BROWSER; - const userBrowserIsChrome = - browserEnvVar === 'chrome' || - browserEnvVar === 'chromium' || - browserEnvVar === 'brave' || - browserEnvVar === 'com.brave.browser'; - - const openOptions = browserEnvVar ? { app: { name: browserEnvVar } } : {}; - - getDefaultBrowser(async (err: any, res: any) => { - try { - if ( - res && - (res.isChrome || - res.isChromium || - res.identity === 'com.brave.browser' || - userBrowserIsChrome) - ) { - // We use betterOpn for Chrome because it is better at handling which chrome tab - // or window the preview loads in. - await betterOpn(address); - } else { - await open(address, openOptions); - } - } catch (error) { - logger.error(dedent` - Could not open ${address} inside a browser. If you're running this command inside a - docker container or on a CI, you need to pass the '--ci' flag to prevent opening a - browser by default. - `); - } - }); -} diff --git a/code/core/src/core-server/utils/stories-json.test.ts b/code/core/src/core-server/utils/stories-json.test.ts index 6f5907f2c38c..0c426a7b960b 100644 --- a/code/core/src/core-server/utils/stories-json.test.ts +++ b/code/core/src/core-server/utils/stories-json.test.ts @@ -150,6 +150,7 @@ describe('useStoriesJson', () => { "type": "docs", }, "a--story-one": { + "exportName": "StoryOne", "id": "a--story-one", "importPath": "./src/A.stories.js", "name": "Story One", @@ -176,6 +177,7 @@ describe('useStoriesJson', () => { "type": "docs", }, "b--story-one": { + "exportName": "StoryOne", "id": "b--story-one", "importPath": "./src/B.stories.ts", "name": "Story One", @@ -189,6 +191,7 @@ describe('useStoriesJson', () => { }, "componentpath-extension--story-one": { "componentPath": "./src/componentPath/component.js", + "exportName": "StoryOne", "id": "componentpath-extension--story-one", "importPath": "./src/componentPath/extension.stories.js", "name": "Story One", @@ -201,6 +204,7 @@ describe('useStoriesJson', () => { }, "componentpath-noextension--story-one": { "componentPath": "./src/componentPath/component.js", + "exportName": "StoryOne", "id": "componentpath-noextension--story-one", "importPath": "./src/componentPath/noExtension.stories.js", "name": "Story One", @@ -213,6 +217,7 @@ describe('useStoriesJson', () => { }, "componentpath-package--story-one": { "componentPath": "component-package", + "exportName": "StoryOne", "id": "componentpath-package--story-one", "importPath": "./src/componentPath/package.stories.js", "name": "Story One", @@ -237,6 +242,7 @@ describe('useStoriesJson', () => { "type": "docs", }, "d--story-one": { + "exportName": "StoryOne", "id": "d--story-one", "importPath": "./src/D.stories.jsx", "name": "Story One", @@ -303,6 +309,7 @@ describe('useStoriesJson', () => { "type": "docs", }, "example-button--story-one": { + "exportName": "StoryOne", "id": "example-button--story-one", "importPath": "./src/Button.stories.ts", "name": "Story One", @@ -315,6 +322,7 @@ describe('useStoriesJson', () => { "type": "story", }, "first-nested-deeply-f--story-one": { + "exportName": "StoryOne", "id": "first-nested-deeply-f--story-one", "importPath": "./src/first-nested/deeply/F.stories.js", "name": "Story One", @@ -326,6 +334,7 @@ describe('useStoriesJson', () => { "type": "story", }, "first-nested-deeply-features--with-csf-1": { + "exportName": "WithCSF1", "id": "first-nested-deeply-features--with-csf-1", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With CSF 1", @@ -337,6 +346,7 @@ describe('useStoriesJson', () => { "type": "story", }, "first-nested-deeply-features--with-play": { + "exportName": "WithPlay", "id": "first-nested-deeply-features--with-play", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With Play", @@ -349,6 +359,7 @@ describe('useStoriesJson', () => { "type": "story", }, "first-nested-deeply-features--with-render": { + "exportName": "WithRender", "id": "first-nested-deeply-features--with-render", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With Render", @@ -360,6 +371,7 @@ describe('useStoriesJson', () => { "type": "story", }, "first-nested-deeply-features--with-story-fn": { + "exportName": "WithStoryFn", "id": "first-nested-deeply-features--with-story-fn", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With Story Fn", @@ -371,6 +383,7 @@ describe('useStoriesJson', () => { "type": "story", }, "first-nested-deeply-features--with-test": { + "exportName": "WithTest", "id": "first-nested-deeply-features--with-test", "importPath": "./src/first-nested/deeply/Features.stories.jsx", "name": "With Test", @@ -396,6 +409,7 @@ describe('useStoriesJson', () => { "type": "docs", }, "h--story-one": { + "exportName": "StoryOne", "id": "h--story-one", "importPath": "./src/H.stories.mjs", "name": "Story One", @@ -408,6 +422,7 @@ describe('useStoriesJson', () => { "type": "story", }, "nested-button--story-one": { + "exportName": "StoryOne", "id": "nested-button--story-one", "importPath": "./src/nested/Button.stories.ts", "name": "Story One", @@ -420,6 +435,7 @@ describe('useStoriesJson', () => { "type": "story", }, "second-nested-g--story-one": { + "exportName": "StoryOne", "id": "second-nested-g--story-one", "importPath": "./src/second-nested/G.stories.ts", "name": "Story One", diff --git a/code/core/src/manager-api/index.mock.ts b/code/core/src/manager-api/index.mock.ts index d79920fec81a..231a30b526ea 100644 --- a/code/core/src/manager-api/index.mock.ts +++ b/code/core/src/manager-api/index.mock.ts @@ -1,5 +1,9 @@ +import { fn } from 'storybook/test'; + export * from './root'; +export const openInEditor = fn(); + export { UniversalStore as experimental_UniversalStore } from '../shared/universal-store'; export { useUniversalStore as experimental_useUniversalStore } from '../shared/universal-store/use-universal-store-manager'; export { MockUniversalStore as experimental_MockUniversalStore } from '../shared/universal-store/mock'; diff --git a/code/core/src/manager-api/lib/shortcut.ts b/code/core/src/manager-api/lib/shortcut.ts index 10ec6130f0ed..afc4472ff718 100644 --- a/code/core/src/manager-api/lib/shortcut.ts +++ b/code/core/src/manager-api/lib/shortcut.ts @@ -41,18 +41,44 @@ export const eventToShortcut = (e: KeyboardEventLike): (string | string[])[] | n keys.push('shift'); } + // Derive a key from the physical code (letter/digit/punctuation) when needed + const codeUpper = e.code?.toUpperCase(); + const codeToCharMap: Record = { + MINUS: '-', + EQUAL: '=', + BRACKETLEFT: '[', + BRACKETRIGHT: ']', + BACKSLASH: '\\', + SEMICOLON: ';', + QUOTE: "'", + BACKQUOTE: '`', + COMMA: ',', + PERIOD: '.', + SLASH: '/', + }; + const codeChar = codeUpper + ? codeUpper.startsWith('KEY') && codeUpper.length === 4 + ? codeUpper.replace('KEY', '') + : codeUpper.startsWith('DIGIT') + ? codeUpper.replace('DIGIT', '') + : codeToCharMap[codeUpper] + : undefined; + if (e.key && e.key.length === 1 && e.key !== ' ') { const key = e.key.toUpperCase(); - // Using `event.code` to support `alt (option) + ` on macos which returns special characters + // Using `event.code` to support `alt (option) + ` on macOS which returns special characters // See full list of event.code here: // https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values - const code = e.code?.toUpperCase().replace('KEY', '').replace('DIGIT', ''); + const code = codeChar; if (code && code.length === 1 && code !== key) { keys.push([key, code]); } else { keys.push(key); } + } else if (e.key === 'Dead' && codeChar) { + // Handle dead keys (e.g., Option+E on macOS) by using the physical key from code + keys.push(codeChar); } if (e.key === ' ') { keys.push('space'); @@ -139,7 +165,7 @@ export const keyToSymbol = (key: string): string => { if (key === 'ArrowRight') { return '→'; } - return key.toUpperCase(); + return key?.toUpperCase(); }; // Display the shortcut as a human readable string diff --git a/code/core/src/manager-api/lib/stories.ts b/code/core/src/manager-api/lib/stories.ts index 3eed9e500890..e4f56e7fc425 100644 --- a/code/core/src/manager-api/lib/stories.ts +++ b/code/core/src/manager-api/lib/stories.ts @@ -330,6 +330,12 @@ export const transformStoryIndexToStoriesHash = ( return currentTags === null ? child.tags : intersect(currentTags, child.tags); }, null); } + + if (item.type === 'component') { + // attach importPath to the component node which should be the same for all children + // this way we can add "open in editor" to the component node + item.importPath = acc[item.children[0]].importPath; + } return acc; } diff --git a/code/core/src/manager-api/modules/notifications.ts b/code/core/src/manager-api/modules/notifications.ts index 14d876274bdd..443f9a3513c6 100644 --- a/code/core/src/manager-api/modules/notifications.ts +++ b/code/core/src/manager-api/modules/notifications.ts @@ -55,5 +55,8 @@ export const init: ModuleFn = ({ store }) => { const state: SubState = { notifications: [] }; - return { api, state }; + return { + api, + state, + }; }; diff --git a/code/core/src/manager-api/modules/open-in-editor.tsx b/code/core/src/manager-api/modules/open-in-editor.tsx new file mode 100644 index 000000000000..5ca681fd5dcd --- /dev/null +++ b/code/core/src/manager-api/modules/open-in-editor.tsx @@ -0,0 +1,77 @@ +import React from 'react'; + +import { + OPEN_IN_EDITOR_REQUEST, + OPEN_IN_EDITOR_RESPONSE, + type OpenInEditorResponsePayload, +} from 'storybook/internal/core-events'; +import type { API_Notification } from 'storybook/internal/types'; + +import { FailedIcon } from '@storybook/icons'; + +import { color } from 'storybook/theming'; + +import type { ModuleFn } from '../lib/types'; + +export interface SubState { + notifications: API_Notification[]; +} + +/** The API for opening files in the editor. */ +export interface SubAPI { + /** + * Opens the file in the editor. You can optionally provide a line and column number to open at a + * more specific location. + */ + openInEditor: (payload: { + file: string; + line?: number; + column?: number; + }) => Promise; +} + +export const init: ModuleFn = ({ provider, fullAPI }) => { + const api: SubAPI = { + openInEditor(payload: { + file: string; + line?: number; + column?: number; + }): Promise { + return new Promise((resolve) => { + const { file, line, column } = payload; + const handler = (res: OpenInEditorResponsePayload) => { + if (res.file === file && res.line === line && res.column === column) { + provider.channel?.off(OPEN_IN_EDITOR_RESPONSE, handler); + resolve(res); + } + }; + provider.channel?.on(OPEN_IN_EDITOR_RESPONSE, handler); + provider.channel?.emit(OPEN_IN_EDITOR_REQUEST, payload); + }); + }, + }; + + const state: SubState = { notifications: [] }; + + return { + api, + state, + init: async () => { + provider.channel?.on(OPEN_IN_EDITOR_RESPONSE, (payload: OpenInEditorResponsePayload) => { + if (payload.error !== null) { + fullAPI.addNotification({ + id: 'open-in-editor-error', + content: { + headline: 'Failed to open in editor', + subHeadline: + payload.error || + 'Check the Storybook process on the command line for more details.', + }, + icon: , + duration: 8_000, + }); + } + }); + }, + }; +}; diff --git a/code/core/src/manager-api/modules/shortcuts.ts b/code/core/src/manager-api/modules/shortcuts.ts index 3970d3cf2324..57233fca2eaa 100644 --- a/code/core/src/manager-api/modules/shortcuts.ts +++ b/code/core/src/manager-api/modules/shortcuts.ts @@ -7,6 +7,8 @@ import { import { global } from '@storybook/global'; +import copy from 'copy-to-clipboard'; + import type { KeyboardEventLike } from '../lib/shortcut'; import { eventToShortcut, shortcutMatchesShortcut } from '../lib/shortcut'; import type { ModuleFn } from '../lib/types'; @@ -110,6 +112,10 @@ export interface API_Shortcuts { collapseAll: API_KeyCollection; expandAll: API_KeyCollection; remount: API_KeyCollection; + openInEditor: API_KeyCollection; + copyStoryLink: API_KeyCollection; + // TODO: bring this back once we want to add shortcuts for this + // copyStoryName: API_KeyCollection; } export type API_Action = keyof API_Shortcuts; @@ -145,6 +151,10 @@ export const defaultShortcuts: API_Shortcuts = Object.freeze({ collapseAll: [controlOrMetaKey(), 'shift', 'ArrowUp'], expandAll: [controlOrMetaKey(), 'shift', 'ArrowDown'], remount: ['alt', 'R'], + openInEditor: ['alt', 'shift', 'E'], + copyStoryLink: ['alt', 'shift', 'L'], + // TODO: bring this back once we want to add shortcuts for this + // copyStoryName: ['alt', 'shift', 'C'], }); const addonsShortcuts: API_AddonShortcuts = {}; @@ -379,6 +389,26 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => { fullAPI.emit(FORCE_REMOUNT, { storyId }); break; } + case 'openInEditor': { + if (global.CONFIG_TYPE === 'DEVELOPMENT') { + fullAPI.openInEditor({ + file: fullAPI.getCurrentStoryData().importPath, + }); + } + break; + } + // TODO: bring this back once we want to add shortcuts for this + // case 'copyStoryName': { + // const storyData = fullAPI.getCurrentStoryData(); + // if (storyData.type === 'story') { + // copy(storyData.exportName); + // } + // break; + // } + case 'copyStoryLink': { + copy(window.location.href); + break; + } default: addonsShortcuts[feature].action(); break; diff --git a/code/core/src/manager-api/root.tsx b/code/core/src/manager-api/root.tsx index f5fa4eea2532..9e77d90d5ece 100644 --- a/code/core/src/manager-api/root.tsx +++ b/code/core/src/manager-api/root.tsx @@ -11,7 +11,6 @@ import React, { } from 'react'; import type { Listener } from 'storybook/internal/channels'; -import { deprecate } from 'storybook/internal/client-logger'; import { DOCS_PREPARED, SET_STORIES, @@ -54,6 +53,7 @@ import * as channel from './modules/channel'; import * as globals from './modules/globals'; import * as layout from './modules/layout'; import * as notifications from './modules/notifications'; +import * as openInEditor from './modules/open-in-editor'; import * as provider from './modules/provider'; import * as refs from './modules/refs'; import * as settings from './modules/settings'; @@ -177,6 +177,7 @@ class ManagerProvider extends Component { url, version, whatsnew, + openInEditor, ].map((m) => m.init({ ...routeData, ...optionsData, ...apiData, state: this.state, fullAPI: this.api }) ); diff --git a/code/core/src/manager-api/tests/stories.test.ts b/code/core/src/manager-api/tests/stories.test.ts index 0a922aa186ec..9ef6b08bb902 100644 --- a/code/core/src/manager-api/tests/stories.test.ts +++ b/code/core/src/manager-api/tests/stories.test.ts @@ -1421,6 +1421,7 @@ describe('stories API', () => { ], "depth": 0, "id": "a", + "importPath": "./a.ts", "name": "a", "parent": undefined, "renderLabel": undefined, @@ -1492,33 +1493,34 @@ describe('stories API', () => { await vi.waitFor(() => { expect(store.getState().filteredIndex).toMatchInlineSnapshot(` - { - "a": { - "children": [ - "a--1", - ], - "depth": 0, - "id": "a", - "name": "a", - "parent": undefined, - "renderLabel": undefined, - "tags": [], - "type": "component", - }, - "a--1": { - "depth": 1, - "id": "a--1", - "importPath": "./a.ts", - "name": "1", - "parent": "a", - "prepared": false, - "renderLabel": undefined, - "tags": [], - "title": "a", - "type": "story", - }, - } - `); + { + "a": { + "children": [ + "a--1", + ], + "depth": 0, + "id": "a", + "importPath": "./a.ts", + "name": "a", + "parent": undefined, + "renderLabel": undefined, + "tags": [], + "type": "component", + }, + "a--1": { + "depth": 1, + "id": "a--1", + "importPath": "./a.ts", + "name": "1", + "parent": "a", + "prepared": false, + "renderLabel": undefined, + "tags": [], + "title": "a", + "type": "story", + }, + } + `); }); }); @@ -1543,6 +1545,7 @@ describe('stories API', () => { ], "depth": 0, "id": "a", + "importPath": "./a.ts", "name": "a", "parent": undefined, "renderLabel": undefined, diff --git a/code/core/src/manager/components/preview/Toolbar.tsx b/code/core/src/manager/components/preview/Toolbar.tsx index 2875b370edbc..028691c8a936 100644 --- a/code/core/src/manager/components/preview/Toolbar.tsx +++ b/code/core/src/manager/components/preview/Toolbar.tsx @@ -19,11 +19,6 @@ import { import { styled } from 'storybook/theming'; import { useLayout } from '../layout/LayoutProvider'; -import { addonsTool } from './tools/addons'; -import { copyTool } from './tools/copy'; -import { ejectTool } from './tools/eject'; -import { remountTool } from './tools/remount'; -import { zoomTool } from './tools/zoom'; import type { PreviewProps } from './utils/types'; export const getTools = (getFn: API['getElements']) => Object.values(getFn(types.TOOL)); @@ -112,14 +107,6 @@ export const createTabsTool = (tabs: Addon_BaseType[]): Addon_BaseType => ({ ), }); -export const defaultTools: Addon_BaseType[] = [remountTool, zoomTool]; -export const defaultToolsExtra: Addon_BaseType[] = [ - addonsTool, - fullScreenTool, - ejectTool, - copyTool, -]; - export interface ToolData { isShown: boolean; tabs: Addon_BaseType[]; diff --git a/code/core/src/manager/components/preview/tools/copy.tsx b/code/core/src/manager/components/preview/tools/copy.tsx deleted file mode 100644 index 9be9b1d35ccd..000000000000 --- a/code/core/src/manager/components/preview/tools/copy.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { IconButton, getStoryHref } from 'storybook/internal/components'; -import type { Addon_BaseType } from 'storybook/internal/types'; - -import { global } from '@storybook/global'; -import { LinkIcon } from '@storybook/icons'; - -import copy from 'copy-to-clipboard'; -import { Consumer, types } from 'storybook/manager-api'; -import type { Combo } from 'storybook/manager-api'; - -const { PREVIEW_URL, document } = global; - -const copyMapper = ({ state }: Combo) => { - const { storyId, refId, refs } = state; - const { location } = document; - // @ts-expect-error (non strict) - const ref = refs[refId]; - let baseUrl = `${location.origin}${location.pathname}`; - - if (!baseUrl.endsWith('/')) { - baseUrl += '/'; - } - - return { - refId, - baseUrl: ref ? `${ref.url}/iframe.html` : (PREVIEW_URL as string) || `${baseUrl}iframe.html`, - storyId, - queryParams: state.customQueryParams, - }; -}; - -export const copyTool: Addon_BaseType = { - title: 'copy', - id: 'copy', - type: types.TOOL, - match: ({ viewMode, tabId }) => viewMode === 'story' && !tabId, - render: () => ( - - {({ baseUrl, storyId, queryParams }) => - storyId ? ( - copy(getStoryHref(baseUrl, storyId, queryParams))} - title="Copy canvas link" - > - - - ) : null - } - - ), -}; diff --git a/code/core/src/manager/components/preview/tools/eject.tsx b/code/core/src/manager/components/preview/tools/eject.tsx deleted file mode 100644 index a6857c778c42..000000000000 --- a/code/core/src/manager/components/preview/tools/eject.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; - -import { IconButton, getStoryHref } from 'storybook/internal/components'; -import type { Addon_BaseType } from 'storybook/internal/types'; - -import { global } from '@storybook/global'; -import { ShareAltIcon } from '@storybook/icons'; - -import { Consumer, types } from 'storybook/manager-api'; -import type { Combo } from 'storybook/manager-api'; - -const { PREVIEW_URL } = global; - -const ejectMapper = ({ state }: Combo) => { - const { storyId, refId, refs } = state; - // @ts-expect-error (non strict) - const ref = refs[refId]; - - return { - refId, - baseUrl: ref ? `${ref.url}/iframe.html` : (PREVIEW_URL as string) || 'iframe.html', - storyId, - queryParams: state.customQueryParams, - }; -}; - -export const ejectTool: Addon_BaseType = { - title: 'eject', - id: 'eject', - type: types.TOOL, - match: ({ viewMode, tabId }) => viewMode === 'story' && !tabId, - render: () => ( - - {({ baseUrl, storyId, queryParams }) => - storyId ? ( - - - - - - ) : null - } - - ), -}; diff --git a/code/core/src/manager/components/preview/tools/open-in-editor.tsx b/code/core/src/manager/components/preview/tools/open-in-editor.tsx new file mode 100644 index 000000000000..8515ab948ca7 --- /dev/null +++ b/code/core/src/manager/components/preview/tools/open-in-editor.tsx @@ -0,0 +1,55 @@ +import React from 'react'; + +import { IconButton } from 'storybook/internal/components'; +import type { Addon_BaseType } from 'storybook/internal/types'; + +import { global } from '@storybook/global'; +import { EditorIcon } from '@storybook/icons'; + +import { Consumer, types, useStorybookApi } from 'storybook/manager-api'; +import type { Combo } from 'storybook/manager-api'; + +const mapper = ({ state, api }: Combo) => { + const { storyId, refId } = state; + const entry = api.getData(storyId, refId); + + const isCompositionStory = !!refId; // Only allow opening local stories in editor + + return { + storyId, + isCompositionStory, + importPath: entry?.importPath as string | undefined, + }; +}; + +export const openInEditorTool: Addon_BaseType = { + title: 'open-in-editor', + id: 'open-in-editor', + type: types.TOOL, + match: ({ viewMode, tabId }) => + global.CONFIG_TYPE === 'DEVELOPMENT' && (viewMode === 'story' || viewMode === 'docs') && !tabId, + render: () => ( + + {({ importPath, isCompositionStory }) => { + const api = useStorybookApi(); + if (isCompositionStory || !importPath) { + return null; + } + return ( + + api.openInEditor({ + file: importPath, + }) + } + title="Open in editor" + aria-label="Open in editor" + > + + + ); + }} + + ), +}; diff --git a/code/core/src/manager/components/preview/tools/share.stories.tsx b/code/core/src/manager/components/preview/tools/share.stories.tsx new file mode 100644 index 000000000000..7884890e1a29 --- /dev/null +++ b/code/core/src/manager/components/preview/tools/share.stories.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +import { global } from '@storybook/global'; + +import type { StoryObj } from '@storybook/react-vite'; + +import { ManagerContext } from 'storybook/manager-api'; +import { expect, screen, waitFor } from 'storybook/test'; + +import { shareTool } from './share'; + +const managerContext = { + state: { + storyId: 'manager-preview-tools-share--default', + refId: undefined, + refs: {}, + customQueryParams: {}, + }, + api: { + getShortcutKeys: () => ({ copyStoryLink: ['alt', 'shift', 'k'] }), + }, +} as any; + +const ManagerDecorator = (Story: any) => ( + +
{Story()}
+
+); + +const meta = { + title: 'Manager/Preview/Tools/Share', + render: shareTool.render, + decorators: [ManagerDecorator], + parameters: { layout: 'centered' }, + tags: ['!vitest'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + beforeEach: () => { + global.STORYBOOK_NETWORK_ADDRESS = 'http://127.0.0.1:6006'; + }, + play: async ({ userEvent, canvas }) => { + await waitFor(async () => { + await userEvent.click(canvas.getByRole('button')); + await expect(await screen.findByText('Scan me')).toBeVisible(); + }); + }, +}; diff --git a/code/core/src/manager/components/preview/tools/share.tsx b/code/core/src/manager/components/preview/tools/share.tsx new file mode 100644 index 000000000000..b374ab5ab1e7 --- /dev/null +++ b/code/core/src/manager/components/preview/tools/share.tsx @@ -0,0 +1,174 @@ +import React, { useMemo, useState } from 'react'; + +import { + IconButton, + TooltipLinkList, + WithTooltip, + getStoryHref, +} from 'storybook/internal/components'; +import type { Addon_BaseType } from 'storybook/internal/types'; + +import { global } from '@storybook/global'; +import { BugIcon, LinkIcon, ShareIcon } from '@storybook/icons'; + +import copy from 'copy-to-clipboard'; +import { QRCodeSVG as QRCode } from 'qrcode.react'; +import { Consumer, types, useStorybookApi } from 'storybook/manager-api'; +import type { Combo } from 'storybook/manager-api'; +import { styled, useTheme } from 'storybook/theming'; + +import { Shortcut } from '../../../container/Menu'; + +const { PREVIEW_URL, document } = global as any; + +const mapper = ({ state }: Combo) => { + const { storyId, refId, refs } = state; + const { location } = document; + // @ts-expect-error (non strict) + const ref = refs[refId]; + let baseUrl = `${location.origin}${location.pathname}`; + + if (!baseUrl.endsWith('/')) { + baseUrl += '/'; + } + + return { + refId, + baseUrl: ref ? `${ref.url}/iframe.html` : (PREVIEW_URL as string) || `${baseUrl}iframe.html`, + storyId, + queryParams: state.customQueryParams, + }; +}; + +const QRContainer = styled.div(() => ({ + display: 'flex', + alignItems: 'center', + padding: 8, + maxWidth: 200, +})); + +const QRImageContainer = styled.div(() => ({ + width: 64, + height: 64, + marginRight: 12, + backgroundColor: 'white', + padding: 2, +})); + +const QRImage = ({ value }: { value: string }) => { + const theme = useTheme(); + return ( + + + + ); +}; + +const QRContent = styled.div(() => ({})); + +const QRTitle = styled.div(({ theme }) => ({ + fontWeight: theme.typography.weight.bold, + fontSize: theme.typography.size.s1, + marginBottom: 4, +})); + +const QRDescription = styled.div(({ theme }) => ({ + fontSize: theme.typography.size.s1, + color: theme.textMutedColor, +})); + +function ShareMenu({ + baseUrl, + storyId, + queryParams, + qrUrl, +}: { + baseUrl: string; + storyId: string; + queryParams: Record; + qrUrl: string; +}) { + const api = useStorybookApi(); + const shortcutKeys = api.getShortcutKeys(); + const enableShortcuts = !!shortcutKeys; + const [copied, setCopied] = useState(false); + const copyStoryLink = shortcutKeys?.copyStoryLink; + + const links = useMemo(() => { + const copyTitle = copied ? 'Copied!' : 'Copy story link'; + const baseLinks = [ + [ + { + id: 'copy-link', + title: copyTitle, + icon: , + right: enableShortcuts ? : null, + onClick: () => { + copy(window.location.href); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }, + }, + { + id: 'open-new-tab', + title: 'Open in isolation mode', + icon: , + onClick: () => { + const href = getStoryHref(baseUrl, storyId, queryParams); + window.open(href, '_blank', 'noopener,noreferrer'); + }, + }, + ], + ]; + + baseLinks.push([ + { + id: 'qr-section', + // @ts-expect-error (non strict) + content: ( + + + + Scan me + Must be on the same network as this device. + + + ), + }, + ]); + + return baseLinks; + }, [baseUrl, storyId, queryParams, copied, qrUrl, enableShortcuts, copyStoryLink]); + + return ; +} + +export const shareTool: Addon_BaseType = { + title: 'share', + id: 'share', + type: types.TOOL, + match: ({ viewMode, tabId }) => viewMode === 'story' && !tabId, + render: () => { + return ( + + {({ baseUrl, storyId, queryParams }) => { + const storyUrl = global.STORYBOOK_NETWORK_ADDRESS + ? `${global.STORYBOOK_NETWORK_ADDRESS}${window.location.search}` + : window.location.href; + + return storyId ? ( + } + > + + + + + ) : null; + }} + + ); + }, +}; diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx index f4fb0ac86a86..5f1ad015f6ba 100644 --- a/code/core/src/manager/components/sidebar/ContextMenu.tsx +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -9,13 +9,15 @@ import { Addon_TypesEnum, } from 'storybook/internal/types'; -import { EllipsisIcon } from '@storybook/icons'; +import { CopyIcon, EditorIcon, EllipsisIcon } from '@storybook/icons'; +import copy from 'copy-to-clipboard'; import { useStorybookApi } from 'storybook/manager-api'; import type { API } from 'storybook/manager-api'; import { styled } from 'storybook/theming'; import type { Link } from '../../../components/components/tooltip/TooltipLinkList'; +import { Shortcut } from '../../container/Menu'; import { StatusButton } from './StatusButton'; import type { ExcludesNull } from './Tree'; @@ -38,6 +40,52 @@ const FloatingStatusButton = styled(StatusButton)({ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) => { const [hoverCount, setHoverCount] = useState(0); const [isOpen, setIsOpen] = useState(false); + const [copyText, setCopyText] = React.useState('Copy story name'); + + const shortcutKeys = api.getShortcutKeys(); + const enableShortcuts = !!shortcutKeys; + + const topLinks = useMemo(() => { + const defaultLinks = []; + + if (context && 'importPath' in context) { + defaultLinks.push({ + id: 'open-in-editor', + title: 'Open in editor', + icon: , + right: enableShortcuts ? : null, + onClick: (e: SyntheticEvent) => { + e.preventDefault(); + api.openInEditor({ + file: context.importPath, + }); + }, + }); + } + + if (context.type === 'story') { + defaultLinks.push({ + id: 'copy-story-name', + title: copyText, + icon: , + // TODO: bring this back once we want to add shortcuts for this + // right: + // enableShortcuts && shortcutKeys.copyStoryName ? ( + // + // ) : null, + onClick: (e: SyntheticEvent) => { + e.preventDefault(); + copy(context.exportName); + setCopyText('Copied!'); + setTimeout(() => { + setCopyText('Copy story name'); + }, 2000); + }, + }); + } + + return defaultLinks; + }, [context, copyText, enableShortcuts, shortcutKeys]); const handlers = useMemo(() => { return { @@ -53,7 +101,6 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) }, }; }, []); - /** * Calculate the providerLinks whenever the user mouses over the container. We use an incrementor, * instead of a simple boolean to ensure that the links are recalculated @@ -67,7 +114,9 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) return []; }, [api, context, hoverCount]); - const isRendered = providerLinks.length > 0 || links.length > 0; + // We just don't want to render the context menu for composed storybook stories + const shouldRender = + !context.refId && (providerLinks.length > 0 || links.length > 0 || topLinks.length > 0); return useMemo(() => { // Never show the SidebarContextMenu in production @@ -77,7 +126,7 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) return { onMouseEnter: handlers.onMouseEnter, - node: isRendered ? ( + node: shouldRender ? ( } + tooltip={} > @@ -98,7 +147,7 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) ) : null, }; - }, [context, handlers, isOpen, isRendered, links]); + }, [context, handlers, isOpen, shouldRender, links, topLinks]); }; /** @@ -115,7 +164,15 @@ const LiveContextMenu: FC<{ context: API_HashEntry } & ComponentProps; diff --git a/code/core/src/manager/components/sidebar/Menu.tsx b/code/core/src/manager/components/sidebar/Menu.tsx index b1fc6eac54e0..f83361a35cfd 100644 --- a/code/core/src/manager/components/sidebar/Menu.tsx +++ b/code/core/src/manager/components/sidebar/Menu.tsx @@ -113,8 +113,8 @@ export const SidebarMenu: FC = ({ menu, isHighlighted, onClick onVisibleChange={setIsTooltipVisible} > ({})), + emit: fn().mockName('api::emit'), + getElements: fn(() => ({})).mockName('api::getElements'), + getShortcutKeys: fn(() => ({})).mockName('api::getShortcutKeys'), }, } as any; diff --git a/code/core/src/manager/components/sidebar/Search.tsx b/code/core/src/manager/components/sidebar/Search.tsx index 598b8fd7e132..a6113815b4e6 100644 --- a/code/core/src/manager/components/sidebar/Search.tsx +++ b/code/core/src/manager/components/sidebar/Search.tsx @@ -129,6 +129,7 @@ const FocusKey = styled.code(({ theme }) => ({ margin: 5, marginTop: 6, height: 16, + fontFamily: theme.typography.fonts.base, lineHeight: '16px', textAlign: 'center', fontSize: '11px', diff --git a/code/core/src/manager/components/sidebar/Tree.stories.tsx b/code/core/src/manager/components/sidebar/Tree.stories.tsx index 4e8a8cfdbad0..beaffabf02bb 100644 --- a/code/core/src/manager/components/sidebar/Tree.stories.tsx +++ b/code/core/src/manager/components/sidebar/Tree.stories.tsx @@ -28,6 +28,8 @@ const managerContext: any = { on: fn().mockName('api::on'), off: fn().mockName('api::off'), emit: fn().mockName('api::emit'), + getShortcutKeys: fn().mockName('api::getShortcutKeys'), + getCurrentStoryData: fn().mockName('api::getCurrentStoryData'), getElements: fn( () => ({ @@ -45,6 +47,7 @@ const managerContext: any = { }, }) satisfies Addon_Collection ), + getData: fn().mockName('api::getData'), }, }; @@ -301,8 +304,8 @@ export const WithContextContent: Story = { const link = await screen.findByText('TooltipBuildList'); await userEvent.hover(link); - const contextButton = await screen.findByTestId('context-menu'); - await userEvent.click(contextButton); + const contextButton = await screen.findAllByTestId('context-menu'); + await userEvent.click(contextButton[0]); const body = await within(document.body); diff --git a/code/core/src/manager/container/Menu.tsx b/code/core/src/manager/container/Menu.tsx index b61dcc06c472..e51f35bb1c9b 100644 --- a/code/core/src/manager/container/Menu.tsx +++ b/code/core/src/manager/container/Menu.tsx @@ -32,13 +32,14 @@ const Key = styled.span(({ theme }) => ({ padding: '0 6px', })); -const KeyChild = styled.code({ +const KeyChild = styled.code(({ theme }) => ({ padding: 0, + fontFamily: theme.typography.fonts.base, verticalAlign: 'middle', '& + &': { marginLeft: 6, }, -}); +})); export const Shortcut: FC<{ keys: string[] }> = ({ keys }) => ( diff --git a/code/core/src/manager/container/Preview.tsx b/code/core/src/manager/container/Preview.tsx index 95973adfc3e7..e410ca0aecac 100644 --- a/code/core/src/manager/container/Preview.tsx +++ b/code/core/src/manager/container/Preview.tsx @@ -14,16 +14,16 @@ import { Preview, createCanvasTab, filterTabs } from '../components/preview/Prev import { filterToolsSide, fullScreenTool } from '../components/preview/Toolbar'; import { defaultWrappers } from '../components/preview/Wrappers'; import { addonsTool } from '../components/preview/tools/addons'; -import { copyTool } from '../components/preview/tools/copy'; -import { ejectTool } from '../components/preview/tools/eject'; import { menuTool } from '../components/preview/tools/menu'; +import { openInEditorTool } from '../components/preview/tools/open-in-editor'; import { remountTool } from '../components/preview/tools/remount'; +import { shareTool } from '../components/preview/tools/share'; import { zoomTool } from '../components/preview/tools/zoom'; import type { PreviewProps } from '../components/preview/utils/types'; const defaultTabs = [createCanvasTab()]; const defaultTools = [menuTool, remountTool, zoomTool]; -const defaultToolsExtra = [addonsTool, fullScreenTool, ejectTool, copyTool]; +const defaultToolsExtra = [addonsTool, fullScreenTool, shareTool, openInEditorTool]; const emptyTabsList: Addon_BaseType[] = []; diff --git a/code/core/src/manager/globals-runtime.ts b/code/core/src/manager/globals-runtime.ts index efec08c4b07b..f1d64d954c65 100644 --- a/code/core/src/manager/globals-runtime.ts +++ b/code/core/src/manager/globals-runtime.ts @@ -10,11 +10,28 @@ globalPackages.forEach((key) => { globalThis[globalsNameReferenceMap[key]] = globalsNameValueMap[key]; }); +const queuedErrors: Error[] = []; + globalThis.sendTelemetryError = (error) => { - if (!shouldSkipError(error)) { - const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; - channel.emit(TELEMETRY_ERROR, prepareForTelemetry(error)); + if (shouldSkipError(error)) { + return; + } + + const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; + const preparedError = prepareForTelemetry(error); + + if (!channel) { + queuedErrors.push(preparedError); + return; } + + // Flush any queued errors first + while (queuedErrors.length > 0) { + const queuedError = queuedErrors.shift(); + channel.emit(TELEMETRY_ERROR, queuedError); + } + + channel.emit(TELEMETRY_ERROR, preparedError); }; // handle all uncaught errors at the root of the application and log to telemetry diff --git a/code/core/src/manager/globals/exports.ts b/code/core/src/manager/globals/exports.ts index 28dad623eec3..b717d6296d56 100644 --- a/code/core/src/manager/globals/exports.ts +++ b/code/core/src/manager/globals/exports.ts @@ -152,6 +152,7 @@ export default { 'DownloadIcon', 'DragIcon', 'EditIcon', + 'EditorIcon', 'EllipsisIcon', 'EmailIcon', 'ExpandAltIcon', @@ -544,65 +545,6 @@ export default { 'resetComponents', 'withReset', ], - 'storybook/internal/core-errors': [ - 'ARGTYPES_INFO_REQUEST', - 'ARGTYPES_INFO_RESPONSE', - 'CHANNEL_CREATED', - 'CHANNEL_WS_DISCONNECT', - 'CONFIG_ERROR', - 'CREATE_NEW_STORYFILE_REQUEST', - 'CREATE_NEW_STORYFILE_RESPONSE', - 'CURRENT_STORY_WAS_SET', - 'DOCS_PREPARED', - 'DOCS_RENDERED', - 'FILE_COMPONENT_SEARCH_REQUEST', - 'FILE_COMPONENT_SEARCH_RESPONSE', - 'FORCE_REMOUNT', - 'FORCE_RE_RENDER', - 'GLOBALS_UPDATED', - 'NAVIGATE_URL', - 'PLAY_FUNCTION_THREW_EXCEPTION', - 'PRELOAD_ENTRIES', - 'PREVIEW_BUILDER_PROGRESS', - 'PREVIEW_KEYDOWN', - 'REGISTER_SUBSCRIPTION', - 'REQUEST_WHATS_NEW_DATA', - 'RESET_STORY_ARGS', - 'RESULT_WHATS_NEW_DATA', - 'SAVE_STORY_REQUEST', - 'SAVE_STORY_RESPONSE', - 'SELECT_STORY', - 'SET_CONFIG', - 'SET_CURRENT_STORY', - 'SET_FILTER', - 'SET_GLOBALS', - 'SET_INDEX', - 'SET_STORIES', - 'SET_WHATS_NEW_CACHE', - 'SHARED_STATE_CHANGED', - 'SHARED_STATE_SET', - 'STORIES_COLLAPSE_ALL', - 'STORIES_EXPAND_ALL', - 'STORY_ARGS_UPDATED', - 'STORY_CHANGED', - 'STORY_ERRORED', - 'STORY_FINISHED', - 'STORY_HOT_UPDATED', - 'STORY_INDEX_INVALIDATED', - 'STORY_MISSING', - 'STORY_PREPARED', - 'STORY_RENDERED', - 'STORY_RENDER_PHASE_CHANGED', - 'STORY_SPECIFIED', - 'STORY_THREW_EXCEPTION', - 'STORY_UNCHANGED', - 'TELEMETRY_ERROR', - 'TOGGLE_WHATS_NEW_NOTIFICATIONS', - 'UNHANDLED_ERRORS_WHILE_PLAYING', - 'UPDATE_GLOBALS', - 'UPDATE_QUERY_PARAMS', - 'UPDATE_STORY_ARGS', - ], 'storybook/internal/core-events': [ 'ARGTYPES_INFO_REQUEST', 'ARGTYPES_INFO_RESPONSE', @@ -620,6 +562,8 @@ export default { 'FORCE_RE_RENDER', 'GLOBALS_UPDATED', 'NAVIGATE_URL', + 'OPEN_IN_EDITOR_REQUEST', + 'OPEN_IN_EDITOR_RESPONSE', 'PLAY_FUNCTION_THREW_EXCEPTION', 'PRELOAD_ENTRIES', 'PREVIEW_BUILDER_PROGRESS', @@ -685,76 +629,4 @@ export default { 'useNavigate', ], 'storybook/internal/types': ['Addon_TypesEnum'], - 'storybook/internal/manager-api': [ - 'ActiveTabs', - 'Consumer', - 'ManagerContext', - 'Provider', - 'RequestResponseError', - 'addons', - 'combineParameters', - 'controlOrMetaKey', - 'controlOrMetaSymbol', - 'eventMatchesShortcut', - 'eventToShortcut', - 'experimental_MockUniversalStore', - 'experimental_UniversalStore', - 'experimental_getStatusStore', - 'experimental_getTestProviderStore', - 'experimental_requestResponse', - 'experimental_useStatusStore', - 'experimental_useTestProviderStore', - 'experimental_useUniversalStore', - 'internal_fullStatusStore', - 'internal_fullTestProviderStore', - 'internal_universalStatusStore', - 'internal_universalTestProviderStore', - 'isMacLike', - 'isShortcutTaken', - 'keyToSymbol', - 'merge', - 'mockChannel', - 'optionOrAltSymbol', - 'shortcutMatchesShortcut', - 'shortcutToHumanString', - 'types', - 'useAddonState', - 'useArgTypes', - 'useArgs', - 'useChannel', - 'useGlobalTypes', - 'useGlobals', - 'useParameter', - 'useSharedState', - 'useStoryPrepared', - 'useStorybookApi', - 'useStorybookState', - ], - 'storybook/internal/theming': [ - 'CacheProvider', - 'ClassNames', - 'Global', - 'ThemeProvider', - 'background', - 'color', - 'convert', - 'create', - 'createCache', - 'createGlobal', - 'createReset', - 'css', - 'darken', - 'ensure', - 'ignoreSsrWarning', - 'isPropValid', - 'jsx', - 'keyframes', - 'lighten', - 'styled', - 'themes', - 'typography', - 'useTheme', - 'withTheme', - ], - 'storybook/internal/theming/create': ['create', 'themes'], } as const; diff --git a/code/core/src/manager/globals/globals.ts b/code/core/src/manager/globals/globals.ts index 36df638333b3..76f240b502d4 100644 --- a/code/core/src/manager/globals/globals.ts +++ b/code/core/src/manager/globals/globals.ts @@ -15,16 +15,10 @@ export const globalsNameReferenceMap = { 'storybook/internal/channels': '__STORYBOOK_CHANNELS__', 'storybook/internal/client-logger': '__STORYBOOK_CLIENT_LOGGER__', 'storybook/internal/components': '__STORYBOOK_COMPONENTS__', - 'storybook/internal/core-errors': '__STORYBOOK_CORE_EVENTS__', 'storybook/internal/core-events': '__STORYBOOK_CORE_EVENTS__', 'storybook/internal/manager-errors': '__STORYBOOK_CORE_EVENTS_MANAGER_ERRORS__', 'storybook/internal/router': '__STORYBOOK_ROUTER__', 'storybook/internal/types': '__STORYBOOK_TYPES__', - - // @deprecated TODO: delete in 9.1 - 'storybook/internal/manager-api': '__STORYBOOK_API__', - 'storybook/internal/theming': '__STORYBOOK_THEMING__', - 'storybook/internal/theming/create': '__STORYBOOK_THEMING_CREATE__', } as const; export const globalPackages = Object.keys(globalsNameReferenceMap) as Array< diff --git a/code/core/src/manager/globals/runtime.ts b/code/core/src/manager/globals/runtime.ts index 8407048e02ba..bf6d7ba9ac29 100644 --- a/code/core/src/manager/globals/runtime.ts +++ b/code/core/src/manager/globals/runtime.ts @@ -36,13 +36,8 @@ export const globalsNameValueMap: Required 'O') + const normalizedShortcut = shortcut.map((key) => + Array.isArray(key) ? key.at(-1) : key + ) as string[]; + // Check we don't match any other shortcuts const error = !!Object.entries(shortcutKeys).find( ([feature, { shortcut: existingShortcut }]) => feature !== activeFeature && existingShortcut && - shortcutMatchesShortcut(shortcut, existingShortcut) + shortcutMatchesShortcut(normalizedShortcut, existingShortcut) ); return this.setState({ - shortcutKeys: { ...shortcutKeys, [activeFeature]: { shortcut, error } }, + shortcutKeys: { ...shortcutKeys, [activeFeature]: { shortcut: normalizedShortcut, error } }, }); }; diff --git a/code/core/src/manager/typings.d.ts b/code/core/src/manager/typings.d.ts index 64bba1c00cdf..fb3fa90c7557 100644 --- a/code/core/src/manager/typings.d.ts +++ b/code/core/src/manager/typings.d.ts @@ -1,6 +1,11 @@ declare var DOCS_OPTIONS: any; declare var CONFIG_TYPE: 'DEVELOPMENT' | 'PRODUCTION'; declare var PREVIEW_URL: any; +/** + * The network address of the Storybook instance. Used by Storybook to generate a QR code so users + * can access the story on mobile devices. + */ +declare var STORYBOOK_NETWORK_ADDRESS: string | undefined; declare var __STORYBOOK_ADDONS_MANAGER: any; declare var RELEASE_NOTES_DATA: any; diff --git a/code/core/src/manager/utils/prepareForTelemetry.ts b/code/core/src/manager/utils/prepareForTelemetry.ts index 00cfdd35dd43..581ef0ddc950 100644 --- a/code/core/src/manager/utils/prepareForTelemetry.ts +++ b/code/core/src/manager/utils/prepareForTelemetry.ts @@ -25,6 +25,8 @@ const errorMessages = [ // Safari does not seem to provide any helpful info on window.onerror // https://bugs.webkit.org/show_bug.cgi?id=132945 'Script error.', + // When react-dev-tools is installed as a browser extension, it will log this error + 'React is running in production mode', ]; export const shouldSkipError = (error: Error) => errorMessages.includes(error?.message); diff --git a/code/core/src/measure/Tool.tsx b/code/core/src/measure/Tool.tsx index 8cebffa01a3e..e259cf5334e0 100644 --- a/code/core/src/measure/Tool.tsx +++ b/code/core/src/measure/Tool.tsx @@ -23,7 +23,7 @@ export const Tool = () => { useEffect(() => { api.setAddonShortcut(ADDON_ID, { - label: 'Toggle Measure [M]', + label: 'Toggle Measure', defaultShortcut: ['M'], actionName: 'measure', showInMenu: false, diff --git a/code/core/src/telemetry/types.ts b/code/core/src/telemetry/types.ts index d1827d07219e..de36f4af5fb1 100644 --- a/code/core/src/telemetry/types.ts +++ b/code/core/src/telemetry/types.ts @@ -24,6 +24,7 @@ export type EventType = | 'save-story' | 'create-new-story-file' | 'create-new-story-file-search' + | 'open-in-editor' | 'testing-module-watch-mode' | 'testing-module-completed-report' | 'testing-module-crash-report' diff --git a/code/core/src/types/modules/api-stories.ts b/code/core/src/types/modules/api-stories.ts index 5dbe24143285..c62892c93750 100644 --- a/code/core/src/types/modules/api-stories.ts +++ b/code/core/src/types/modules/api-stories.ts @@ -46,6 +46,7 @@ export interface API_StoryEntry extends API_BaseEntry { parent: StoryId; title: ComponentTitle; importPath: Path; + exportName: string; prepared: boolean; parameters?: { [parameterName: string]: any; diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index caff102504df..b3032b07c91d 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -204,6 +204,7 @@ export interface BuilderOptions { versionCheck?: VersionCheck; disableWebpackDefaults?: boolean; serverChannelUrl?: string; + networkAddress?: string; } export interface StorybookConfigOptions { diff --git a/code/core/src/typings.d.ts b/code/core/src/typings.d.ts index 9184576d9808..cba197e782fe 100644 --- a/code/core/src/typings.d.ts +++ b/code/core/src/typings.d.ts @@ -26,9 +26,7 @@ declare var __STORYBOOK_VITEST_MOCKER__: any; declare module '@aw-web-design/x-default-browser'; declare module 'ansi-to-html'; -declare module 'better-opn'; declare module 'lazy-universal-dotenv'; -declare module 'open'; declare module 'pnp-webpack-plugin'; declare module 'react-inspector'; diff --git a/code/e2e-tests/manager.spec.ts b/code/e2e-tests/manager.spec.ts index 27812644f173..c0b1e3d15a41 100644 --- a/code/e2e-tests/manager.spec.ts +++ b/code/e2e-tests/manager.spec.ts @@ -5,6 +5,7 @@ import { SbPage } from './util'; const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001'; const templateName = process.env.STORYBOOK_TEMPLATE_NAME; +const type = process.env.STORYBOOK_TYPE || 'dev'; test.describe('Manager UI', () => { test.beforeEach(async ({ page }) => { @@ -16,6 +17,23 @@ test.describe('Manager UI', () => { test.describe('Desktop', () => { // TODO: test dragging and resizing + test('Settings tooltip', async ({ page }) => { + await page.locator('[aria-label="Settings"]').click(); + + // should only hide if pressing Escape, and not other keyboard inputs + await expect(page.getByTestId('tooltip')).toBeVisible(); + await page.keyboard.press('A'); + await expect(page.getByTestId('tooltip')).toBeVisible(); + await page.keyboard.press('Escape'); + await expect(page.getByTestId('tooltip')).toBeHidden(); + + // should also hide if clicking anywhere outside the tooltip + await page.locator('[aria-label="Settings"]').click(); + await expect(page.getByTestId('tooltip')).toBeVisible(); + await page.click('body'); + await expect(page.getByTestId('tooltip')).toBeHidden(); + }); + test('Sidebar toggling', async ({ page }) => { const sbPage = new SbPage(page, expect); @@ -28,7 +46,7 @@ test.describe('Manager UI', () => { await expect(sbPage.page.locator('.sidebar-container')).toBeVisible(); // toggle with menu item - await sbPage.page.locator('[aria-label="Shortcuts"]').click(); + await sbPage.page.locator('[aria-label="Settings"]').click(); await sbPage.page.locator('#list-item-S').click(); await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); @@ -37,6 +55,77 @@ test.describe('Manager UI', () => { await expect(sbPage.page.locator('.sidebar-container')).toBeVisible(); }); + test('Story context menu actions', async ({ page }) => { + test.skip(type !== 'dev', 'These actions are only applicable in dev mode'); + const sbPage = new SbPage(page, expect); + await sbPage.navigateToStory('example/button', 'docs'); + + // Context menu should contain open in editor for component node + await page.locator('[data-item-id="example-button"]').hover(); + await page + .locator('[data-item-id="example-button"] div[data-testid="context-menu"] button') + .click(); + const sidebarContextMenu = page.getByTestId('tooltip'); + await expect( + sidebarContextMenu.getByRole('button', { name: /open in editor/i }) + ).toBeVisible(); + await page.click('body'); + + // Context menu should contain open in editor for docs node + await page.locator('[data-item-id="example-button--docs"]').hover(); + await page + .locator('[data-item-id="example-button--docs"] div[data-testid="context-menu"] button') + .click(); + await expect( + page.getByTestId('tooltip').getByRole('button', { name: /open in editor/i }) + ).toBeVisible(); + await page.click('body'); + + // Context menu should contain open in editor and copy story name for story node + await page.locator('[data-item-id="example-button--primary"]').hover(); + await page + .locator('[data-item-id="example-button--primary"] div[data-testid="context-menu"] button') + .click(); + await expect( + page.getByTestId('tooltip').getByRole('button', { name: /open in editor/i }) + ).toBeVisible(); + await page + .getByTestId('tooltip') + .getByRole('button', { name: /copy story name/i }) + .click(); + + await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain( + 'Primary' + ); + }); + + test('Story share actions (dev)', async ({ page }) => { + test.skip(type !== 'dev', 'These actions are only applicable in dev mode'); + const sbPage = new SbPage(page, expect); + await sbPage.navigateToStory('example/button', 'primary'); + await expect(page.getByRole('button', { name: 'Open in editor' })).toBeVisible(); + await page.getByRole('button', { name: 'Share' }).click(); + await expect(page.getByRole('button', { name: /copy story link/i })).toBeVisible(); + await page.getByRole('button', { name: /copy story link/i }).click(); + + await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain( + `${storybookUrl}/?path=/story/example-button--primary` + ); + }); + + test('Story share actions (build)', async ({ page }) => { + test.skip(type !== 'build', 'These actions are only applicable in build mode'); + const sbPage = new SbPage(page, expect); + await sbPage.navigateToStory('example/button', 'primary'); + await page.getByRole('button', { name: 'Share' }).click(); + await expect(page.getByRole('button', { name: /copy story link/i })).toBeVisible(); + await page.getByRole('button', { name: /copy story link/i }).click(); + + await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain( + `${storybookUrl}/?path=/story/example-button--primary` + ); + }); + test('Toolbar toggling', async ({ page }) => { const sbPage = new SbPage(page, expect); const expectToolbarVisibility = async (visible: boolean) => { @@ -58,10 +147,10 @@ test.describe('Manager UI', () => { await expectToolbarVisibility(true); // toggle with menu item - await sbPage.page.locator('[aria-label="Shortcuts"]').click(); + await sbPage.page.locator('[aria-label="Settings"]').click(); await sbPage.page.locator('#list-item-T').click(); await expectToolbarVisibility(false); - await sbPage.page.locator('[aria-label="Shortcuts"]').click(); + await sbPage.page.locator('[aria-label="Settings"]').click(); await sbPage.page.locator('#list-item-T').click(); await expectToolbarVisibility(true); }); @@ -97,7 +186,7 @@ test.describe('Manager UI', () => { await expect(sbPage.page.locator('#storybook-panel-root')).toBeVisible(); // toggle with menu item - await sbPage.page.locator('[aria-label="Shortcuts"]').click(); + await sbPage.page.locator('[aria-label="Settings"]').click(); await sbPage.page.locator('#list-item-A').click(); await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); @@ -148,7 +237,7 @@ test.describe('Manager UI', () => { await expect(sbPage.page.locator('.sidebar-container')).toBeVisible(); // toggle with menu item - await sbPage.page.locator('[aria-label="Shortcuts"]').click(); + await sbPage.page.locator('[aria-label="Settings"]').click(); await sbPage.page.locator('#list-item-F').click(); await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); @@ -177,7 +266,7 @@ test.describe('Manager UI', () => { test('Settings page', async ({ page }) => { const sbPage = new SbPage(page, expect); - await sbPage.page.locator('[aria-label="Shortcuts"]').click(); + await sbPage.page.locator('[aria-label="Settings"]').click(); await sbPage.page.locator('#list-item-about').click(); expect(sbPage.page.url()).toContain('/settings/about'); diff --git a/code/frameworks/angular/template/components/form.component.ts b/code/frameworks/angular/template/components/form.component.ts index 272e79eebf7f..0d7316454a51 100644 --- a/code/frameworks/angular/template/components/form.component.ts +++ b/code/frameworks/angular/template/components/form.component.ts @@ -1,36 +1,39 @@ -import { Component, Output, EventEmitter } from '@angular/core'; +import { Component, output, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; @Component({ - standalone: false, + standalone: true, + imports: [FormsModule], selector: 'storybook-form', template: ` -
+ -

Completed!!

+ @if (complete()) { +

Completed!!

+ }
`, }) export default class FormComponent { /** Optional success handler */ - @Output() - onSuccess = new EventEmitter(); + onSuccess = output(); value = ''; - complete = false; + complete = signal(false); handleSubmit(event: SubmitEvent) { event.preventDefault(); this.onSuccess.emit(this.value); setTimeout(() => { - this.complete = true; + this.complete.set(true); }, 500); setTimeout(() => { - this.complete = false; + this.complete.set(false); }, 1500); } } diff --git a/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.test.ts b/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.test.ts index 3e25c97ff84f..b9d250686cec 100644 --- a/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.test.ts +++ b/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.test.ts @@ -1,14 +1,12 @@ -import * as fs from 'node:fs/promises'; - -import { describe, expect, it } from 'vitest'; - -import * as babel from 'storybook/internal/babel'; +import { describe, expect, it, vi } from 'vitest'; import * as find from 'empathic/find'; import { vitestConfigFiles } from './vitestConfigFiles'; -const liveContext: any = { babel, empathic: find, fs }; +vi.mock('empathic/find', () => ({ + any: vi.fn(), +})); const fileMocks = { 'vitest.config.ts': ` @@ -70,54 +68,37 @@ const fileMocks = { `, }; -const mockContext: any = { - ...liveContext, - empathic: { any: ([name]: string[]) => name }, - fs: { - readFile: async (path: keyof typeof fileMocks) => fileMocks[path], - }, -}; +vi.mock(import('node:fs/promises'), async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readFile: vi + .fn() + .mockImplementation((filePath) => fileMocks[filePath as keyof typeof fileMocks]), + }; +}); + +const mockContext: any = {}; const coerce = (from: string, to: string) => - async ([name]: string[]) => + ([name]: string[]) => name.includes(from) ? to : name; const state: any = { directory: '.', }; -// TODO @ghengeveld, I am in the process of removing the context -describe.skip('these tests need to be updated', () => { - it('should run properly with live dependencies', async () => { - const result = await vitestConfigFiles.condition(liveContext, state); - expect(result).toEqual({ type: 'compatible' }); - }); - +describe('vitestConfigFiles', () => { it('should run properly with mock dependencies', async () => { const result = await vitestConfigFiles.condition(mockContext, state); expect(result).toEqual({ type: 'compatible' }); }); - it('should disallow missing dependencies', async () => { - const result = await vitestConfigFiles.condition({} as any, state); - expect(result).toEqual({ - type: 'incompatible', - reasons: ['Missing babel on context', 'Missing empathic on context', 'Missing fs on context'], - }); - }); - describe('Check Vitest workspace files', () => { it('should disallow JSON workspace file', async () => { - const result = await vitestConfigFiles.condition( - { - ...mockContext, - empathic: { - up: coerce('workspace', 'vitest.workspace.json'), - }, - }, - state - ); + vi.mocked(find.any).mockImplementation(coerce('workspace', 'vitest.workspace.json')); + const result = await vitestConfigFiles.condition(mockContext, state); expect(result).toEqual({ type: 'incompatible', reasons: ['Cannot auto-update JSON workspace file: vitest.workspace.json'], @@ -125,15 +106,8 @@ describe.skip('these tests need to be updated', () => { }); it('should disallow invalid workspace file', async () => { - const result = await vitestConfigFiles.condition( - { - ...mockContext, - empathic: { - up: coerce('workspace', 'invalidWorkspace.ts'), - }, - }, - state - ); + vi.mocked(find.any).mockImplementation(coerce('workspace', 'invalidWorkspace.ts')); + const result = await vitestConfigFiles.condition(mockContext, state); expect(result).toEqual({ type: 'incompatible', reasons: ['Found an invalid workspace config file: invalidWorkspace.ts'], @@ -141,30 +115,16 @@ describe.skip('these tests need to be updated', () => { }); it('should allow defineWorkspace syntax', async () => { - const result = await vitestConfigFiles.condition( - { - ...mockContext, - empathic: { - up: coerce('workspace', 'defineWorkspace.ts'), - }, - }, - state - ); + vi.mocked(find.any).mockImplementation(coerce('workspace', 'defineWorkspace.ts')); + const result = await vitestConfigFiles.condition(mockContext, state); expect(result).toEqual({ type: 'compatible', }); }); it('should disallow invalid defineWorkspace syntax', async () => { - const result = await vitestConfigFiles.condition( - { - ...mockContext, - empathic: { - up: coerce('workspace', 'defineWorkspace-invalid.ts'), - }, - }, - state - ); + vi.mocked(find.any).mockImplementation(coerce('workspace', 'defineWorkspace-invalid.ts')); + const result = await vitestConfigFiles.condition(mockContext, state); expect(result).toEqual({ type: 'incompatible', reasons: ['Found an invalid workspace config file: defineWorkspace-invalid.ts'], @@ -174,15 +134,8 @@ describe.skip('these tests need to be updated', () => { describe('Check Vitest config files', () => { it('should disallow CommonJS config file', async () => { - const result = await vitestConfigFiles.condition( - { - ...mockContext, - empathic: { - up: coerce('config', 'vitest.config.cjs'), - }, - }, - state - ); + vi.mocked(find.any).mockImplementation(coerce('config', 'vitest.config.cjs')); + const result = await vitestConfigFiles.condition(mockContext, state); expect(result).toEqual({ type: 'incompatible', reasons: ['Cannot auto-update CommonJS config file: vitest.config.cjs'], @@ -190,15 +143,8 @@ describe.skip('these tests need to be updated', () => { }); it('should disallow invalid config file', async () => { - const result = await vitestConfigFiles.condition( - { - ...mockContext, - empathic: { - up: coerce('config', 'invalidConfig.ts'), - }, - }, - state - ); + vi.mocked(find.any).mockImplementation(coerce('config', 'invalidConfig.ts')); + const result = await vitestConfigFiles.condition(mockContext, state); expect(result).toEqual({ type: 'incompatible', reasons: ['Found an invalid Vitest config file: invalidConfig.ts'], @@ -206,30 +152,16 @@ describe.skip('these tests need to be updated', () => { }); it('should allow existing test config option', async () => { - const result = await vitestConfigFiles.condition( - { - ...mockContext, - empathic: { - up: coerce('config', 'testConfig.ts'), - }, - }, - state - ); + vi.mocked(find.any).mockImplementation(coerce('config', 'testConfig.ts')); + const result = await vitestConfigFiles.condition(mockContext, state); expect(result).toEqual({ type: 'compatible', }); }); it('should disallow invalid test config option', async () => { - const result = await vitestConfigFiles.condition( - { - ...mockContext, - empathic: { - up: coerce('config', 'testConfig-invalid.ts'), - }, - }, - state - ); + vi.mocked(find.any).mockImplementation(coerce('config', 'testConfig-invalid.ts')); + const result = await vitestConfigFiles.condition(mockContext, state); expect(result).toEqual({ type: 'incompatible', reasons: ['Found an invalid Vitest config file: testConfig-invalid.ts'], @@ -237,30 +169,16 @@ describe.skip('these tests need to be updated', () => { }); it('should allow existing test.workspace config option', async () => { - const result = await vitestConfigFiles.condition( - { - ...mockContext, - empathic: { - up: coerce('config', 'workspaceConfig.ts'), - }, - }, - state - ); + vi.mocked(find.any).mockImplementation(coerce('config', 'workspaceConfig.ts')); + const result = await vitestConfigFiles.condition(mockContext, state); expect(result).toEqual({ type: 'compatible', }); }); it('should disallow invalid test.workspace config option', async () => { - const result = await vitestConfigFiles.condition( - { - ...mockContext, - empathic: { - up: coerce('config', 'workspaceConfig-invalid.ts'), - }, - }, - state - ); + vi.mocked(find.any).mockImplementation(coerce('config', 'workspaceConfig-invalid.ts')); + const result = await vitestConfigFiles.condition(mockContext, state); expect(result).toEqual({ type: 'incompatible', reasons: ['Found an invalid Vitest config file: workspaceConfig-invalid.ts'], diff --git a/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.tsx b/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.tsx index 0b2188840035..56c3870c58f6 100644 --- a/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.tsx +++ b/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.tsx @@ -88,60 +88,51 @@ export const isValidWorkspaceConfigFile: (fileContents: string, babel: any) => b * - No -> exit */ export const vitestConfigFiles: Check = { - condition: async (context, state) => { - const deps = ['babel', 'empathic', 'fs']; - if (babel && find && fs) { - const reasons = []; - - const projectRoot = getProjectRoot(); - - const vitestWorkspaceFile = find.any( - ['ts', 'js', 'json'].flatMap((ex) => [`vitest.workspace.${ex}`, `vitest.projects.${ex}`]), - { cwd: state.directory, last: projectRoot } - ); - if (vitestWorkspaceFile?.endsWith('.json')) { - reasons.push(`Cannot auto-update JSON workspace file: ${vitestWorkspaceFile}`); - } else if (vitestWorkspaceFile) { - const fileContents = await fs.readFile(vitestWorkspaceFile, 'utf8'); - if (!isValidWorkspaceConfigFile(fileContents, babel)) { - reasons.push(`Found an invalid workspace config file: ${vitestWorkspaceFile}`); - } + condition: async (_context, state) => { + const reasons = []; + + const projectRoot = getProjectRoot(); + + const vitestWorkspaceFile = find.any( + ['ts', 'js', 'json'].flatMap((ex) => [`vitest.workspace.${ex}`, `vitest.projects.${ex}`]), + { cwd: state.directory, last: projectRoot } + ); + if (vitestWorkspaceFile?.endsWith('.json')) { + reasons.push(`Cannot auto-update JSON workspace file: ${vitestWorkspaceFile}`); + } else if (vitestWorkspaceFile) { + const fileContents = await fs.readFile(vitestWorkspaceFile, 'utf8'); + if (!isValidWorkspaceConfigFile(fileContents, babel)) { + reasons.push(`Found an invalid workspace config file: ${vitestWorkspaceFile}`); } + } - const vitestConfigFile = find.any( - ['ts', 'js', 'tsx', 'jsx', 'cts', 'cjs', 'mts', 'mjs'].map((ex) => `vitest.config.${ex}`), - { cwd: state.directory, last: projectRoot } - ); - if (vitestConfigFile?.endsWith('.cts') || vitestConfigFile?.endsWith('.cjs')) { - reasons.push(`Cannot auto-update CommonJS config file: ${vitestConfigFile}`); - } else if (vitestConfigFile) { - let isValidVitestConfig = false; - const configContent = await fs.readFile(vitestConfigFile, 'utf8'); - const parsedConfig = babel.babelParse(configContent); - babel.traverse(parsedConfig, { - ExportDefaultDeclaration(path) { - if ( - isDefineConfigExpression(path.node.declaration) && - isSafeToExtendWorkspace(path.node.declaration as CallExpression) - ) { - isValidVitestConfig = true; - } - }, - }); - if (!isValidVitestConfig) { - reasons.push(`Found an invalid Vitest config file: ${vitestConfigFile}`); - } + const vitestConfigFile = find.any( + ['ts', 'js', 'tsx', 'jsx', 'cts', 'cjs', 'mts', 'mjs'].map((ex) => `vitest.config.${ex}`), + { cwd: state.directory, last: projectRoot } + ); + if (vitestConfigFile?.endsWith('.cts') || vitestConfigFile?.endsWith('.cjs')) { + reasons.push(`Cannot auto-update CommonJS config file: ${vitestConfigFile}`); + } else if (vitestConfigFile) { + let isValidVitestConfig = false; + const configContent = await fs.readFile(vitestConfigFile, 'utf8'); + const parsedConfig = babel.babelParse(configContent); + babel.traverse(parsedConfig, { + ExportDefaultDeclaration(path) { + if ( + isDefineConfigExpression(path.node.declaration) && + isSafeToExtendWorkspace(path.node.declaration as CallExpression) + ) { + isValidVitestConfig = true; + } + }, + }); + if (!isValidVitestConfig) { + reasons.push(`Found an invalid Vitest config file: ${vitestConfigFile}`); } - - return reasons.length - ? { type: CompatibilityType.INCOMPATIBLE, reasons } - : { type: CompatibilityType.COMPATIBLE }; } - return { - type: CompatibilityType.INCOMPATIBLE, - reasons: deps - .filter((p) => !context[p as keyof typeof context]) - .map((p) => `Missing ${p} on context`), - }; + + return reasons.length + ? { type: CompatibilityType.INCOMPATIBLE, reasons } + : { type: CompatibilityType.COMPATIBLE }; }, }; diff --git a/code/package.json b/code/package.json index 5621344409de..d4bd07b0d021 100644 --- a/code/package.json +++ b/code/package.json @@ -110,7 +110,7 @@ "@nx/workspace": "20.8.2", "@playwright/test": "1.52.0", "@storybook/addon-a11y": "workspace:*", - "@storybook/addon-designs": "9.0.0-next.1", + "@storybook/addon-designs": "10.0.3--canary.67522d1.0", "@storybook/addon-docs": "workspace:*", "@storybook/addon-jest": "workspace:*", "@storybook/addon-links": "workspace:*", @@ -284,5 +284,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "10.0.0-beta.6" } diff --git a/code/yarn.lock b/code/yarn.lock index 4c21e8ac58f7..a40557f3f250 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -199,7 +199,7 @@ __metadata: languageName: node linkType: hard -"@angular-devkit/core@npm:19.2.16": +"@angular-devkit/core@npm:19.2.16, @angular-devkit/core@npm:^19.1.1": version: 19.2.16 resolution: "@angular-devkit/core@npm:19.2.16" dependencies: @@ -218,25 +218,6 @@ __metadata: languageName: node linkType: hard -"@angular-devkit/core@npm:^19.1.1": - version: 19.2.9 - resolution: "@angular-devkit/core@npm:19.2.9" - dependencies: - ajv: "npm:8.17.1" - ajv-formats: "npm:3.0.1" - jsonc-parser: "npm:3.3.1" - picomatch: "npm:4.0.2" - rxjs: "npm:7.8.1" - source-map: "npm:0.7.4" - peerDependencies: - chokidar: ^4.0.0 - peerDependenciesMeta: - chokidar: - optional: true - checksum: 10c0/c1a7fde1d7346ffdf32ad0b8030ad00d2ce5a41abd51f468454fecb23e1590d1fd32272c553d7677501228767181ddc36833a60888464137cfefcc918342c780 - languageName: node - linkType: hard - "@angular/animations@npm:^19.1.1": version: 19.2.15 resolution: "@angular/animations@npm:19.2.15" @@ -1099,18 +1080,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.25.9, @babel/plugin-transform-block-scoping@npm:^7.8.3": - version: 7.27.0 - resolution: "@babel/plugin-transform-block-scoping@npm:7.27.0" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.26.5" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10c0/15a604fac04151a795ff3213c73ece06bda7cd5f7c8cb7a3b29563ab243f0b3f7cba9e6facfc9d70e3e63b21af32f9d26bd10ccc58e1c425c7801186014b5ce4 - languageName: node - linkType: hard - -"@babel/plugin-transform-block-scoping@npm:^7.28.0": +"@babel/plugin-transform-block-scoping@npm:^7.25.9, @babel/plugin-transform-block-scoping@npm:^7.28.0, @babel/plugin-transform-block-scoping@npm:^7.8.3": version: 7.28.4 resolution: "@babel/plugin-transform-block-scoping@npm:7.28.4" dependencies: @@ -3659,8 +3629,8 @@ __metadata: linkType: hard "@jsonjoy.com/json-pack@npm:^1.11.0": - version: 1.11.0 - resolution: "@jsonjoy.com/json-pack@npm:1.11.0" + version: 1.14.0 + resolution: "@jsonjoy.com/json-pack@npm:1.14.0" dependencies: "@jsonjoy.com/base64": "npm:^1.1.2" "@jsonjoy.com/buffers": "npm:^1.0.0" @@ -3671,7 +3641,7 @@ __metadata: thingies: "npm:^2.5.0" peerDependencies: tslib: 2 - checksum: 10c0/2eeea1fbe410ddaedab43c7f22d869441e9f60062fdf7cd2b91b8ae7954965ec4caeb3d328a01caef6c09bfc760b60f8e2aaba2f1f7777c8bfdf918c568a1c6c + checksum: 10c0/af69d7911553cae3a69fdc444a8c2ea8f15ee2e2622da1b4b74f1873274e00db227fbd0f187ab49b8a36a869d090e91ebb8a23e5771175466d29974bd3a40553 languageName: node linkType: hard @@ -6115,7 +6085,7 @@ __metadata: dependencies: "@radix-ui/react-tabs": "npm:1.0.4" "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.4.0" + "@storybook/icons": "npm:^1.6.0" "@testing-library/react": "npm:^14.0.0" axe-core: "npm:^4.2.0" execa: "npm:^9.5.2" @@ -6129,23 +6099,24 @@ __metadata: languageName: unknown linkType: soft -"@storybook/addon-designs@npm:9.0.0-next.1": - version: 9.0.0-next.1 - resolution: "@storybook/addon-designs@npm:9.0.0-next.1" +"@storybook/addon-designs@npm:10.0.3--canary.67522d1.0": + version: 10.0.3--canary.67522d1.0 + resolution: "@storybook/addon-designs@npm:10.0.3--canary.67522d1.0" dependencies: "@figspec/react": "npm:^1.0.0" peerDependencies: + "@storybook/addon-docs": ^10.0.0 || ^10.0.0-0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^0.0.0-0 || ^9.0.0 || ^9.0.0-0 + storybook: ^10.0.0 || ^10.0.0-0 peerDependenciesMeta: - "@storybook/blocks": + "@storybook/addon-docs": optional: true react: optional: true react-dom: optional: true - checksum: 10c0/6989444d6caeb7abbb1a763e0a29d0749c529216d03d690618575378845e4259ddcf1935f7990fb05667a373940086dbacdcc6c97ff3f11eaa76f132bb11a1d5 + checksum: 10c0/050fa029304f998904944300c19a9fa85393e7177b0a84a2125fbccce55c21c4edd2be58a94ca27d1164d67bee07a5b4b09ca1e933c794987b2426857875e542 languageName: node linkType: hard @@ -6157,7 +6128,7 @@ __metadata: "@mdx-js/react": "npm:^3.0.0" "@rollup/pluginutils": "npm:^5.0.2" "@storybook/csf-plugin": "workspace:*" - "@storybook/icons": "npm:^1.4.0" + "@storybook/icons": "npm:^1.6.0" "@storybook/react-dom-shim": "workspace:*" "@types/color-convert": "npm:^2.0.0" "@types/react": "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -6186,7 +6157,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/addon-jest@workspace:addons/jest" dependencies: - "@storybook/icons": "npm:^1.4.0" + "@storybook/icons": "npm:^1.6.0" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" react-resize-detector: "npm:^7.1.2" @@ -6218,7 +6189,7 @@ __metadata: resolution: "@storybook/addon-onboarding@workspace:addons/onboarding" dependencies: "@neoconfetti/react": "npm:^1.0.0" - "@storybook/icons": "npm:^1.4.0" + "@storybook/icons": "npm:^1.6.0" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" react-joyride: "npm:^2.8.2" @@ -6232,7 +6203,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/addon-themes@workspace:addons/themes" dependencies: - "@storybook/icons": "npm:^1.4.0" + "@storybook/icons": "npm:^1.6.0" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" ts-dedent: "npm:^2.0.0" @@ -6247,7 +6218,7 @@ __metadata: resolution: "@storybook/addon-vitest@workspace:addons/vitest" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.4.0" + "@storybook/icons": "npm:^1.6.0" "@types/istanbul-lib-report": "npm:^3.0.3" "@types/micromatch": "npm:^4.0.0" "@types/node": "npm:^22.0.0" @@ -6575,13 +6546,13 @@ __metadata: languageName: unknown linkType: soft -"@storybook/icons@npm:^1.4.0": - version: 1.5.0 - resolution: "@storybook/icons@npm:1.5.0" +"@storybook/icons@npm:^1.6.0": + version: 1.6.0 + resolution: "@storybook/icons@npm:1.6.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - checksum: 10c0/cc8189a7d431929ccc079438b4ad55cd606421cfcd167459d1fd60656798315b620183b7fb95f2dd36859cad08f5ec8160dc8d4fe1a91b96ba8faeee10db8cc7 + checksum: 10c0/bbec9201a78a730195f9cf377b15856dc414a54d04e30d16c379d062425cc617bfd0d6586ba1716012cfbdab461f0c9693a6a52920f9bd09c7b4291fb116f59c languageName: node linkType: hard @@ -6898,7 +6869,7 @@ __metadata: "@nx/workspace": "npm:20.8.2" "@playwright/test": "npm:1.52.0" "@storybook/addon-a11y": "workspace:*" - "@storybook/addon-designs": "npm:9.0.0-next.1" + "@storybook/addon-designs": "npm:10.0.3--canary.67522d1.0" "@storybook/addon-docs": "workspace:*" "@storybook/addon-jest": "workspace:*" "@storybook/addon-links": "workspace:*" @@ -8009,11 +7980,11 @@ __metadata: linkType: hard "@types/node@npm:^22.0.0": - version: 22.18.4 - resolution: "@types/node@npm:22.18.4" + version: 22.18.5 + resolution: "@types/node@npm:22.18.5" dependencies: undici-types: "npm:~6.21.0" - checksum: 10c0/3869c65f8c79029bf6a692fbe03aac412d01b7e6e544a3670a32963591d885e0d139b0ccca996e22ef98645713f018e47ac6f8d6840cfe4761adde5612286eb9 + checksum: 10c0/2a664e24f1b4bc7d49905cd2a416c2e2dbf8dd09d35d783922e447983817100fb637135a9d3fa1d98b790b48f214a68fda941afed56641c172bd2ce23b5cf57a languageName: node linkType: hard @@ -10010,7 +9981,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.12.1": +"axios@npm:^1.12.1, axios@npm:^1.8.3": version: 1.12.2 resolution: "axios@npm:1.12.2" dependencies: @@ -10021,17 +9992,6 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.8.3": - version: 1.9.0 - resolution: "axios@npm:1.9.0" - dependencies: - follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.0" - proxy-from-env: "npm:^1.1.0" - checksum: 10c0/9371a56886c2e43e4ff5647b5c2c3c046ed0a3d13482ef1d0135b994a628c41fbad459796f101c655e62f0c161d03883454474d2e435b2e021b1924d9f24994c - languageName: node - linkType: hard - "axobject-query@npm:^4.1.0": version: 4.1.0 resolution: "axobject-query@npm:4.1.0" @@ -10248,7 +10208,7 @@ __metadata: languageName: node linkType: hard -"baseline-browser-mapping@npm:^2.8.2": +"baseline-browser-mapping@npm:^2.8.3": version: 2.8.4 resolution: "baseline-browser-mapping@npm:2.8.4" bin: @@ -10318,15 +10278,6 @@ __metadata: languageName: node linkType: hard -"better-opn@npm:^3.0.2": - version: 3.0.2 - resolution: "better-opn@npm:3.0.2" - dependencies: - open: "npm:^8.0.4" - checksum: 10c0/911ef25d44da75aabfd2444ce7a4294a8000ebcac73068c04a60298b0f7c7506b60421aa4cd02ac82502fb42baaff7e4892234b51e6923eded44c5a11185f2f5 - languageName: node - linkType: hard - "big-integer@npm:^1.6.44": version: 1.6.52 resolution: "big-integer@npm:1.6.52" @@ -10806,17 +10757,17 @@ __metadata: linkType: hard "browserslist@npm:^4.21.5, browserslist@npm:^4.23.0, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0, browserslist@npm:^4.24.2, browserslist@npm:^4.25.3": - version: 4.26.0 - resolution: "browserslist@npm:4.26.0" + version: 4.26.2 + resolution: "browserslist@npm:4.26.2" dependencies: - baseline-browser-mapping: "npm:^2.8.2" + baseline-browser-mapping: "npm:^2.8.3" caniuse-lite: "npm:^1.0.30001741" electron-to-chromium: "npm:^1.5.218" node-releases: "npm:^2.0.21" update-browserslist-db: "npm:^1.1.3" bin: browserslist: cli.js - checksum: 10c0/49be9a9cb27ca959f85c23f638c60680e21aab0e34130ff7cb8dae0be9426769124576e834c971150a471561abc67afee4e864262ce7345f5e961ff465879cac + checksum: 10c0/1146339dad33fda77786b11ea07f1c40c48899edd897d73a9114ee0dbb1ee6475bb4abda263a678c104508bdca8e66760ff8e10be1947d3e20d34bae01d8b89b languageName: node linkType: hard @@ -11028,9 +10979,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001646, caniuse-lite@npm:^1.0.30001687, caniuse-lite@npm:^1.0.30001741": - version: 1.0.30001741 - resolution: "caniuse-lite@npm:1.0.30001741" - checksum: 10c0/45746f896205a61a8eeb85a32aeca243ebce640cd6eb80d04949d9389a13f4659c737860300d7b988057599f0958c55eeab74ec02ce9ef137feb7d006e75fec1 + version: 1.0.30001743 + resolution: "caniuse-lite@npm:1.0.30001743" + checksum: 10c0/1bd730ca10d881a1ca9f55ce864d34c3b18501718c03976e0d3419f4694b715159e13fdef6d58ad47b6d2445d315940f3a01266658876828c820a3331aac021d languageName: node linkType: hard @@ -12995,9 +12946,9 @@ __metadata: linkType: hard "electron-to-chromium@npm:^1.5.218": - version: 1.5.218 - resolution: "electron-to-chromium@npm:1.5.218" - checksum: 10c0/d625e825834d862a33ed17da60d607267569cea6ff7c498c181dac285e037cce18b46ae810e8328a4dc26559a358d22b77d0a5c75aba182d39621947a7c62a34 + version: 1.5.220 + resolution: "electron-to-chromium@npm:1.5.220" + checksum: 10c0/5d0fd9304a25cb6043593f3e7e7c17c5af8b5eb5cc7896b523cbc08eb5e02db0e20e768551b8f866478c95921ee479190e712eb6a3279ea3b7384e2043b1f078 languageName: node linkType: hard @@ -13250,17 +13201,7 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.17.1, enhanced-resolve@npm:^5.7.0": - version: 5.18.1 - resolution: "enhanced-resolve@npm:5.18.1" - dependencies: - graceful-fs: "npm:^4.2.4" - tapable: "npm:^2.2.0" - checksum: 10c0/4cffd9b125225184e2abed9fdf0ed3dbd2224c873b165d0838fd066cde32e0918626cba2f1f4bf6860762f13a7e2364fd89a82b99566be2873d813573ac71846 - languageName: node - linkType: hard - -"enhanced-resolve@npm:^5.17.3": +"enhanced-resolve@npm:^5.17.1, enhanced-resolve@npm:^5.17.3, enhanced-resolve@npm:^5.7.0": version: 5.18.3 resolution: "enhanced-resolve@npm:5.18.3" dependencies: @@ -14847,18 +14788,6 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.4": - version: 6.4.6 - resolution: "fdir@npm:6.4.6" - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - checksum: 10c0/45b559cff889934ebb8bc498351e5acba40750ada7e7d6bde197768d2fa67c149be8ae7f8ff34d03f4e1eb20f2764116e56440aaa2f6689e9a4aa7ef06acafe9 - languageName: node - linkType: hard - "fetch-retry@npm:^6.0.0": version: 6.0.0 resolution: "fetch-retry@npm:6.0.0" @@ -15104,15 +15033,15 @@ __metadata: linkType: hard "flow-parser@npm:0.*": - version: 0.281.0 - resolution: "flow-parser@npm:0.281.0" - checksum: 10c0/fced1e9bad65d057f4f07a4e3e32eae719e1bbb95327f7e98455cf47a3aa451aac19d19c57d5304ac80cc6272eb4f3de45f1fe5e11eba8726e8ebd9b3a3c6176 + version: 0.283.0 + resolution: "flow-parser@npm:0.283.0" + checksum: 10c0/24a1553d4897c5d6befcc81d74dc2a42337554d0926760b2fa9c92bd1e35089f39893b286cda522b89b3708f2604e282277426cea32289cffe8c6268a0a96097 languageName: node linkType: hard "flow-remove-types@npm:^2.158.0": - version: 2.281.0 - resolution: "flow-remove-types@npm:2.281.0" + version: 2.283.0 + resolution: "flow-remove-types@npm:2.283.0" dependencies: hermes-parser: "npm:0.32.0" pirates: "npm:^3.0.2" @@ -15120,7 +15049,7 @@ __metadata: bin: flow-node: flow-node flow-remove-types: flow-remove-types - checksum: 10c0/fb9a0616c6feedc128d60d70da258c0f6d19d62032bc8a5518e4d3552bebd8373880d8cc615f0e94a2ddd4d348b4c38fd5479374a1be79711da52e4ba2c1965a + checksum: 10c0/205234ae2708192f4a85390aba45c75ca51a82c400a0fa33cf2ca06bd15ec5d18f9bda5584d8cd0fd93c860abb64681ab9af4b142cab5b5e53f0aa16e0064d35 languageName: node linkType: hard @@ -15202,18 +15131,6 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0": - version: 4.0.2 - resolution: "form-data@npm:4.0.2" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - es-set-tostringtag: "npm:^2.1.0" - mime-types: "npm:^2.1.12" - checksum: 10c0/e534b0cf025c831a0929bf4b9bbe1a9a6b03e273a8161f9947286b9b13bf8fb279c6944aae0070c4c311100c6d6dbb815cd955dc217728caf73fad8dc5b8ee9c - languageName: node - linkType: hard - "form-data@npm:^4.0.4": version: 4.0.4 resolution: "form-data@npm:4.0.4" @@ -16705,20 +16622,13 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^7.0.0": +"ignore@npm:^7.0.0, ignore@npm:^7.0.3": version: 7.0.5 resolution: "ignore@npm:7.0.5" checksum: 10c0/ae00db89fe873064a093b8999fe4cc284b13ef2a178636211842cceb650b9c3e390d3339191acb145d81ed5379d2074840cf0c33a20bdbd6f32821f79eb4ad5d languageName: node linkType: hard -"ignore@npm:^7.0.3": - version: 7.0.4 - resolution: "ignore@npm:7.0.4" - checksum: 10c0/90e1f69ce352b9555caecd9cbfd07abe7626d312a6f90efbbb52c7edca6ea8df065d66303863b30154ab1502afb2da8bc59d5b04e1719a52ef75bbf675c488eb - languageName: node - linkType: hard - "image-size@npm:^2.0.0, image-size@npm:^2.0.2": version: 2.0.2 resolution: "image-size@npm:2.0.2" @@ -17211,9 +17121,9 @@ __metadata: linkType: hard "is-network-error@npm:^1.0.0": - version: 1.1.0 - resolution: "is-network-error@npm:1.1.0" - checksum: 10c0/89eef83c2a4cf43d853145ce175d1cf43183b7a58d48c7a03e7eed4eb395d0934c1f6d101255cdd8c8c2980ab529bfbe5dd9edb24e1c3c28d2b3c814469b5b7d + version: 1.2.0 + resolution: "is-network-error@npm:1.2.0" + checksum: 10c0/9c46ca357ec512f602ffb841ef4e61d5b60933153822e047bef143650e95064918e2100bf67c88de09aed10957ab5545cf1fa17a29505efefd9c3e0748bf8d73 languageName: node linkType: hard @@ -18117,7 +18027,7 @@ __metadata: languageName: node linkType: hard -"launch-editor@npm:^2.6.1": +"launch-editor@npm:^2.11.1, launch-editor@npm:^2.6.1": version: 2.11.1 resolution: "launch-editor@npm:2.11.1" dependencies: @@ -19035,8 +18945,8 @@ __metadata: linkType: hard "memfs@npm:^4.11.1, memfs@npm:^4.6.0": - version: 4.39.0 - resolution: "memfs@npm:4.39.0" + version: 4.42.0 + resolution: "memfs@npm:4.42.0" dependencies: "@jsonjoy.com/json-pack": "npm:^1.11.0" "@jsonjoy.com/util": "npm:^1.9.0" @@ -19044,7 +18954,7 @@ __metadata: thingies: "npm:^2.5.0" tree-dump: "npm:^1.0.3" tslib: "npm:^2.0.0" - checksum: 10c0/9eee5b847d1a5e2a7a31b8bbd590dc24c0e0ce7f63e9c92b37d6ae7e6fe639c37bdc82558fdf7ce81e490cb012b73cfbcce2850bb09abdface85c8740ccf6e6b + checksum: 10c0/b0b80c92c72d1a73b9e935900454b43805837235e613f82daa0258ce31b1c5fb25f826b379040d3aff8e1dfc82fd759bdbad2cc382b0f5aa86a6819dbe5d741a languageName: node linkType: hard @@ -20601,7 +20511,7 @@ __metadata: languageName: node linkType: hard -"open@npm:^8.0.4, open@npm:^8.4.0": +"open@npm:^8.4.0": version: 8.4.2 resolution: "open@npm:8.4.2" dependencies: @@ -22163,6 +22073,15 @@ __metadata: languageName: node linkType: hard +"qrcode.react@npm:^4.2.0": + version: 4.2.0 + resolution: "qrcode.react@npm:4.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/68c691d130e5fda2f57cee505ed7aea840e7d02033100687b764601f9595e1116e34c13876628a93e1a5c2b85e4efc27d30b2fda72e2050c02f3e1c4e998d248 + languageName: node + linkType: hard + "qs@npm:6.13.0": version: 6.13.0 resolution: "qs@npm:6.13.0" @@ -23939,19 +23858,7 @@ __metadata: languageName: node linkType: hard -"sha.js@npm:^2.4.0, sha.js@npm:^2.4.8": - version: 2.4.11 - resolution: "sha.js@npm:2.4.11" - dependencies: - inherits: "npm:^2.0.1" - safe-buffer: "npm:^5.0.1" - bin: - sha.js: ./bin.js - checksum: 10c0/b7a371bca8821c9cc98a0aeff67444a03d48d745cb103f17228b96793f455f0eb0a691941b89ea1e60f6359207e36081d9be193252b0f128e0daf9cfea2815a5 - languageName: node - linkType: hard - -"sha.js@npm:^2.4.11": +"sha.js@npm:^2.4.0, sha.js@npm:^2.4.11, sha.js@npm:^2.4.8": version: 2.4.12 resolution: "sha.js@npm:2.4.12" dependencies: @@ -24544,7 +24451,7 @@ __metadata: version: 0.0.0-use.local resolution: "storybook-addon-pseudo-states@workspace:addons/pseudo-states" dependencies: - "@storybook/icons": "npm:^1.4.0" + "@storybook/icons": "npm:^1.6.0" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" typescript: "npm:^5.8.3" @@ -24581,7 +24488,7 @@ __metadata: "@rolldown/pluginutils": "npm:1.0.0-beta.18" "@storybook/docs-mdx": "npm:4.0.0-next.1" "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.4.0" + "@storybook/icons": "npm:^1.6.0" "@tanstack/react-virtual": "npm:^3.3.0" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:^6.6.3" @@ -24610,7 +24517,6 @@ __metadata: "@yarnpkg/fslib": "npm:2.10.3" "@yarnpkg/libzip": "npm:2.3.0" ansi-to-html: "npm:^0.7.2" - better-opn: "npm:^3.0.2" boxen: "npm:^8.0.1" browser-dtector: "npm:^3.4.0" bundle-require: "npm:^5.1.0" @@ -24642,13 +24548,14 @@ __metadata: jiti: "npm:^2.4.2" js-yaml: "npm:^4.1.0" jsdoc-type-pratt-parser: "npm:^4.0.0" + launch-editor: "npm:^2.11.1" lazy-universal-dotenv: "npm:^4.0.0" leven: "npm:^4.0.0" memfs: "npm:^4.11.1" memoizerific: "npm:^1.11.3" nanoid: "npm:^4.0.2" npmlog: "npm:^7.0.0" - open: "npm:^8.4.0" + open: "npm:^10.2.0" p-limit: "npm:^6.2.0" package-manager-detector: "npm:^1.1.0" picocolors: "npm:^1.1.0" @@ -24659,6 +24566,7 @@ __metadata: prettier: "npm:^3.5.3" pretty-hrtime: "npm:^1.0.3" prompts: "npm:^2.4.0" + qrcode.react: "npm:^4.2.0" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" react-helmet-async: "npm:^1.3.0" @@ -25212,14 +25120,14 @@ __metadata: linkType: hard "tar-fs@npm:^2.1.1": - version: 2.1.3 - resolution: "tar-fs@npm:2.1.3" + version: 2.1.4 + resolution: "tar-fs@npm:2.1.4" dependencies: chownr: "npm:^1.1.1" mkdirp-classic: "npm:^0.5.2" pump: "npm:^3.0.0" tar-stream: "npm:^2.1.4" - checksum: 10c0/472ee0c3c862605165163113ab6924f411c07506a1fb24c51a1a80085f0d4d381d86d2fd6b189236c8d932d1cd97b69cce35016767ceb658a35f7584fe77f305 + checksum: 10c0/decb25acdc6839182c06ec83cba6136205bda1db984e120c8ffd0d80182bc5baa1d916f9b6c5c663ea3f9975b4dd49e3c6bb7b1707cbcdaba4e76042f43ec84c languageName: node linkType: hard @@ -25428,17 +25336,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.10, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.9": - version: 0.2.14 - resolution: "tinyglobby@npm:0.2.14" - dependencies: - fdir: "npm:^6.4.4" - picomatch: "npm:^4.0.2" - checksum: 10c0/f789ed6c924287a9b7d3612056ed0cda67306cd2c80c249fd280cf1504742b12583a2089b61f4abbd24605f390809017240e250241f09938054c9b363e51c0a6 - languageName: node - linkType: hard - -"tinyglobby@npm:^0.2.15": +"tinyglobby@npm:^0.2.10, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.9": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: diff --git a/docs/_snippets/storybook-addon-toolkit-types.md b/docs/_snippets/storybook-addon-toolkit-types.md index 067ca2e03a4d..2f8547ce146c 100644 --- a/docs/_snippets/storybook-addon-toolkit-types.md +++ b/docs/_snippets/storybook-addon-toolkit-types.md @@ -21,8 +21,8 @@ export const Tool = memo(function MyAddonSelector() { useEffect(() => { api.setAddonShortcut(ADDON_ID, { - label: 'Toggle Measure [O]', - defaultShortcut: ['O'], + label: 'Toggle Outline', + defaultShortcut: ['alt', 'O'], actionName: 'outline', showInMenu: false, action: toggleMyTool, diff --git a/docs/versions/next.json b/docs/versions/next.json index 16a95ef204fe..eed75662ad7f 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"10.0.0-beta.5","info":{"plain":"- Dependencies: Update `vite-plugin-storybook-nextjs` to 2.0.7 - [#32331](https://github.com/storybookjs/storybook/pull/32331), thanks @k35o!\n- Fix: ESLint plugin homepage URL updates - [#32445](https://github.com/storybookjs/storybook/pull/32445), thanks @VivekKavala!\n- Upgrades: Packages `boxen` `commander` `giget` - [#32469](https://github.com/storybookjs/storybook/pull/32469), thanks @ndelangen!"}} \ No newline at end of file +{"version":"10.0.0-beta.6","info":{"plain":"- Core: Add \\\"open in editor\\\" feature - [#32452](https://github.com/storybookjs/storybook/pull/32452), thanks @yannbf!\n- Dev: Improve the browser opening experience - [#32488](https://github.com/storybookjs/storybook/pull/32488), thanks @ndelangen!\n- Maintenance: Remove globalization for dropped entrypoints - [#32491](https://github.com/storybookjs/storybook/pull/32491), thanks @ndelangen!\n- Telemetry: Queue error reporting & filter browser-extention - [#32499](https://github.com/storybookjs/storybook/pull/32499), thanks @ndelangen!\n- Upgrade: Packages `open` - [#32484](https://github.com/storybookjs/storybook/pull/32484), thanks @ndelangen!"}} \ No newline at end of file diff --git a/scripts/release/__tests__/version.test.ts b/scripts/release/__tests__/version.test.ts index ba71e024a56e..3feb73531c74 100644 --- a/scripts/release/__tests__/version.test.ts +++ b/scripts/release/__tests__/version.test.ts @@ -252,13 +252,13 @@ describe('Version', () => { expect(fspExtra.writeFile).toHaveBeenCalledWith( CODE_PACKAGE_JSON_PATH, // this call is the write that removes the "deferredNextVersion" property - JSON.stringify({ version: currentVersion }, null, 2) + expect.stringContaining(JSON.stringify({ version: currentVersion }, null, 2)) ); } expect(fspExtra.writeFile).toHaveBeenCalledWith( CODE_PACKAGE_JSON_PATH, - JSON.stringify({ version: expectedVersion }, null, 2) + expect.stringContaining(JSON.stringify({ version: expectedVersion }, null, 2)) ); expect(fspExtra.writeFile).toHaveBeenCalledWith( MANAGER_API_VERSION_PATH, @@ -270,13 +270,15 @@ describe('Version', () => { ); expect(fspExtra.writeFile).toHaveBeenCalledWith( A11Y_PACKAGE_JSON_PATH, - JSON.stringify( - { - // should update package version - version: expectedVersion, - }, - null, - 2 + expect.stringContaining( + JSON.stringify( + { + // should update package version + version: expectedVersion, + }, + null, + 2 + ) ) ); expect(execaCommand).toHaveBeenCalledWith('yarn install --mode=update-lockfile', { @@ -297,7 +299,9 @@ describe('Version', () => { expect(fspExtra.writeFile).toHaveBeenCalledTimes(1); expect(fspExtra.writeFile).toHaveBeenCalledWith( CODE_PACKAGE_JSON_PATH, - JSON.stringify({ version: '1.0.0', deferredNextVersion: '2.0.0-beta.0' }, null, 2) + expect.stringContaining( + JSON.stringify({ version: '1.0.0', deferredNextVersion: '2.0.0-beta.0' }, null, 2) + ) ); expect(execaCommand).not.toHaveBeenCalled(); }); diff --git a/scripts/release/version.ts b/scripts/release/version.ts index a9cd2d72098c..98fb24413d59 100644 --- a/scripts/release/version.ts +++ b/scripts/release/version.ts @@ -127,7 +127,7 @@ const bumpCodeVersion = async (nextVersion: string) => { const codePkgJson = await readJson(CODE_PACKAGE_JSON_PATH); codePkgJson.version = nextVersion; - await writeFile(CODE_PACKAGE_JSON_PATH, JSON.stringify(codePkgJson, null, 2)); + await writeFile(CODE_PACKAGE_JSON_PATH, JSON.stringify(codePkgJson, null, 2) + '\n'); console.log(`✅ Bumped version of ${picocolors.cyan('code')}'s package.json`); }; @@ -202,7 +202,7 @@ const bumpDeferred = async (nextVersion: string) => { } codePkgJson.deferredNextVersion = nextVersion; - await writeFile(CODE_PACKAGE_JSON_PATH, JSON.stringify(codePkgJson, null, 2)); + await writeFile(CODE_PACKAGE_JSON_PATH, JSON.stringify(codePkgJson, null, 2) + '\n'); console.log(`✅ Set a ${picocolors.cyan('deferred')} version bump. Not bumping any packages.`); }; @@ -224,7 +224,7 @@ const applyDeferredVersionBump = async () => { } delete codePkgJson.deferredNextVersion; - await writeFile(CODE_PACKAGE_JSON_PATH, JSON.stringify(codePkgJson, null, 2)); + await writeFile(CODE_PACKAGE_JSON_PATH, JSON.stringify(codePkgJson, null, 2) + '\n'); console.log( `✅ Extracted and removed deferred version ${picocolors.green(