diff --git a/packages/@n8n/mcp-apps/src/apps/workflow-preview/tokens.scss b/packages/@n8n/mcp-apps/src/apps/workflow-preview/tokens.scss
index ed7fe24e352..bed5f36cec3 100644
--- a/packages/@n8n/mcp-apps/src/apps/workflow-preview/tokens.scss
+++ b/packages/@n8n/mcp-apps/src/apps/workflow-preview/tokens.scss
@@ -1,6 +1,6 @@
@use '@n8n/design-system/css/_primitives' as primitives;
@use '@n8n/design-system/css/_tokens' as tokens;
-@use '@n8n/design-system/css/fonts/inter';
+@use '@n8n/design-system/css/fonts/geist';
:root {
color-scheme: light dark;
@@ -22,13 +22,7 @@
body {
margin: 0;
- font-family:
- InterVariable,
- -apple-system,
- BlinkMacSystemFont,
- 'Segoe UI',
- Roboto,
- sans-serif;
+ font-family: var(--font-family);
-webkit-font-smoothing: antialiased;
background: transparent;
color: var(--color--text--shade-1);
diff --git a/packages/@n8n/mcp-apps/src/components/open-in-n8n-button.vue b/packages/@n8n/mcp-apps/src/components/open-in-n8n-button.vue
index 958c4f41cf3..0d472bf7d47 100644
--- a/packages/@n8n/mcp-apps/src/components/open-in-n8n-button.vue
+++ b/packages/@n8n/mcp-apps/src/components/open-in-n8n-button.vue
@@ -6,11 +6,11 @@ import { useI18n } from '@mcp-apps/i18n';
withDefaults(
defineProps<{
label?: string;
- variant?: 'solid' | 'subtle';
+ variant?: 'solid' | 'outline';
size?: 'small' | 'medium';
}>(),
{
- variant: 'subtle',
+ variant: 'outline',
size: 'small',
},
);
diff --git a/packages/@n8n/mcp-browser-extension/src/ui/tokens.scss b/packages/@n8n/mcp-browser-extension/src/ui/tokens.scss
index 3ca56f16386..b636057896b 100644
--- a/packages/@n8n/mcp-browser-extension/src/ui/tokens.scss
+++ b/packages/@n8n/mcp-browser-extension/src/ui/tokens.scss
@@ -1,14 +1,12 @@
@use '@n8n/design-system/css/_primitives' as primitives;
@use '@n8n/design-system/css/_tokens' as tokens;
-@use '@n8n/design-system/css/fonts/inter';
+@use '@n8n/design-system/css/fonts/geist';
:root {
color-scheme: light dark;
@include primitives.primitives;
@include tokens.theme;
-
- // Shared icon column, so info rows and tab rows keep one text edge and one icon centre
- --row-marker-size: 40px;
+ --row-marker-size: var(--height--xl);
}
@media (prefers-color-scheme: dark) {
@@ -26,25 +24,19 @@
body {
margin: 0;
padding: 0;
- font-family:
- InterVariable,
- -apple-system,
- BlinkMacSystemFont,
- 'Segoe UI',
- Roboto,
- sans-serif;
+ font-family: var(--font-family);
-webkit-font-smoothing: antialiased;
background-color: var(--color--background);
color: var(--color--text--shade-1);
}
-// Grouping box shared by the views and the allowed-instances list.
+/** Keep the shared card layout for extension panels. */
.panel {
display: flex;
flex-direction: column;
gap: var(--spacing--lg);
background: var(--background--surface);
- border: var(--border-width) var(--border-style) var(--color--foreground--tint-1);
+ border: var(--border);
border-radius: var(--radius--lg);
padding: var(--spacing--lg);
min-height: 0;
diff --git a/packages/frontend/@n8n/design-system/assets/fonts/CommitMonoVariable.woff2 b/packages/frontend/@n8n/design-system/assets/fonts/CommitMonoVariable.woff2
deleted file mode 100644
index f9ac1ee498c..00000000000
Binary files a/packages/frontend/@n8n/design-system/assets/fonts/CommitMonoVariable.woff2 and /dev/null differ
diff --git a/packages/frontend/@n8n/design-system/assets/fonts/Geist-Italic-Variable.woff2 b/packages/frontend/@n8n/design-system/assets/fonts/Geist-Italic-Variable.woff2
new file mode 100644
index 00000000000..58b157a8a6c
Binary files /dev/null and b/packages/frontend/@n8n/design-system/assets/fonts/Geist-Italic-Variable.woff2 differ
diff --git a/packages/frontend/@n8n/design-system/assets/fonts/Geist-OFL.txt b/packages/frontend/@n8n/design-system/assets/fonts/Geist-OFL.txt
new file mode 100644
index 00000000000..04e95fc5508
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/assets/fonts/Geist-OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
\ No newline at end of file
diff --git a/packages/frontend/@n8n/design-system/assets/fonts/Geist-Variable.woff2 b/packages/frontend/@n8n/design-system/assets/fonts/Geist-Variable.woff2
new file mode 100644
index 00000000000..5c5999d617e
Binary files /dev/null and b/packages/frontend/@n8n/design-system/assets/fonts/Geist-Variable.woff2 differ
diff --git a/packages/frontend/@n8n/design-system/assets/fonts/GeistMono-Italic-Variable.woff2 b/packages/frontend/@n8n/design-system/assets/fonts/GeistMono-Italic-Variable.woff2
new file mode 100644
index 00000000000..c663ca57222
Binary files /dev/null and b/packages/frontend/@n8n/design-system/assets/fonts/GeistMono-Italic-Variable.woff2 differ
diff --git a/packages/frontend/@n8n/design-system/assets/fonts/GeistMono-Variable.woff2 b/packages/frontend/@n8n/design-system/assets/fonts/GeistMono-Variable.woff2
new file mode 100644
index 00000000000..efaf95dba19
Binary files /dev/null and b/packages/frontend/@n8n/design-system/assets/fonts/GeistMono-Variable.woff2 differ
diff --git a/packages/frontend/@n8n/design-system/assets/fonts/InterVariable-Italic.woff2 b/packages/frontend/@n8n/design-system/assets/fonts/InterVariable-Italic.woff2
deleted file mode 100644
index b3530f3f526..00000000000
Binary files a/packages/frontend/@n8n/design-system/assets/fonts/InterVariable-Italic.woff2 and /dev/null differ
diff --git a/packages/frontend/@n8n/design-system/assets/fonts/InterVariable.woff2 b/packages/frontend/@n8n/design-system/assets/fonts/InterVariable.woff2
deleted file mode 100644
index 5a8d3e72ad7..00000000000
Binary files a/packages/frontend/@n8n/design-system/assets/fonts/InterVariable.woff2 and /dev/null differ
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue
index 0841f784cf8..9f885681bff 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue
@@ -557,7 +557,7 @@ defineExpose({
>
onQuickReply(opt)"
>
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/__snapshots__/AskAssistantChat.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/__snapshots__/AskAssistantChat.test.ts.snap
index 61f9d4d39af..d5b9c8b056e 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/__snapshots__/AskAssistantChat.test.ts.snap
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/__snapshots__/AskAssistantChat.test.ts.snap
@@ -289,7 +289,7 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
icononly="false"
loading="false"
size="xsmall"
- variant="subtle"
+ variant="outline"
/>
renders chat with messages correctly 1`] = `
icononly="false"
loading="false"
size="xsmall"
- variant="subtle"
+ variant="outline"
/>
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/BlockMessage.stories.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/BlockMessage.stories.ts
new file mode 100644
index 00000000000..7e6f234caf9
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/BlockMessage.stories.ts
@@ -0,0 +1,40 @@
+import type { StoryFn } from '@storybook/vue3-vite';
+
+import BlockMessage from './BlockMessage.vue';
+
+export default {
+ title: 'Areas/Assistant/BlockMessage',
+ component: BlockMessage,
+ parameters: {
+ docs: {
+ description: {
+ component: 'A titled summary block in Assistant chat, with markdown body content.',
+ },
+ },
+ },
+};
+
+const Template: StoryFn = (args) => ({
+ setup: () => ({ args }),
+ components: { BlockMessage },
+ template: `
+
+
+
+ `,
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ isFirstOfRole: true,
+ user: { firstName: 'Max', lastName: 'Test' },
+ message: {
+ id: 'block-1',
+ role: 'assistant',
+ type: 'block',
+ title: "Credential doesn't have correct permissions to send a message",
+ content:
+ '### Solution steps\n1. Check the credential **scopes**\n2. Reconnect the account\n3. Retry the node\n\n- Unordered item 1\n- Unordered item 2',
+ read: false,
+ },
+};
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/BlockMessage.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/BlockMessage.vue
index 535d7b2c07c..91fdc826083 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/BlockMessage.vue
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/BlockMessage.vue
@@ -61,7 +61,7 @@ const { renderMarkdown } = useMarkdown();
.blockTitle {
border-bottom: var(--border);
padding: var(--spacing--2xs);
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
}
.blockBody {
@@ -77,7 +77,7 @@ const { renderMarkdown } = useMarkdown();
h1,
h2,
h3 {
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
font-size: var(--font-size--xs);
margin: var(--spacing--xs) 0 var(--spacing--4xs);
}
@@ -85,7 +85,7 @@ const { renderMarkdown } = useMarkdown();
h4,
h5,
h6 {
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
font-size: var(--font-size--2xs);
}
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ErrorMessage.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ErrorMessage.vue
index 15014646a6f..75e1d8d49b9 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ErrorMessage.vue
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ErrorMessage.vue
@@ -27,7 +27,7 @@ const { t } = useI18n();
({
+ setup: () => ({ args }),
+ components: { RestoreVersionConfirm },
+ template: ' ',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ versionId: 'version-abc123',
+};
+
+export const WithHourLimit = Template.bind({});
+WithHourLimit.args = {
+ versionId: 'version-abc123',
+ pruneTimeHours: 1,
+};
+
+export const WithDayLimit = Template.bind({});
+WithDayLimit.args = {
+ versionId: 'version-abc123',
+ pruneTimeHours: 72,
+};
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/RestoreVersionConfirm.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/RestoreVersionConfirm.vue
index 2bfa7996e9c..abfe8b3e504 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/RestoreVersionConfirm.vue
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/RestoreVersionConfirm.vue
@@ -109,7 +109,7 @@ function handleConfirm() {
.title {
margin: 0;
font-size: var(--font-size--md);
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
color: var(--color--text--shade-1);
}
@@ -137,7 +137,7 @@ function handleConfirm() {
padding: 0;
color: var(--color--text--shade-1);
font-size: var(--font-size--sm);
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
cursor: pointer;
padding: var(--spacing--3xs) var(--spacing--sm);
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/TextMessage.stories.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/TextMessage.stories.ts
new file mode 100644
index 00000000000..d1c3e3ba7c9
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/TextMessage.stories.ts
@@ -0,0 +1,66 @@
+import type { StoryFn } from '@storybook/vue3-vite';
+
+import TextMessage from './TextMessage.vue';
+
+export default {
+ title: 'Areas/Assistant/TextMessage',
+ component: TextMessage,
+ parameters: {
+ docs: {
+ description: {
+ component: 'A text message in Assistant chat, for both user and assistant roles.',
+ },
+ },
+ },
+};
+
+const Template: StoryFn = (args) => ({
+ setup: () => ({ args }),
+ components: { TextMessage },
+ template: `
+
+
+
+ `,
+});
+
+export const Assistant = Template.bind({});
+Assistant.args = {
+ isFirstOfRole: true,
+ user: { firstName: 'Max', lastName: 'Test' },
+ message: {
+ id: 'text-1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'Hi Max! Here is my top solution to fix the error in your **Transform data** node.\n\n### Next steps\n1. Open the node\n2. Update the expression',
+ read: false,
+ },
+};
+
+export const User = Template.bind({});
+User.args = {
+ isFirstOfRole: true,
+ user: { firstName: 'Max', lastName: 'Test' },
+ message: {
+ id: 'text-2',
+ role: 'user',
+ type: 'text',
+ content: 'Give it to me **ignore this markdown**',
+ read: false,
+ },
+};
+
+export const WithCodeSnippet = Template.bind({});
+WithCodeSnippet.args = {
+ isFirstOfRole: true,
+ user: { firstName: 'Max', lastName: 'Test' },
+ message: {
+ id: 'text-3',
+ role: 'assistant',
+ type: 'text',
+ content: 'Use this expression in the node:',
+ codeSnippet: '```js\n{{ $json.data }}\n```',
+ read: false,
+ },
+};
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/TextMessage.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/TextMessage.vue
index 313f9fed38d..181d0ffb254 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/TextMessage.vue
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/TextMessage.vue
@@ -256,7 +256,7 @@ async function onCopyButtonClick(content: string, e: MouseEvent) {
h1,
h2,
h3 {
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
font-size: var(--font-size--xs);
margin: var(--spacing--xs) 0 var(--spacing--4xs);
}
@@ -264,7 +264,7 @@ async function onCopyButtonClick(content: string, e: MouseEvent) {
h4,
h5,
h6 {
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
font-size: var(--font-size--2xs);
}
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ToolMessage.stories.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ToolMessage.stories.ts
new file mode 100644
index 00000000000..11fa040f25b
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ToolMessage.stories.ts
@@ -0,0 +1,72 @@
+import type { StoryFn } from '@storybook/vue3-vite';
+
+import ToolMessage from './ToolMessage.vue';
+import type { ChatUI } from '../../../types/assistant';
+
+export default {
+ title: 'Areas/Assistant/ToolMessage',
+ component: ToolMessage,
+ parameters: {
+ docs: {
+ description: {
+ component: 'A tool-call status row in Assistant chat.',
+ },
+ },
+ },
+};
+
+const Template: StoryFn = (args) => ({
+ setup: () => ({ args }),
+ components: { ToolMessage },
+ template: `
+
+
+
+ `,
+});
+
+const toolMessage = (
+ overrides: Partial & { id?: string; read?: boolean } = {},
+): ChatUI.ToolMessage & { id: string; read: boolean } => ({
+ id: 'tool-1',
+ role: 'assistant',
+ type: 'tool',
+ toolName: 'search_files',
+ toolCallId: 'call_456',
+ displayTitle: 'Search Files',
+ status: 'completed',
+ updates: [],
+ read: false,
+ ...overrides,
+});
+
+export const Completed = Template.bind({});
+Completed.args = {
+ isFirstOfRole: true,
+ user: { firstName: 'Max', lastName: 'Test' },
+ message: toolMessage({ status: 'completed' }),
+};
+
+export const Running = Template.bind({});
+Running.args = {
+ isFirstOfRole: true,
+ user: { firstName: 'Max', lastName: 'Test' },
+ message: toolMessage({
+ id: 'tool-2',
+ status: 'running',
+ toolName: 'code_tool',
+ displayTitle: 'Code Tool',
+ }),
+};
+
+export const Error = Template.bind({});
+Error.args = {
+ isFirstOfRole: true,
+ user: { firstName: 'Max', lastName: 'Test' },
+ message: toolMessage({
+ id: 'tool-3',
+ status: 'error',
+ toolName: 'database_query',
+ displayTitle: 'Database Query',
+ }),
+};
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ToolMessage.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ToolMessage.vue
index 5cdb12fd0b5..a4ae73511c9 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ToolMessage.vue
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/ToolMessage.vue
@@ -134,7 +134,7 @@ const statusColor = computed(() => {
}
.sectionTitle {
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
font-size: var(--font-size--2xs);
color: var(--color--text--shade-1);
margin-bottom: var(--spacing--3xs);
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/__snapshots__/MessageRating.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/__snapshots__/MessageRating.test.ts.snap
index cf9f8b34d50..09f6fbac131 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/__snapshots__/MessageRating.test.ts.snap
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/messages/__snapshots__/MessageRating.test.ts.snap
@@ -3,8 +3,8 @@
exports[`MessageRating > should render correctly with default props 1`] = `
"
-
-
+
+
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantText/AssistantText.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantText/AssistantText.vue
index ddb656e4db6..62c281b5b26 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantText/AssistantText.vue
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantText/AssistantText.vue
@@ -15,7 +15,7 @@ withDefaults(defineProps<{ text: string; size?: 'small' | 'medium' | 'large' | '
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
}
.small {
diff --git a/packages/frontend/@n8n/design-system/src/components/CanvasCollaborationPill/CanvasCollaborationPill.vue b/packages/frontend/@n8n/design-system/src/components/CanvasCollaborationPill/CanvasCollaborationPill.vue
index 8ec618d3fb5..a78e1068d7e 100644
--- a/packages/frontend/@n8n/design-system/src/components/CanvasCollaborationPill/CanvasCollaborationPill.vue
+++ b/packages/frontend/@n8n/design-system/src/components/CanvasCollaborationPill/CanvasCollaborationPill.vue
@@ -44,7 +44,7 @@ const handleButtonClick = () => {
{{ message }}
-
+
{{ t('collaboration.canvas.acquireEditing') }}
diff --git a/packages/frontend/@n8n/design-system/src/components/CodeDiff/CodeDiff.vue b/packages/frontend/@n8n/design-system/src/components/CodeDiff/CodeDiff.vue
index 26fe46c11b2..5322df45e82 100644
--- a/packages/frontend/@n8n/design-system/src/components/CodeDiff/CodeDiff.vue
+++ b/packages/frontend/@n8n/design-system/src/components/CodeDiff/CodeDiff.vue
@@ -113,7 +113,7 @@ const diffs = computed(() => {
{
{
.title {
padding: var(--spacing--2xs);
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
font-size: var(--font-size--2xs);
// ensure consistent spacing even if title is empty
min-height: 32.5px;
diff --git a/packages/frontend/@n8n/design-system/src/components/CodeDiff/__snapshots__/CodeDiff.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/CodeDiff/__snapshots__/CodeDiff.test.ts.snap
index 478dc404046..87a1ea663a7 100644
--- a/packages/frontend/@n8n/design-system/src/components/CodeDiff/__snapshots__/CodeDiff.test.ts.snap
+++ b/packages/frontend/@n8n/design-system/src/components/CodeDiff/__snapshots__/CodeDiff.test.ts.snap
@@ -854,7 +854,7 @@ exports[`CodeDiff > renders replaced code diff correctly 1`] = `
icononly="false"
loading="false"
size="xsmall"
- variant="subtle"
+ variant="outline"
/>
renders replacing code diff correctly 1`] = `
icononly="false"
loading="true"
size="xsmall"
- variant="subtle"
+ variant="outline"
/>
diff --git a/packages/frontend/@n8n/design-system/src/components/DateRangePicker/DateRangePicker.vue b/packages/frontend/@n8n/design-system/src/components/DateRangePicker/DateRangePicker.vue
index 3f833e84228..2996b371948 100644
--- a/packages/frontend/@n8n/design-system/src/components/DateRangePicker/DateRangePicker.vue
+++ b/packages/frontend/@n8n/design-system/src/components/DateRangePicker/DateRangePicker.vue
@@ -48,7 +48,7 @@ const forwarded = useForwardPropsEmits(props, emit);
-
+
@@ -63,11 +63,11 @@ const forwarded = useForwardPropsEmits(props, emit);
-
+
-
+
@@ -116,7 +116,7 @@ const forwarded = useForwardPropsEmits(props, emit);
{
},
});
- expect(getByRole('button').querySelector('svg')).toHaveAttribute('data-icon', 'ellipsis');
+ expect(getByRole('button').querySelector('svg')).toHaveAttribute('data-icon', 'ellipsis-12');
});
it('should have bottom-end as default placement', async function () {
const { container, getByRole } = render(ActionToggle, {
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nActionToggle/ActionToggle.vue b/packages/frontend/@n8n/design-system/src/components/N8nActionToggle/ActionToggle.vue
index 5bf78d416cd..db90158a81a 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nActionToggle/ActionToggle.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nActionToggle/ActionToggle.vue
@@ -101,7 +101,7 @@ defineExpose({
{
.title {
font-size: var.$alert-title-font-size;
line-height: 18px;
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
}
.description {
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nAlertDialog/AlertDialog.vue b/packages/frontend/@n8n/design-system/src/components/N8nAlertDialog/AlertDialog.vue
index f3747dd9d5a..9dfcc6d1daf 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nAlertDialog/AlertDialog.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nAlertDialog/AlertDialog.vue
@@ -109,7 +109,7 @@ const handleCancel = () => {
-
+
AVATAR_SIZES[size];
.initials {
position: absolute;
font-size: var(--font-size--2xs);
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
color: var(--avatar--color--text);
text-shadow: 0 1px 6px rgba(25, 11, 9, 0.3);
text-transform: uppercase;
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nBadge/Badge.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nBadge/Badge.stories.ts
index 3079582c137..1dbb2d7417e 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nBadge/Badge.stories.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nBadge/Badge.stories.ts
@@ -99,16 +99,28 @@ export const Sizes: Story = {
},
components: { N8nBadge },
template: `
-
-
- Active
-
+
+
+
+ Active
+
+
+
+
+ Active
+
+
`,
};
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nBadge/Badge.vue b/packages/frontend/@n8n/design-system/src/components/N8nBadge/Badge.vue
index bcb5d67f0a3..7136749f1e5 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nBadge/Badge.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nBadge/Badge.vue
@@ -79,7 +79,6 @@ const effectiveTextSize = computed(function getEffectiveTextSize() {
display: inline-flex;
align-items: center;
white-space: nowrap;
- border-radius: var(--radius--full);
user-select: none;
appearance: none;
width: fit-content;
@@ -91,7 +90,10 @@ const effectiveTextSize = computed(function getEffectiveTextSize() {
--n8n-badge--padding-inline-start: var(--spacing--2xs);
--n8n-badge--padding-inline-end: var(--spacing--2xs);
--n8n-badge--gap: var(--spacing--4xs);
+ --n8n-badge--radius: var(--radius--xs);
+ --n8n-badge--label-shift: 0;
+ border-radius: var(--n8n-badge--radius);
gap: var(--n8n-badge--gap);
background-color: var(--n8n-badge--background);
border: 1px solid var(--n8n-badge--border-color);
@@ -102,6 +104,7 @@ const effectiveTextSize = computed(function getEffectiveTextSize() {
.label {
@include utils.utils-ellipsis;
+ transform: translateY(var(--n8n-badge--label-shift));
}
.clickable {
@@ -130,36 +133,45 @@ const effectiveTextSize = computed(function getEffectiveTextSize() {
--n8n-badge--height: var(--height--2xs);
--n8n-badge--padding-inline-start: var(--spacing--2xs);
--n8n-badge--padding-inline-end: var(--spacing--2xs);
+ --n8n-badge--radius: var(--radius--2xs);
+ --n8n-badge--label-shift: 1px;
}
.xsmall {
--n8n-badge--height: var(--height--xs);
--n8n-badge--padding-inline-start: var(--spacing--2xs);
--n8n-badge--padding-inline-end: var(--spacing--2xs);
+ --n8n-badge--radius: var(--radius--2xs);
+ --n8n-badge--label-shift: 1px;
}
.small {
--n8n-badge--height: var(--height--sm);
--n8n-badge--padding-inline-start: var(--spacing--2xs);
--n8n-badge--padding-inline-end: var(--spacing--2xs);
+ --n8n-badge--label-shift: 0;
}
.medium {
--n8n-badge--height: var(--height--md);
--n8n-badge--padding-inline-start: var(--spacing--xs);
--n8n-badge--padding-inline-end: var(--spacing--xs);
+ --n8n-badge--label-shift: 0;
}
.large {
--n8n-badge--height: var(--height--lg);
--n8n-badge--padding-inline-start: var(--spacing--sm);
--n8n-badge--padding-inline-end: var(--spacing--sm);
+ --n8n-badge--label-shift: 0;
}
.xlarge {
--n8n-badge--height: var(--height--xl);
--n8n-badge--padding-inline-start: var(--spacing--sm);
--n8n-badge--padding-inline-end: var(--spacing--sm);
+ --n8n-badge--radius: var(--radius--sm);
+ --n8n-badge--label-shift: 0;
}
.filled {
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/__snapshots__/BreadCrumbs.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/__snapshots__/BreadCrumbs.test.ts.snap
index da857b6fdae..8e3584bca1e 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/__snapshots__/BreadCrumbs.test.ts.snap
+++ b/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/__snapshots__/BreadCrumbs.test.ts.snap
@@ -6,11 +6,11 @@ exports[`Breadcrumbs > does not highlight last item for "highlightLastItem = fal
-
Folder 1
+
Folder 1
/
-
Folder 2
+
Folder 2
/
-
Folder 3
+
Folder 3
/
Current
@@ -24,11 +24,11 @@ exports[`Breadcrumbs > renders custom separator correctly 1`] = `
-
Folder 1
+
Folder 1
➮
-
Folder 2
+
Folder 2
➮
-
Folder 3
+
Folder 3
➮
Current
@@ -42,11 +42,11 @@ exports[`Breadcrumbs > renders default version correctly 1`] = `
-
Folder 1
+
Folder 1
/
-
Folder 2
+
Folder 2
/
-
Folder 3
+
Folder 3
/
Current
@@ -61,11 +61,11 @@ exports[`Breadcrumbs > renders slots correctly 1`] = `
/
-
Folder 1
+
Folder 1
/
-
Folder 2
+
Folder 2
/
-
Folder 3
+
Folder 3
/
Current
@@ -80,11 +80,11 @@ exports[`Breadcrumbs > renders small version correctly 1`] = `
-
Folder 1
+
Folder 1
/
-
Folder 2
+
Folder 2
/
-
Folder 3
+
Folder 3
/
Current
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.stories.ts
index 035bbfd3d3e..7000f99853e 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.stories.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.stories.ts
@@ -11,7 +11,7 @@ const meta = {
argTypes: {
variant: {
control: 'select',
- options: ['solid', 'subtle', 'ghost', 'outline', 'destructive', 'success'],
+ options: ['solid', 'brand', 'ghost', 'outline', 'destructive', 'success'],
},
size: {
control: 'select',
@@ -73,7 +73,7 @@ export const Variants: Story = {
template: `
Solid
-
Subtle
+
Brand
Outline
Ghost
Destructive
@@ -130,7 +130,7 @@ export const Loading: Story = {
template: `
Solid
-
Subtle
+
Brand
Outline
Ghost
Destructive
@@ -147,7 +147,7 @@ export const Link: Story = {
template: `
Link
-
Link
+
Link
Link
Link
Link
@@ -181,7 +181,7 @@ export const Disabled: Story = {
template: `
Solid
-
Subtle
+
Brand
Outline
Ghost
Destructive
@@ -200,7 +200,7 @@ export const Disabled: Story = {
* | Legacy `type` | Current `variant` |
* |---------------|-------------------|
* | `primary` | `solid` |
- * | `secondary` | `subtle` |
+ * | `secondary` | `outline` |
* | `tertiary` | `ghost` |
* | `danger` | `destructive` |
* | `success` | `success` |
@@ -224,8 +224,8 @@ export const TypeToVariantMapping: Story = {
Solid
type="secondary"
-
variant="subtle"
-
Subtle
+
variant="outline"
+
Outline
type="tertiary"
variant="ghost"
@@ -261,7 +261,6 @@ export const IconButtonPatterns: Story = {
-
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.test.ts
index a8a32155e65..153f534e1ea 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.test.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.test.ts
@@ -7,6 +7,11 @@ const stubs = ['N8nSpinner', 'N8nIcon'];
describe('components', () => {
describe('N8nButton', () => {
+ it('maps the legacy subtle variant to brand', () => {
+ const wrapper = render(N8nButton, { props: { variant: 'subtle' }, global: { stubs } });
+ expect(wrapper.getByRole('button').className).toContain('brand');
+ });
+
describe('rendering', () => {
it('should render correctly with default props', () => {
const wrapper = render(N8nButton, {
@@ -82,7 +87,7 @@ describe('components', () => {
describe('props', () => {
describe('variant', () => {
- it.each(['solid', 'subtle', 'ghost', 'outline', 'destructive', 'success'] as const)(
+ it.each(['solid', 'brand', 'ghost', 'outline', 'destructive', 'success'] as const)(
'should render %s variant',
(variant) => {
const wrapper = render(N8nButton, {
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.vue b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.vue
index dab2538be5a..2d2549b6b45 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.vue
@@ -29,6 +29,7 @@ const effectiveSize = computed(() => {
// Map legacy variant values to current ones
const effectiveVariant = computed(() => {
+ if (props.variant === 'subtle') return 'brand';
if (props.variant === 'highlight') return 'ghost';
if (props.variant === 'highlight-fill') return 'ghost';
return props.variant;
@@ -145,7 +146,7 @@ const handleClick = (event: MouseEvent) => {
height: var(--button--height);
padding: var(--button--padding);
- border-radius: var(--button--radius);
+ border-radius: var(--radius--full);
font-size: var(--button--font-size);
--button--color--background: transparent;
@@ -201,43 +202,72 @@ const handleClick = (event: MouseEvent) => {
&.xsmall {
--button--height: var(--height--xs);
--button--padding: 0 var(--spacing--2xs);
- --button--radius: var(--radius--3xs);
+ --button--radius: var(--radius--xs);
--button--font-size: var(--font-size--2xs);
}
&.small {
--button--height: var(--height--sm);
--button--padding: 0 var(--spacing--xs);
- --button--radius: var(--radius--3xs);
+ --button--radius: var(--radius--xs);
--button--font-size: var(--font-size--xs);
}
&.medium {
--button--height: var(--height--md);
--button--padding: 0 var(--spacing--xs);
- --button--radius: var(--radius--3xs);
+ --button--radius: var(--radius--xs);
--button--font-size: var(--font-size--sm);
}
&.large {
--button--height: var(--height--lg);
--button--padding: 0 var(--spacing--sm);
- --button--radius: var(--radius--2xs);
+ --button--radius: var(--radius--sm);
--button--font-size: var(--font-size--sm);
}
&.xlarge {
--button--height: var(--height--xl);
--button--padding: 0 var(--spacing--sm);
- --button--radius: var(--radius--xs);
+ --button--radius: var(--radius--sm);
--button--font-size: var(--font-size--md);
}
&.solid {
+ --button--color--background: var(--color--primary);
+ --button--color--background-hover: color-mix(
+ in srgb,
+ var(--color--primary),
+ light-dark(var(--color--neutral-white), var(--color--neutral-black)) 10%
+ );
+ --button--color--background-active: color-mix(
+ in srgb,
+ var(--color--primary),
+ light-dark(var(--color--neutral-white), var(--color--neutral-black)) 12%
+ );
+ --button--color: var(--text-color--inverse);
+ --button--border-color: var(--color--primary);
+ --button--border-color--hover: color-mix(
+ in srgb,
+ var(--button--border-color),
+ light-dark(var(--color--neutral-white), var(--color--neutral-black)) 10%
+ );
+ --button--border-color--active: color-mix(
+ in srgb,
+ var(--button--border-color),
+ light-dark(var(--color--neutral-white), var(--color--neutral-black)) 12%
+ );
+ --button--border--shadow: 0 0 0 1px var(--button--border-color);
+ --button--border--shadow--hover: 0 0 0 1px var(--button--border-color--hover);
+ --button--border--shadow--active: 0 0 0 1px var(--button--border-color--active);
+ }
+
+ &.brand {
--button--color--background: var(--background--brand);
--button--color--background-hover: var(--background--brand--hover);
--button--color--background-active: var(--background--brand--active);
- --button--color: var(--color--neutral-white);
+ --button--color: var(--color--neutral-black);
--button--shadow: var(--shadow--xs);
--button--shadow--hover: var(--shadow--xs);
--button--shadow--active: var(--shadow--xs);
@@ -249,7 +279,7 @@ const handleClick = (event: MouseEvent) => {
--button--border--shadow--active: 0 0 0 1px var(--button--border-color--active);
}
- &.subtle {
+ &.outline {
--button--color--background: var(--background--surface);
--button--color--background-hover: color-mix(
in srgb,
@@ -261,34 +291,20 @@ const handleClick = (event: MouseEvent) => {
var(--button--color--background),
light-dark(var(--color--neutral-black), var(--color--neutral-white)) 10%
);
+ --button--border-color: var(--border-color);
+ --button--border-color--hover: color-mix(
+ in srgb,
+ var(--button--border-color),
+ light-dark(var(--color--neutral-black), var(--color--neutral-white)) 5%
+ );
+ --button--border-color--active: color-mix(
+ in srgb,
+ var(--button--border-color),
+ light-dark(var(--color--neutral-black), var(--color--neutral-white)) 10%
+ );
--button--shadow: var(--shadow--xs);
--button--shadow--hover: var(--shadow--xs);
--button--shadow--active: var(--shadow--xs);
- --button--border-color: var(--border-color);
- --button--border-color--hover: light-dark(
- var(--color--black-alpha-200),
- var(--color--white-alpha-200)
- );
- --button--border-color--active: light-dark(
- var(--color--black-alpha-300),
- var(--color--white-alpha-300)
- );
- --button--border--shadow: 0 0 0 1px var(--button--border-color);
- --button--border--shadow--hover: 0 0 0 1px var(--button--border-color--hover);
- --button--border--shadow--active: 0 0 0 1px var(--button--border-color--active);
- }
-
- &.outline {
- --button--color--background: transparent;
- --button--border-color: var(--border-color);
- --button--border-color--hover: light-dark(
- var(--color--black-alpha-200),
- var(--color--white-alpha-200)
- );
- --button--border-color--active: light-dark(
- var(--color--black-alpha-300),
- var(--color--white-alpha-300)
- );
--button--border--shadow: 0 0 0 1px var(--button--border-color);
--button--border--shadow--hover: 0 0 0 1px var(--button--border-color--hover);
--button--border--shadow--active: 0 0 0 1px var(--button--border-color--active);
@@ -416,28 +432,11 @@ const handleClick = (event: MouseEvent) => {
@include motion.spin;
}
-/* TODO: Move to global animations css library */
-:global(.n8n-button-fade-enter-active),
-:global(.n8n-button-fade-leave-active) {
- --easing--ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
- transition:
- opacity 0.2s var(--easing--ease-out),
- transform 0.2s var(--easing--ease-out);
-
- @media (prefers-reduced-motion: reduce) {
- transition: opacity 0.1s;
- }
+:global(.n8n-button-fade-enter-active) {
+ @include motion.fade-in-up;
}
-:global(.n8n-button-fade-enter-from),
-:global(.n8n-button-fade-leave-to) {
- opacity: 0;
- transform: translateY(4px);
- filter: blur(2px);
-
- @media (prefers-reduced-motion: reduce) {
- transform: none;
- filter: none;
- }
+:global(.n8n-button-fade-leave-active) {
+ @include motion.fade-out-down;
}
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nButtonList/ButtonList.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nButtonList/ButtonList.stories.ts
index 77745e2c6e0..cfb0d7cada5 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nButtonList/ButtonList.stories.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nButtonList/ButtonList.stories.ts
@@ -42,7 +42,7 @@ export const Default: Story = {
},
template: `
- Save
+ Save
Publish
More
@@ -61,9 +61,9 @@ export const IconButtons: Story = {
},
template: `
-
-
-
+
+
+
`,
}),
@@ -80,9 +80,9 @@ export const Vertical: Story = {
},
template: `
-
-
-
+
+
+
`,
}),
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nCallout/Callout.vue b/packages/frontend/@n8n/design-system/src/components/N8nCallout/Callout.vue
index 77a0e328d1b..1adad0d2171 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nCallout/Callout.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nCallout/Callout.vue
@@ -170,11 +170,15 @@ const getIconSize = computed
(() => {
.secondary {
font-size: var(--font-size--2xs);
- font-weight: var(--font-weight--bold);
+ font-weight: var(--font-weight--medium);
border-color: var(--callout--border-color--secondary);
background-color: var(--callout--color--background--secondary);
color: var(--callout--color--text--secondary);
+ .messageSection > :global(.n8n-text) {
+ font-weight: inherit;
+ }
+
.icon {
color: var(--callout--icon-color--secondary);
}
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nCard/Card.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nCard/Card.test.ts
index 544ce61c237..10a00bbfb69 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nCard/Card.test.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nCard/Card.test.ts
@@ -1,9 +1,37 @@
import { render } from '@testing-library/vue';
+import { mount } from '@vue/test-utils';
+import { defineComponent, h, nextTick, ref } from 'vue';
import N8nCard from './Card.vue';
describe('components', () => {
describe('N8nCard', () => {
+ it('updates hover classes when the click listener changes', async () => {
+ const onClick = vi.fn();
+ const clickHandler = ref();
+ const hoverable = ref(false);
+ const wrapper = mount(
+ defineComponent({
+ setup: () => () =>
+ h(N8nCard, { onClick: clickHandler.value, hoverable: hoverable.value }),
+ }),
+ );
+ const card = wrapper.getComponent(N8nCard);
+ expect(card.classes()).not.toContain('hoverable');
+ clickHandler.value = onClick;
+ await nextTick();
+ expect(card.classes()).toContain('hoverable');
+ await card.trigger('click');
+ expect(onClick).toHaveBeenCalledOnce();
+ clickHandler.value = undefined;
+ await nextTick();
+ expect(card.classes()).not.toContain('hoverable');
+ hoverable.value = true;
+ await nextTick();
+ expect(card.classes()).toContain('hoverable');
+ wrapper.unmount();
+ });
+
it('should render correctly', () => {
const wrapper = render(N8nCard, {
slots: {
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nCard/Card.vue b/packages/frontend/@n8n/design-system/src/components/N8nCard/Card.vue
index d5e8fee1b71..a2216d319a4 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nCard/Card.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nCard/Card.vue
@@ -1,5 +1,5 @@
-
+
@@ -46,9 +54,10 @@ const classes = computed(() => ({
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nInput/Input.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nInput/Input.stories.ts
index a281665f67a..9126c0c3188 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nInput/Input.stories.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nInput/Input.stories.ts
@@ -342,7 +342,7 @@ export const InlineWithButton = {
- Invite
+ Invite
`,
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nLink/Link.vue b/packages/frontend/@n8n/design-system/src/components/N8nLink/Link.vue
index 6257f42f583..4f77d171fa5 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nLink/Link.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nLink/Link.vue
@@ -22,7 +22,7 @@ withDefaults(defineProps(), {
to: undefined,
size: undefined,
bold: false,
- underline: false,
+ underline: true,
theme: 'primary',
});
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nLink/__snapshots__/Link.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/N8nLink/__snapshots__/Link.test.ts.snap
index c0fd5111bcd..8f18d0111ab 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nLink/__snapshots__/Link.test.ts.snap
+++ b/packages/frontend/@n8n/design-system/src/components/N8nLink/__snapshots__/Link.test.ts.snap
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`N8nLink > should render external links 1`] = `"External Link "`;
+exports[`N8nLink > should render external links 1`] = `"External Link "`;
exports[`N8nLink > should render internal router links 1`] = `" "`;
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nLogo/Logo.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nLogo/Logo.test.ts
index d6756610f30..590494ca11d 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nLogo/Logo.test.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nLogo/Logo.test.ts
@@ -1,8 +1,51 @@
+import { readFileSync } from 'node:fs';
import { render } from '@testing-library/vue';
import Logo from './Logo.vue';
+const source = readFileSync('src/components/N8nLogo/Logo.vue', 'utf8');
+
+const { useFavicon } = vi.hoisted(() => ({ useFavicon: vi.fn() }));
+vi.mock('@vueuse/core', () => ({ useFavicon }));
describe('Logo', () => {
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ vi.clearAllMocks();
+ });
+
+ it.each(['stable', 'beta', 'nightly', 'rc'] as const)(
+ 'leaves the favicon unchanged for %s',
+ (releaseChannel) => {
+ const createObjectURL = vi.fn();
+ vi.stubGlobal('URL', { createObjectURL });
+ render(Logo, { props: { size: 'large', releaseChannel } });
+ expect(createObjectURL).not.toHaveBeenCalled();
+ expect(useFavicon).not.toHaveBeenCalled();
+ },
+ );
+
+ it('uses the gray SVG favicon for dev', () => {
+ const createObjectURL = vi.fn(() => 'blob:dev-logo');
+ const blob = vi.fn();
+ vi.stubGlobal('URL', { createObjectURL });
+ vi.stubGlobal('Blob', blob);
+ render(Logo, { props: { size: 'large', releaseChannel: 'dev' } });
+ expect(blob).toHaveBeenCalledWith([expect.stringContaining('path { fill: #898989; }')], {
+ type: 'image/svg+xml',
+ });
+ expect(useFavicon).toHaveBeenCalledWith('blob:dev-logo');
+ });
+
+ it('skips the dev favicon without object URL support', () => {
+ vi.stubGlobal('URL', {});
+ render(Logo, { props: { size: 'large', releaseChannel: 'dev' } });
+ expect(useFavicon).not.toHaveBeenCalled();
+ });
+
+ it('uses height tokens for logo sizes', () => {
+ expect(source).toContain('height: var(--height--3xs)');
+ expect(source).toContain('height: var(--height--2xs)');
+ });
it('renders the logo for authView location', () => {
const wrapper = render(Logo, {
props: { size: 'large', releaseChannel: 'stable' },
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nLogo/Logo.vue b/packages/frontend/@n8n/design-system/src/components/N8nLogo/Logo.vue
index 0952a53732a..630d45e925f 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nLogo/Logo.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nLogo/Logo.vue
@@ -40,25 +40,23 @@ const containerClasses = computed(() => {
const svg = useTemplateRef<{ $el: Element }>('logo');
onMounted(() => {
- if (!releaseChannel || releaseChannel === 'stable' || !('createObjectURL' in URL)) {
+ if (releaseChannel !== 'dev' || !('createObjectURL' in URL)) {
return;
}
const logoEl = svg.value!.$el;
- // Change the logo fill color inline, so that favicon can also use it
- const logoColor = releaseChannel === 'dev' ? '#838383' : '#E9984B';
- logoEl.querySelector('path')?.setAttribute('fill', logoColor);
-
- // Reuse the SVG as favicon
- const blob = new Blob([logoEl.outerHTML], { type: 'image/svg+xml' });
+ /** Reuse the SVG as the favicon. These must use HEX values. */
+ const hexColor = '#898989';
+ const faviconSvg = logoEl.outerHTML.replace('>', `>`);
+ const blob = new Blob([faviconSvg], { type: 'image/svg+xml' });
useFavicon(URL.createObjectURL(blob));
});
-
+
@@ -69,37 +67,39 @@ onMounted(() => {
display: flex;
justify-content: center;
align-items: center;
+ gap: var(--spacing--3xs);
+ fill: var(--text-color--subtler);
}
+.logo,
.logoText {
- margin-left: var(--spacing--5xs);
+ width: auto;
+ height: var(--height--3xs);
+ max-width: 100%;
+}
+
+.logo {
+ --logo-icon-fill: var(--background--brand);
+
+ &.dev {
+ --logo-icon-fill: var(--text-color--subtler);
+ }
+
path {
- fill: var(--color--text--shade-1);
+ fill: var(--logo-icon-fill);
}
}
+.logoText path {
+ fill: var(--text-color);
+}
+
.large {
- transform: scale(2);
margin-bottom: var(--spacing--xl);
.logo,
.logoText {
- transform: scale(1.3) translateY(-2px);
+ height: var(--height--2xs);
}
-
- .logoText {
- margin-left: var(--spacing--xs);
- margin-right: var(--spacing--3xs);
- }
-}
-
-.sidebarExpanded .logo {
- margin-left: var(--spacing--2xs);
-}
-
-.sidebarCollapsed .logo {
- width: 40px;
- height: 30px;
- padding: 0 var(--spacing--4xs);
}
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nLogo/__snapshots__/Logo.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/N8nLogo/__snapshots__/Logo.test.ts.snap
index 1cf06c089bc..440883852f6 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nLogo/__snapshots__/Logo.test.ts.snap
+++ b/packages/frontend/@n8n/design-system/src/components/N8nLogo/__snapshots__/Logo.test.ts.snap
@@ -1,27 +1,25 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Logo > renders the logo for authView location 1`] = `
-"
-
-
-
-
+""
`;
exports[`Logo > renders the logo for sidebar location when sidebar is collapsed 1`] = `
-"