From 0dfd32132603bb7aae91547a0d9f2812272a5215 Mon Sep 17 00:00:00 2001 From: Dan Rukas Date: Mon, 29 Jun 2026 12:38:46 -0400 Subject: [PATCH] feat(ui-next): Adds appearance dialog with theme presets and custom theme support (#6041) --- .../default/src/ViewerLayout/ViewerHeader.tsx | 17 ++ .../appearanceModalCustomization.tsx | 143 ++++++++++++ .../default/src/getCustomizationModule.tsx | 5 + extensions/default/src/init.ts | 12 + platform/app/public/config/netlify.js | 3 + .../WorkList/StudyListSettingsPopover.tsx | 19 +- .../src/locales/en-US/AppearanceModal.json | 12 + platform/i18n/src/locales/en-US/Header.json | 1 + platform/i18n/src/locales/en-US/index.js | 2 + .../components/OHIFModals/AppearanceModal.tsx | 38 ++++ .../src/components/OHIFModals/index.ts | 1 + platform/ui-next/src/components/index.ts | 3 +- .../contextProviders/ActiveThemeProvider.tsx | 204 +++++++++++++++++ .../ui-next/src/contextProviders/index.ts | 3 + platform/ui-next/src/themes/arctic.json | 31 +++ platform/ui-next/src/themes/deep.json | 31 +++ platform/ui-next/src/themes/index.ts | 16 ++ platform/ui-next/src/themes/midnight.json | 31 +++ platform/ui-next/src/themes/orchid.json | 31 +++ platform/ui-next/src/themes/slate.json | 31 +++ platform/ui-next/src/themes/themes.css | 206 ++++++++++++++++++ platform/ui-next/src/themes/verdant.json | 31 +++ 22 files changed, 868 insertions(+), 3 deletions(-) create mode 100644 extensions/default/src/customizations/appearanceModalCustomization.tsx create mode 100644 platform/i18n/src/locales/en-US/AppearanceModal.json create mode 100644 platform/ui-next/src/components/OHIFModals/AppearanceModal.tsx create mode 100644 platform/ui-next/src/contextProviders/ActiveThemeProvider.tsx create mode 100644 platform/ui-next/src/themes/arctic.json create mode 100644 platform/ui-next/src/themes/deep.json create mode 100644 platform/ui-next/src/themes/index.ts create mode 100644 platform/ui-next/src/themes/midnight.json create mode 100644 platform/ui-next/src/themes/orchid.json create mode 100644 platform/ui-next/src/themes/slate.json create mode 100644 platform/ui-next/src/themes/themes.css create mode 100644 platform/ui-next/src/themes/verdant.json diff --git a/extensions/default/src/ViewerLayout/ViewerHeader.tsx b/extensions/default/src/ViewerLayout/ViewerHeader.tsx index e2c0a34e6..9f6425776 100644 --- a/extensions/default/src/ViewerLayout/ViewerHeader.tsx +++ b/extensions/default/src/ViewerLayout/ViewerHeader.tsx @@ -43,6 +43,10 @@ function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config } 'ohif.aboutModal' ) as Types.MenuComponentCustomization; + const AppearanceModal = customizationService.getCustomization( + 'ohif.appearanceModal' + ) as Types.MenuComponentCustomization; + const UserPreferencesModal = customizationService.getCustomization( 'ohif.userPreferencesModal' ) as Types.MenuComponentCustomization; @@ -71,6 +75,19 @@ function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config } }, ]; + if (AppearanceModal) { + menuOptions.splice(1, 0, { + title: AppearanceModal.menuTitle ?? t('Header:Appearance'), + icon: 'ColorChange', + onClick: () => + show({ + content: AppearanceModal, + title: AppearanceModal.title ?? t('AppearanceModal:Appearance'), + containerClassName: AppearanceModal.containerClassName ?? 'max-w-md', + }), + }); + } + if (appConfig.oidc) { menuOptions.push({ title: t('Header:Logout'), diff --git a/extensions/default/src/customizations/appearanceModalCustomization.tsx b/extensions/default/src/customizations/appearanceModalCustomization.tsx new file mode 100644 index 000000000..9230dcd6b --- /dev/null +++ b/extensions/default/src/customizations/appearanceModalCustomization.tsx @@ -0,0 +1,143 @@ +import React from 'react'; +import { + AppearanceModal, + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, + Button, + useActiveTheme, + themePresets, +} from '@ohif/ui-next'; +import { useTranslation } from 'react-i18next'; + +function AppearanceModalDefault() { + const { activeTheme, setActiveTheme, customCss, applyCustomTheme, clearCustomTheme } = + useActiveTheme(); + const { t } = useTranslation('AppearanceModal'); + + const [draftCss, setDraftCss] = React.useState(() => customCss); + const [isCustomOpen, setIsCustomOpen] = React.useState(() => activeTheme === 'custom'); + const [parseFailed, setParseFailed] = React.useState(false); + + const handleToggleCustom = () => { + setIsCustomOpen(!isCustomOpen); + }; + + const handleSave = () => { + setParseFailed(!applyCustomTheme(draftCss)); + }; + + const handleClear = () => { + clearCustomTheme(); + setDraftCss(''); + setParseFailed(false); + setIsCustomOpen(false); + }; + + const handlePresetChange = (value: string) => { + if (value === 'custom') { + // Selecting "Custom" restores the saved custom theme; the draft is only + // committed through the explicit Apply button. + setIsCustomOpen(true); + if (customCss) { + applyCustomTheme(customCss); + } + } else { + setIsCustomOpen(false); + setActiveTheme(value); + } + }; + + const handleTextChange = (e: React.ChangeEvent) => { + setDraftCss(e.target.value); + setParseFailed(false); + }; + + return ( + + +
+ {t('Theme')} +
+ +
+ +
+ +
+
+ + {isCustomOpen && ( +
+