diff --git a/packages/@n8n/eslint-config/src/configs/frontend.ts b/packages/@n8n/eslint-config/src/configs/frontend.ts
index ac2bdf60b15..715589dacb3 100644
--- a/packages/@n8n/eslint-config/src/configs/frontend.ts
+++ b/packages/@n8n/eslint-config/src/configs/frontend.ts
@@ -64,7 +64,6 @@ export const frontendConfig = tseslint.config(
'error',
{
ignorePatterns: [
- 'FontAwesomeIcon', // Globally registered in plugins/icons/index.ts
'RouterLink', // Vue Router global component
'RouterView', // Vue Router global component
'Teleport', // Vue 3 built-in
diff --git a/packages/frontend/@n8n/design-system/.storybook/preview.ts b/packages/frontend/@n8n/design-system/.storybook/preview.ts
index 577e51127a1..4326f01f6a5 100644
--- a/packages/frontend/@n8n/design-system/.storybook/preview.ts
+++ b/packages/frontend/@n8n/design-system/.storybook/preview.ts
@@ -1,5 +1,3 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { fas } from '@fortawesome/free-solid-svg-icons';
import { sharedTags } from '@n8n/storybook/main';
import { withThemeByDataAttribute } from '@storybook/addon-themes';
import { setup } from '@storybook/vue3';
@@ -13,8 +11,6 @@ import './storybook.scss';
// import '../src/css/tailwind/index.css';
setup((app) => {
- library.add(fas);
-
app.use(ElementPlus, {
locale: lang,
});
diff --git a/packages/frontend/@n8n/design-system/package.json b/packages/frontend/@n8n/design-system/package.json
index 99b5f8f2f8a..0ee3a972a1b 100644
--- a/packages/frontend/@n8n/design-system/package.json
+++ b/packages/frontend/@n8n/design-system/package.json
@@ -50,9 +50,6 @@
"@vue/test-utils": "catalog:frontend"
},
"dependencies": {
- "@fortawesome/fontawesome-svg-core": "^1.2.36",
- "@fortawesome/free-solid-svg-icons": "^5.15.4",
- "@fortawesome/vue-fontawesome": "^3.0.3",
"@n8n/composables": "workspace:*",
"@n8n/utils": "workspace:*",
"@tanstack/vue-table": "^8.21.2",
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIconPicker/IconPicker.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nIconPicker/IconPicker.test.ts
index 099f6f92be8..321d072afc7 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nIconPicker/IconPicker.test.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nIconPicker/IconPicker.test.ts
@@ -5,21 +5,6 @@ import { createRouter, createWebHistory } from 'vue-router';
import IconPicker from '.';
import { ALL_ICON_PICKER_ICONS } from './constants';
-// Create a proxy handler that returns a mock icon object for any icon name
-// and mock the entire icon library with the proxy
-vi.mock(
- '@fortawesome/free-solid-svg-icons',
- () =>
- new Proxy(
- {},
- {
- get: (_target, prop) => {
- return { prefix: 'fas', iconName: prop.toString().replace('fa', '').toLowerCase() };
- },
- },
- ),
-);
-
const router = createRouter({
history: createWebHistory(),
routes: [
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/IconContent.vue b/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/IconContent.vue
new file mode 100644
index 00000000000..7ff7a937aef
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/IconContent.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
![]()
+
+
+ {{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
+
+
+
+
+
+
+
+
+ {{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
+
+
+
+
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.test.ts
new file mode 100644
index 00000000000..2643291b6d1
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.test.ts
@@ -0,0 +1,204 @@
+import { render } from '@testing-library/vue';
+import { describe, expect, it, vi } from 'vitest';
+
+import NodeIcon from './NodeIcon.vue';
+
+// Mock N8nTooltip component
+vi.mock('../N8nTooltip', () => ({
+ default: {
+ name: 'N8nTooltip',
+ template: '
',
+ props: ['placement', 'disabled'],
+ },
+}));
+
+// Mock N8nIcon component
+vi.mock('../N8nIcon', () => ({
+ default: {
+ name: 'N8nIcon',
+ template: '',
+ props: ['icon'],
+ },
+}));
+
+describe('NodeIcon', () => {
+ describe('icon rendering', () => {
+ it('renders file type icon with image', () => {
+ const { container } = render(NodeIcon, {
+ props: {
+ type: 'file',
+ src: 'https://example.com/icon.png',
+ },
+ });
+
+ const img = container.querySelector('img');
+ expect(img).toBeTruthy();
+ expect(img?.getAttribute('src')).toBe('https://example.com/icon.png');
+ });
+
+ it('renders icon type with supported icon name', () => {
+ const { container } = render(NodeIcon, {
+ props: {
+ type: 'icon',
+ name: 'check',
+ },
+ });
+
+ // N8nIcon should be rendered (via IconContent)
+ const icon = container.querySelector('[data-testid="n8n-icon"]');
+ expect(icon).toBeTruthy();
+ expect(icon?.getAttribute('data-icon')).toBe('check');
+ });
+
+ it('renders unknown type with placeholder', () => {
+ const { getByText } = render(NodeIcon, {
+ props: {
+ type: 'unknown',
+ nodeTypeName: 'TestNode',
+ },
+ });
+
+ expect(getByText('T')).toBeTruthy();
+ });
+
+ it('renders unknown type with question mark when no nodeTypeName', () => {
+ const { getByText } = render(NodeIcon, {
+ props: {
+ type: 'unknown',
+ },
+ });
+
+ expect(getByText('?')).toBeTruthy();
+ });
+ });
+
+ describe('styling', () => {
+ it('applies custom size to wrapper', () => {
+ const { container } = render(NodeIcon, {
+ props: {
+ type: 'file',
+ src: 'test.png',
+ size: 40,
+ },
+ });
+
+ const wrapper = container.querySelector('[class*="nodeIconWrapper"]');
+ expect(wrapper).toBeTruthy();
+ const style = (wrapper as HTMLElement).style;
+ expect(style.width).toBe('40px');
+ expect(style.height).toBe('40px');
+ });
+
+ it('applies custom color', () => {
+ const { container } = render(NodeIcon, {
+ props: {
+ type: 'file',
+ src: 'test.png',
+ color: '#ff0000',
+ },
+ });
+
+ const wrapper = container.querySelector('[class*="nodeIconWrapper"]');
+ const style = (wrapper as HTMLElement).style;
+ expect(style.color).toBe('rgb(255, 0, 0)');
+ });
+
+ it('applies circle class when circle prop is true', () => {
+ const { container } = render(NodeIcon, {
+ props: {
+ type: 'file',
+ src: 'test.png',
+ circle: true,
+ },
+ });
+
+ const wrapper = container.querySelector('[class*="nodeIconWrapper"]');
+ expect(wrapper?.className).toContain('circle');
+ });
+
+ it('applies disabled class when disabled prop is true', () => {
+ const { container } = render(NodeIcon, {
+ props: {
+ type: 'file',
+ src: 'test.png',
+ disabled: true,
+ },
+ });
+
+ const wrapper = container.querySelector('[class*="nodeIconWrapper"]');
+ expect(wrapper?.className).toContain('disabled');
+ });
+ });
+
+ describe('tooltip', () => {
+ it('renders tooltip when showTooltip is true', () => {
+ const { container } = render(NodeIcon, {
+ props: {
+ type: 'file',
+ src: 'test.png',
+ showTooltip: true,
+ nodeTypeName: 'Test Node',
+ },
+ });
+
+ const tooltip = container.querySelector('[data-testid="tooltip"]');
+ expect(tooltip).toBeTruthy();
+ });
+
+ it('does not render tooltip when showTooltip is false', () => {
+ const { container } = render(NodeIcon, {
+ props: {
+ type: 'file',
+ src: 'test.png',
+ showTooltip: false,
+ },
+ });
+
+ const tooltip = container.querySelector('[data-testid="tooltip"]');
+ expect(tooltip).toBeNull();
+ });
+ });
+
+ describe('badge', () => {
+ it('renders badge when badge prop is provided', () => {
+ const { container } = render(NodeIcon, {
+ props: {
+ type: 'file',
+ src: 'test.png',
+ size: 40,
+ badge: {
+ type: 'file',
+ src: 'badge.png',
+ },
+ },
+ });
+
+ const badge = container.querySelector('[class*="badge"]');
+ expect(badge).toBeTruthy();
+ });
+
+ it('does not render badge when badge prop is not provided', () => {
+ const { container } = render(NodeIcon, {
+ props: {
+ type: 'file',
+ src: 'test.png',
+ },
+ });
+
+ const badge = container.querySelector('[class*="badge"]');
+ expect(badge).toBeNull();
+ });
+
+ it('renders placeholder when icon name is not supported', () => {
+ const { getByText } = render(NodeIcon, {
+ props: {
+ type: 'icon',
+ name: 'unsupported-icon-name',
+ nodeTypeName: 'CustomNode',
+ },
+ });
+
+ expect(getByText('C')).toBeTruthy();
+ });
+ });
+});
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.vue b/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.vue
index 46ab08999d7..1655301129a 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.vue
@@ -1,12 +1,9 @@
@@ -100,32 +57,24 @@ const N8nNodeIcon = getCurrentInstance()?.type;
{{ nodeTypeName }}
-
-
![]()
-
-
-
- {{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
-
+
-
-
-
![]()
-
-
-
-
-
-
-
- {{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
-
-
+
@@ -143,40 +92,6 @@ const N8nNodeIcon = getCurrentInstance()?.type;
font-size: 20px;
}
-.icon {
- height: 100%;
- width: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- position: relative;
-
- svg {
- max-width: 100%;
- max-height: 100%;
- }
-
- img,
- svg {
- pointer-events: none;
- }
-}
-.nodeIconPlaceholder {
- text-align: center;
-}
-.nodeIconImage {
- max-width: 100%;
- max-height: 100%;
- width: auto;
- height: auto;
-}
-
-.badge {
- position: absolute;
- background: var(--color-background-node-icon-badge, var(--color--background));
- border-radius: 50%;
-}
-
.circle {
border-radius: 50%;
}
diff --git a/packages/frontend/editor-ui/package.json b/packages/frontend/editor-ui/package.json
index 538acd4ce0f..009dc6d0e50 100644
--- a/packages/frontend/editor-ui/package.json
+++ b/packages/frontend/editor-ui/package.json
@@ -143,11 +143,5 @@
"vitest-mock-extended": "catalog:",
"vue-tsc": "catalog:frontend",
"z-vue-scan": "^0.0.35"
- },
- "peerDependencies": {
- "@fortawesome/fontawesome-svg-core": "*",
- "@fortawesome/free-regular-svg-icons": "*",
- "@fortawesome/free-solid-svg-icons": "*",
- "@fortawesome/vue-fontawesome": "*"
}
}
diff --git a/packages/frontend/editor-ui/src/__tests__/render.ts b/packages/frontend/editor-ui/src/__tests__/render.ts
index 5f7652f5e33..0cd0da7f7e7 100644
--- a/packages/frontend/editor-ui/src/__tests__/render.ts
+++ b/packages/frontend/editor-ui/src/__tests__/render.ts
@@ -2,7 +2,6 @@ import type { Plugin } from 'vue';
import { render, type RenderOptions as TestingLibraryRenderOptions } from '@testing-library/vue';
import { i18nInstance } from '@n8n/i18n';
import { GlobalDirectivesPlugin } from '@/plugins/directives';
-import { FontAwesomePlugin } from '@/plugins/icons';
import { N8nPlugin } from '@n8n/design-system';
import type { Pinia } from 'pinia';
import { PiniaVuePlugin } from 'pinia';
@@ -33,14 +32,7 @@ const defaultOptions = {
},
VueJsonPretty: vueJsonPretty,
},
- plugins: [
- i18nInstance,
- PiniaVuePlugin,
- FontAwesomePlugin,
- N8nPlugin,
- GlobalDirectivesPlugin,
- TelemetryPlugin,
- ],
+ plugins: [i18nInstance, PiniaVuePlugin, N8nPlugin, GlobalDirectivesPlugin, TelemetryPlugin],
},
};
diff --git a/packages/frontend/editor-ui/src/components/CredentialIcon.test.ts b/packages/frontend/editor-ui/src/components/CredentialIcon.test.ts
index 13aae7438b6..0c5e9c6fcc0 100644
--- a/packages/frontend/editor-ui/src/components/CredentialIcon.test.ts
+++ b/packages/frontend/editor-ui/src/components/CredentialIcon.test.ts
@@ -13,7 +13,15 @@ describe('CredentialIcon', () => {
const renderComponent = createComponentRenderer(CredentialIcon, {
pinia: createTestingPinia(),
global: {
- stubs: ['N8nTooltip'],
+ stubs: {
+ N8nTooltip: true,
+ N8nNodeIcon: {
+ template: `
+
+ `,
+ props: ['type', 'src', 'name', 'color', 'size'],
+ },
+ },
},
});
let pinia: TestingPinia;
@@ -50,16 +58,16 @@ describe('CredentialIcon', () => {
}),
]);
- const { getByRole } = renderComponent({
+ const { container } = renderComponent({
pinia,
props: {
credentialTypeName: 'test',
},
});
- const icon = getByRole('img', { hidden: true });
- expect(icon.tagName).toBe('svg');
- expect(icon).toHaveClass('fa-clock');
+ const icon = container.querySelector('.n8n-icon');
+ expect(icon).toBeInTheDocument();
+ expect(icon?.getAttribute('data-icon')).toBe('clock');
});
it('shows correct icon when credential has an icon with node: prefix', () => {
diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/LinkItem.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/LinkItem.vue
index 8b7a2261a4a..73ba2a89cf9 100644
--- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/LinkItem.vue
+++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/LinkItem.vue
@@ -19,13 +19,7 @@ defineProps();
:show-action-arrow="true"
>
-
+
diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/OpenTemplateItem.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/OpenTemplateItem.vue
index b643c5b13a3..5c63f4aef0d 100644
--- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/OpenTemplateItem.vue
+++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/OpenTemplateItem.vue
@@ -20,13 +20,7 @@ defineProps();
:is-trigger="false"
>
-
+
diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/SubcategoryItem.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/SubcategoryItem.vue
index 78adb8ea6e7..a257a32b036 100644
--- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/SubcategoryItem.vue
+++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/SubcategoryItem.vue
@@ -31,7 +31,6 @@ const subcategoryName = computed(() => camelCase(props.item.subcategory || props
:name="item.icon"
:circle="false"
:show-tooltip="false"
- :use-updated-icons="true"
v-bind="item.iconProps"
/>
diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/ViewItem.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/ViewItem.vue
index ec52a6d091a..302973980ca 100644
--- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/ViewItem.vue
+++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/ViewItem.vue
@@ -19,13 +19,7 @@ defineProps();
:show-action-arrow="true"
>
-
+
diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue
index 8253f4eacfe..ae3fbaceb57 100644
--- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue
+++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue
@@ -194,7 +194,6 @@ function onBackButton() {
:circle="false"
:show-tooltip="false"
:size="20"
- :use-updated-icons="true"
/>
@@ -286,6 +285,7 @@ function onBackButton() {
}
.nodeIcon {
--node-icon-size: 20px;
+ --node-icon-color: var(--color--text);
margin-right: var(--spacing--sm);
}
.renderedItems {
diff --git a/packages/frontend/editor-ui/src/components/NodeSettingsHint.vue b/packages/frontend/editor-ui/src/components/NodeSettingsHint.vue
index f6a3eb9a264..d81b0931ff7 100644
--- a/packages/frontend/editor-ui/src/components/NodeSettingsHint.vue
+++ b/packages/frontend/editor-ui/src/components/NodeSettingsHint.vue
@@ -71,8 +71,7 @@ const activeSettings = computed(() => {
-
-
+
{{ setting.message }}
diff --git a/packages/frontend/editor-ui/src/components/PanelDragButtonV2.vue b/packages/frontend/editor-ui/src/components/PanelDragButtonV2.vue
index 4e45f6be2d9..025fc709b95 100644
--- a/packages/frontend/editor-ui/src/components/PanelDragButtonV2.vue
+++ b/packages/frontend/editor-ui/src/components/PanelDragButtonV2.vue
@@ -1,4 +1,5 @@