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/package.json b/code/core/package.json index 86cccc7c7bc3..37827577c9c3 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -228,7 +228,7 @@ }, "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", @@ -317,6 +317,7 @@ "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", @@ -334,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 5effeac9d73d..375917558ad9 100644 --- a/code/core/src/core-server/dev-server.ts +++ b/code/core/src/core-server/dev-server.ts @@ -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(); } 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/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/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/exports.ts b/code/core/src/manager/globals/exports.ts index ae8a97dca41e..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', @@ -561,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', diff --git a/code/core/src/manager/settings/defaultShortcuts.tsx b/code/core/src/manager/settings/defaultShortcuts.tsx index cefc82db1ddd..2c7fa19e6d2e 100644 --- a/code/core/src/manager/settings/defaultShortcuts.tsx +++ b/code/core/src/manager/settings/defaultShortcuts.tsx @@ -20,4 +20,8 @@ export const defaultShortcuts: State['shortcuts'] = { collapseAll: ['ctrl', 'shift', 'ArrowUp'], expandAll: ['ctrl', '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'], }; diff --git a/code/core/src/manager/settings/shortcuts.tsx b/code/core/src/manager/settings/shortcuts.tsx index 94bc6bfc6505..03c644e18be5 100644 --- a/code/core/src/manager/settings/shortcuts.tsx +++ b/code/core/src/manager/settings/shortcuts.tsx @@ -132,6 +132,10 @@ const shortcutLabels = { collapseAll: 'Collapse all items on sidebar', expandAll: 'Expand all items on sidebar', remount: 'Remount component', + openInEditor: 'Open story in editor', + copyStoryLink: 'Copy story link to clipboard', + // TODO: bring this back once we want to add shortcuts for this + // copyStoryName: 'Copy story name to clipboard', }; export type Feature = keyof typeof shortcutLabels; @@ -194,16 +198,21 @@ class ShortcutsScreen extends Component '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/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/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/yarn.lock b/code/yarn.lock index a7a16d75b5e1..a40557f3f250 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -3629,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" @@ -3641,7 +3641,7 @@ __metadata: thingies: "npm:^2.5.0" peerDependencies: tslib: 2 - checksum: 10c0/2eeea1fbe410ddaedab43c7f22d869441e9f60062fdf7cd2b91b8ae7954965ec4caeb3d328a01caef6c09bfc760b60f8e2aaba2f1f7777c8bfdf918c568a1c6c + checksum: 10c0/af69d7911553cae3a69fdc444a8c2ea8f15ee2e2622da1b4b74f1873274e00db227fbd0f187ab49b8a36a869d090e91ebb8a23e5771175466d29974bd3a40553 languageName: node linkType: hard @@ -6085,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" @@ -6128,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" @@ -6157,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" @@ -6189,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" @@ -6203,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" @@ -6218,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" @@ -6546,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 @@ -7980,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 @@ -10208,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: @@ -10757,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 @@ -10979,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 @@ -12946,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 @@ -15033,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" @@ -15049,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 @@ -17121,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 @@ -18027,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: @@ -18945,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" @@ -18954,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 @@ -22073,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" @@ -24442,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" @@ -24479,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" @@ -24539,6 +24548,7 @@ __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" @@ -24556,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" @@ -25109,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 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,