diff --git a/code/addons/interactions/src/components/EmptyState.tsx b/code/addons/interactions/src/components/EmptyState.tsx
new file mode 100644
index 000000000000..d4fa62c144a4
--- /dev/null
+++ b/code/addons/interactions/src/components/EmptyState.tsx
@@ -0,0 +1,99 @@
+import React, { useEffect, useState } from 'react';
+import { Link } from '@storybook/components';
+import { DocumentIcon, VideoIcon } from '@storybook/icons';
+import { Consumer, useStorybookApi } from '@storybook/manager-api';
+import { styled } from '@storybook/theming';
+
+import { DOCUMENTATION_LINK, TUTORIAL_VIDEO_LINK } from '../constants';
+
+const Wrapper = styled.div(({ theme }) => ({
+ height: '100%',
+ display: 'flex',
+ padding: 0,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexDirection: 'column',
+ gap: 15,
+ background: theme.background.content,
+}));
+
+const Content = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 4,
+ maxWidth: 415,
+});
+
+const Title = styled.div(({ theme }) => ({
+ fontWeight: theme.typography.weight.bold,
+ fontSize: theme.typography.size.s2 - 1,
+ textAlign: 'center',
+ color: theme.textColor,
+}));
+
+const Description = styled.div(({ theme }) => ({
+ fontWeight: theme.typography.weight.regular,
+ fontSize: theme.typography.size.s2 - 1,
+ textAlign: 'center',
+ color: theme.textMutedColor,
+}));
+
+const Links = styled.div(({ theme }) => ({
+ display: 'flex',
+ fontSize: theme.typography.size.s2 - 1,
+ gap: 25,
+}));
+
+const Divider = styled.div(({ theme }) => ({
+ width: 1,
+ height: 16,
+ backgroundColor: theme.appBorderColor,
+}));
+
+export const Empty = () => {
+ const [isLoading, setIsLoading] = useState(true);
+ const api = useStorybookApi();
+ const docsUrl = api.getDocsUrl({
+ subpath: DOCUMENTATION_LINK,
+ versioned: true,
+ renderer: true,
+ });
+
+ // We are adding a small delay to avoid flickering when the story is loading.
+ // It takes a bit of time for the controls to appear, so we don't want
+ // to show the empty state for a split second.
+ useEffect(() => {
+ const load = setTimeout(() => {
+ setIsLoading(false);
+ }, 100);
+
+ return () => clearTimeout(load);
+ }, []);
+
+ if (isLoading) return null;
+
+ return (
+
+
+ Interaction testing
+
+ Interaction tests allow you to verify the functional aspects of UIs. Write a play function
+ for your story and you'll see it run here.
+
+
+
+
+ Watch 8m video
+
+
+
+ {({ state }) => (
+
+ Read docs
+
+ )}
+
+
+
+ );
+};
diff --git a/code/addons/interactions/src/components/InteractionsPanel.tsx b/code/addons/interactions/src/components/InteractionsPanel.tsx
index 0663656cc14b..a5b58d94b680 100644
--- a/code/addons/interactions/src/components/InteractionsPanel.tsx
+++ b/code/addons/interactions/src/components/InteractionsPanel.tsx
@@ -1,5 +1,4 @@
import * as React from 'react';
-import { Link, Placeholder } from '@storybook/components';
import { type Call, CallStates, type ControlStates } from '@storybook/instrumenter';
import { styled } from '@storybook/theming';
import { transparentize } from 'polished';
@@ -8,6 +7,7 @@ import { Subnav } from './Subnav';
import { Interaction } from './Interaction';
import { isTestAssertionError } from '../utils';
+import { Empty } from './EmptyState';
export interface Controls {
start: (args: any) => void;
@@ -40,7 +40,7 @@ interface InteractionsPanelProps {
}
const Container = styled.div(({ theme }) => ({
- minHeight: '100%',
+ height: '100%',
background: theme.background.content,
}));
@@ -153,18 +153,7 @@ export const InteractionsPanel: React.FC = React.memo(
)}
- {!isPlaying && !caughtException && interactions.length === 0 && (
-
- No interactions found
-
- Learn how to add interactions to your story
-
-
- )}
+ {!isPlaying && !caughtException && interactions.length === 0 && }
);
}
diff --git a/code/addons/interactions/src/constants.ts b/code/addons/interactions/src/constants.ts
index 3c723e93852c..1b70ededc7c4 100644
--- a/code/addons/interactions/src/constants.ts
+++ b/code/addons/interactions/src/constants.ts
@@ -1,2 +1,5 @@
export const ADDON_ID = 'storybook/interactions';
export const PANEL_ID = `${ADDON_ID}/panel`;
+
+export const TUTORIAL_VIDEO_LINK = 'https://youtu.be/Waht9qq7AoA';
+export const DOCUMENTATION_LINK = 'writing-tests/interaction-testing';
diff --git a/code/builders/builder-manager/src/index.ts b/code/builders/builder-manager/src/index.ts
index ef71811029f4..28e6fdd5a55e 100644
--- a/code/builders/builder-manager/src/index.ts
+++ b/code/builders/builder-manager/src/index.ts
@@ -24,6 +24,7 @@ import type {
import { getData } from './utils/data';
import { safeResolve } from './utils/safeResolve';
import { readOrderedFiles } from './utils/files';
+import { buildFrameworkGlobalsFromOptions } from './utils/framework';
let compilation: Compilation;
let asyncIterator: ReturnType | ReturnType;
@@ -163,6 +164,9 @@ const starter: StarterFunction = async function* starterGeneratorFn({
const { cssFiles, jsFiles } = await readOrderedFiles(addonsDir, compilation?.outputFiles);
+ // Build additional global values
+ const globals: Record = await buildFrameworkGlobalsFromOptions(options);
+
yield;
const html = await renderHTML(
@@ -177,7 +181,8 @@ const starter: StarterFunction = async function* starterGeneratorFn({
logLevel,
docsOptions,
tagsOptions,
- options
+ options,
+ globals
);
yield;
@@ -252,6 +257,9 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime,
});
const { cssFiles, jsFiles } = await readOrderedFiles(addonsDir, compilation?.outputFiles);
+ // Build additional global values
+ const globals: Record = await buildFrameworkGlobalsFromOptions(options);
+
yield;
const html = await renderHTML(
@@ -266,7 +274,8 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime,
logLevel,
docsOptions,
tagsOptions,
- options
+ options,
+ globals
);
await Promise.all([
diff --git a/code/builders/builder-manager/src/utils/framework.test.ts b/code/builders/builder-manager/src/utils/framework.test.ts
new file mode 100644
index 000000000000..ecdcb42ead50
--- /dev/null
+++ b/code/builders/builder-manager/src/utils/framework.test.ts
@@ -0,0 +1,48 @@
+import path from 'node:path';
+import { describe, it, expect } from 'vitest';
+
+import {
+ pluckNameFromConfigProperty,
+ pluckStorybookPackageFromPath,
+ pluckThirdPartyPackageFromPath,
+} from './framework';
+
+describe('UTILITIES: Framework information', () => {
+ describe('UTILITY: pluckNameFromConfigProperty', () => {
+ it('should return undefined if the property is undefined', () => {
+ expect(pluckNameFromConfigProperty(undefined)).toBe(undefined);
+ });
+
+ it('should return the name if the property is a string', () => {
+ expect(pluckNameFromConfigProperty('foo')).toBe('foo');
+ });
+
+ it('should return the name if the property is an object', () => {
+ expect(pluckNameFromConfigProperty({ name: 'foo' })).toBe('foo');
+ });
+ });
+
+ describe('UTILITY: pluckStorybookPackageFromPath', () => {
+ it('should return the package name if the path is a storybook package', () => {
+ const packagePath = path.join(process.cwd(), 'node_modules', '@storybook', 'foo');
+ expect(pluckStorybookPackageFromPath(packagePath)).toBe('@storybook/foo');
+ });
+
+ it('should return undefined if the path is not a storybook package', () => {
+ const packagePath = path.join(process.cwd(), 'foo');
+ expect(pluckStorybookPackageFromPath(packagePath)).toBe(undefined);
+ });
+ });
+
+ describe('UTILITY: pluckThirdPartyPackageFromPath', () => {
+ it('should return the package name if the path is a third party package', () => {
+ const packagePath = path.join(process.cwd(), 'node_modules', 'bar');
+ expect(pluckThirdPartyPackageFromPath(packagePath)).toBe('bar');
+ });
+
+ it('should return the given path if the path is not a third party package', () => {
+ const packagePath = path.join(process.cwd(), 'foo', 'bar', 'baz');
+ expect(pluckThirdPartyPackageFromPath(packagePath)).toBe(packagePath);
+ });
+ });
+});
diff --git a/code/builders/builder-manager/src/utils/framework.ts b/code/builders/builder-manager/src/utils/framework.ts
new file mode 100644
index 000000000000..165b018ddf8f
--- /dev/null
+++ b/code/builders/builder-manager/src/utils/framework.ts
@@ -0,0 +1,51 @@
+import path from 'path';
+import type { Options } from '@storybook/types';
+
+interface PropertyObject {
+ name: string;
+ options?: Record;
+}
+
+type Property = string | PropertyObject | undefined;
+
+export const pluckNameFromConfigProperty = (property: Property) => {
+ if (!property) {
+ return undefined;
+ }
+
+ return typeof property === 'string' ? property : property.name;
+};
+
+// For replacing Windows backslashes with forward slashes
+const normalizePath = (packagePath: string) => packagePath.replaceAll(path.sep, '/');
+
+export const pluckStorybookPackageFromPath = (packagePath: string) =>
+ normalizePath(packagePath).match(/(@storybook\/.*)$/)?.[1];
+
+export const pluckThirdPartyPackageFromPath = (packagePath: string) =>
+ normalizePath(packagePath).split('node_modules/')[1] ?? packagePath;
+
+export const buildFrameworkGlobalsFromOptions = async (options: Options) => {
+ const globals: Record = {};
+
+ const { renderer, builder } = await options.presets.apply('core');
+
+ const rendererName = pluckNameFromConfigProperty(renderer);
+ if (rendererName) {
+ globals.STORYBOOK_RENDERER =
+ pluckStorybookPackageFromPath(rendererName) ?? pluckThirdPartyPackageFromPath(rendererName);
+ }
+
+ const builderName = pluckNameFromConfigProperty(builder);
+ if (builderName) {
+ globals.STORYBOOK_BUILDER =
+ pluckStorybookPackageFromPath(builderName) ?? pluckThirdPartyPackageFromPath(builderName);
+ }
+
+ const framework = pluckNameFromConfigProperty(await options.presets.apply('framework'));
+ if (framework) {
+ globals.STORYBOOK_FRAMEWORK = framework;
+ }
+
+ return globals;
+};
diff --git a/code/builders/builder-manager/src/utils/template.ts b/code/builders/builder-manager/src/utils/template.ts
index 4ccb2d50864a..dd2d1bc88e20 100644
--- a/code/builders/builder-manager/src/utils/template.ts
+++ b/code/builders/builder-manager/src/utils/template.ts
@@ -35,10 +35,15 @@ export const renderHTML = async (
logLevel: Promise,
docsOptions: Promise,
tagsOptions: Promise,
- { versionCheck, previewUrl, configType, ignorePreview }: Options
+ { versionCheck, previewUrl, configType, ignorePreview }: Options,
+ globals: Record
) => {
const titleRef = await title;
const templateRef = await template;
+ const stringifiedGlobals = Object.entries(globals).reduce(
+ (transformed, [key, value]) => ({ ...transformed, [key]: JSON.stringify(value) }),
+ {}
+ );
return render(templateRef, {
title: titleRef ? `${titleRef} - Storybook` : 'Storybook',
@@ -54,6 +59,7 @@ export const renderHTML = async (
VERSIONCHECK: JSON.stringify(JSON.stringify(versionCheck), null, 2),
PREVIEW_URL: JSON.stringify(previewUrl, null, 2), // global preview URL
TAGS_OPTIONS: JSON.stringify(await tagsOptions, null, 2),
+ ...stringifiedGlobals,
},
head: (await customHead) || '',
ignorePreview,
diff --git a/code/lib/manager-api/src/modules/versions.ts b/code/lib/manager-api/src/modules/versions.ts
index a230da3a501f..a414c07fe073 100644
--- a/code/lib/manager-api/src/modules/versions.ts
+++ b/code/lib/manager-api/src/modules/versions.ts
@@ -23,6 +23,14 @@ const getVersionCheckData = memoize(1)((): API_Versions => {
}
});
+const normalizeRendererName = (renderer: string) => {
+ if (renderer.includes('vue')) {
+ return 'vue';
+ }
+
+ return renderer;
+};
+
export interface SubAPI {
/**
* Returns the current version of the Storybook Manager.
@@ -36,6 +44,12 @@ export interface SubAPI {
* @returns {API_Version} The latest version of the Storybook Manager.
*/
getLatestVersion: () => API_Version;
+ /**
+ * Returns the URL of the Storybook documentation for the current version.
+ *
+ * @returns {string} The URL of the Storybook Manager documentation.
+ */
+ getDocsUrl: (options: { subpath?: string; versioned?: boolean; renderer?: boolean }) => string;
/**
* Checks if an update is available for the Storybook Manager.
*
@@ -73,6 +87,37 @@ export const init: ModuleFn = ({ store }) => {
}
return latest as API_Version;
},
+ // TODO: Move this to it's own "info" module later
+ getDocsUrl: ({ subpath, versioned, renderer }) => {
+ const {
+ versions: { latest, current },
+ } = store.getState();
+
+ let url = 'https://storybook.js.org/docs/';
+
+ if (versioned && current?.version && latest?.version) {
+ const versionDiff = semver.diff(latest.version, current.version);
+ const isLatestDocs = versionDiff === 'patch' || versionDiff === null;
+
+ if (!isLatestDocs) {
+ url += `${semver.major(current.version)}.${semver.minor(current.version)}/`;
+ }
+ }
+
+ if (subpath) {
+ url += `${subpath}/`;
+ }
+
+ if (renderer && typeof global.STORYBOOK_RENDERER !== 'undefined') {
+ const rendererName = (global.STORYBOOK_RENDERER as string).split('/').pop()?.toLowerCase();
+
+ if (rendererName) {
+ url += `?renderer=${normalizeRendererName(rendererName)}`;
+ }
+ }
+
+ return url;
+ },
versionUpdateAvailable: () => {
const latest = api.getLatestVersion();
const current = api.getCurrentVersion();
diff --git a/code/lib/manager-api/src/tests/versions.test.js b/code/lib/manager-api/src/tests/versions.test.js
index 4f3e1cf93d7f..fbd29142b8d0 100644
--- a/code/lib/manager-api/src/tests/versions.test.js
+++ b/code/lib/manager-api/src/tests/versions.test.js
@@ -1,4 +1,6 @@
-import { describe, it, expect, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { global } from '@storybook/global';
+
import { init as initVersions } from '../modules/versions';
vi.mock('../version', () => ({
@@ -122,6 +124,135 @@ describe('versions API', () => {
});
});
+ describe('METHOD: getDocsUrl()', () => {
+ beforeEach(() => {
+ global.STORYBOOK_RENDERER = undefined;
+ });
+
+ it('returns the latest url when current version is latest', async () => {
+ const store = createMockStore();
+ const {
+ init,
+ api,
+ state: initialState,
+ } = initVersions({
+ store,
+ });
+
+ await init();
+
+ store.setState({
+ ...initialState,
+ versions: {
+ ...initialState.versions,
+ current: { version: '7.6.1' },
+ latest: { version: '7.6.1' },
+ },
+ });
+
+ expect(api.getDocsUrl({ versioned: true })).toEqual('https://storybook.js.org/docs/');
+ });
+
+ it('returns the latest url when version has patch diff with latest', async () => {
+ const store = createMockStore();
+ const {
+ init,
+ api,
+ state: initialState,
+ } = initVersions({
+ store,
+ });
+
+ await init();
+
+ store.setState({
+ ...initialState,
+ versions: {
+ ...initialState.versions,
+ current: { version: '7.6.1' },
+ latest: { version: '7.6.10' },
+ },
+ });
+
+ expect(api.getDocsUrl({ versioned: true })).toEqual('https://storybook.js.org/docs/');
+ });
+
+ it('returns the versioned url when current has different docs to latest', async () => {
+ const store = createMockStore();
+ const {
+ init,
+ api,
+ state: initialState,
+ } = initVersions({
+ store,
+ });
+
+ await init();
+
+ store.setState({
+ ...initialState,
+ versions: {
+ ...initialState.versions,
+ current: { version: '7.2.5' },
+ latest: { version: '7.6.10' },
+ },
+ });
+
+ expect(api.getDocsUrl({ versioned: true })).toEqual('https://storybook.js.org/docs/7.2/');
+ });
+
+ it('returns the versioned url when current is a prerelease', async () => {
+ const store = createMockStore();
+ const {
+ init,
+ api,
+ state: initialState,
+ } = initVersions({
+ store,
+ });
+
+ await init();
+
+ store.setState({
+ ...initialState,
+ versions: {
+ ...initialState.versions,
+ current: { version: '8.0.0-beta' },
+ latest: { version: '7.6.10' },
+ },
+ });
+
+ expect(api.getDocsUrl({ versioned: true })).toEqual('https://storybook.js.org/docs/8.0/');
+ });
+
+ it('returns a Url with a renderer query param when "renderer" is true', async () => {
+ const store = createMockStore();
+ const {
+ init,
+ api,
+ state: initialState,
+ } = initVersions({
+ store,
+ });
+ store.setState({
+ ...initialState,
+ versions: {
+ ...initialState.versions,
+ current: { version: '5.2.1' },
+ latest: { version: '5.2.1' },
+ },
+ });
+
+ await init();
+
+ global.STORYBOOK_RENDERER = 'vue';
+
+ expect(api.getDocsUrl({ renderer: true })).toEqual(
+ 'https://storybook.js.org/docs/?renderer=vue'
+ );
+ });
+ });
+
describe('versionUpdateAvailable', () => {
it('matching version', async () => {
const store = createMockStore();
diff --git a/code/lib/manager-api/src/typings.d.ts b/code/lib/manager-api/src/typings.d.ts
index 63edef9413da..afbe02b0417f 100644
--- a/code/lib/manager-api/src/typings.d.ts
+++ b/code/lib/manager-api/src/typings.d.ts
@@ -8,3 +8,6 @@ declare var REFS: any;
declare var VERSIONCHECK: any;
declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined;
declare var STORYBOOK_ADDON_STATE: Record;
+declare var STORYBOOK_RENDERER: string | undefined;
+declare var STORYBOOK_BUILDER: string | undefined;
+declare var STORYBOOK_FRAMEWORK: string | undefined;
diff --git a/code/ui/blocks/src/components/ArgsTable/ArgsTable.tsx b/code/ui/blocks/src/components/ArgsTable/ArgsTable.tsx
index c0a9e8b0d0bc..70e4e1f0edd7 100644
--- a/code/ui/blocks/src/components/ArgsTable/ArgsTable.tsx
+++ b/code/ui/blocks/src/components/ArgsTable/ArgsTable.tsx
@@ -7,7 +7,7 @@ import { includeConditionalArg } from '@storybook/csf';
import { once } from '@storybook/client-logger';
import { IconButton, ResetWrapper, Link } from '@storybook/components';
-import { UndoIcon } from '@storybook/icons';
+import { DocumentIcon, UndoIcon } from '@storybook/icons';
import { ArgRow } from './ArgRow';
import { SectionRow } from './SectionRow';
import type { ArgType, ArgTypes, Args, Globals } from './types';
@@ -323,7 +323,7 @@ export const ArgsTable: FC = (props) => {
{error}
- Read the docs
+ Read the docs
);
diff --git a/code/ui/blocks/src/components/ArgsTable/Empty.tsx b/code/ui/blocks/src/components/ArgsTable/Empty.tsx
index 796a3dcfba39..c4269a605f95 100644
--- a/code/ui/blocks/src/components/ArgsTable/Empty.tsx
+++ b/code/ui/blocks/src/components/ArgsTable/Empty.tsx
@@ -2,7 +2,7 @@ import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import { styled } from '@storybook/theming';
import { Link } from '@storybook/components';
-import { VideoIcon } from '@storybook/icons';
+import { DocumentIcon, SupportIcon, VideoIcon } from '@storybook/icons';
interface EmptyProps {
inAddonPanel?: boolean;
@@ -92,21 +92,17 @@ export const Empty: FC = ({ inAddonPanel }) => {
- Read docs
+ Read docs
>
)}
{!inAddonPanel && (
-
- Learn how to set that up
+
+ Learn how to set that up
)}
diff --git a/code/ui/manager/src/components/sidebar/Menu.stories.tsx b/code/ui/manager/src/components/sidebar/Menu.stories.tsx
index ca89361f179e..98788518db4b 100644
--- a/code/ui/manager/src/components/sidebar/Menu.stories.tsx
+++ b/code/ui/manager/src/components/sidebar/Menu.stories.tsx
@@ -56,6 +56,7 @@ export const Expanded: Story = {
getAddonsShortcuts: () => ({}),
versionUpdateAvailable: () => false,
isWhatsNewUnread: () => true,
+ getDocsUrl: () => 'https://storybook.js.org/docs/',
},
false,
false,
@@ -99,6 +100,7 @@ export const ExpandedWithoutWhatsNew: Story = {
getAddonsShortcuts: () => ({}),
versionUpdateAvailable: () => false,
isWhatsNewUnread: () => false,
+ getDocsUrl: () => 'https://storybook.js.org/docs/',
},
false,
false,
diff --git a/code/ui/manager/src/container/Menu.tsx b/code/ui/manager/src/container/Menu.tsx
index 2c8fb64c01a3..77df6aecdd01 100644
--- a/code/ui/manager/src/container/Menu.tsx
+++ b/code/ui/manager/src/container/Menu.tsx
@@ -5,7 +5,7 @@ import { Badge } from '@storybook/components';
import type { API, State } from '@storybook/manager-api';
import { shortcutToHumanString } from '@storybook/manager-api';
import { styled, useTheme } from '@storybook/theming';
-import { CheckIcon } from '@storybook/icons';
+import { CheckIcon, InfoIcon, ShareAltIcon, WandIcon } from '@storybook/icons';
const focusableUIElements = {
storySearchField: 'storybook-explorer-searchfield',
@@ -65,10 +65,22 @@ export const useMenu = (
id: 'about',
title: 'About your Storybook',
onClick: () => api.changeSettingsTab('about'),
+ icon: ,
}),
[api]
);
+ const documentation = useMemo(() => {
+ const docsUrl = api.getDocsUrl({ versioned: true, renderer: true });
+
+ return {
+ id: 'documentation',
+ title: 'Documentation',
+ href: docsUrl,
+ icon: ,
+ };
+ }, [api]);
+
const whatsNewNotificationsEnabled =
state.whatsNewData?.status === 'SUCCESS' && !state.disableWhatsNewNotifications;
const isWhatsNewUnread = api.isWhatsNewUnread();
@@ -80,6 +92,7 @@ export const useMenu = (
right: whatsNewNotificationsEnabled && isWhatsNewUnread && (
Check it out
),
+ icon: ,
}),
[api, whatsNewNotificationsEnabled, isWhatsNewUnread]
);
@@ -104,7 +117,7 @@ export const useMenu = (
onClick: () => api.toggleNav(),
active: isNavShown,
right: enableShortcuts ? : null,
- left: isNavShown ? : null,
+ icon: isNavShown ? : null,
}),
[api, enableShortcuts, shortcutKeys, isNavShown]
);
@@ -116,7 +129,7 @@ export const useMenu = (
onClick: () => api.toggleToolbar(),
active: showToolbar,
right: enableShortcuts ? : null,
- left: showToolbar ? : null,
+ icon: showToolbar ? : null,
}),
[api, enableShortcuts, shortcutKeys, showToolbar]
);
@@ -128,7 +141,7 @@ export const useMenu = (
onClick: () => api.togglePanel(),
active: isPanelShown,
right: enableShortcuts ? : null,
- left: isPanelShown ? : null,
+ icon: isPanelShown ? : null,
}),
[api, enableShortcuts, shortcutKeys, isPanelShown]
);
@@ -150,7 +163,7 @@ export const useMenu = (
onClick: () => api.toggleFullscreen(),
active: isFullscreen,
right: enableShortcuts ? : null,
- left: isFullscreen ? : null,
+ icon: isFullscreen ? : null,
}),
[api, enableShortcuts, shortcutKeys, isFullscreen]
);
@@ -232,6 +245,7 @@ export const useMenu = (
() => [
about,
...(state.whatsNewData?.status === 'SUCCESS' ? [whatsNew] : []),
+ documentation,
shortcuts,
sidebarToggle,
toolbarToogle,
@@ -250,6 +264,7 @@ export const useMenu = (
about,
state,
whatsNew,
+ documentation,
shortcuts,
sidebarToggle,
toolbarToogle,