From 371ef9a8b5c4fe1db1330f4e6718c7c3148294db Mon Sep 17 00:00:00 2001 From: kalinco-glitch <283803646+kalinco-glitch@users.noreply.github.com> Date: Tue, 12 May 2026 16:43:07 -0400 Subject: [PATCH 1/5] Fix layout.showPanel manager config --- code/core/src/manager-api/modules/layout.ts | 62 +++++++++++++++---- .../core/src/manager-api/tests/layout.test.ts | 38 ++++++++++++ code/core/src/types/modules/addons.ts | 3 +- code/core/src/types/modules/api.ts | 5 ++ 4 files changed, 94 insertions(+), 14 deletions(-) diff --git a/code/core/src/manager-api/modules/layout.ts b/code/core/src/manager-api/modules/layout.ts index f8d053773926..23a444fa8837 100644 --- a/code/core/src/manager-api/modules/layout.ts +++ b/code/core/src/manager-api/modules/layout.ts @@ -2,6 +2,7 @@ import { SET_CONFIG } from 'storybook/internal/core-events'; import type { API_Layout, API_LayoutCustomisations, + API_LayoutOptions, API_PanelPositions, API_UI, } from 'storybook/internal/types'; @@ -192,6 +193,38 @@ const getRecentVisibleSizes = (layoutState: API_Layout) => { }; }; +const applyLayoutOptions = ( + layoutState: API_Layout, + options: API_LayoutOptions | undefined, + singleStory: boolean +) => { + const { showPanel, showSidebar, ...layoutOptions } = options ?? {}; + const layoutKeys = Object.keys(layoutState) as (keyof API_Layout)[]; + const nextLayoutState = toMerged(layoutState, pick(layoutOptions, layoutKeys)) as API_Layout; + + if (showSidebar === false) { + nextLayoutState.recentVisibleSizes = getRecentVisibleSizes(nextLayoutState); + nextLayoutState.navSize = 0; + } else if (showSidebar === true && !singleStory) { + nextLayoutState.navSize = nextLayoutState.recentVisibleSizes.navSize; + } + + if (showPanel === false) { + nextLayoutState.recentVisibleSizes = getRecentVisibleSizes(nextLayoutState); + nextLayoutState.bottomPanelHeight = 0; + nextLayoutState.rightPanelWidth = 0; + } else if (showPanel === true) { + nextLayoutState.bottomPanelHeight = nextLayoutState.recentVisibleSizes.bottomPanelHeight; + nextLayoutState.rightPanelWidth = nextLayoutState.recentVisibleSizes.rightPanelWidth; + } + + if (singleStory) { + nextLayoutState.navSize = 0; + } + + return nextLayoutState; +}; + export const init: ModuleFn = ({ store, provider, singleStory }) => { const api = { toggleFullscreen(nextState?: boolean) { @@ -444,13 +477,14 @@ export const init: ModuleFn = ({ store, provider, singleStory return { ...defaultLayoutState, - layout: { - ...toMerged( - defaultLayoutState.layout, - pick(options, Object.keys(defaultLayoutState.layout)) - ), - ...(singleStory && { navSize: 0 }), - }, + layout: applyLayoutOptions( + defaultLayoutState.layout, + { + ...options.layout, + ...pick(options, Object.keys(defaultLayoutState.layout)), + }, + !!singleStory + ), layoutCustomisations: { ...defaultLayoutState.layoutCustomisations, ...(layoutCustomisations ?? {}), @@ -513,12 +547,14 @@ export const init: ModuleFn = ({ store, provider, singleStory return; } - const updatedLayout = { - ...layout, - ...(options.layout || {}), - ...pick(options, Object.keys(layout)), - ...(singleStory && { navSize: 0 }), - }; + const updatedLayout = applyLayoutOptions( + layout, + { + ...options.layout, + ...pick(options, Object.keys(layout)), + }, + !!singleStory + ); const updatedUi = { ...ui, diff --git a/code/core/src/manager-api/tests/layout.test.ts b/code/core/src/manager-api/tests/layout.test.ts index 30fd16e0e4ad..73b2c5526c78 100644 --- a/code/core/src/manager-api/tests/layout.test.ts +++ b/code/core/src/manager-api/tests/layout.test.ts @@ -486,6 +486,44 @@ describe('layout API', () => { expect(getLastSetStateArgs()[0].selectedPanel).toEqual(panelName); }); + + it('should hide the panel when layout.showPanel is false', () => { + layoutApi.setSizes({ + bottomPanelHeight: 200, + rightPanelWidth: 250, + }); + + layoutApi.setOptions({ layout: { showPanel: false } }); + + expect(currentState.layout.bottomPanelHeight).toBe(0); + expect(currentState.layout.rightPanelWidth).toBe(0); + expect(currentState.layout.recentVisibleSizes.bottomPanelHeight).toBe(200); + expect(currentState.layout.recentVisibleSizes.rightPanelWidth).toBe(250); + + layoutApi.togglePanel(true); + + expect(currentState.layout.bottomPanelHeight).toBe(200); + expect(currentState.layout.rightPanelWidth).toBe(250); + }); + }); + + describe('getInitialOptions', () => { + it('should apply layout.showPanel from the initial config', () => { + (provider.getConfig as Mock).mockReturnValue({ + layout: { showPanel: false }, + }); + + const { state } = initLayout({ + store, + provider, + singleStory: false, + } as unknown as ModuleArgs); + + expect(state.layout.bottomPanelHeight).toBe(0); + expect(state.layout.rightPanelWidth).toBe(0); + expect(state.layout.recentVisibleSizes.bottomPanelHeight).toBe(300); + expect(state.layout.recentVisibleSizes.rightPanelWidth).toBe(400); + }); }); describe('state getters', () => { diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index 31af4e84b326..ef83aae3ad62 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -2,7 +2,7 @@ import type { FC, PropsWithChildren, ReactElement, ReactNode } from 'react'; import type { RenderData as RouterData } from '../../router/types.ts'; import type { ThemeVars } from '../../theming/types.ts'; -import type { API_LayoutCustomisations, API_SidebarOptions } from './api.ts'; +import type { API_LayoutCustomisations, API_LayoutOptions, API_SidebarOptions } from './api.ts'; import type { API_HashEntry, API_StoryEntry } from './api-stories.ts'; import type { Args, @@ -479,6 +479,7 @@ export interface Addon_ToolbarConfig { } export interface Addon_Config { theme?: ThemeVars; + layout?: API_LayoutOptions; layoutCustomisations?: { showPanel?: API_LayoutCustomisations['showPanel']; showSidebar?: API_LayoutCustomisations['showSidebar']; diff --git a/code/core/src/types/modules/api.ts b/code/core/src/types/modules/api.ts index 0e94d96699b6..75e0916502d9 100644 --- a/code/core/src/types/modules/api.ts +++ b/code/core/src/types/modules/api.ts @@ -95,6 +95,11 @@ export interface API_Layout { showToolbar: boolean; } +export interface API_LayoutOptions extends Partial { + showPanel?: boolean; + showSidebar?: boolean; +} + export interface API_LayoutCustomisations { showPanel?: (state: State, defaultValue: boolean) => boolean | undefined; showSidebar?: (state: State, defaultValue: boolean) => boolean | undefined; From 39a9d4bab5781f02e2307dc7f2e5e4a3dce40d69 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Wed, 20 May 2026 08:38:58 +0200 Subject: [PATCH 2/5] docs(layout): clarify applyLayoutOptions intent Add jsdoc explaining the capture-after-merge ordering of recentVisibleSizes, plus inline notes on the unknown-key safety net and the singleStory nav guard. Co-Authored-By: Claude Opus 4.7 (1M context) --- code/core/src/manager-api/modules/layout.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/code/core/src/manager-api/modules/layout.ts b/code/core/src/manager-api/modules/layout.ts index 23a444fa8837..b33faecf6dc5 100644 --- a/code/core/src/manager-api/modules/layout.ts +++ b/code/core/src/manager-api/modules/layout.ts @@ -193,6 +193,15 @@ const getRecentVisibleSizes = (layoutState: API_Layout) => { }; }; +/** + * Merges layout options into the existing layout state and translates the + * `showSidebar` / `showPanel` booleans into the underlying size fields. + * + * Numeric sizes from `options` are merged in first, so `recentVisibleSizes` is + * captured *after* that merge — meaning if a caller passes both a size and + * `show*: false` in the same payload, the new size is what we remember for + * later restoration via `togglePanel(true)` / `toggleNav(true)`. + */ const applyLayoutOptions = ( layoutState: API_Layout, options: API_LayoutOptions | undefined, @@ -200,6 +209,7 @@ const applyLayoutOptions = ( ) => { const { showPanel, showSidebar, ...layoutOptions } = options ?? {}; const layoutKeys = Object.keys(layoutState) as (keyof API_Layout)[]; + // Safety net: drop any unknown keys that aren't part of API_Layout. const nextLayoutState = toMerged(layoutState, pick(layoutOptions, layoutKeys)) as API_Layout; if (showSidebar === false) { @@ -218,6 +228,7 @@ const applyLayoutOptions = ( nextLayoutState.rightPanelWidth = nextLayoutState.recentVisibleSizes.rightPanelWidth; } + // singleStory always hides the sidebar regardless of the showSidebar option. if (singleStory) { nextLayoutState.navSize = 0; } From 0ca848a714298b74f532c93828ac20eaf0f87edb Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Wed, 20 May 2026 08:56:38 +0200 Subject: [PATCH 3/5] refactor(layout): tighten applyLayoutOptions and API_LayoutOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fold the `singleStory` nav guard into the `showSidebar === false` branch so the function ends on a single return; equivalent behavior, easier to read. - Add the missing `showPanel: undefined` entry to the default `layoutCustomisations` so it matches `API_LayoutCustomisations`. - Omit `recentVisibleSizes` from `API_LayoutOptions` — it is internal restoration bookkeeping and not something callers should set via `addons.setConfig`. - Note in `API_LayoutOptions` that `showToolbar` already comes from `Partial` and is intentionally not redeclared. Co-Authored-By: Claude Opus 4.7 (1M context) --- code/core/src/manager-api/modules/layout.ts | 11 ++++------- code/core/src/types/modules/api.ts | 4 +++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/code/core/src/manager-api/modules/layout.ts b/code/core/src/manager-api/modules/layout.ts index b33faecf6dc5..1d1deed17548 100644 --- a/code/core/src/manager-api/modules/layout.ts +++ b/code/core/src/manager-api/modules/layout.ts @@ -146,6 +146,7 @@ export const getDefaultLayoutState: () => SubState = () => { showTabs: true, }, layoutCustomisations: { + showPanel: undefined, showSidebar: undefined, showToolbar: undefined, }, @@ -212,10 +213,11 @@ const applyLayoutOptions = ( // Safety net: drop any unknown keys that aren't part of API_Layout. const nextLayoutState = toMerged(layoutState, pick(layoutOptions, layoutKeys)) as API_Layout; - if (showSidebar === false) { + // singleStory always hides the sidebar; otherwise honor showSidebar. + if (showSidebar === false || singleStory) { nextLayoutState.recentVisibleSizes = getRecentVisibleSizes(nextLayoutState); nextLayoutState.navSize = 0; - } else if (showSidebar === true && !singleStory) { + } else if (showSidebar === true) { nextLayoutState.navSize = nextLayoutState.recentVisibleSizes.navSize; } @@ -228,11 +230,6 @@ const applyLayoutOptions = ( nextLayoutState.rightPanelWidth = nextLayoutState.recentVisibleSizes.rightPanelWidth; } - // singleStory always hides the sidebar regardless of the showSidebar option. - if (singleStory) { - nextLayoutState.navSize = 0; - } - return nextLayoutState; }; diff --git a/code/core/src/types/modules/api.ts b/code/core/src/types/modules/api.ts index 75e0916502d9..83edd4dcbf3e 100644 --- a/code/core/src/types/modules/api.ts +++ b/code/core/src/types/modules/api.ts @@ -95,9 +95,11 @@ export interface API_Layout { showToolbar: boolean; } -export interface API_LayoutOptions extends Partial { +export interface API_LayoutOptions extends Omit, 'recentVisibleSizes'> { showPanel?: boolean; showSidebar?: boolean; + // Note: `showToolbar` is intentionally not declared here — it already comes + // from Partial as the underlying layout field. } export interface API_LayoutCustomisations { From 503ed0ce90553c4666a2646980c792275ded10f9 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Wed, 20 May 2026 09:48:31 +0200 Subject: [PATCH 4/5] fix(layout): revert API_LayoutOptions Omit that broke dts build The `Omit, 'recentVisibleSizes'>` introduced in the previous commit breaks the rollup-plugin-dts build of core because `applyLayoutOptions` calls `pick(layoutOptions, layoutKeys)` where `layoutKeys` is typed as `(keyof API_Layout)[]` (includes `recentVisibleSizes`) but the destructured `layoutOptions` no longer accepts it. Reverting to `extends Partial` restores the type compatibility. The `recentVisibleSizes` field is internal restoration bookkeeping and practically no caller sets it via `addons.setConfig`, so the type-tightening was nice-to-have and not worth the breakage. Co-Authored-By: Claude Opus 4.7 (1M context) --- code/core/src/types/modules/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/core/src/types/modules/api.ts b/code/core/src/types/modules/api.ts index 83edd4dcbf3e..51d9f5eb7739 100644 --- a/code/core/src/types/modules/api.ts +++ b/code/core/src/types/modules/api.ts @@ -95,7 +95,7 @@ export interface API_Layout { showToolbar: boolean; } -export interface API_LayoutOptions extends Omit, 'recentVisibleSizes'> { +export interface API_LayoutOptions extends Partial { showPanel?: boolean; showSidebar?: boolean; // Note: `showToolbar` is intentionally not declared here — it already comes From 02b976bb93a6dfd2332bfd3d0b5722544cf89182 Mon Sep 17 00:00:00 2001 From: Steve Dodier-Lazaro Date: Thu, 28 May 2026 17:04:56 +0200 Subject: [PATCH 5/5] Finalise fixes to layout state management --- code/core/src/manager-api/modules/layout.ts | 96 +++++++---- .../core/src/manager-api/tests/layout.test.ts | 163 +++++++++++++++++- code/core/src/types/modules/addons.ts | 11 +- code/core/src/types/modules/api.ts | 23 +-- 4 files changed, 227 insertions(+), 66 deletions(-) diff --git a/code/core/src/manager-api/modules/layout.ts b/code/core/src/manager-api/modules/layout.ts index 1d1deed17548..5fdaf61d99b3 100644 --- a/code/core/src/manager-api/modules/layout.ts +++ b/code/core/src/manager-api/modules/layout.ts @@ -2,7 +2,6 @@ import { SET_CONFIG } from 'storybook/internal/core-events'; import type { API_Layout, API_LayoutCustomisations, - API_LayoutOptions, API_PanelPositions, API_UI, } from 'storybook/internal/types'; @@ -12,6 +11,7 @@ import { global } from '@storybook/global'; import { pick, toMerged } from 'es-toolkit/object'; import { isEqual as deepEqual } from 'es-toolkit/predicate'; import type { ThemeVars } from 'storybook/theming'; +import { deprecate } from 'storybook/internal/client-logger'; import { create } from 'storybook/theming/create'; import merge from '../lib/merge.ts'; @@ -133,7 +133,6 @@ export const getDefaultLayoutState: () => SubState = () => { }, layout: { initialActive: ActiveTabs.CANVAS, - showToolbar: true, navSize: DEFAULT_NAV_SIZE, bottomPanelHeight: DEFAULT_BOTTOM_PANEL_HEIGHT, rightPanelWidth: DEFAULT_RIGHT_PANEL_WIDTH, @@ -143,7 +142,10 @@ export const getDefaultLayoutState: () => SubState = () => { rightPanelWidth: DEFAULT_RIGHT_PANEL_WIDTH, }, panelPosition: 'bottom', + showNav: true, + showPanel: true, showTabs: true, + showToolbar: true, }, layoutCustomisations: { showPanel: undefined, @@ -195,29 +197,41 @@ const getRecentVisibleSizes = (layoutState: API_Layout) => { }; /** - * Merges layout options into the existing layout state and translates the - * `showSidebar` / `showPanel` booleans into the underlying size fields. + * Merges layout options into the existing layout state and translates + * `showNav` / `showPanel` booleans into the underlying size fields. + * + * Layout keys can be provided either at the top level (deprecated) or under + * `options.layout` (preferred). Nested layout keys take precedence. * - * Numeric sizes from `options` are merged in first, so `recentVisibleSizes` is - * captured *after* that merge — meaning if a caller passes both a size and - * `show*: false` in the same payload, the new size is what we remember for - * later restoration via `togglePanel(true)` / `toggleNav(true)`. + * Numeric sizes are merged in before applying show/hide flags, so + * `recentVisibleSizes` is captured from the latest size values. */ const applyLayoutOptions = ( layoutState: API_Layout, - options: API_LayoutOptions | undefined, + options: { layout?: Partial; [key: string]: any }, singleStory: boolean ) => { - const { showPanel, showSidebar, ...layoutOptions } = options ?? {}; - const layoutKeys = Object.keys(layoutState) as (keyof API_Layout)[]; + const layoutKeys = Object.keys(layoutState); + const layoutAtTopLevel = pick(options, layoutKeys); + + for (const key of Object.keys(layoutAtTopLevel)) { + deprecate( + `Calling \`setConfig({ ${key}: ... })\` is deprecated. Please call \`setConfig({ layout: { ${key}: ... } })\` instead.` + ); + } + + const mergedLayoutOptions = toMerged(layoutAtTopLevel, options.layout || {}); + const { showPanel, showNav } = mergedLayoutOptions; + // Safety net: drop any unknown keys that aren't part of API_Layout. - const nextLayoutState = toMerged(layoutState, pick(layoutOptions, layoutKeys)) as API_Layout; + const typedLayoutKeys = layoutKeys as (keyof API_Layout)[]; + const nextLayoutState = toMerged(layoutState, pick(mergedLayoutOptions, typedLayoutKeys)); // singleStory always hides the sidebar; otherwise honor showSidebar. - if (showSidebar === false || singleStory) { + if (showNav === false || singleStory) { nextLayoutState.recentVisibleSizes = getRecentVisibleSizes(nextLayoutState); nextLayoutState.navSize = 0; - } else if (showSidebar === true) { + } else if (showNav === true) { nextLayoutState.navSize = nextLayoutState.recentVisibleSizes.navSize; } @@ -233,6 +247,30 @@ const applyLayoutOptions = ( return nextLayoutState; }; +/** + * Merges ui options into the existing ui state. + * + * Ui keys can be provided either at the top level (deprecated) or under + * `options.ui` (preferred). Nested ui keys take precedence. + * + * Numeric sizes are merged in before applying show/hide flags, so + * `recentVisibleSizes` is captured from the latest size values. + */ +const applyUiOptions = (uiState: API_UI, options: { ui?: Partial; [key: string]: any }) => { + const uiKeys = Object.keys(uiState); + const uiAtTopLevel = pick(options, uiKeys); + + for (const key of Object.keys(uiAtTopLevel)) { + deprecate( + `Calling \`setConfig({ ${key}: ... })\` is deprecated. Please call \`setConfig({ ui: { ${key}: ... } })\` instead.` + ); + } + + // Safety net: drop any unknown keys that aren't part of API_UI. + const typedUiKeys = uiKeys as (keyof API_UI)[]; + return toMerged(uiState, pick(toMerged(uiAtTopLevel, options.ui || {}), typedUiKeys)); +}; + export const init: ModuleFn = ({ store, provider, singleStory }) => { const api = { toggleFullscreen(nextState?: boolean) { @@ -480,24 +518,19 @@ export const init: ModuleFn = ({ store, provider, singleStory }, getInitialOptions() { - const { theme, selectedPanel, layoutCustomisations, ...options } = provider.getConfig(); + const userConfig = provider.getConfig(); const defaultLayoutState = getDefaultLayoutState(); + const { theme, selectedPanel, layoutCustomisations } = userConfig; + return { ...defaultLayoutState, - layout: applyLayoutOptions( - defaultLayoutState.layout, - { - ...options.layout, - ...pick(options, Object.keys(defaultLayoutState.layout)), - }, - !!singleStory - ), + layout: applyLayoutOptions(defaultLayoutState.layout, userConfig, !!singleStory), layoutCustomisations: { ...defaultLayoutState.layoutCustomisations, ...(layoutCustomisations ?? {}), }, - ui: toMerged(defaultLayoutState.ui, pick(options, Object.keys(defaultLayoutState.ui))), + ui: applyUiOptions(defaultLayoutState.ui, userConfig), selectedPanel: selectedPanel || defaultLayoutState.selectedPanel, theme: theme || defaultLayoutState.theme, }; @@ -555,20 +588,9 @@ export const init: ModuleFn = ({ store, provider, singleStory return; } - const updatedLayout = applyLayoutOptions( - layout, - { - ...options.layout, - ...pick(options, Object.keys(layout)), - }, - !!singleStory - ); + const updatedLayout = applyLayoutOptions(layout, options, !!singleStory); - const updatedUi = { - ...ui, - ...options.ui, - ...toMerged(options.ui || {}, pick(options, Object.keys(ui))), - }; + const updatedUi = applyUiOptions(ui, options); const updatedTheme = { ...theme, diff --git a/code/core/src/manager-api/tests/layout.test.ts b/code/core/src/manager-api/tests/layout.test.ts index 73b2c5526c78..f8c8c0b45e56 100644 --- a/code/core/src/manager-api/tests/layout.test.ts +++ b/code/core/src/manager-api/tests/layout.test.ts @@ -2,6 +2,7 @@ import type { Mock } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { API_Provider } from 'storybook/internal/types'; +import * as clientLogger from 'storybook/internal/client-logger'; import EventEmitter from 'events'; import { themes } from 'storybook/theming'; @@ -450,7 +451,7 @@ describe('layout API', () => { }); it('should not change selectedPanel if it is undefined in the options, but something else has changed', () => { - layoutApi.setOptions({ panelPosition: 'right' }); + layoutApi.setOptions({ layout: { panelPosition: 'right' } }); expect(getLastSetStateArgs()[0].selectedPanel).toBeUndefined(); }); @@ -467,7 +468,10 @@ describe('layout API', () => { it('should not change selectedPanel if it is currently the same, but something else has changed', () => { layoutApi.setOptions({}); // second call is needed to overwrite initial layout - layoutApi.setOptions({ panelPosition: 'right', selectedPanel: currentState.selectedPanel }); + layoutApi.setOptions({ + layout: { panelPosition: 'right' }, + selectedPanel: currentState.selectedPanel, + }); expect(getLastSetStateArgs()[0].selectedPanel).toBeUndefined(); }); @@ -505,6 +509,80 @@ describe('layout API', () => { expect(currentState.layout.bottomPanelHeight).toBe(200); expect(currentState.layout.rightPanelWidth).toBe(250); }); + + it('should hide nav and preserve provided navSize when layout.showNav is false', () => { + layoutApi.setOptions({ layout: { navSize: 180, showNav: false } }); + + expect(currentState.layout.navSize).toBe(0); + expect(currentState.layout.recentVisibleSizes.navSize).toBe(180); + + layoutApi.toggleNav(true); + + expect(currentState.layout.navSize).toBe(180); + }); + + it('should hide panel and preserve provided sizes when layout.showPanel is false', () => { + layoutApi.setOptions({ + layout: { bottomPanelHeight: 210, rightPanelWidth: 260, showPanel: false }, + }); + + expect(currentState.layout.bottomPanelHeight).toBe(0); + expect(currentState.layout.rightPanelWidth).toBe(0); + expect(currentState.layout.recentVisibleSizes.bottomPanelHeight).toBe(210); + expect(currentState.layout.recentVisibleSizes.rightPanelWidth).toBe(260); + + layoutApi.togglePanel(true); + + expect(currentState.layout.bottomPanelHeight).toBe(210); + expect(currentState.layout.rightPanelWidth).toBe(260); + }); + + it('should prioritize options.layout over top-level layout keys', () => { + const deprecateSpy = vi.spyOn(clientLogger, 'deprecate').mockImplementation(() => {}); + + layoutApi.setOptions({ + showNav: true, + showPanel: true, + layout: { showNav: false, showPanel: false }, + }); + + expect(currentState.layout.navSize).toBe(0); + expect(currentState.layout.bottomPanelHeight).toBe(0); + expect(currentState.layout.rightPanelWidth).toBe(0); + expect(deprecateSpy).toHaveBeenCalled(); + }); + + it('should deprecate top-level layout keys in setOptions', () => { + const deprecateSpy = vi.spyOn(clientLogger, 'deprecate').mockImplementation(() => {}); + + layoutApi.setOptions({ showNav: false, panelPosition: 'right' }); + + expect(deprecateSpy).toHaveBeenCalledWith( + 'Calling `setConfig({ showNav: ... })` is deprecated. Please call `setConfig({ layout: { showNav: ... } })` instead.' + ); + expect(deprecateSpy).toHaveBeenCalledWith( + 'Calling `setConfig({ panelPosition: ... })` is deprecated. Please call `setConfig({ layout: { panelPosition: ... } })` instead.' + ); + }); + + it('should prioritize options.ui over top-level ui keys', () => { + layoutApi.setOptions({ + enableShortcuts: false, + ui: { enableShortcuts: true }, + }); + + expect(currentState.ui.enableShortcuts).toBe(true); + }); + + it('should deprecate top-level ui keys in setOptions', () => { + const deprecateSpy = vi.spyOn(clientLogger, 'deprecate').mockImplementation(() => {}); + + layoutApi.setOptions({ enableShortcuts: false }); + + expect(deprecateSpy).toHaveBeenCalledWith( + 'Calling `setConfig({ enableShortcuts: ... })` is deprecated. Please call `setConfig({ ui: { enableShortcuts: ... } })` instead.' + ); + }); }); describe('getInitialOptions', () => { @@ -513,8 +591,13 @@ describe('layout API', () => { layout: { showPanel: false }, }); + const storeWithoutPersistedLayout = { + ...store, + getState: () => ({ selectedPanel: currentState.selectedPanel }) as unknown as State, + } as unknown as Store; + const { state } = initLayout({ - store, + store: storeWithoutPersistedLayout, provider, singleStory: false, } as unknown as ModuleArgs); @@ -524,6 +607,80 @@ describe('layout API', () => { expect(state.layout.recentVisibleSizes.bottomPanelHeight).toBe(300); expect(state.layout.recentVisibleSizes.rightPanelWidth).toBe(400); }); + + it('should apply layout.showNav from the initial config', () => { + (provider.getConfig as Mock).mockReturnValue({ + layout: { showNav: false }, + }); + + const storeWithoutPersistedLayout = { + ...store, + getState: () => ({ selectedPanel: currentState.selectedPanel }) as unknown as State, + } as unknown as Store; + + const { state } = initLayout({ + store: storeWithoutPersistedLayout, + provider, + singleStory: false, + } as unknown as ModuleArgs); + + expect(state.layout.navSize).toBe(0); + expect(state.layout.recentVisibleSizes.navSize).toBe(300); + }); + + it('should prioritize layout over top-level config keys', () => { + const deprecateSpy = vi.spyOn(clientLogger, 'deprecate').mockImplementation(() => {}); + (provider.getConfig as Mock).mockReturnValue({ + showPanel: true, + showNav: true, + layout: { showPanel: false, showNav: false }, + }); + + const storeWithoutPersistedLayout = { + ...store, + getState: () => ({ selectedPanel: currentState.selectedPanel }) as unknown as State, + } as unknown as Store; + + const { state } = initLayout({ + store: storeWithoutPersistedLayout, + provider, + singleStory: false, + } as unknown as ModuleArgs); + + expect(state.layout.navSize).toBe(0); + expect(state.layout.bottomPanelHeight).toBe(0); + expect(state.layout.rightPanelWidth).toBe(0); + expect(deprecateSpy).toHaveBeenCalledWith( + 'Calling `setConfig({ showPanel: ... })` is deprecated. Please call `setConfig({ layout: { showPanel: ... } })` instead.' + ); + expect(deprecateSpy).toHaveBeenCalledWith( + 'Calling `setConfig({ showNav: ... })` is deprecated. Please call `setConfig({ layout: { showNav: ... } })` instead.' + ); + }); + + it('should prioritize ui over top-level config keys', () => { + const deprecateSpy = vi.spyOn(clientLogger, 'deprecate').mockImplementation(() => {}); + (provider.getConfig as Mock).mockReturnValue({ + enableShortcuts: false, + ui: { enableShortcuts: true }, + }); + + const storeWithoutPersistedLayout = { + ...store, + getState: () => ({ selectedPanel: currentState.selectedPanel }) as unknown as State, + } as unknown as Store; + + const { state } = initLayout({ + store: storeWithoutPersistedLayout, + provider, + singleStory: false, + } as unknown as ModuleArgs); + + expect(state.ui.enableShortcuts).toBe(true); + expect(deprecateSpy).toHaveBeenCalledWith( + 'Calling `setConfig({ enableShortcuts: ... })` is deprecated. Please call `setConfig({ ui: { enableShortcuts: ... } })` instead.' + ); + }); }); describe('state getters', () => { diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index ef83aae3ad62..c2abc0a134c1 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -2,7 +2,7 @@ import type { FC, PropsWithChildren, ReactElement, ReactNode } from 'react'; import type { RenderData as RouterData } from '../../router/types.ts'; import type { ThemeVars } from '../../theming/types.ts'; -import type { API_LayoutCustomisations, API_LayoutOptions, API_SidebarOptions } from './api.ts'; +import type { API_Layout, API_LayoutCustomisations, API_SidebarOptions, API_UI } from './api.ts'; import type { API_HashEntry, API_StoryEntry } from './api-stories.ts'; import type { Args, @@ -479,16 +479,13 @@ export interface Addon_ToolbarConfig { } export interface Addon_Config { theme?: ThemeVars; - layout?: API_LayoutOptions; - layoutCustomisations?: { - showPanel?: API_LayoutCustomisations['showPanel']; - showSidebar?: API_LayoutCustomisations['showSidebar']; - showToolbar?: API_LayoutCustomisations['showToolbar']; - }; + layout?: Partial; + layoutCustomisations?: Partial; toolbar?: { [id: string]: Addon_ToolbarConfig; }; sidebar?: API_SidebarOptions; + ui?: Partial; [key: string]: any; } diff --git a/code/core/src/types/modules/api.ts b/code/core/src/types/modules/api.ts index 51d9f5eb7739..2efb562885be 100644 --- a/code/core/src/types/modules/api.ts +++ b/code/core/src/types/modules/api.ts @@ -48,9 +48,10 @@ export interface API_Provider { getConfig(): { sidebar?: API_SidebarOptions; theme?: ThemeVars; + selectedPanel?: string; StoryMapper?: API_StoryMapper; [k: string]: any; - } & Partial; + }; [key: string]: any; } @@ -63,17 +64,6 @@ export type API_IframeRenderer = ( queryParams: Record ) => ReactElement | null; -export interface API_UIOptions { - name?: string; - url?: string; - goFullScreen: boolean; - showStoriesPanel: boolean; - showAddonPanel: boolean; - addonPanelInRight: boolean; - theme?: ThemeVars; - selectedPanel?: string; -} - export type FilterFunction = (entry: API_PreparedIndexEntry, excluded?: boolean) => boolean; export interface API_Layout { @@ -91,17 +81,12 @@ export interface API_Layout { rightPanelWidth: number; }; panelPosition: API_PanelPositions; + showNav: boolean; + showPanel: boolean; showTabs: boolean; showToolbar: boolean; } -export interface API_LayoutOptions extends Partial { - showPanel?: boolean; - showSidebar?: boolean; - // Note: `showToolbar` is intentionally not declared here — it already comes - // from Partial as the underlying layout field. -} - export interface API_LayoutCustomisations { showPanel?: (state: State, defaultValue: boolean) => boolean | undefined; showSidebar?: (state: State, defaultValue: boolean) => boolean | undefined;