From e592b210c1f439cb2ea68902c519b8a2cea246e3 Mon Sep 17 00:00:00 2001 From: Sebastien Powell Date: Mon, 27 Jul 2026 12:50:28 +0100 Subject: [PATCH] feat(core): Allow Storybook components to be viewed in light and dark mode (#34361) --- .../AiModelSelectorDropdown.stories.ts | 42 +++- .../N8nChatInput/ChatInput.stories.ts | 25 +- .../FloatingWindow.stories.ts | 95 ++++--- .../KeyboardShortcut.stories.ts | 6 +- .../MarkdownEditor.stories.ts | 194 ++++++++------- .../SelectableList.stories.ts | 18 +- .../components/N8nSticky/Sticky.stories.ts | 231 +++++++++++------- .../N8nUserStack/UserStack.stories.ts | 7 +- .../@n8n/design-system/src/css/_tokens.scss | 5 +- .../@n8n/design-system/src/css/markdown.scss | 2 +- .../@n8n/storybook/.storybook/main.ts | 1 - .../@n8n/storybook/.storybook/modes.ts | 8 - .../@n8n/storybook/.storybook/preview.ts | 53 +--- .../@n8n/storybook/.storybook/storybook.scss | 19 ++ .../storybook/.storybook/withThemePreview.ts | 82 +++++++ packages/frontend/@n8n/storybook/package.json | 1 - pnpm-lock.yaml | 16 -- pnpm-workspace.yaml | 1 - 18 files changed, 481 insertions(+), 325 deletions(-) delete mode 100644 packages/frontend/@n8n/storybook/.storybook/modes.ts create mode 100644 packages/frontend/@n8n/storybook/.storybook/withThemePreview.ts diff --git a/packages/frontend/@n8n/design-system/src/components/N8nAiModelSelectorDropdown/AiModelSelectorDropdown.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nAiModelSelectorDropdown/AiModelSelectorDropdown.stories.ts index a7dd39c2eb5..b85495de477 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nAiModelSelectorDropdown/AiModelSelectorDropdown.stories.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nAiModelSelectorDropdown/AiModelSelectorDropdown.stories.ts @@ -39,6 +39,23 @@ function findItemByLabel( return undefined; } +function getProviderLogoStyle(logoUrl: string) { + return { + width: '20px', + height: '20px', + borderRadius: '999px', + backgroundColor: 'var(--icon-color)', + maskImage: `url(${logoUrl})`, + WebkitMaskImage: `url(${logoUrl})`, + maskSize: 'contain', + WebkitMaskSize: 'contain', + maskRepeat: 'no-repeat', + WebkitMaskRepeat: 'no-repeat', + maskPosition: 'center', + WebkitMaskPosition: 'center', + }; +} + const meta = { title: 'AI/AiModelSelectorDropdown', component: N8nAiModelSelectorDropdown, @@ -94,25 +111,32 @@ const meta = { }, ); - return { storyArgs, selectedProviderLogo, getProviderLogo, handleSearch, handleSelect }; + return { + storyArgs, + selectedProviderLogo, + getProviderLogo, + getProviderLogoStyle, + handleSearch, + handleSelect, + }; }, template: ` diff --git a/packages/frontend/@n8n/design-system/src/components/N8nChatInput/ChatInput.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nChatInput/ChatInput.stories.ts index 9bf271ec563..95a2d731420 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nChatInput/ChatInput.stories.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nChatInput/ChatInput.stories.ts @@ -386,7 +386,7 @@ const InteractiveTemplate: StoryFn = (args, { argTypes }) => ({ }, template: `
-
+
({ @blur="onBlur" />
-
+

Current value: {{ val }}

Character count: {{ val.length }} / {{ args.maxLength }}

Streaming: {{ streaming }}

-
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nFloatingWindow/FloatingWindow.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nFloatingWindow/FloatingWindow.stories.ts index 4bddbbd8f62..38d20d44b02 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nFloatingWindow/FloatingWindow.stories.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nFloatingWindow/FloatingWindow.stories.ts @@ -1,10 +1,10 @@ -import type { StoryFn } from '@storybook/vue3-vite'; +import type { Meta, StoryObj } from '@storybook/vue3-vite'; import { action } from 'storybook/actions'; import N8nFloatingWindow from './FloatingWindow.vue'; import N8nIcon from '../N8nIcon'; -export default { +const meta: Meta = { title: 'Core/FloatingWindow', component: N8nFloatingWindow, argTypes: { @@ -31,40 +31,63 @@ export default { iframeHeight: 500, }, }, + themePreview: { + minHeight: 520, + }, }, -}; - -const Template: StoryFn = (args, { argTypes }) => ({ - setup: () => ({ - args, - onClose: action('close'), - onResize: action('resize'), - onMove: action('move'), + render: (args) => ({ + components: { + N8nFloatingWindow, + N8nIcon, + }, + setup: () => ({ + args, + onClose: action('close'), + onResize: action('resize'), + onMove: action('move'), + }), + template: ` +
+ + + + +
+

This is the default slot content. The window can be dragged by its header and resized from any edge or corner.

+
+
+
+ `, }), - props: Object.keys(argTypes), - components: { - N8nFloatingWindow, - N8nIcon, - }, - template: ` - - - -
-

This is the default slot content. The window can be dragged by its header and resized from any edge or corner.

-
-
`, -}); - -export const Default = Template.bind({}); -Default.args = { - width: 560, - height: 400, - minWidth: 400, - minHeight: 300, - initialPosition: { x: 0, y: 0 }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + width: 360, + height: 280, + minWidth: 240, + minHeight: 180, + initialPosition: { x: 16, y: 16 }, + }, }; diff --git a/packages/frontend/@n8n/design-system/src/components/N8nKeyboardShortcut/KeyboardShortcut.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nKeyboardShortcut/KeyboardShortcut.stories.ts index 659f5595af8..0aa7757db07 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nKeyboardShortcut/KeyboardShortcut.stories.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nKeyboardShortcut/KeyboardShortcut.stories.ts @@ -1,10 +1,10 @@ import type { StoryFn } from '@storybook/vue3-vite'; -import N8nKeyboardShorcut from './N8nKeyboardShortcut.vue'; +import N8nKeyboardShortcut from './N8nKeyboardShortcut.vue'; export default { title: 'Core/KeyboardShortcut', - component: N8nKeyboardShorcut, + component: N8nKeyboardShortcut, parameters: { docs: { @@ -17,7 +17,7 @@ const template: StoryFn = (args, { argTypes }) => ({ setup: () => ({ args }), props: Object.keys(argTypes), components: { - N8nKeyboardShorcut, + N8nKeyboardShortcut, }, template: '', }); diff --git a/packages/frontend/@n8n/design-system/src/components/N8nMarkdownEditor/MarkdownEditor.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nMarkdownEditor/MarkdownEditor.stories.ts index e9d8cc8f966..b033a638fb3 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nMarkdownEditor/MarkdownEditor.stories.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nMarkdownEditor/MarkdownEditor.stories.ts @@ -1,5 +1,6 @@ -import type { StoryFn } from '@storybook/vue3-vite'; +import type { Meta, StoryObj } from '@storybook/vue3-vite'; import { action } from 'storybook/actions'; +import { computed, ref } from 'vue'; import N8nMarkdownEditor from './MarkdownEditor.vue'; @@ -63,15 +64,7 @@ Escalate to a human when: When a workflow error is provided, identify the failing node, summarize the likely cause, and suggest the smallest next diagnostic step.`; -const methods = { - onUpdateModelValue: action('update:modelValue'), - onInput: action('input'), - onFocus: action('focus'), - onBlur: action('blur'), - onReady: action('ready'), -}; - -export default { +const meta: Meta = { title: 'Core/Markdown Editor', component: N8nMarkdownEditor, argTypes: { @@ -107,108 +100,123 @@ export default { }, }, }, -}; + render: (args) => ({ + components: { N8nMarkdownEditor }, + setup() { + const value = ref(args.modelValue); + const editorArgs = computed(() => { + const { modelValue: _modelValue, ...rest } = args; + return rest; + }); -const Template: StoryFn = (args, { argTypes }) => ({ - setup: () => ({ args }), - props: Object.keys(argTypes), - components: { - N8nMarkdownEditor, - }, - data() { - return { - value: args.modelValue, - }; - }, - watch: { - 'args.modelValue'(value: string) { - this.value = value; + return { + value, + editorArgs, + onUpdateModelValue: action('update:modelValue'), + onInput: action('input'), + onFocus: action('focus'), + onBlur: action('blur'), + onReady: action('ready'), + }; }, + template: ` +
+ +
+ `, + }), +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + modelValue: defaultMarkdown, + variant: 'contained', + placeholder: 'Write Markdown...', + showToolbar: 'always', + maxHeight: '480px', + disabled: false, + readonly: false, }, - template: ` -
- -
- `, - methods, -}); - -export const Default = Template.bind({}); -Default.args = { - modelValue: defaultMarkdown, - variant: 'contained', - placeholder: 'Write Markdown...', - showToolbar: 'always', - maxHeight: '480px', - disabled: false, - readonly: false, }; -export const Contained = Template.bind({}); -Contained.args = { - ...Default.args, - variant: 'contained', +export const Contained: Story = { + args: { + ...Default.args, + variant: 'contained', + }, }; -export const Ghost = Template.bind({}); -Ghost.args = { - ...Default.args, - variant: 'ghost', +export const Ghost: Story = { + args: { + ...Default.args, + variant: 'ghost', + }, }; -export const WithoutToolbar = Template.bind({}); -WithoutToolbar.args = { - ...Default.args, - showToolbar: 'never', +export const WithoutToolbar: Story = { + args: { + ...Default.args, + showToolbar: 'never', + }, }; -export const AlwaysVisibleToolbar = Template.bind({}); -AlwaysVisibleToolbar.args = { - ...Default.args, - showToolbar: 'always', +export const AlwaysVisibleToolbar: Story = { + args: { + ...Default.args, + showToolbar: 'always', + }, }; -export const GfmContent = Template.bind({}); -GfmContent.args = { - ...Default.args, - modelValue: gfmMarkdown, - variant: 'contained', +export const GfmContent: Story = { + args: { + ...Default.args, + modelValue: gfmMarkdown, + variant: 'contained', + }, }; -export const Disabled = Template.bind({}); -Disabled.args = { - ...Default.args, - disabled: true, - variant: 'contained', +export const Disabled: Story = { + args: { + ...Default.args, + disabled: true, + variant: 'contained', + }, }; -export const Readonly = Template.bind({}); -Readonly.args = { - ...Default.args, - readonly: true, - variant: 'contained', +export const Readonly: Story = { + args: { + ...Default.args, + readonly: true, + variant: 'contained', + }, }; -export const LongInstructions = Template.bind({}); -LongInstructions.args = { - ...Default.args, - modelValue: longInstructionsMarkdown, - variant: 'contained', +export const LongInstructions: Story = { + args: { + ...Default.args, + modelValue: longInstructionsMarkdown, + variant: 'contained', + }, }; -export const Empty = Template.bind({}); -Empty.args = { - ...Default.args, - modelValue: '', - showToolbar: 'never', - variant: 'contained', - placeholder: 'Write instructions...', +export const Empty: Story = { + args: { + ...Default.args, + modelValue: '', + showToolbar: 'never', + variant: 'contained', + placeholder: 'Write instructions...', + }, }; diff --git a/packages/frontend/@n8n/design-system/src/components/N8nSelectableList/SelectableList.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nSelectableList/SelectableList.stories.ts index e40f9caf110..5c792c8a224 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nSelectableList/SelectableList.stories.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nSelectableList/SelectableList.stories.ts @@ -1,4 +1,6 @@ import type { StoryFn } from '@storybook/vue3-vite'; +import type { Component } from 'vue'; +import { ref } from 'vue'; import N8nSelectableList from './SelectableList.vue'; @@ -14,14 +16,14 @@ export default { }, }; -const Template: StoryFn = (args, { argTypes }) => ({ - setup: () => ({ - args: { ...args, modelValue: undefined }, - model: args.modelValue, - }), - props: Object.keys(argTypes), - // Generics make this difficult to type - components: N8nSelectableList as never, +const Template: StoryFn = (args) => ({ + setup: () => { + const model = ref(args.modelValue); + return { args, model }; + }, + components: { + N8nSelectableList: N8nSelectableList as unknown as Component, + }, template: '', }); diff --git a/packages/frontend/@n8n/design-system/src/components/N8nSticky/Sticky.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nSticky/Sticky.stories.ts index 7883f8b4e6e..1e4c5e88650 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nSticky/Sticky.stories.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nSticky/Sticky.stories.ts @@ -1,11 +1,11 @@ -import type { StoryFn } from '@storybook/vue3-vite'; +import type { Meta, StoryObj } from '@storybook/vue3-vite'; import { action } from 'storybook/actions'; -import { ref } from 'vue'; +import { computed, ref } from 'vue'; import N8nSticky from './Sticky.vue'; import N8nResizeWrapper from '../N8nResizeWrapper/ResizeWrapper.vue'; -export default { +const meta: Meta = { title: 'Core/Sticky', component: N8nSticky, argTypes: { @@ -17,27 +17,27 @@ export default { }, height: { control: { - control: 'number', + type: 'number', }, }, minHeight: { control: { - control: 'number', + type: 'number', }, }, minWidth: { control: { - control: 'number', + type: 'number', }, }, readOnly: { control: { - control: 'Boolean', + type: 'boolean', }, }, width: { control: { - control: 'number', + type: 'number', }, }, }, @@ -47,93 +47,144 @@ export default { component: 'An editable sticky-note component with markdown content and color variants.', }, }, + themePreview: { + minHeight: 240, + }, }, -}; + render: (args) => ({ + components: { N8nSticky }, + setup() { + const value = ref(args.modelValue); + const editMode = ref(false); + const stickyArgs = computed(() => { + const { modelValue: _modelValue, editMode: _editMode, ...rest } = args; + return rest; + }); -const methods = { - onEdit: action('edit'), - onMarkdownClick: action('markdown-click'), - onUpdateModelValue: action('update:modelValue'), - onResize: action('resize'), - onResizeEnd: action('resizeend'), - onResizeStart: action('resizestart'), -}; + const onEdit = (editing: boolean) => { + editMode.value = editing; + action('edit')(editing); + }; -const Template: StoryFn = (args, { argTypes }) => ({ - setup: () => ({ args }), - props: Object.keys(argTypes), - components: { - N8nSticky, - }, - template: ` - - `, - methods, -}); - -export const Sticky = Template.bind({}); -Sticky.args = { - height: 180, - width: 240, - modelValue: - "## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/components/sticky-notes/)", - minHeight: 80, - minWidth: 150, - readOnly: false, - backgroundColor: 1, -}; - -const ResizableTemplate: StoryFn = (args) => ({ - setup: () => { - const width = ref(args.width ?? 240); - const height = ref(args.height ?? 180); - - const onResize = (resizeData: { height: number; width: number }) => { - action('resize')(resizeData); - height.value = resizeData.height; - width.value = resizeData.width; - }; - - return { args, width, height, onResize }; - }, - components: { - N8nResizeWrapper, - N8nSticky, - }, - template: ` -
- + return { + value, + editMode, + stickyArgs, + onEdit, + onMarkdownClick: action('markdown-click'), + onUpdateModelValue: action('update:modelValue'), + }; + }, + template: ` +
- -
- `, - methods, -}); - -export const Resizable = ResizableTemplate.bind({}); -Resizable.args = { - ...Sticky.args, + /> +
+ `, + }), +}; + +export default meta; + +type Story = StoryObj; + +export const Sticky: Story = { + args: { + height: 180, + width: 240, + modelValue: + "## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/components/sticky-notes/)", + minHeight: 80, + minWidth: 150, + readOnly: false, + backgroundColor: 1, + }, +}; + +export const Resizable: Story = { + args: { + ...Sticky.args, + }, + render: (args) => ({ + components: { + N8nResizeWrapper, + N8nSticky, + }, + setup() { + const value = ref(args.modelValue); + const editMode = ref(false); + const width = ref(args.width ?? 240); + const height = ref(args.height ?? 180); + + const stickyArgs = computed(() => { + const { + modelValue: _modelValue, + editMode: _editMode, + width: _width, + height: _height, + ...rest + } = args; + return rest; + }); + + const onEdit = (editing: boolean) => { + editMode.value = editing; + action('edit')(editing); + }; + + const onResize = (resizeData: { height: number; width: number }) => { + action('resize')(resizeData); + height.value = resizeData.height; + width.value = resizeData.width; + }; + + return { + args, + value, + editMode, + width, + height, + stickyArgs, + onEdit, + onResize, + onMarkdownClick: action('markdown-click'), + onUpdateModelValue: action('update:modelValue'), + onResizeEnd: action('resizeend'), + onResizeStart: action('resizestart'), + }; + }, + template: ` +
+ + + +
+ `, + }), }; diff --git a/packages/frontend/@n8n/design-system/src/components/N8nUserStack/UserStack.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nUserStack/UserStack.stories.ts index 45fc17025ff..a031da4be61 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nUserStack/UserStack.stories.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nUserStack/UserStack.stories.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { StoryFn } from '@storybook/vue3-vite'; -import UserStack from './UserStack.vue'; +import N8nUserStack from './UserStack.vue'; export default { title: 'Core/UserStack', - component: UserStack, + component: N8nUserStack, parameters: { docs: { @@ -16,9 +16,8 @@ export default { const Template: StoryFn = (args) => ({ setup: () => ({ args }), - props: args, components: { - UserStack, + N8nUserStack, }, template: '', }); diff --git a/packages/frontend/@n8n/design-system/src/css/_tokens.scss b/packages/frontend/@n8n/design-system/src/css/_tokens.scss index e3ffafd69f1..61f5545477e 100644 --- a/packages/frontend/@n8n/design-system/src/css/_tokens.scss +++ b/packages/frontend/@n8n/design-system/src/css/_tokens.scss @@ -1245,11 +1245,12 @@ @include theme; } -body[data-theme='light'] { +[data-theme='light'] { + @include theme; color-scheme: light; } -body[data-theme='dark'] { +[data-theme='dark'] { @include theme-dark; color-scheme: dark; } diff --git a/packages/frontend/@n8n/design-system/src/css/markdown.scss b/packages/frontend/@n8n/design-system/src/css/markdown.scss index 8d4ab0f63de..662cf9811ad 100644 --- a/packages/frontend/@n8n/design-system/src/css/markdown.scss +++ b/packages/frontend/@n8n/design-system/src/css/markdown.scss @@ -6,7 +6,7 @@ @include meta.load-css('highlight.js/styles/github-dark-dimmed.css'); } -body[data-theme='dark'] { +[data-theme='dark'] { @include hljs-dark-theme; } diff --git a/packages/frontend/@n8n/storybook/.storybook/main.ts b/packages/frontend/@n8n/storybook/.storybook/main.ts index 50589c2404d..0b2d4aa4282 100644 --- a/packages/frontend/@n8n/storybook/.storybook/main.ts +++ b/packages/frontend/@n8n/storybook/.storybook/main.ts @@ -31,7 +31,6 @@ const config: StorybookConfig = { }, }, }, - getAbsolutePath('@storybook/addon-themes'), getAbsolutePath('storybook-addon-vue-mdx'), ], framework: getAbsolutePath('@storybook/vue3-vite'), diff --git a/packages/frontend/@n8n/storybook/.storybook/modes.ts b/packages/frontend/@n8n/storybook/.storybook/modes.ts deleted file mode 100644 index a8aab05956d..00000000000 --- a/packages/frontend/@n8n/storybook/.storybook/modes.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const allModes = { - dark: { - theme: 'dark', - }, - light: { - theme: 'light', - }, -} as const; diff --git a/packages/frontend/@n8n/storybook/.storybook/preview.ts b/packages/frontend/@n8n/storybook/.storybook/preview.ts index 0285933d2ca..8e988fe2621 100644 --- a/packages/frontend/@n8n/storybook/.storybook/preview.ts +++ b/packages/frontend/@n8n/storybook/.storybook/preview.ts @@ -1,7 +1,6 @@ import { IconBodyLoaderKey, N8nPlugin } from '@n8n/design-system'; import { loadLucideIconBody } from '@n8n/design-system/icons/lucide'; import { i18nInstance } from '@n8n/i18n'; -import { withThemeByDataAttribute } from '@storybook/addon-themes'; import { setup } from '@storybook/vue3'; import ElementPlus from 'element-plus'; // @ts-expect-error no types @@ -10,7 +9,7 @@ import { createPinia } from 'pinia'; import { createMemoryHistory, createRouter } from 'vue-router'; import './storybook.scss'; -import { allModes } from './modes'; +import { withThemePreview } from './withThemePreview'; // import '../src/css/tailwind/index.css'; setup((app) => { @@ -42,40 +41,10 @@ export const parameters = { }, }, backgrounds: { - default: '--color--background--light-3', - values: [ - { - name: '--color--background--shade-2', - value: 'var(--color--background--shade-2)', - }, - { - name: '--color--background', - value: 'var(--color--background)', - }, - { - name: '--color--background--light-2', - value: 'var(--color--background--light-2)', - }, - { - name: '--color--background--light-3', - value: 'var(--color--background--light-3)', - }, - ], + disable: true, }, themes: { - default: 'light', - list: [ - { - name: 'light', - class: 'theme-light', - color: '#fff', - }, - { - name: 'dark', - class: 'theme-dark-beta', - color: '#000', - }, - ], + disable: true, }, options: { storySort: { @@ -93,24 +62,10 @@ export const parameters = { }, }, chromatic: { - modes: { - light: allModes['light'], - dark: allModes['dark'], - }, disableSnapshot: false, }, }; -export const decorators = [ - withThemeByDataAttribute({ - themes: { - light: 'light', - dark: 'dark', - }, - defaultTheme: 'light', - attributeName: 'data-theme', - parentSelector: 'body', - }), -]; +export const decorators = [withThemePreview]; export const tags = ['autodocs']; diff --git a/packages/frontend/@n8n/storybook/.storybook/storybook.scss b/packages/frontend/@n8n/storybook/.storybook/storybook.scss index 0bf63805169..a15ab403c11 100644 --- a/packages/frontend/@n8n/storybook/.storybook/storybook.scss +++ b/packages/frontend/@n8n/storybook/.storybook/storybook.scss @@ -21,6 +21,25 @@ body { gap: 1rem; } +.theme-side-by-side { + display: flex; + flex-direction: column; + gap: var(--spacing--md); +} + +.theme-side-by-side__panel { + position: relative; + padding: var(--spacing--md); + background: var(--background--surface); + color: var(--text-color); + border-radius: var(--radius--lg); + overflow: auto; + + &[data-theme='light'] { + border: var(--border); + } +} + .sbdocs .sbdocs-content table:not(.sbdocs-preview table, .docs-story table) { width: 100%; border-collapse: collapse; diff --git a/packages/frontend/@n8n/storybook/.storybook/withThemePreview.ts b/packages/frontend/@n8n/storybook/.storybook/withThemePreview.ts new file mode 100644 index 00000000000..f513faf3750 --- /dev/null +++ b/packages/frontend/@n8n/storybook/.storybook/withThemePreview.ts @@ -0,0 +1,82 @@ +import type { Decorator } from '@storybook/vue3'; + +type Theme = 'light' | 'dark'; + +function applyBodyTheme(theme: Theme) { + document.body.dataset.theme = theme; + document.body.style.colorScheme = theme; +} + +function panelMinHeight(value: unknown): string | undefined { + if (typeof value === 'number') { + return `${value}px`; + } + if (typeof value === 'string' && value.length > 0) { + return value; + } + return undefined; +} + +function themePreviewMinHeight(parameters: Record): unknown { + const themePreview = parameters.themePreview; + if (typeof themePreview !== 'object' || themePreview === null || !('minHeight' in themePreview)) { + return undefined; + } + return themePreview.minHeight; +} + +function isTheme(value: string | undefined): value is Theme { + return value === 'light' || value === 'dark'; +} + +export const withThemePreview: Decorator = (story, context) => { + const minHeight = panelMinHeight(themePreviewMinHeight(context.parameters)); + + return { + components: { storyComponent: story() }, + setup: () => ({ + panelStyle: minHeight ? { minHeight } : undefined, + }), + mounted() { + applyBodyTheme('light'); + }, + unmounted() { + document.body.removeAttribute('data-theme'); + document.body.style.removeProperty('color-scheme'); + }, + methods: { + onPanelInteract(event: Event) { + const target = event.currentTarget; + if (!(target instanceof HTMLElement)) { + return; + } + const theme = target.dataset.theme; + if (isTheme(theme)) { + applyBodyTheme(theme); + } + }, + }, + template: ` +
+
+ +
+
+ +
+
+ `, + }; +}; diff --git a/packages/frontend/@n8n/storybook/package.json b/packages/frontend/@n8n/storybook/package.json index 2fe28a9c757..549e44c322b 100644 --- a/packages/frontend/@n8n/storybook/package.json +++ b/packages/frontend/@n8n/storybook/package.json @@ -33,7 +33,6 @@ "@n8n/typescript-config": "workspace:*", "@storybook/addon-a11y": "catalog:storybook", "@storybook/addon-docs": "catalog:storybook", - "@storybook/addon-themes": "catalog:storybook", "@storybook/addon-vitest": "catalog:storybook", "@storybook/vue3": "catalog:storybook", "@storybook/vue3-vite": "catalog:storybook", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af9ce9f77d8..aff62abb349 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -643,9 +643,6 @@ catalogs: '@storybook/addon-docs': specifier: ^10.1.11 version: 10.1.11 - '@storybook/addon-themes': - specifier: ^10.1.11 - version: 10.1.11 '@storybook/addon-vitest': specifier: ^10.1.11 version: 10.1.11 @@ -5649,9 +5646,6 @@ importers: '@storybook/addon-docs': specifier: catalog:storybook version: 10.1.11(@types/react@18.0.27)(esbuild@0.28.1)(rollup@4.52.4)(storybook@10.1.11(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(prettier@3.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(utf-8-validate@5.0.10))(vite@8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)) - '@storybook/addon-themes': - specifier: catalog:storybook - version: 10.1.11(storybook@10.1.11(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(prettier@3.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(utf-8-validate@5.0.10)) '@storybook/addon-vitest': specifier: catalog:storybook version: 10.1.11(@vitest/browser-playwright@4.1.9)(@vitest/browser@4.1.9)(@vitest/runner@4.1.9)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.1.11(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(prettier@3.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(utf-8-validate@5.0.10))(vitest@4.1.9) @@ -11908,11 +11902,6 @@ packages: peerDependencies: storybook: ^10.1.11 - '@storybook/addon-themes@10.1.11': - resolution: {integrity: sha512-tUX5C1ms+W4GFK8UBWd3Fq4irkLc3h092BqW90tZghcoOmGY/sfKR+PlcLhoaTs/kkHQSSHPrz8HSFR1AXVbHA==} - peerDependencies: - storybook: ^10.1.11 - '@storybook/addon-vitest@10.1.11': resolution: {integrity: sha512-YbZzeKO3v+Xr97/malT4DZIATkVZT5EHNYx3xzEfPVuk19dDETAVYXO+tzcqCQHsgdKQHkmd56vv8nN3J3/kvw==} peerDependencies: @@ -29238,11 +29227,6 @@ snapshots: - vite - webpack - '@storybook/addon-themes@10.1.11(storybook@10.1.11(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(prettier@3.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(utf-8-validate@5.0.10))': - dependencies: - storybook: 10.1.11(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(prettier@3.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(utf-8-validate@5.0.10) - ts-dedent: 2.2.0 - '@storybook/addon-vitest@10.1.11(@vitest/browser-playwright@4.1.9)(@vitest/browser@4.1.9)(@vitest/runner@4.1.9)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.1.11(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(prettier@3.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(utf-8-validate@5.0.10))(vitest@4.1.9)': dependencies: '@storybook/global': 5.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a1a41a936b5..61fdce779de 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -262,7 +262,6 @@ catalogs: '@chromatic-com/storybook': ^4.1.3 '@storybook/addon-a11y': ^10.1.11 '@storybook/addon-docs': ^10.1.11 - '@storybook/addon-themes': ^10.1.11 '@storybook/addon-vitest': ^10.1.11 '@storybook/vue3': ^10.1.11 '@storybook/vue3-vite': ^10.1.11