feat: Roll out Lucide icons to Nodes, remove FontAwesome icons (#20477)

Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
Mutasem Aldmour 2025-10-10 11:22:11 +02:00 committed by GitHub
parent 18a871efe8
commit 596cdfec91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 452 additions and 1124 deletions

View File

@ -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

View File

@ -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,
});

View File

@ -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",

View File

@ -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: [

View File

@ -0,0 +1,112 @@
<script lang="ts" setup>
import { computed } from 'vue';
import N8nNodeIcon from '.';
import N8nIcon from '../N8nIcon';
import type { IconName } from '../N8nIcon/icons';
import { isSupportedIconName } from '../N8nIcon/icons';
type IconType = 'file' | 'icon' | 'unknown';
interface IconContentProps {
type: IconType;
src?: string;
name?: string;
nodeTypeName?: string;
size?: number;
badge?: { src: string; type: IconType };
}
const props = defineProps<IconContentProps>();
const badgeSize = computed((): number => {
switch (props.size) {
case 40:
return 18;
case 24:
return 10;
case 18:
default:
return 12;
}
});
const fontStyleData = computed((): Record<string, string> => {
if (!props.size) {
return {};
}
return {
'max-width': `${props.size}px`,
};
});
const badgeStyleData = computed((): Record<string, string> => {
const size = badgeSize.value;
return {
padding: `${Math.floor(size / 4)}px`,
right: `-${Math.floor(size / 2)}px`,
bottom: `-${Math.floor(size / 2)}px`,
};
});
const supportedIconName = computed((): IconName | undefined => {
return isSupportedIconName(props.name) ? props.name : undefined;
});
</script>
<template>
<div v-if="type !== 'unknown'" :class="$style.icon">
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
<N8nIcon v-else-if="supportedIconName" :icon="supportedIconName" :style="fontStyleData" />
<div v-else :class="$style.nodeIconPlaceholder">
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
</div>
<!-- Badge icon, for example used for HTTP based nodes -->
<div v-if="badge" :class="$style.badge" :style="badgeStyleData">
<N8nNodeIcon :type="badge.type" :src="badge.src" :size="badgeSize" />
</div>
</div>
<div v-else :class="$style.nodeIconPlaceholder">
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
</div>
</template>
<style lang="scss" module>
.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%;
}
</style>

View File

@ -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: '<div data-testid="tooltip"><slot /></div>',
props: ['placement', 'disabled'],
},
}));
// Mock N8nIcon component
vi.mock('../N8nIcon', () => ({
default: {
name: 'N8nIcon',
template: '<span data-testid="n8n-icon" :data-icon="icon"></span>',
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();
});
});
});

View File

@ -1,12 +1,9 @@
<script lang="ts" setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import type { Placement } from 'element-plus';
import { computed, getCurrentInstance } from 'vue';
import { computed } from 'vue';
import N8nIcon from '../N8nIcon';
import type { IconName } from '../N8nIcon/icons';
import { isSupportedIconName } from '../N8nIcon/icons';
import N8nTooltip from '../N8nTooltip';
import IconContent from './IconContent.vue';
type IconType = 'file' | 'icon' | 'unknown';
@ -22,8 +19,6 @@ interface NodeIconProps {
showTooltip?: boolean;
tooltipPosition?: Placement;
badge?: { src: string; type: IconType };
// temporarily until we roll out FA icons for all nodes
useUpdatedIcons?: boolean;
}
const props = withDefaults(defineProps<NodeIconProps>(), {
@ -45,44 +40,6 @@ const iconStyleData = computed((): Record<string, string> => {
'line-height': `${props.size}px`,
};
});
const badgeSize = computed((): number => {
switch (props.size) {
case 40:
return 18;
case 24:
return 10;
case 18:
default:
return 12;
}
});
const fontStyleData = computed((): Record<string, string> => {
if (!props.size) {
return {};
}
return {
'max-width': `${props.size}px`,
};
});
const badgeStyleData = computed((): Record<string, string> => {
const size = badgeSize.value;
return {
padding: `${Math.floor(size / 4)}px`,
right: `-${Math.floor(size / 2)}px`,
bottom: `-${Math.floor(size / 2)}px`,
};
});
const updatedIconName = computed((): IconName | undefined => {
return props.useUpdatedIcons && isSupportedIconName(props.name) ? props.name : undefined;
});
// Get self component to avoid dependency cycle
const N8nNodeIcon = getCurrentInstance()?.type;
</script>
<template>
@ -100,32 +57,24 @@ const N8nNodeIcon = getCurrentInstance()?.type;
<template #content>
{{ nodeTypeName }}
</template>
<div v-if="type !== 'unknown'" :class="$style.icon">
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
<FontAwesomeIcon v-else :icon="`${name}`" :class="$style.iconFa" :style="fontStyleData" />
</div>
<div v-else :class="$style.nodeIconPlaceholder">
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
</div>
<IconContent
:type="type"
:src="src"
:name="name"
:node-type-name="nodeTypeName"
:size="size"
:badge="badge"
/>
</N8nTooltip>
<template v-else>
<div v-if="type !== 'unknown'" :class="$style.icon">
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
<N8nIcon
v-else-if="updatedIconName"
:icon="updatedIconName"
:style="fontStyleData"
size="xlarge"
/>
<FontAwesomeIcon v-else :icon="`${name}`" :style="fontStyleData" />
<div v-if="badge" :class="$style.badge" :style="badgeStyleData">
<N8nNodeIcon :type="badge.type" :src="badge.src" :size="badgeSize" />
</div>
</div>
<div v-else :class="$style.nodeIconPlaceholder">
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
</div>
</template>
<IconContent
v-else
:type="type"
:src="src"
:name="name"
:node-type-name="nodeTypeName"
:size="size"
:badge="badge"
/>
</div>
</div>
</template>
@ -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%;
}

View File

@ -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": "*"
}
}

View File

@ -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],
},
};

View File

@ -13,7 +13,15 @@ describe('CredentialIcon', () => {
const renderComponent = createComponentRenderer(CredentialIcon, {
pinia: createTestingPinia(),
global: {
stubs: ['N8nTooltip'],
stubs: {
N8nTooltip: true,
N8nNodeIcon: {
template: `
<svg v-if="type === 'icon'" class="n8n-icon" :data-icon="name"></svg>
`,
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', () => {

View File

@ -19,13 +19,7 @@ defineProps<Props>();
:show-action-arrow="true"
>
<template #icon>
<N8nNodeIcon
type="icon"
:name="link.icon"
:circle="false"
:show-tooltip="false"
:use-updated-icons="true"
/>
<N8nNodeIcon type="icon" :name="link.icon" :circle="false" :show-tooltip="false" />
</template>
</N8nNodeCreatorNode>
</template>

View File

@ -20,13 +20,7 @@ defineProps<Props>();
:is-trigger="false"
>
<template v-if="openTemplate.icon" #icon>
<N8nNodeIcon
type="icon"
:name="openTemplate.icon"
:circle="false"
:show-tooltip="false"
:use-updated-icons="true"
/>
<N8nNodeIcon type="icon" :name="openTemplate.icon" :circle="false" :show-tooltip="false" />
</template>
<template v-if="openTemplate.nodes" #extraDetails>

View File

@ -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"
/>
</template>

View File

@ -19,13 +19,7 @@ defineProps<Props>();
:show-action-arrow="true"
>
<template #icon>
<N8nNodeIcon
type="icon"
:name="view.icon"
:circle="false"
:show-tooltip="false"
:use-updated-icons="true"
/>
<N8nNodeIcon type="icon" :name="view.icon" :circle="false" :show-tooltip="false" />
</template>
</N8nNodeCreatorNode>
</template>

View File

@ -194,7 +194,6 @@ function onBackButton() {
:circle="false"
:show-tooltip="false"
:size="20"
:use-updated-icons="true"
/>
<p v-if="activeViewStack.title" :class="$style.title" v-text="activeViewStack.title" />
@ -286,6 +285,7 @@ function onBackButton() {
}
.nodeIcon {
--node-icon-size: 20px;
--node-icon-color: var(--color--text);
margin-right: var(--spacing--sm);
}
.renderedItems {

View File

@ -71,8 +71,7 @@ const activeSettings = computed(() => {
<div v-if="activeSettings.length > 0" :class="$style.settingsHint">
<div v-for="setting in activeSettings" :key="setting.key" :class="$style.settingItem">
<div :class="$style.iconWrapper">
<FontAwesomeIcon v-if="setting.icon === 'power'" icon="power" :class="$style.icon" />
<N8nIcon v-else :icon="setting.icon" :class="$style.icon" />
<N8nIcon :icon="setting.icon" :class="$style.icon" />
</div>
<N8nText size="small" :class="$style.message">
{{ setting.message }}

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { N8nIcon } from '@n8n/design-system';
import Draggable from './Draggable.vue';
import type { XYPosition } from '@/Interface';
@ -38,9 +39,9 @@ const onDragStart = () => {
>
<template #default="{ isDragging }">
<button :class="[$style.dragButton, { [$style.dragging]: isDragging }]">
<FontAwesomeIcon v-if="canMoveLeft" :class="$style.arrow" icon="arrow-left" />
<FontAwesomeIcon :class="$style.handle" icon="bars" />
<FontAwesomeIcon v-if="canMoveRight" :class="$style.arrow" icon="arrow-right" />
<N8nIcon v-if="canMoveLeft" :class="$style.arrow" icon="arrow-left" />
<N8nIcon :class="$style.handle" icon="menu" />
<N8nIcon v-if="canMoveRight" :class="$style.arrow" icon="arrow-right" />
</button>
</template>
</Draggable>
@ -65,7 +66,7 @@ const onDragStart = () => {
.arrow {
opacity: 0;
width: 7px;
width: 10px;
}
.handle {

View File

@ -68,7 +68,6 @@ const renderComponent = createComponentRenderer(ResourceLocator, {
ExpressionParameterInput: true,
ParameterIssues: true,
N8nCallout: true,
FontAwesomeIcon: true,
FromAiOverrideField: true,
FromAiOverrideButton: true,
ParameterOverrideSelectableList: true,

View File

@ -8,7 +8,6 @@ import type { usePinnedData } from '@/composables/usePinnedData';
const renderComponent = createComponentRenderer(RunDataPinButton, {
global: {
stubs: ['FontAwesomeIcon'],
plugins: [
createTestingPinia({
initialState: {

View File

@ -210,6 +210,10 @@ describe('VirtualSchema.vue', () => {
DynamicScrollerItem: DynamicScrollerItemStub,
N8nIcon: true,
Notice: NoticeStub,
NodeIcon: {
template: '<node-icon-stub :node-type="nodeType.name" :size="size"></node-icon-stub>',
props: ['node-type', 'size'],
},
},
},
pinia,

View File

@ -45,28 +45,12 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
spin="false"
/>
</div>
<div
class="n8n-node-icon nodeIcon icon nodeIcon icon"
<node-icon-stub
class="icon"
data-v-882a318e=""
>
<div
class="nodeIconWrapper"
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="icon"
>
<img
class="nodeIconImage"
src="/nodes/test-node/icon.svg"
/>
<!--v-if-->
</div>
</div>
</div>
node-type="n8n-nodes-base.set"
size="12"
/>
<div
class="title"
data-v-882a318e=""
@ -333,28 +317,12 @@ exports[`VirtualSchema.vue > renders previous nodes schema for AI tools 1`] = `
spin="false"
/>
</div>
<div
class="n8n-node-icon nodeIcon icon nodeIcon icon"
<node-icon-stub
class="icon"
data-v-882a318e=""
>
<div
class="nodeIconWrapper"
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="icon"
>
<img
class="nodeIconImage"
src="/nodes/test-node/icon.svg"
/>
<!--v-if-->
</div>
</div>
</div>
node-type="n8n-nodes-base.if"
size="12"
/>
<div
class="title"
data-v-882a318e=""
@ -418,28 +386,12 @@ exports[`VirtualSchema.vue > renders schema for empty objects and arrays 1`] = `
spin="false"
/>
</div>
<div
class="n8n-node-icon nodeIcon icon icon-trigger nodeIcon icon icon-trigger"
<node-icon-stub
class="icon icon-trigger"
data-v-882a318e=""
>
<div
class="nodeIconWrapper"
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="icon"
>
<img
class="nodeIconImage"
src="/nodes/test-node/icon.svg"
/>
<!--v-if-->
</div>
</div>
</div>
node-type="n8n-nodes-base.manualTrigger"
size="12"
/>
<div
class="title"
data-v-882a318e=""
@ -1309,28 +1261,12 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
spin="false"
/>
</div>
<div
class="n8n-node-icon nodeIcon icon icon-trigger nodeIcon icon icon-trigger"
<node-icon-stub
class="icon icon-trigger"
data-v-882a318e=""
>
<div
class="nodeIconWrapper"
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="icon"
>
<img
class="nodeIconImage"
src="/nodes/test-node/icon.svg"
/>
<!--v-if-->
</div>
</div>
</div>
node-type="n8n-nodes-base.manualTrigger"
size="12"
/>
<div
class="title"
data-v-882a318e=""
@ -1713,28 +1649,12 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
spin="false"
/>
</div>
<div
class="n8n-node-icon nodeIcon icon nodeIcon icon"
<node-icon-stub
class="icon"
data-v-882a318e=""
>
<div
class="nodeIconWrapper"
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="icon"
>
<img
class="nodeIconImage"
src="/nodes/test-node/icon.svg"
/>
<!--v-if-->
</div>
</div>
</div>
node-type="n8n-nodes-base.set"
size="12"
/>
<div
class="title"
data-v-882a318e=""
@ -1845,28 +1765,12 @@ exports[`VirtualSchema.vue > renders variables and context section 1`] = `
spin="false"
/>
</div>
<div
class="n8n-node-icon nodeIcon icon icon-trigger nodeIcon icon icon-trigger"
<node-icon-stub
class="icon icon-trigger"
data-v-882a318e=""
>
<div
class="nodeIconWrapper"
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="icon"
>
<img
class="nodeIconImage"
src="/nodes/test-node/icon.svg"
/>
<!--v-if-->
</div>
</div>
</div>
node-type="n8n-nodes-base.manualTrigger"
size="12"
/>
<div
class="title"
data-v-882a318e=""
@ -2669,28 +2573,12 @@ exports[`VirtualSchema.vue > should expand all nodes when searching 1`] = `
spin="false"
/>
</div>
<div
class="n8n-node-icon nodeIcon icon icon-trigger nodeIcon icon icon-trigger"
<node-icon-stub
class="icon icon-trigger"
data-v-882a318e=""
>
<div
class="nodeIconWrapper"
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="icon"
>
<img
class="nodeIconImage"
src="/nodes/test-node/icon.svg"
/>
<!--v-if-->
</div>
</div>
</div>
node-type="n8n-nodes-base.manualTrigger"
size="12"
/>
<div
class="title"
data-v-882a318e=""
@ -2808,28 +2696,12 @@ exports[`VirtualSchema.vue > should expand all nodes when searching 1`] = `
spin="false"
/>
</div>
<div
class="n8n-node-icon nodeIcon icon nodeIcon icon"
<node-icon-stub
class="icon"
data-v-882a318e=""
>
<div
class="nodeIconWrapper"
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="icon"
>
<img
class="nodeIconImage"
src="/nodes/test-node/icon.svg"
/>
<!--v-if-->
</div>
</div>
</div>
node-type="n8n-nodes-base.set"
size="12"
/>
<div
class="title"
data-v-882a318e=""

View File

@ -104,7 +104,6 @@ const renderComponent = createComponentRenderer(ExecutionsList, {
params: {},
},
},
stubs: ['FontAwesomeIcon'],
},
});

View File

@ -21,7 +21,6 @@ const globalExecutionsListItemQueuedTooltipRenderSpy = vi.fn();
const renderComponent = createComponentRenderer(GlobalExecutionsListItem, {
global: {
stubs: {
FontAwesomeIcon: true,
N8nTooltip: true,
N8nButton: true,
I18nT: true,

View File

@ -9,8 +9,17 @@ import { fireEvent } from '@testing-library/vue';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { createTestWorkflowObject } from '@/__tests__/mocks';
const stubs = {
NodeIcon: {
template:
'<node-icon-stub :icon-source="iconSource" :size="size" :shrink="shrink" :disabled="disabled"></node-icon-stub>',
props: ['icon-source', 'size', 'shrink', 'disabled'],
},
};
const renderComponent = createComponentRenderer(CanvasNodeDefault, {
global: {
stubs,
provide: {
...createCanvasProvide(),
},
@ -32,6 +41,7 @@ describe('CanvasNodeDefault', () => {
provide: {
...createCanvasNodeProvide(),
},
stubs,
},
});
@ -51,6 +61,7 @@ describe('CanvasNodeDefault', () => {
(inputCount, outputCount, expected) => {
const { getByText } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide({
data: {
@ -78,6 +89,7 @@ describe('CanvasNodeDefault', () => {
it('should apply selected class when node is selected', () => {
const { getByText } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide({ selected: true }),
},
@ -89,6 +101,7 @@ describe('CanvasNodeDefault', () => {
it('should not apply selected class when node is not selected', () => {
const { getByText } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide(),
},
@ -102,6 +115,7 @@ describe('CanvasNodeDefault', () => {
it('should apply disabled class when node is disabled', () => {
const { getByText } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide({
data: {
@ -119,6 +133,7 @@ describe('CanvasNodeDefault', () => {
it('should not apply disabled class when node is enabled', () => {
const { getByText } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide(),
},
@ -130,6 +145,7 @@ describe('CanvasNodeDefault', () => {
it('should render strike-through when node is disabled and has node input and output handles', () => {
const { container } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide({
data: {
@ -162,6 +178,7 @@ describe('CanvasNodeDefault', () => {
it('should apply waiting class when node is waiting', () => {
const { getByText } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide({ data: { execution: { running: true, waiting: '123' } } }),
},
@ -175,6 +192,7 @@ describe('CanvasNodeDefault', () => {
it('should apply running class when node is running', () => {
const { getByText } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide({ data: { execution: { running: true } } }),
},
@ -188,6 +206,7 @@ describe('CanvasNodeDefault', () => {
it('should render configurable node correctly', () => {
const { getByTestId } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide({
data: {
@ -246,6 +265,7 @@ describe('CanvasNodeDefault', () => {
(_, nonMainInputs, expected) => {
const { getByText } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide({
data: {
@ -273,6 +293,7 @@ describe('CanvasNodeDefault', () => {
it('should render configuration node correctly', () => {
const { getByTestId } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide({
data: {
@ -292,6 +313,7 @@ describe('CanvasNodeDefault', () => {
it('should render configurable configuration node correctly', () => {
const { getByTestId } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide({
data: {
@ -313,6 +335,7 @@ describe('CanvasNodeDefault', () => {
it('should render trigger node correctly', () => {
const { getByTestId } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide({
data: {
@ -333,6 +356,7 @@ describe('CanvasNodeDefault', () => {
it('should emit "activate" on double click', async () => {
const { getByText, emitted } = renderComponent({
global: {
stubs,
provide: {
...createCanvasNodeProvide(),
},

View File

@ -7,24 +7,12 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
style="--canvas-node--width: 224px; --canvas-node--height: 96px; --node-icon-size: 40px;"
>
<!--v-if-->
<div
class="n8n-node-icon nodeIcon icon nodeIcon icon"
<node-icon-stub
class="icon"
disabled="false"
shrink="false"
>
<div
class="nodeIconWrapper"
style="width: 40px; height: 40px; font-size: 40px; line-height: 40px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="nodeIconPlaceholder"
>
?
</div>
</div>
</div>
size="40"
/>
<div
class="settingsIcons"
>
@ -60,24 +48,12 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
style="--canvas-node--width: 240px; --canvas-node--height: 80px; --node-icon-size: 30px;"
>
<!--v-if-->
<div
class="n8n-node-icon nodeIcon icon nodeIcon icon"
<node-icon-stub
class="icon"
disabled="false"
shrink="false"
>
<div
class="nodeIconWrapper"
style="width: 30px; height: 30px; font-size: 30px; line-height: 30px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="nodeIconPlaceholder"
>
?
</div>
</div>
</div>
size="30"
/>
<!--v-if-->
<!--v-if-->
<div
@ -106,24 +82,12 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
style="--canvas-node--width: 80px; --canvas-node--height: 80px; --node-icon-size: 30px;"
>
<!--v-if-->
<div
class="n8n-node-icon nodeIcon icon nodeIcon icon"
<node-icon-stub
class="icon"
disabled="false"
shrink="false"
>
<div
class="nodeIconWrapper"
style="width: 30px; height: 30px; font-size: 30px; line-height: 30px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="nodeIconPlaceholder"
>
?
</div>
</div>
</div>
size="30"
/>
<!--v-if-->
<!--v-if-->
<div
@ -152,24 +116,12 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
style="--canvas-node--width: 96px; --canvas-node--height: 96px; --node-icon-size: 40px;"
>
<!--v-if-->
<div
class="n8n-node-icon nodeIcon icon nodeIcon icon"
<node-icon-stub
class="icon"
disabled="false"
shrink="false"
>
<div
class="nodeIconWrapper"
style="width: 40px; height: 40px; font-size: 40px; line-height: 40px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="nodeIconPlaceholder"
>
?
</div>
</div>
</div>
size="40"
/>
<div
class="settingsIcons"
>
@ -205,24 +157,12 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
style="--canvas-node--width: 96px; --canvas-node--height: 96px; --node-icon-size: 40px;"
>
<!--v-if-->
<div
class="n8n-node-icon nodeIcon icon nodeIcon icon"
<node-icon-stub
class="icon"
disabled="false"
shrink="false"
>
<div
class="nodeIconWrapper"
style="width: 40px; height: 40px; font-size: 40px; line-height: 40px;"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<div
class="nodeIconPlaceholder"
>
?
</div>
</div>
</div>
size="40"
/>
<div
class="settingsIcons"
>

View File

@ -22,7 +22,6 @@ import { i18nInstance } from '@n8n/i18n';
import { TelemetryPlugin } from './plugins/telemetry';
import { GlobalComponentsPlugin } from './plugins/components';
import { GlobalDirectivesPlugin } from './plugins/directives';
import { FontAwesomePlugin } from './plugins/icons';
import { createPinia, PiniaVuePlugin } from 'pinia';
import { ChartJSPlugin } from '@/plugins/chartjs';
@ -43,7 +42,6 @@ registerModuleRoutes(router);
app.use(TelemetryPlugin);
app.use(PiniaVuePlugin);
app.use(FontAwesomePlugin);
app.use(GlobalComponentsPlugin);
app.use(GlobalDirectivesPlugin);
app.use(pinia);

File diff suppressed because one or more lines are too long

View File

@ -1,416 +0,0 @@
import type { Plugin } from 'vue';
import { library } from '@fortawesome/fontawesome-svg-core';
import type { IconDefinition, Library } from '@fortawesome/fontawesome-svg-core';
import {
faAngleDoubleLeft,
faAngleDown,
faAngleLeft,
faAngleRight,
faAngleUp,
faArchive,
faArrowLeft,
faArrowRight,
faArrowUp,
faArrowDown,
faAt,
faBan,
faBalanceScaleLeft,
faBars,
faBell,
faBolt,
faBook,
faBoxOpen,
faBug,
faBrain,
faCalculator,
faCalendar,
faCaretDown,
faCaretRight,
faCaretUp,
faChartBar,
faCheck,
faCheckCircle,
faCheckDouble,
faCheckSquare,
faChevronDown,
faChevronUp,
faCircle,
faChevronLeft,
faChevronRight,
faCode,
faCodeBranch,
faCog,
faCogs,
faComment,
faComments,
faCompress,
faClipboardList,
faClock,
faClone,
faCloud,
faCloudDownloadAlt,
faCopy,
faCube,
faCut,
faDatabase,
faDotCircle,
faEdit,
faEllipsisH,
faEllipsisV,
faEnvelope,
faEquals,
faEye,
faEyeSlash,
faExclamationTriangle,
faExpand,
faExpandAlt,
faExternalLinkAlt,
faExchangeAlt,
faFile,
faFileAlt,
faFileArchive,
faFileCode,
faFileDownload,
faFileExport,
faFileImport,
faFilePdf,
faFilter,
faFingerprint,
faFlask,
faFolder,
faFolderOpen,
faFolderPlus,
faFont,
faGlobeAmericas,
faGift,
faGlobe,
faGraduationCap,
faGripLinesVertical,
faGripVertical,
faHandHoldingUsd,
faHandScissors,
faHandPointLeft,
faHandshake,
faUserCheck,
faHashtag,
faHdd,
faHistory,
faHome,
faHourglass,
faImage,
faInbox,
faInfo,
faInfoCircle,
faKey,
faLanguage,
faLayerGroup,
faLink,
faList,
faLightbulb,
faLock,
faMapSigns,
faMousePointer,
faNetworkWired,
faPalette,
faPause,
faPauseCircle,
faPen,
faPencilAlt,
faPlay,
faPlayCircle,
faPlug,
faPlus,
faPlusCircle,
faPlusSquare,
faQuestion,
faQuestionCircle,
faRedo,
faRobot,
faRss,
faSave,
faSatelliteDish,
faSearch,
faSearchMinus,
faSearchPlus,
faServer,
faScrewdriver,
faShare,
faSmile,
faSignInAlt,
faSignOutAlt,
faSlidersH,
faSpinner,
faStop,
faSun,
faSync,
faSyncAlt,
faTable,
faTags,
faTasks,
faTerminal,
faThLarge,
faThumbtack,
faThumbsDown,
faThumbsUp,
faTimes,
faTimesCircle,
faToolbox,
faTrash,
faUndo,
faUnlink,
faUser,
faUserCircle,
faUserFriends,
faUsers,
faVectorSquare,
faVideo,
faTree,
faStickyNote as faSolidStickyNote,
faUserLock,
faGem,
faDownload,
faRemoveFormat,
faTools,
faProjectDiagram,
faStream,
faPowerOff,
faPaperPlane,
faExclamationCircle,
faMinusCircle,
faAdjust,
} from '@fortawesome/free-solid-svg-icons';
import {
faVariable,
faXmark,
faVault,
faRefresh,
faTriangle,
statusCompleted,
statusWaiting,
statusError,
statusCanceled,
statusNew,
statusUnknown,
statusWarning,
faPopOut,
faJSON,
faSchema,
faBinary,
faText,
} from './custom';
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
function addIcon(icon: IconDefinition) {
library.add(icon);
}
// this can be removed once nodes stop using FA icons
export const FontAwesomePlugin: Plugin = {
install: (app) => {
addIcon(faAngleDoubleLeft);
addIcon(faAngleDown);
addIcon(faAngleLeft);
addIcon(faAngleRight);
addIcon(faAngleUp);
addIcon(faArchive);
addIcon(faArrowLeft);
addIcon(faArrowRight);
addIcon(faArrowUp);
addIcon(faArrowDown);
addIcon(faAt);
addIcon(faBan);
addIcon(faBalanceScaleLeft);
addIcon(faBars);
addIcon(faBell);
addIcon(faBolt);
addIcon(faBook);
addIcon(faBoxOpen);
addIcon(faBug);
addIcon(faBrain);
addIcon(faCalculator);
addIcon(faCalendar);
addIcon(faCaretDown);
addIcon(faCaretRight);
addIcon(faCaretUp);
addIcon(faChartBar);
addIcon(faCheck);
addIcon(faCheckCircle);
addIcon(faCheckDouble);
addIcon(faCheckSquare);
addIcon(faChevronLeft);
addIcon(faChevronRight);
addIcon(faChevronDown);
addIcon(faChevronUp);
addIcon(faCircle);
addIcon(faCode);
addIcon(faCodeBranch);
addIcon(faCog);
addIcon(faCogs);
addIcon(faComment);
addIcon(faComments);
addIcon(faCompress);
addIcon(faClipboardList);
addIcon(faClock);
addIcon(faClone);
addIcon(faCloud);
addIcon(faCloudDownloadAlt);
addIcon(faCopy);
addIcon(faCube);
addIcon(faCut);
addIcon(faDatabase);
addIcon(faDotCircle);
addIcon(faGripLinesVertical);
addIcon(faGripVertical);
addIcon(faEdit);
addIcon(faEllipsisH);
addIcon(faEllipsisV);
addIcon(faEnvelope);
addIcon(faEquals);
addIcon(faEye);
addIcon(faEyeSlash);
addIcon(faExclamationTriangle);
addIcon(faExclamationCircle);
addIcon(faExpand);
addIcon(faExpandAlt);
addIcon(faExternalLinkAlt);
addIcon(faExchangeAlt);
addIcon(faFile);
addIcon(faFileAlt);
addIcon(faFileArchive);
addIcon(faFileCode);
addIcon(faFileDownload);
addIcon(faFileExport);
addIcon(faFileImport);
addIcon(faFilePdf);
addIcon(faFilter);
addIcon(faFingerprint);
addIcon(faFlask);
addIcon(faFolder);
addIcon(faFolderOpen);
addIcon(faFolderPlus);
addIcon(faFont);
addIcon(faGift);
addIcon(faGlobe);
addIcon(faGlobeAmericas);
addIcon(faGraduationCap);
addIcon(faHandHoldingUsd);
addIcon(faHandScissors);
addIcon(faHandshake);
addIcon(faHandPointLeft);
addIcon(faHashtag);
addIcon(faUserCheck);
addIcon(faHdd);
addIcon(faHistory);
addIcon(faHome);
addIcon(faHourglass);
addIcon(faImage);
addIcon(faInbox);
addIcon(faInfo);
addIcon(faInfoCircle);
addIcon(faKey);
addIcon(faLanguage);
addIcon(faLayerGroup);
addIcon(faLink);
addIcon(faList);
addIcon(faLightbulb);
addIcon(faLock);
addIcon(faMapSigns);
addIcon(faMousePointer);
addIcon(faNetworkWired);
addIcon(faPalette);
addIcon(faPause);
addIcon(faPauseCircle);
addIcon(faPen);
addIcon(faPencilAlt);
addIcon(faPlay);
addIcon(faPlayCircle);
addIcon(faPlug);
addIcon(faPlus);
addIcon(faPlusCircle);
addIcon(faPlusSquare);
addIcon(faProjectDiagram);
addIcon(faQuestion);
addIcon(faQuestionCircle);
addIcon(faRedo);
addIcon(faRemoveFormat);
addIcon(faRobot);
addIcon(faRss);
addIcon(faSave);
addIcon(faSatelliteDish);
addIcon(faSearch);
addIcon(faSearchMinus);
addIcon(faSearchPlus);
addIcon(faServer);
addIcon(faScrewdriver);
addIcon(faShare);
addIcon(faSmile);
addIcon(faSignInAlt);
addIcon(faSignOutAlt);
addIcon(faSlidersH);
addIcon(faSpinner);
addIcon(faSolidStickyNote);
addIcon(faStickyNote as IconDefinition);
addIcon(faStop);
addIcon(faStream);
addIcon(faSun);
addIcon(faSync);
addIcon(faSyncAlt);
addIcon(faTable);
addIcon(faTags);
addIcon(faTasks);
addIcon(faTerminal);
addIcon(faThLarge);
addIcon(faThumbtack);
addIcon(faThumbsDown);
addIcon(faThumbsUp);
addIcon(faTimes);
addIcon(faTimesCircle);
addIcon(faToolbox);
addIcon(faTools);
addIcon(faTrash);
addIcon(faTriangle);
addIcon(faUndo);
addIcon(faUnlink);
addIcon(faUser);
addIcon(faUserCircle);
addIcon(faUserFriends);
addIcon(faUsers);
addIcon(faVariable);
addIcon(faVault);
addIcon(faVectorSquare);
addIcon(faVideo);
addIcon(faTree);
addIcon(faUserLock);
addIcon(faGem);
addIcon(faXmark);
addIcon(faDownload);
addIcon(faPowerOff);
addIcon(faPaperPlane);
addIcon(faRefresh);
addIcon(faMinusCircle);
addIcon(faAdjust);
// statuses
addIcon(statusCompleted);
addIcon(statusWaiting);
addIcon(statusError);
addIcon(statusCanceled);
addIcon(statusNew);
addIcon(statusUnknown);
addIcon(statusWarning);
addIcon(faPopOut);
addIcon(faSchema);
addIcon(faJSON);
addIcon(faBinary);
addIcon(faText);
app.component('FontAwesomeIcon', FontAwesomeIcon);
},
};
type LibraryWithDefinitions = Library & {
definitions: Record<string, Record<string, IconDefinition>>;
};
export const iconLibrary = library as LibraryWithDefinitions;

View File

@ -2160,15 +2160,6 @@ importers:
packages/frontend/@n8n/design-system:
dependencies:
'@fortawesome/fontawesome-svg-core':
specifier: ^1.2.36
version: 1.2.36
'@fortawesome/free-solid-svg-icons':
specifier: ^5.15.4
version: 5.15.4
'@fortawesome/vue-fontawesome':
specifier: ^3.0.3
version: 3.0.3(@fortawesome/fontawesome-svg-core@1.2.36)(vue@3.5.13(typescript@5.9.2))
'@n8n/composables':
specifier: workspace:*
version: link:../composables
@ -2545,18 +2536,6 @@ importers:
'@dagrejs/dagre':
specifier: ^1.1.4
version: 1.1.4
'@fortawesome/fontawesome-svg-core':
specifier: '*'
version: 1.2.36
'@fortawesome/free-regular-svg-icons':
specifier: '*'
version: 6.2.0
'@fortawesome/free-solid-svg-icons':
specifier: '*'
version: 5.15.4
'@fortawesome/vue-fontawesome':
specifier: '*'
version: 3.0.3(@fortawesome/fontawesome-svg-core@1.2.36)(vue@3.5.13(typescript@5.9.2))
'@lezer/common':
specifier: 1.1.0
version: 1.1.0
@ -4790,32 +4769,6 @@ packages:
'@floating-ui/vue@1.1.6':
resolution: {integrity: sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==}
'@fortawesome/fontawesome-common-types@0.2.36':
resolution: {integrity: sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==}
engines: {node: '>=6'}
'@fortawesome/fontawesome-common-types@6.2.0':
resolution: {integrity: sha512-rBevIsj2nclStJ7AxTdfsa3ovHb1H+qApwrxcTVo+NNdeJiB9V75hsKfrkG5AwNcRUNxrPPiScGYCNmLMoh8pg==}
engines: {node: '>=6'}
'@fortawesome/fontawesome-svg-core@1.2.36':
resolution: {integrity: sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==}
engines: {node: '>=6'}
'@fortawesome/free-regular-svg-icons@6.2.0':
resolution: {integrity: sha512-M1dG+PAmkYMTL9BSUHFXY5oaHwBYfHCPhbJ8qj8JELsc9XCrUJ6eEHWip4q0tE+h9C0DVyFkwIM9t7QYyCpprQ==}
engines: {node: '>=6'}
'@fortawesome/free-solid-svg-icons@5.15.4':
resolution: {integrity: sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==}
engines: {node: '>=6'}
'@fortawesome/vue-fontawesome@3.0.3':
resolution: {integrity: sha512-KCPHi9QemVXGMrfuwf3nNnNo129resAIQWut9QTAMXmXqL2ErABC6ohd2yY5Ipq0CLWNbKHk8TMdTXL/Zf3ZhA==}
peerDependencies:
'@fortawesome/fontawesome-svg-core': ~1 || ~6
vue: '>= 3.0.0 < 4'
'@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
@ -19448,27 +19401,6 @@ snapshots:
- '@vue/composition-api'
- vue
'@fortawesome/fontawesome-common-types@0.2.36': {}
'@fortawesome/fontawesome-common-types@6.2.0': {}
'@fortawesome/fontawesome-svg-core@1.2.36':
dependencies:
'@fortawesome/fontawesome-common-types': 0.2.36
'@fortawesome/free-regular-svg-icons@6.2.0':
dependencies:
'@fortawesome/fontawesome-common-types': 6.2.0
'@fortawesome/free-solid-svg-icons@5.15.4':
dependencies:
'@fortawesome/fontawesome-common-types': 0.2.36
'@fortawesome/vue-fontawesome@3.0.3(@fortawesome/fontawesome-svg-core@1.2.36)(vue@3.5.13(typescript@5.9.2))':
dependencies:
'@fortawesome/fontawesome-svg-core': 1.2.36
vue: 3.5.13(typescript@5.9.2)
'@gar/promisify@1.1.3':
optional: true