diff --git a/workspaces/theme/.changeset/nasty-bears-relate.md b/workspaces/theme/.changeset/nasty-bears-relate.md new file mode 100644 index 0000000000..3584d20792 --- /dev/null +++ b/workspaces/theme/.changeset/nasty-bears-relate.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-theme': patch +--- + +Align the navigation sidebar with merged `palette.navigation` and `rhdh.general` colors, including submenu rows and selected/active `BackstageSidebarItem` states. Add `rhdh.general.pageInsetBackgroundColor` so the page inset shell can use its own color (defaults match the previous app bar fill; falls back to `appBarBackgroundColor` when unset). Main content area remains on `mainSectionBackgroundColor`. diff --git a/workspaces/theme/plugins/theme/report.api.md b/workspaces/theme/plugins/theme/report.api.md index 3d3a33a668..9aded93861 100644 --- a/workspaces/theme/plugins/theme/report.api.md +++ b/workspaces/theme/plugins/theme/report.api.md @@ -54,51 +54,52 @@ export const lightThemeProvider: (props: { // @public (undocumented) export interface RHDHThemePalette { - // (undocumented) - cards?: { - headerTextColor: string; - headerBackgroundColor: string; - headerBackgroundImage: string; - }; - // (undocumented) - general: { - pageInset: string; - disabled: string; - disabledBackground: string; - paperBackgroundImage: string; - paperBorderColor: string; - popoverBoxShadow: string; - cardBackgroundColor: string; - cardBorderColor: string; - mainSectionBackgroundColor: string; - formControlBackgroundColor: string; - sidebarBackgroundColor: string; - sidebarDividerColor: string; - sidebarItemSelectedBackgroundColor: string; - tableTitleColor: string; - tableSubtitleColor: string; - tableColumnTitleColor: string; - tableRowHover: string; - tableBorderColor: string; - tableBackgroundColor: string; - tabsLinkHoverBackgroundColor: string; - contrastText: string; - appBarBackgroundScheme: 'light' | 'dark'; - appBarBackgroundColor: string; - appBarForegroundColor: string; - appBarBackgroundImage: string; - starredItemsColor: string; - }; - // (undocumented) - primary: { - main: string; - focusVisibleBorder: string; - }; - // (undocumented) - secondary: { - main: string; - focusVisibleBorder: string; - }; + // (undocumented) + cards?: { + headerTextColor: string; + headerBackgroundColor: string; + headerBackgroundImage: string; + }; + // (undocumented) + general: { + pageInset: string; + pageInsetBackgroundColor: string; + disabled: string; + disabledBackground: string; + paperBackgroundImage: string; + paperBorderColor: string; + popoverBoxShadow: string; + cardBackgroundColor: string; + cardBorderColor: string; + mainSectionBackgroundColor: string; + formControlBackgroundColor: string; + sidebarBackgroundColor: string; + sidebarDividerColor: string; + sidebarItemSelectedBackgroundColor: string; + tableTitleColor: string; + tableSubtitleColor: string; + tableColumnTitleColor: string; + tableRowHover: string; + tableBorderColor: string; + tableBackgroundColor: string; + tabsLinkHoverBackgroundColor: string; + contrastText: string; + appBarBackgroundScheme: 'light' | 'dark'; + appBarBackgroundColor: string; + appBarForegroundColor: string; + appBarBackgroundImage: string; + starredItemsColor: string; + }; + // (undocumented) + primary: { + main: string; + focusVisibleBorder: string; + }; + // (undocumented) + secondary: { + main: string; + focusVisibleBorder: string; + }; } // @public (undocumented) diff --git a/workspaces/theme/plugins/theme/src/darkTheme.ts b/workspaces/theme/plugins/theme/src/darkTheme.ts index 0af7f08c6a..e7d14a59ea 100644 --- a/workspaces/theme/plugins/theme/src/darkTheme.ts +++ b/workspaces/theme/plugins/theme/src/darkTheme.ts @@ -49,6 +49,7 @@ export const darkThemeOverrides: Partial = { rhdh: { general: { pageInset: '1.5rem', + pageInsetBackgroundColor: '#151515', disabled: '#AAABAC', disabledBackground: '#444548', diff --git a/workspaces/theme/plugins/theme/src/lightTheme.ts b/workspaces/theme/plugins/theme/src/lightTheme.ts index cde2aa3544..bcbebd4829 100644 --- a/workspaces/theme/plugins/theme/src/lightTheme.ts +++ b/workspaces/theme/plugins/theme/src/lightTheme.ts @@ -27,7 +27,7 @@ export const lightThemeOverrides: Partial = { main: '#0066CC', }, navigation: { - background: '#222427', + background: '#f2f2f2', indicator: 'transparent', color: '#151515', selectedColor: '#151515', @@ -49,6 +49,7 @@ export const lightThemeOverrides: Partial = { rhdh: { general: { pageInset: '1.5rem', + pageInsetBackgroundColor: '#f2f2f2', disabled: '#6A6E73', disabledBackground: '#D2D2D2', diff --git a/workspaces/theme/plugins/theme/src/types.ts b/workspaces/theme/plugins/theme/src/types.ts index 6bba931ca0..d77555c347 100644 --- a/workspaces/theme/plugins/theme/src/types.ts +++ b/workspaces/theme/plugins/theme/src/types.ts @@ -21,6 +21,7 @@ export type BackstageThemePalette = UnifiedThemeOptions['palette']; export interface RHDHThemePalette { general: { pageInset: string; + pageInsetBackgroundColor: string; disabled: string; disabledBackground: string; diff --git a/workspaces/theme/plugins/theme/src/utils/createComponents.ts b/workspaces/theme/plugins/theme/src/utils/createComponents.ts index c6715b8ce4..2bf0a6bb12 100644 --- a/workspaces/theme/plugins/theme/src/utils/createComponents.ts +++ b/workspaces/theme/plugins/theme/src/utils/createComponents.ts @@ -22,6 +22,7 @@ import { type CSSObject } from '@mui/material/styles'; import { ThemeConfig, ThemeConfigOptions, RHDHThemePalette } from '../types'; import { redHatFontFaces, redHatFonts } from '../fonts'; +import { resolveNavigationSidebarColors } from './navigationSidebarColors'; export type Component = { defaultProps?: unknown; @@ -617,16 +618,33 @@ export const createComponents = (themeConfig: ThemeConfig): Components => { } if (options.sidebars !== 'mui') { + const { + sidebarBackgroundColor, + sidebarItemInteractionBackgroundColor, + navigationItemColor, + navigationSelectedColor, + } = resolveNavigationSidebarColors(themeConfig); + components.BackstageSidebar = { styleOverrides: { drawer: { gap: '0.25rem', - borderRight: `0.5rem solid ${general.sidebarBackgroundColor}`, + borderRight: `0.5rem solid ${sidebarBackgroundColor}`, paddingBottom: '1.5rem', - backgroundColor: general.sidebarBackgroundColor, + backgroundColor: sidebarBackgroundColor, '& hr': { backgroundColor: general.sidebarDividerColor, }, + '& [class*="BackstageSidebarItem-selected-"][class*="BackstageSidebarItem-root-"]': + { + backgroundColor: `${sidebarItemInteractionBackgroundColor} !important`, + color: `${navigationSelectedColor} !important`, + }, + + '& [class*="BackstageSidebarSubmenuItem-selected-"]': { + background: `${sidebarItemInteractionBackgroundColor} !important`, + color: `${navigationSelectedColor} !important`, + }, }, }, }; @@ -638,7 +656,7 @@ export const createComponents = (themeConfig: ThemeConfig): Components => { marginLeft: '0.5rem !important', textDecorationLine: 'none', '&:hover, &:focus-visible': { - backgroundColor: general.sidebarItemSelectedBackgroundColor, + backgroundColor: sidebarItemInteractionBackgroundColor, }, }, label: { @@ -647,15 +665,16 @@ export const createComponents = (themeConfig: ThemeConfig): Components => { }, }, selected: { - backgroundColor: general.sidebarItemSelectedBackgroundColor, + backgroundColor: sidebarItemInteractionBackgroundColor, + color: navigationSelectedColor, }, }, }; components.MuiBottomNavigation = { styleOverrides: { root: { - backgroundColor: `${general.sidebarBackgroundColor} !important`, - borderColor: `${general.sidebarBackgroundColor} !important`, + backgroundColor: `${sidebarBackgroundColor} !important`, + borderColor: `${sidebarBackgroundColor} !important`, }, }, }; @@ -665,19 +684,19 @@ export const createComponents = (themeConfig: ThemeConfig): Components => { }, styleOverrides: { root: { - color: `${palette.text?.primary} !important`, - backgroundColor: `${general.sidebarBackgroundColor} !important`, + color: `${navigationItemColor} !important`, + backgroundColor: `${sidebarBackgroundColor} !important`, borderRadius: '6px', borderTop: '3px solid transparent !important', // default mui selected styling paddingTop: '6px !important', // default mui selected styling marginTop: '-1px !important', // default mui selected styling '&:hover, &:focus-visible': { - backgroundColor: `${general.sidebarItemSelectedBackgroundColor} !important`, + backgroundColor: `${sidebarItemInteractionBackgroundColor} !important`, }, }, selected: { - backgroundColor: `${general.sidebarItemSelectedBackgroundColor} !important`, - color: `${palette.text?.primary} !important`, + backgroundColor: `${sidebarItemInteractionBackgroundColor} !important`, + color: `${navigationSelectedColor} !important`, }, }, }; @@ -686,7 +705,7 @@ export const createComponents = (themeConfig: ThemeConfig): Components => { root: { // undocumented Backstage makeStyles "& [class*='makeStyles-overlay-']": { - backgroundColor: `${general.sidebarBackgroundColor} !important`, + backgroundColor: `${sidebarBackgroundColor} !important`, }, '& hr': { backgroundColor: general.sidebarDividerColor, @@ -729,7 +748,8 @@ export const createComponents = (themeConfig: ThemeConfig): Components => { root: { // Controls the page inset as in PF6 -- only in desktop view '@media (min-width: 600px)': { - backgroundColor: general.sidebarBackgroundColor, + backgroundColor: + general.pageInsetBackgroundColor ?? general.appBarBackgroundColor, // Prevents the main content from scrolling weird overflowY: 'auto', // Cancel out the spacing produced by the page inset border when diff --git a/workspaces/theme/plugins/theme/src/utils/navigationSidebarChrome.test.ts b/workspaces/theme/plugins/theme/src/utils/navigationSidebarChrome.test.ts new file mode 100644 index 0000000000..71a1a2707b --- /dev/null +++ b/workspaces/theme/plugins/theme/src/utils/navigationSidebarChrome.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ThemeConfig } from '../types'; +import { getDefaultThemeConfig } from '../rhdh'; +import { resolveNavigationSidebarColors } from './navigationSidebarColors'; + +describe('resolveNavigationSidebarChrome', () => { + const defaultLight = getDefaultThemeConfig('light'); + + it('matches baseline defaults', () => { + const c = resolveNavigationSidebarColors(defaultLight); + expect(c.sidebarBackgroundColor).toBe( + defaultLight.palette?.rhdh?.general?.sidebarBackgroundColor, + ); + expect(c.sidebarItemInteractionBackgroundColor).toBe( + defaultLight.palette?.rhdh?.general?.sidebarItemSelectedBackgroundColor, + ); + expect(c.navigationItemColor).toBe(defaultLight.palette?.navigation?.color); + expect(c.navigationSelectedColor).toBe( + defaultLight.palette?.navigation?.selectedColor, + ); + }); + + it('uses palette.navigation.background when only that differs from baseline', () => { + const config = { + ...defaultLight, + palette: { + ...defaultLight.palette, + navigation: { + ...defaultLight.palette?.navigation, + background: '#aabbcc', + }, + }, + } as ThemeConfig; + expect(resolveNavigationSidebarColors(config).sidebarBackgroundColor).toBe( + '#aabbcc', + ); + }); + + it('uses rhdh.general.sidebarBackgroundColor when only that differs from baseline', () => { + const config = { + ...defaultLight, + palette: { + ...defaultLight.palette, + rhdh: { + ...defaultLight.palette?.rhdh, + general: { + ...defaultLight.palette?.rhdh?.general, + sidebarBackgroundColor: '#ddeeff', + }, + }, + }, + } as ThemeConfig; + expect(resolveNavigationSidebarColors(config).sidebarBackgroundColor).toBe( + '#ddeeff', + ); + }); + + it('uses palette.navigation.navItem.hoverBackground when only that differs from baseline', () => { + const config = { + ...defaultLight, + palette: { + ...defaultLight.palette, + navigation: { + ...defaultLight.palette?.navigation, + navItem: { + ...defaultLight.palette?.navigation?.navItem, + hoverBackground: '#0a0b0c', + }, + }, + }, + } as ThemeConfig; + expect( + resolveNavigationSidebarColors(config) + .sidebarItemInteractionBackgroundColor, + ).toBe('#0a0b0c'); + }); +}); diff --git a/workspaces/theme/plugins/theme/src/utils/navigationSidebarColors.ts b/workspaces/theme/plugins/theme/src/utils/navigationSidebarColors.ts new file mode 100644 index 0000000000..27cf3c12bd --- /dev/null +++ b/workspaces/theme/plugins/theme/src/utils/navigationSidebarColors.ts @@ -0,0 +1,157 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as backstage from '../backstage'; +import * as rhdh from '../rhdh'; +import { + type RHDHThemePalette, + type ThemeConfig, + type ThemeConfigPalette, +} from '../types'; + +function baselinePalette( + themeConfig: ThemeConfig, +): ThemeConfigPalette | undefined { + const mode = themeConfig.mode ?? 'light'; + const variant = themeConfig.variant ?? 'rhdh'; + const baseline = + variant === 'backstage' + ? backstage.getDefaultThemeConfig(mode) + : rhdh.getDefaultThemeConfig(mode); + return baseline.palette as ThemeConfigPalette | undefined; +} + +function hasRhdhGeneral( + baseline: ThemeConfigPalette | undefined, + palette: ThemeConfigPalette, +): boolean { + return !!baseline?.rhdh?.general || !!palette.rhdh?.general; +} + +/** + * One conceptual color can be set in two places in the merged theme: + * - Backstage: `palette.navigation…` + * - RHDH: `palette.rhdh.general…` + * + * We load the **default** palette for this mode/variant and compare: + * - If only the Backstage value differs from its default → use Backstage (user + * likely customized `navigation` in app config). + * - If only the RHDH value differs from its default → use RHDH. + * - If both differ and disagree → prefer **RHDH**. + */ +type NavigationVsRhdhGeneralPick = { + /** Current merged `palette.navigation…` color. */ + navigationColor: string | undefined; + /** Current merged `palette.rhdh.general…` color. */ + rhdhGeneralColor: string | undefined; + /** That same navigation field on the **default** theme (for this mode). */ + defaultNavigationColor: (baseline: ThemeConfigPalette) => string | undefined; + /** That same RHDH general field on the **default** theme. */ + defaultRhdhGeneralColor: (baseline: ThemeConfigPalette) => string | undefined; +}; + +function pickNavigationOrRhdhGeneralColor( + themeConfig: ThemeConfig, + palette: ThemeConfigPalette, + pick: NavigationVsRhdhGeneralPick, +): string { + const { + navigationColor: fromNavigation, + rhdhGeneralColor: fromRhdh, + defaultNavigationColor, + defaultRhdhGeneralColor, + } = pick; + + const baseline = baselinePalette(themeConfig); + if (!baseline) { + return fromRhdh || fromNavigation || ''; + } + if (!hasRhdhGeneral(baseline, palette)) { + return fromNavigation || fromRhdh || ''; + } + + const defaultNav = defaultNavigationColor(baseline); + const defaultRhdh = defaultRhdhGeneralColor(baseline); + + if (defaultNav === undefined && defaultRhdh === undefined) { + return fromRhdh || fromNavigation || ''; + } + + const navigationCustomized = + fromNavigation !== undefined && fromNavigation !== defaultNav; + const rhdhCustomized = fromRhdh !== undefined && fromRhdh !== defaultRhdh; + + if (navigationCustomized && !rhdhCustomized) { + return fromNavigation!; + } + if (rhdhCustomized && !navigationCustomized) { + return fromRhdh!; + } + if (navigationCustomized && rhdhCustomized && fromNavigation !== fromRhdh) { + return fromRhdh ?? fromNavigation ?? ''; + } + return fromRhdh || fromNavigation || ''; +} + +export type NavigationSidebarChrome = { + /** `navigation.background` and `rhdh.general.sidebarBackgroundColor` */ + sidebarBackgroundColor: string; + /** `navigation.navItem.hoverBackground` and `sidebarItemSelectedBackgroundColor` */ + sidebarItemInteractionBackgroundColor: string; + /** `navigation.color` (fallback: `text.primary`) */ + navigationItemColor: string; + /** `navigation.selectedColor` (fallback: `navigation.color`, then `text.primary`) */ + navigationSelectedColor: string; +}; + +/** + * Resolves all navigation/sidebar chrome colors for RHDH component overrides. + */ +export function resolveNavigationSidebarColors( + themeConfig: ThemeConfig, +): NavigationSidebarChrome { + const palette = (themeConfig.palette ?? {}) as ThemeConfigPalette; + const general = palette.rhdh?.general ?? ({} as RHDHThemePalette['general']); + const textPrimary = palette.text?.primary ?? ''; + + const navigationItemColor = palette.navigation?.color ?? textPrimary; + const navigationSelectedColor = + palette.navigation?.selectedColor ?? + palette.navigation?.color ?? + textPrimary; + + /** `themeConfig` + `palette` fixed; only pass which navigation vs RHDH fields to compare. */ + const pickNavigationColor = (fields: NavigationVsRhdhGeneralPick) => + pickNavigationOrRhdhGeneralColor(themeConfig, palette, fields); + + return { + sidebarBackgroundColor: pickNavigationColor({ + navigationColor: palette.navigation?.background, + rhdhGeneralColor: general.sidebarBackgroundColor, + defaultNavigationColor: b => b.navigation?.background, + defaultRhdhGeneralColor: b => b.rhdh?.general?.sidebarBackgroundColor, + }), + sidebarItemInteractionBackgroundColor: pickNavigationColor({ + navigationColor: palette.navigation?.navItem?.hoverBackground, + rhdhGeneralColor: general.sidebarItemSelectedBackgroundColor, + defaultNavigationColor: b => b.navigation?.navItem?.hoverBackground, + defaultRhdhGeneralColor: b => + b.rhdh?.general?.sidebarItemSelectedBackgroundColor, + }), + navigationItemColor, + navigationSelectedColor, + }; +}