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: `
+
+
+
+
+
+ Floating Window
+
+
+
+
+
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: `
-
-
-
- Floating Window
-
-
-
-
-
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:
'Slot content for {{name}}',
});
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