From ed8d9ce67cbd95ccea4ad19386b549f6380c0f36 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 7 Mar 2026 16:36:45 +0100 Subject: [PATCH 1/6] further NFS icon migration and alignment Signed-off-by: Patrik Oldsberg --- .../src/layout/Sidebar/Items.tsx | 15 ++++++++++- .../IconsApi/DefaultIconsApi.test.ts | 27 ++++++++++++++++++- .../IconsApi/DefaultIconsApi.ts | 26 +++++++++++++++++- .../frontend-plugin-api/src/icons/types.ts | 8 +++++- plugins/api-docs/src/alpha.tsx | 4 +-- plugins/app-visualizer/src/plugin.tsx | 2 +- plugins/app/src/extensions/AppNav.tsx | 3 +++ .../extensions/{IconsApi.ts => IconsApi.tsx} | 10 ++++++- .../src/alpha/plugin.tsx | 2 +- plugins/catalog/src/alpha/pages.tsx | 2 +- plugins/catalog/src/alpha/plugin.tsx | 2 +- plugins/devtools/src/alpha/plugin.tsx | 2 +- plugins/scaffolder/src/alpha/plugin.tsx | 2 +- plugins/search/src/alpha.tsx | 2 +- plugins/techdocs/src/alpha/index.tsx | 2 +- plugins/user-settings/src/alpha.tsx | 2 +- 16 files changed, 95 insertions(+), 16 deletions(-) rename plugins/app/src/extensions/{IconsApi.ts => IconsApi.tsx} (85%) diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index efe1f97dfb..984488cdbb 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -116,6 +116,17 @@ const makeSidebarStyles = (sidebarConfig: SidebarConfig) => font: 'inherit', textTransform: 'none', }, + itemIcon: { + display: 'inline-flex', + fontSize: theme.typography.fontSize, + lineHeight: 0, + '& svg': { + width: '1.5em', + height: '1.5em', + fontSize: 'inherit', + flexShrink: 0, + }, + }, closed: { width: sidebarConfig.drawerWidthClosed, justifyContent: 'center', @@ -401,7 +412,9 @@ const SidebarItemBase = forwardRef< const displayItemIcon = ( - + + + {!isOpen && hasSubmenu ? : <>} ); diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts index cfe6236b95..b739eea09a 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts @@ -59,6 +59,10 @@ describe('DefaultIconsApi', () => { expect(result).toBeTruthy(); // @ts-expect-error accessing internal React element structure expect(result.type).toBe(MyIcon); + // @ts-expect-error accessing internal React element structure + expect(result.props.fontSize).toBe('inherit'); + // @ts-expect-error accessing internal React element structure + expect(result.props.size).toBe('1em'); }); it('should wrap IconElement values in a component for getIcon()', () => { @@ -69,10 +73,31 @@ describe('DefaultIconsApi', () => { expect(icon).toBeDefined(); expect(typeof icon).toBe('function'); // @ts-expect-error testing runtime behavior - expect(icon({})).toBe(element); + const result = icon({}); + // @ts-expect-error accessing internal React element structure + expect(result.type).toBe('span'); + // @ts-expect-error accessing internal React element structure + expect(result.props.style).toEqual({ + display: 'inline-flex', + fontSize: '1.5rem', + lineHeight: 0, + }); + // @ts-expect-error accessing internal React element structure + expect(result.props.children).toBe(element); expect(api.getIcon('myIcon')).toBe(icon); }); + it('should honor fontSize for getIcon()', () => { + const element = createElement('svg'); + const api = new DefaultIconsApi({ myIcon: element }); + const icon = api.getIcon('myIcon'); + + // @ts-expect-error testing runtime behavior + const result = icon({ fontSize: 'small' }); + // @ts-expect-error accessing internal React element structure + expect(result.props.style.fontSize).toBe('1.25rem'); + }); + it('should wrap null IconElement in a component for getIcon()', () => { const api = new DefaultIconsApi({ empty: null }); diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts index cc5ef69b6f..1dfdfbdc9e 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts @@ -21,6 +21,13 @@ import { } from '@backstage/frontend-plugin-api'; import { createElement, isValidElement } from 'react'; +const legacyFontSizeMap = { + inherit: 'inherit', + small: '1.25rem', + medium: '1.5rem', + large: '2.1875rem', +} as const; + /** * Implementation for the {@link IconsApi} * @@ -65,7 +72,24 @@ export class DefaultIconsApi implements IconsApi { if (el === undefined) { return undefined; } - component = () => el; + component = ({ fontSize = 'medium' }) => { + if (el === null) { + return null; + } + + return createElement( + // eslint-disable-next-line react/forbid-elements + 'span', + { + style: { + display: 'inline-flex', + fontSize: legacyFontSizeMap[fontSize], + lineHeight: 0, + }, + }, + el, + ); + }; this.#components.set(key, component); return component; } diff --git a/packages/frontend-plugin-api/src/icons/types.ts b/packages/frontend-plugin-api/src/icons/types.ts index 1a45bcd8fc..68ae60a2f6 100644 --- a/packages/frontend-plugin-api/src/icons/types.ts +++ b/packages/frontend-plugin-api/src/icons/types.ts @@ -38,12 +38,18 @@ export type IconComponent = ComponentType<{ }>; /** - * The type used for icon elements throughout Backstage. + * The type used for icon elements throughout Backstage. It is recommended to + * use icons from `@remixicon/react`. * * @remarks * * Icons should be exactly 24x24 pixels in size. * + * Using icons from `@remixicon/react` is preferred, but using icons from + * `@material-ui/icons` or `AppIcon` and its variants from + * `@backstage/core-components` is supported but depreceated. When using these + * icons, you must set the `fontSize` to `'inherit'`. + * * @public */ export type IconElement = JSX.Element | null; diff --git a/plugins/api-docs/src/alpha.tsx b/plugins/api-docs/src/alpha.tsx index 4afe97dee9..2724619a68 100644 --- a/plugins/api-docs/src/alpha.tsx +++ b/plugins/api-docs/src/alpha.tsx @@ -43,7 +43,7 @@ const apiDocsNavItem = NavItemBlueprint.make({ params: { title: 'APIs', routeRef: rootRoute, - icon: () => , + icon: () => , }, }); @@ -211,7 +211,7 @@ const apiDocsApisEntityContent = EntityContentBlueprint.make({ export default createFrontendPlugin({ pluginId: 'api-docs', title: 'APIs', - icon: , + icon: , info: { packageJson: () => import('../package.json') }, routes: { root: rootRoute, diff --git a/plugins/app-visualizer/src/plugin.tsx b/plugins/app-visualizer/src/plugin.tsx index 9fa7b2cd72..869f64db39 100644 --- a/plugins/app-visualizer/src/plugin.tsx +++ b/plugins/app-visualizer/src/plugin.tsx @@ -102,7 +102,7 @@ export const visualizerPlugin = createFrontendPlugin({ appVisualizerTreePage, appVisualizerDetailedPage, appVisualizerTextPage, - appVisualizerNavItem, + // appVisualizerNavItem, copyTreeAsJson, ], }); diff --git a/plugins/app/src/extensions/AppNav.tsx b/plugins/app/src/extensions/AppNav.tsx index ffa6ced7b0..129a94cd9e 100644 --- a/plugins/app/src/extensions/AppNav.tsx +++ b/plugins/app/src/extensions/AppNav.tsx @@ -172,6 +172,7 @@ function NavContentRenderer(props: { // We want the priority: page (config/params) -> nav item -> plugin -> pluginId const resolvedTitle = node.instance.getData(coreExtensionData.title); const pluginTitle = node.spec.plugin.title; + const pluginIcon = node.spec.plugin.icon; const pluginId = node.spec.plugin.pluginId; const hasExplicitPageTitle = resolvedTitle !== undefined && @@ -194,6 +195,8 @@ function NavContentRenderer(props: { icon = ; } else if (resolvedIcon) { icon = resolvedIcon; + } else if (pluginIcon) { + icon = pluginIcon; } if (!title || !icon) { diff --git a/plugins/app/src/extensions/IconsApi.ts b/plugins/app/src/extensions/IconsApi.tsx similarity index 85% rename from plugins/app/src/extensions/IconsApi.ts rename to plugins/app/src/extensions/IconsApi.tsx index d8a97eb7c7..6293c84ed8 100644 --- a/plugins/app/src/extensions/IconsApi.ts +++ b/plugins/app/src/extensions/IconsApi.tsx @@ -45,7 +45,15 @@ export const IconsApi = ApiBlueprint.makeWithOverrides({ return new DefaultIconsApi( inputs.icons .map(i => i.get(IconBundleBlueprint.dataRefs.icons)) - .reduce((acc, bundle) => ({ ...acc, ...bundle }), defaultIcons), + .reduce( + (acc, bundle) => ({ ...acc, ...bundle }), + Object.fromEntries( + Object.entries(defaultIcons).map(([key, Icon]) => [ + key, + , + ]), + ), + ), ); }, }), diff --git a/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx b/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx index 8fd614c886..d2987f3197 100644 --- a/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx +++ b/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx @@ -69,7 +69,7 @@ export const catalogUnprocessedEntitiesNavItem = NavItemBlueprint.make({ export default createFrontendPlugin({ pluginId: 'catalog-unprocessed-entities', title: 'Unprocessed Entities', - icon: , + icon: , info: { packageJson: () => import('../../package.json') }, routes: { root: rootRouteRef, diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index ab71cf39dc..6d64c65b74 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -59,7 +59,7 @@ export const catalogPage = PageBlueprint.makeWithOverrides({ return originalFactory({ path: '/catalog', routeRef: rootRouteRef, - icon: , + icon: , title: 'Catalog', loader: async () => { const { BaseCatalogPage } = await import('../components/CatalogPage'); diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index 8db026016a..243b93db9b 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -40,7 +40,7 @@ import contextMenuItems from './contextMenuItems'; export default createFrontendPlugin({ pluginId: 'catalog', title: 'Catalog', - icon: , + icon: , info: { packageJson: () => import('../../package.json'), }, diff --git a/plugins/devtools/src/alpha/plugin.tsx b/plugins/devtools/src/alpha/plugin.tsx index b1cfa7027f..b991e34bf9 100644 --- a/plugins/devtools/src/alpha/plugin.tsx +++ b/plugins/devtools/src/alpha/plugin.tsx @@ -89,7 +89,7 @@ export const devToolsNavItem = NavItemBlueprint.make({ export default createFrontendPlugin({ pluginId: 'devtools', title: 'DevTools', - icon: , + icon: , info: { packageJson: () => import('../../package.json') }, routes: { root: rootRouteRef, diff --git a/plugins/scaffolder/src/alpha/plugin.tsx b/plugins/scaffolder/src/alpha/plugin.tsx index 374249a804..0c105f3a7c 100644 --- a/plugins/scaffolder/src/alpha/plugin.tsx +++ b/plugins/scaffolder/src/alpha/plugin.tsx @@ -61,7 +61,7 @@ const scaffolderEntityIconLink = EntityIconLinkBlueprint.make({ export default createFrontendPlugin({ pluginId: 'scaffolder', title: 'Create', - icon: , + icon: , info: { packageJson: () => import('../../package.json') }, routes: { root: rootRouteRef, diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index a2ecb61e0e..9625f5e946 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -277,7 +277,7 @@ export const searchNavItem = NavItemBlueprint.make({ export default createFrontendPlugin({ pluginId: 'search', title: 'Search', - icon: , + icon: , info: { packageJson: () => import('../package.json') }, extensions: [searchApi, searchPage, searchNavItem], routes: { diff --git a/plugins/techdocs/src/alpha/index.tsx b/plugins/techdocs/src/alpha/index.tsx index 6acc6b5e6d..1ec92edfd3 100644 --- a/plugins/techdocs/src/alpha/index.tsx +++ b/plugins/techdocs/src/alpha/index.tsx @@ -280,7 +280,7 @@ const techDocsNavItem = NavItemBlueprint.make({ export default createFrontendPlugin({ pluginId: 'techdocs', title: 'Docs', - icon: , + icon: , info: { packageJson: () => import('../../package.json') }, extensions: [ techDocsClientApi, diff --git a/plugins/user-settings/src/alpha.tsx b/plugins/user-settings/src/alpha.tsx index c0342a9b46..87b858fcec 100644 --- a/plugins/user-settings/src/alpha.tsx +++ b/plugins/user-settings/src/alpha.tsx @@ -63,7 +63,7 @@ export const settingsNavItem = NavItemBlueprint.make({ export default createFrontendPlugin({ pluginId: 'user-settings', title: 'Settings', - icon: , + icon: , info: { packageJson: () => import('../package.json') }, extensions: [userSettingsPage, settingsNavItem], routes: { From 3f36ce12572a5d27749f79615375a4886ac1c7aa Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 7 Mar 2026 18:38:02 +0100 Subject: [PATCH 2/6] Clarify icon sizing rules for NFS icons Document the IconElement sizing contract, ensure deprecated icon component registrations inherit size correctly, and add changesets for the affected icon migration packages. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/nfs-icon-alpha-plugins.md | 13 +++++++++++++ .changeset/nfs-icon-foundations.md | 7 +++++++ .../IconsApi/DefaultIconsApi.test.ts | 2 -- .../implementations/IconsApi/DefaultIconsApi.ts | 5 ++++- packages/frontend-plugin-api/src/icons/types.ts | 13 +++++++------ plugins/app/src/extensions/IconsApi.tsx | 10 +--------- 6 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 .changeset/nfs-icon-alpha-plugins.md create mode 100644 .changeset/nfs-icon-foundations.md diff --git a/.changeset/nfs-icon-alpha-plugins.md b/.changeset/nfs-icon-alpha-plugins.md new file mode 100644 index 0000000000..067feb2714 --- /dev/null +++ b/.changeset/nfs-icon-alpha-plugins.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-api-docs': patch +'@backstage/plugin-app-visualizer': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-unprocessed-entities': patch +'@backstage/plugin-devtools': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-search': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-user-settings': patch +--- + +Updated alpha plugin icons to follow the new frontend icon sizing rules when rendered in plugin and navigation surfaces. diff --git a/.changeset/nfs-icon-foundations.md b/.changeset/nfs-icon-foundations.md new file mode 100644 index 0000000000..b36ee47573 --- /dev/null +++ b/.changeset/nfs-icon-foundations.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-components': patch +'@backstage/frontend-app-api': patch +'@backstage/frontend-plugin-api': patch +--- + +Clarified the `IconElement` sizing contract for the new frontend system and aligned legacy system icon rendering with the new icon API. diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts index b739eea09a..87f884d2f2 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts @@ -61,8 +61,6 @@ describe('DefaultIconsApi', () => { expect(result.type).toBe(MyIcon); // @ts-expect-error accessing internal React element structure expect(result.props.fontSize).toBe('inherit'); - // @ts-expect-error accessing internal React element structure - expect(result.props.size).toBe('1em'); }); it('should wrap IconElement values in a component for getIcon()', () => { diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts index 1dfdfbdc9e..9d36ee72d3 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts @@ -46,7 +46,10 @@ export class DefaultIconsApi implements IconsApi { return [key, value]; } deprecatedKeys.push(key); - return [key, createElement(value as IconComponent)]; + return [ + key, + createElement(value as IconComponent, { fontSize: 'inherit' }), + ]; }), ); diff --git a/packages/frontend-plugin-api/src/icons/types.ts b/packages/frontend-plugin-api/src/icons/types.ts index 68ae60a2f6..180763ad09 100644 --- a/packages/frontend-plugin-api/src/icons/types.ts +++ b/packages/frontend-plugin-api/src/icons/types.ts @@ -38,17 +38,18 @@ export type IconComponent = ComponentType<{ }>; /** - * The type used for icon elements throughout Backstage. It is recommended to - * use icons from `@remixicon/react`. + * The type used for icon elements throughout Backstage. * * @remarks * - * Icons should be exactly 24x24 pixels in size. + * Icon elements should behave like rendering a plain icon directly, for example + * from `@remixicon/react`, and are expected to be sized by the surrounding UI. + * Icons should be exactly 24x24 pixels in size by default. * - * Using icons from `@remixicon/react` is preferred, but using icons from + * Using icons from `@remixicon/react` is preferred. Using icons from * `@material-ui/icons` or `AppIcon` and its variants from - * `@backstage/core-components` is supported but depreceated. When using these - * icons, you must set the `fontSize` to `'inherit'`. + * `@backstage/core-components` is supported while migrating, but deprecated. + * When using those icons, you must set `fontSize="inherit"` on the element. * * @public */ diff --git a/plugins/app/src/extensions/IconsApi.tsx b/plugins/app/src/extensions/IconsApi.tsx index 6293c84ed8..d8a97eb7c7 100644 --- a/plugins/app/src/extensions/IconsApi.tsx +++ b/plugins/app/src/extensions/IconsApi.tsx @@ -45,15 +45,7 @@ export const IconsApi = ApiBlueprint.makeWithOverrides({ return new DefaultIconsApi( inputs.icons .map(i => i.get(IconBundleBlueprint.dataRefs.icons)) - .reduce( - (acc, bundle) => ({ ...acc, ...bundle }), - Object.fromEntries( - Object.entries(defaultIcons).map(([key, Icon]) => [ - key, - , - ]), - ), - ), + .reduce((acc, bundle) => ({ ...acc, ...bundle }), defaultIcons), ); }, }), From c0ab3763e553cb58cb6d87accaa5471303019bfb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 14:16:26 +0100 Subject: [PATCH 3/6] Fix tsc errors and add missing changeset Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/nfs-icon-plugin-app.md | 5 +++++ .../IconsApi/DefaultIconsApi.test.ts | 4 ---- plugins/app-visualizer/report.api.md | 22 ------------------- 3 files changed, 5 insertions(+), 26 deletions(-) create mode 100644 .changeset/nfs-icon-plugin-app.md diff --git a/.changeset/nfs-icon-plugin-app.md b/.changeset/nfs-icon-plugin-app.md new file mode 100644 index 0000000000..0f6280a6fb --- /dev/null +++ b/.changeset/nfs-icon-plugin-app.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app': patch +--- + +The app nav now falls back to `plugin.icon` for navigation items that don't have an explicit icon set. diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts index 87f884d2f2..310e9e0184 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts @@ -72,15 +72,12 @@ describe('DefaultIconsApi', () => { expect(typeof icon).toBe('function'); // @ts-expect-error testing runtime behavior const result = icon({}); - // @ts-expect-error accessing internal React element structure expect(result.type).toBe('span'); - // @ts-expect-error accessing internal React element structure expect(result.props.style).toEqual({ display: 'inline-flex', fontSize: '1.5rem', lineHeight: 0, }); - // @ts-expect-error accessing internal React element structure expect(result.props.children).toBe(element); expect(api.getIcon('myIcon')).toBe(icon); }); @@ -92,7 +89,6 @@ describe('DefaultIconsApi', () => { // @ts-expect-error testing runtime behavior const result = icon({ fontSize: 'small' }); - // @ts-expect-error accessing internal React element structure expect(result.props.style.fontSize).toBe('1.25rem'); }); diff --git a/plugins/app-visualizer/report.api.md b/plugins/app-visualizer/report.api.md index 9ee06eccb0..ad8d78c6cb 100644 --- a/plugins/app-visualizer/report.api.md +++ b/plugins/app-visualizer/report.api.md @@ -8,7 +8,6 @@ import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; -import { IconComponent } from '@backstage/frontend-plugin-api'; import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; @@ -20,27 +19,6 @@ const visualizerPlugin: OverridableFrontendPlugin< {}, {}, { - 'nav-item:app-visualizer': OverridableExtensionDefinition<{ - kind: 'nav-item'; - name: undefined; - config: {}; - configInput: {}; - output: ExtensionDataRef< - { - title: string; - icon: IconComponent; - routeRef: RouteRef; - }, - 'core.nav-item.target', - {} - >; - inputs: {}; - params: { - title: string; - icon: IconComponent; - routeRef: RouteRef; - }; - }>; 'page:app-visualizer': OverridableExtensionDefinition<{ kind: 'page'; name: undefined; From 80fed0e8f0f607ffb4dae9efd9dffc52d87ade65 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 19:42:15 +0100 Subject: [PATCH 4/6] Preserve CSS sizing for translated system icons. Keep the original icon element as the rendered root so legacy MUI-backed icons can still be styled through CSS like other Backstage UI icons. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../IconsApi/DefaultIconsApi.test.ts | 28 ++++++++--- .../IconsApi/DefaultIconsApi.ts | 47 ++++++++++++++----- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts index 310e9e0184..558c160ecb 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts @@ -73,12 +73,8 @@ describe('DefaultIconsApi', () => { // @ts-expect-error testing runtime behavior const result = icon({}); expect(result.type).toBe('span'); - expect(result.props.style).toEqual({ - display: 'inline-flex', - fontSize: '1.5rem', - lineHeight: 0, - }); - expect(result.props.children).toBe(element); + expect(result.props.style).toEqual({ fontSize: '1.5rem' }); + expect(result.props.children).toBe(element.props.children); expect(api.getIcon('myIcon')).toBe(icon); }); @@ -92,6 +88,26 @@ describe('DefaultIconsApi', () => { expect(result.props.style.fontSize).toBe('1.25rem'); }); + it('should forward runtime props to the original icon element', () => { + const element = createElement('svg', { + className: 'existing', + style: { color: 'red' }, + }); + const api = new DefaultIconsApi({ myIcon: element }); + const icon = api.getIcon('myIcon'); + + // @ts-expect-error testing runtime behavior + const result = icon({ className: 'extra', style: { width: '2em' } }); + + expect(result.type).toBe('svg'); + expect(result.props.className).toBe('existing extra'); + expect(result.props.style).toEqual({ + color: 'red', + fontSize: '1.5rem', + width: '2em', + }); + }); + it('should wrap null IconElement in a component for getIcon()', () => { const api = new DefaultIconsApi({ empty: null }); diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts index 9d36ee72d3..52ac07551a 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts @@ -19,7 +19,7 @@ import { IconElement, IconsApi, } from '@backstage/frontend-plugin-api'; -import { createElement, isValidElement } from 'react'; +import { cloneElement, createElement, isValidElement } from 'react'; const legacyFontSizeMap = { inherit: 'inherit', @@ -28,6 +28,14 @@ const legacyFontSizeMap = { large: '2.1875rem', } as const; +function mergeClassNames(...classNames: Array) { + const merged = classNames.filter(Boolean).join(' '); + if (merged) { + return merged; + } + return undefined; +} + /** * Implementation for the {@link IconsApi} * @@ -75,23 +83,36 @@ export class DefaultIconsApi implements IconsApi { if (el === undefined) { return undefined; } - component = ({ fontSize = 'medium' }) => { + component = props => { if (el === null) { return null; } - return createElement( - // eslint-disable-next-line react/forbid-elements - 'span', - { - style: { - display: 'inline-flex', - fontSize: legacyFontSizeMap[fontSize], - lineHeight: 0, - }, + const { + fontSize = 'medium', + className, + style, + ...rest + } = props as { + fontSize?: keyof typeof legacyFontSizeMap; + className?: string; + style?: Record; + } & Record; + + const elementProps = el.props as { + className?: string; + style?: Record; + }; + + return cloneElement(el, { + ...rest, + className: mergeClassNames(elementProps.className, className), + style: { + ...elementProps.style, + fontSize: legacyFontSizeMap[fontSize], + ...style, }, - el, - ); + }); }; this.#components.set(key, component); return component; From db69a544194f1760da442f1ae133e34a5f515ae4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 21:37:00 +0100 Subject: [PATCH 5/6] Restore the App Visualizer nav item. Add the App Visualizer navigation item back to the plugin extension list and include the regenerated API report for the restored public surface. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- plugins/app-visualizer/report.api.md | 22 ++++++++++++++++++++++ plugins/app-visualizer/src/plugin.tsx | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/plugins/app-visualizer/report.api.md b/plugins/app-visualizer/report.api.md index ad8d78c6cb..9ee06eccb0 100644 --- a/plugins/app-visualizer/report.api.md +++ b/plugins/app-visualizer/report.api.md @@ -8,6 +8,7 @@ import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { IconComponent } from '@backstage/frontend-plugin-api'; import { IconElement } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; @@ -19,6 +20,27 @@ const visualizerPlugin: OverridableFrontendPlugin< {}, {}, { + 'nav-item:app-visualizer': OverridableExtensionDefinition<{ + kind: 'nav-item'; + name: undefined; + config: {}; + configInput: {}; + output: ExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >; + inputs: {}; + params: { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }; + }>; 'page:app-visualizer': OverridableExtensionDefinition<{ kind: 'page'; name: undefined; diff --git a/plugins/app-visualizer/src/plugin.tsx b/plugins/app-visualizer/src/plugin.tsx index 869f64db39..9fa7b2cd72 100644 --- a/plugins/app-visualizer/src/plugin.tsx +++ b/plugins/app-visualizer/src/plugin.tsx @@ -102,7 +102,7 @@ export const visualizerPlugin = createFrontendPlugin({ appVisualizerTreePage, appVisualizerDetailedPage, appVisualizerTextPage, - // appVisualizerNavItem, + appVisualizerNavItem, copyTreeAsJson, ], }); From 4581c003f934be7427f87efe6c9c708a6536f91d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Mar 2026 21:57:43 +0100 Subject: [PATCH 6/6] Apply suggestions from code review Co-authored-by: Patrik Oldsberg Signed-off-by: Patrik Oldsberg --- .changeset/nfs-icon-alpha-plugins.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/nfs-icon-alpha-plugins.md b/.changeset/nfs-icon-alpha-plugins.md index 067feb2714..42c22bbd6e 100644 --- a/.changeset/nfs-icon-alpha-plugins.md +++ b/.changeset/nfs-icon-alpha-plugins.md @@ -1,6 +1,5 @@ --- '@backstage/plugin-api-docs': patch -'@backstage/plugin-app-visualizer': patch '@backstage/plugin-catalog': patch '@backstage/plugin-catalog-unprocessed-entities': patch '@backstage/plugin-devtools': patch