further NFS icon migration and alignment

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-03-07 16:36:45 +01:00
parent 22f101e208
commit ed8d9ce67c
16 changed files with 95 additions and 16 deletions
@@ -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 = (
<Box style={divStyle}>
<Icon fontSize="small" />
<Box component="span" className={classes.itemIcon}>
<Icon fontSize="inherit" />
</Box>
{!isOpen && hasSubmenu ? <ArrowRightIcon fontSize="small" /> : <></>}
</Box>
);
@@ -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 });
@@ -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;
}
@@ -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;
+2 -2
View File
@@ -43,7 +43,7 @@ const apiDocsNavItem = NavItemBlueprint.make({
params: {
title: 'APIs',
routeRef: rootRoute,
icon: () => <AppIcon id="kind:api" />,
icon: () => <AppIcon fontSize="inherit" id="kind:api" />,
},
});
@@ -211,7 +211,7 @@ const apiDocsApisEntityContent = EntityContentBlueprint.make({
export default createFrontendPlugin({
pluginId: 'api-docs',
title: 'APIs',
icon: <AppIcon id="kind:api" />,
icon: <AppIcon fontSize="inherit" id="kind:api" />,
info: { packageJson: () => import('../package.json') },
routes: {
root: rootRoute,
+1 -1
View File
@@ -102,7 +102,7 @@ export const visualizerPlugin = createFrontendPlugin({
appVisualizerTreePage,
appVisualizerDetailedPage,
appVisualizerTextPage,
appVisualizerNavItem,
// appVisualizerNavItem,
copyTreeAsJson,
],
});
+3
View File
@@ -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 = <NavItemIcon />;
} else if (resolvedIcon) {
icon = resolvedIcon;
} else if (pluginIcon) {
icon = pluginIcon;
}
if (!title || !icon) {
@@ -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,
<Icon />,
]),
),
),
);
},
}),
@@ -69,7 +69,7 @@ export const catalogUnprocessedEntitiesNavItem = NavItemBlueprint.make({
export default createFrontendPlugin({
pluginId: 'catalog-unprocessed-entities',
title: 'Unprocessed Entities',
icon: <QueueIcon />,
icon: <QueueIcon fontSize="inherit" />,
info: { packageJson: () => import('../../package.json') },
routes: {
root: rootRouteRef,
+1 -1
View File
@@ -59,7 +59,7 @@ export const catalogPage = PageBlueprint.makeWithOverrides({
return originalFactory({
path: '/catalog',
routeRef: rootRouteRef,
icon: <CategoryIcon />,
icon: <CategoryIcon fontSize="inherit" />,
title: 'Catalog',
loader: async () => {
const { BaseCatalogPage } = await import('../components/CatalogPage');
+1 -1
View File
@@ -40,7 +40,7 @@ import contextMenuItems from './contextMenuItems';
export default createFrontendPlugin({
pluginId: 'catalog',
title: 'Catalog',
icon: <CategoryIcon />,
icon: <CategoryIcon fontSize="inherit" />,
info: {
packageJson: () => import('../../package.json'),
},
+1 -1
View File
@@ -89,7 +89,7 @@ export const devToolsNavItem = NavItemBlueprint.make({
export default createFrontendPlugin({
pluginId: 'devtools',
title: 'DevTools',
icon: <BuildIcon />,
icon: <BuildIcon fontSize="inherit" />,
info: { packageJson: () => import('../../package.json') },
routes: {
root: rootRouteRef,
+1 -1
View File
@@ -61,7 +61,7 @@ const scaffolderEntityIconLink = EntityIconLinkBlueprint.make({
export default createFrontendPlugin({
pluginId: 'scaffolder',
title: 'Create',
icon: <CreateComponentIcon />,
icon: <CreateComponentIcon fontSize="inherit" />,
info: { packageJson: () => import('../../package.json') },
routes: {
root: rootRouteRef,
+1 -1
View File
@@ -277,7 +277,7 @@ export const searchNavItem = NavItemBlueprint.make({
export default createFrontendPlugin({
pluginId: 'search',
title: 'Search',
icon: <SearchIcon />,
icon: <SearchIcon fontSize="inherit" />,
info: { packageJson: () => import('../package.json') },
extensions: [searchApi, searchPage, searchNavItem],
routes: {
+1 -1
View File
@@ -280,7 +280,7 @@ const techDocsNavItem = NavItemBlueprint.make({
export default createFrontendPlugin({
pluginId: 'techdocs',
title: 'Docs',
icon: <LibraryBooks />,
icon: <LibraryBooks fontSize="inherit" />,
info: { packageJson: () => import('../../package.json') },
extensions: [
techDocsClientApi,
+1 -1
View File
@@ -63,7 +63,7 @@ export const settingsNavItem = NavItemBlueprint.make({
export default createFrontendPlugin({
pluginId: 'user-settings',
title: 'Settings',
icon: <SettingsIcon />,
icon: <SettingsIcon fontSize="inherit" />,
info: { packageJson: () => import('../package.json') },
extensions: [userSettingsPage, settingsNavItem],
routes: {