Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
80baba9
Remove hard coded colors
dan-rukas Feb 18, 2026
f6806dd
Removed hard coded colors from icons
dan-rukas Feb 18, 2026
969a9e5
Fix helper icon colors
dan-rukas Feb 18, 2026
21e555d
Revert legacy ui icons to hard coded colors
dan-rukas Feb 18, 2026
1986e02
Fixes for loading spinner in toast
dan-rukas Feb 18, 2026
cf08bda
Simpler theme apply
dan-rukas May 18, 2026
351c5a6
Add i18n for appearance modal strings
dan-rukas May 19, 2026
b1bb5be
Fix classList mutation during iteration in ActiveThemeProvider cleanup
dan-rukas May 19, 2026
887f2e0
Remove duplicate className
dan-rukas May 19, 2026
12b16f0
Sanitize custom theme input to prevent CSS injection
dan-rukas May 19, 2026
179c56c
Remove duplicated custom theme cleanup from appearance modal
dan-rukas May 19, 2026
67a7990
Clean up unused prop, unnecessary directive, and redundant theme tokens
dan-rukas May 19, 2026
852dbf4
Centralize theme state in ActiveThemeProvider and fix custom theme re…
dan-rukas May 19, 2026
e3bedd0
Merge branch 'OHIF:master' into feat/appearance-dialog-simple
dan-rukas May 19, 2026
1d16c7d
Merge branch 'OHIF:master' into feat/appearance-dialog-simple
dan-rukas May 26, 2026
2592507
Merge branch 'OHIF:master' into fix/hardcoded-colors
dan-rukas May 26, 2026
f6f74c8
Merge branch 'fix/hardcoded-colors' into feat/appearance-dialog-simple
dan-rukas May 26, 2026
6e96854
Merge branch 'OHIF:master' into feat/appearance-dialog-simple
dan-rukas May 26, 2026
5f621b5
Merge branch 'feat/appearance-dialog-simple' of https://github.com/da…
dan-rukas May 26, 2026
a5e5f96
Revert "Merge branch 'fix/hardcoded-colors' into feat/appearance-dial…
dan-rukas May 26, 2026
ada965f
strip CSS comment tokens from custom theme input
dan-rukas May 26, 2026
ef7296b
harden custom CSS parsing and theme state validation
dan-rukas Jun 10, 2026
3b8faad
Merge branch 'master' into feat/appearance-dialog-simple
dan-rukas Jun 10, 2026
40b0a94
Added Appearance dialog to ui-next Study List
dan-rukas Jun 10, 2026
03c4aed
Merge branch 'master' into feat/appearance-dialog-simple
dan-rukas Jun 24, 2026
44d2e14
Merge branch 'master' into feat/appearance-dialog-simple
dan-rukas Jun 24, 2026
53b5701
make Appearance dialog opt-in via named customization module
dan-rukas Jun 24, 2026
7635fb2
rename theme module + conditional ActiveThemeProvider
dan-rukas Jun 24, 2026
7ae5eae
move ActiveThemeProvider registration to extension via serviceProvide…
dan-rukas Jun 25, 2026
71fd609
Merge branch 'master' into feat/appearance-dialog-simple
dan-rukas Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions extensions/default/src/ViewerLayout/ViewerHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -58,6 +62,16 @@ function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config }
containerClassName: AboutModal?.containerClassName ?? 'max-w-md',
}),
},
{
title: AppearanceModal?.menuTitle ?? t('Header:Appearance'),
icon: 'ColorChange',
onClick: () =>
show({
content: AppearanceModal,
title: AppearanceModal?.title ?? t('AppearanceModal:Appearance'),
containerClassName: AppearanceModal?.containerClassName ?? 'max-w-md',
}),
},
{
title: UserPreferencesModal.menuTitle ?? t('Header:Preferences'),
icon: 'settings',
Expand Down
143 changes: 143 additions & 0 deletions extensions/default/src/customizations/appearanceModalCustomization.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
setIsCustomOpen(false);
setActiveTheme(value);
}
};

const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setDraftCss(e.target.value);
setParseFailed(false);
};

return (
<AppearanceModal>
<AppearanceModal.Body>
<div className="grid grid-cols-[auto_1fr] items-center gap-x-6 gap-y-4">
<AppearanceModal.SectionLabel>{t('Theme')}</AppearanceModal.SectionLabel>
<div>
<Select
value={activeTheme}
onValueChange={handlePresetChange}
>
<SelectTrigger
className="w-[200px]"
aria-label={t('Theme')}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">{t('Default Theme')}</SelectItem>
{themePresets.map(preset => (
<SelectItem
key={preset.name}
value={preset.name}
>
{preset.label}
</SelectItem>
))}
{(customCss || draftCss) && <SelectItem value="custom">{t('Custom')}</SelectItem>}
</SelectContent>
</Select>
</div>

<div className="col-start-2">
<Button
variant="ghost"
size="sm"
onClick={handleToggleCustom}
>
{isCustomOpen ? t('Hide') : t('Custom Theme')}
</Button>
</div>
</div>

{isCustomOpen && (
<div className="mt-4 flex flex-col space-y-2">
<textarea
value={draftCss}
onChange={handleTextChange}
placeholder={t('Paste your custom theme color tokens here')}
aria-label={t('Paste your custom theme color tokens here')}
rows={8}
className="bg-muted text-foreground border-input placeholder:text-muted-foreground focus:ring-ring rounded-md border px-3 py-2 font-mono text-sm focus:outline-none focus:ring-1 focus:ring-inset"
/>
{parseFailed && (
<span
className="text-destructive text-sm"
role="alert"
>
{t('No valid theme tokens found')}
</span>
)}
<div className="flex space-x-2">
<Button
variant="default"
size="sm"
onClick={handleSave}
>
{t('Apply')}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleClear}
>
{t('Clear')}
</Button>
</div>
</div>
)}
</AppearanceModal.Body>
</AppearanceModal>
);
}

export default {
'ohif.appearanceModal': AppearanceModalDefault,
};
2 changes: 2 additions & 0 deletions extensions/default/src/getCustomizationModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import progressLoadingBarCustomization from './customizations/progressLoadingBar
import labellingFlowCustomization from './customizations/labellingFlowCustomization';
import viewportNotificationCustomization from './customizations/notificationCustomization';
import aboutModalCustomization from './customizations/aboutModalCustomization';
import appearanceModalCustomization from './customizations/appearanceModalCustomization';
import userPreferencesCustomization from './customizations/userPreferencesCustomization';
import reportDialogCustomization from './customizations/reportDialogCustomization';
import hotkeyBindingsCustomization from './customizations/hotkeyBindingsCustomization';
Expand Down Expand Up @@ -67,6 +68,7 @@ export default function getCustomizationModule({ servicesManager, extensionManag
...contextMenuUICustomization,
...viewportNotificationCustomization,
...aboutModalCustomization,
...appearanceModalCustomization,
...userPreferencesCustomization,
...reportDialogCustomization,
...hotkeyBindingsCustomization,
Expand Down
2 changes: 2 additions & 0 deletions platform/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '@ohif/core';
import {
ThemeWrapper as ThemeWrapperNext,
ActiveThemeProvider,
NotificationProvider,
ViewportGridProvider,
DialogProvider,
Expand Down Expand Up @@ -118,6 +119,7 @@ function App({
[UserAuthenticationProvider, { service: userAuthenticationService }],
[I18nextProvider, { i18n }],
[ThemeWrapperNext],
[ActiveThemeProvider],
[SystemContextProvider, { commandsManager, extensionManager, hotkeysManager, servicesManager }],
[ViewportRefsProvider],
[ViewportGridProvider, { service: viewportGridService }],
Expand Down
12 changes: 12 additions & 0 deletions platform/app/src/routes/WorkList/StudyListSettingsPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ export function defaultSettingsMenuItems({
});
},
},
{
id: 'appearance',
label: 'Appearance',
onClick: () => {
const AppearanceModal = customizationService.getCustomization('ohif.appearanceModal');
show({
content: AppearanceModal,
title: AppearanceModal?.title ?? t('AppearanceModal:Appearance'),
containerClassName: AppearanceModal?.containerClassName ?? 'max-w-md',
});
},
},
{
id: 'userPreferences',
label: 'User Preferences',
Expand Down
12 changes: 12 additions & 0 deletions platform/i18n/src/locales/en-US/AppearanceModal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"Appearance": "Appearance",
"Theme": "Theme",
"Default Theme": "Tonal: OHIF Blue",
"Custom": "Custom",
"Custom Theme": "Custom Theme",
"Hide": "Hide",
"Apply": "Apply",
"Clear": "Clear",
"No valid theme tokens found": "No valid theme tokens found. Expected lines like --primary: 220 90% 60%",
"Paste your custom theme color tokens here": "Paste your custom theme color tokens here"
}
1 change: 1 addition & 0 deletions platform/i18n/src/locales/en-US/Header.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"About": "About",
"Appearance": "Appearance",
"Back to Viewer": "Back to Viewer",
"INVESTIGATIONAL USE ONLY": "INVESTIGATIONAL USE ONLY",
"Options": "Options",
Expand Down
2 changes: 2 additions & 0 deletions platform/i18n/src/locales/en-US/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AboutModal from './AboutModal.json';
import AppearanceModal from './AppearanceModal.json';
import Buttons from './Buttons.json';
import CineDialog from './CineDialog.json';
import Common from './Common.json';
Expand Down Expand Up @@ -36,6 +37,7 @@ import USAnnotationPanel from './USAnnotationPanel.json';
export default {
'en-US': {
AboutModal,
AppearanceModal,
Buttons,
CineDialog,
Common,
Expand Down
38 changes: 38 additions & 0 deletions platform/ui-next/src/components/OHIFModals/AppearanceModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as React from 'react';
import { cn } from '../../lib/utils';

interface AppearanceModalProps {
children: React.ReactNode;
className?: string;
}

export function AppearanceModal({ children, className }: AppearanceModalProps) {
return (
<div className={cn('flex max-h-[80vh] w-full max-w-md flex-col overflow-hidden', className)}>
{children}
</div>
);
}

interface BodyProps {
children: React.ReactNode;
className?: string;
}
function Body({ children, className }: BodyProps) {
return (
<div className={cn('flex-1 overflow-y-auto', className)}>
<div className="mt-1 mb-4 flex flex-col space-y-4">{children}</div>
</div>
);
}

interface SectionLabelProps {
children: React.ReactNode;
className?: string;
}
function SectionLabel({ children, className }: SectionLabelProps) {
return <span className={cn('text-muted-foreground text-lg', className)}>{children}</span>;
}

AppearanceModal.Body = Body;
AppearanceModal.SectionLabel = SectionLabel;
1 change: 1 addition & 0 deletions platform/ui-next/src/components/OHIFModals/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { UserPreferencesModal } from './UserPreferencesModal';
export { ImageModal } from './ImageModal';
export { AboutModal } from './AboutModal';
export { AppearanceModal } from './AppearanceModal';
3 changes: 2 additions & 1 deletion platform/ui-next/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tool
import { ToolboxUI } from './OHIFToolbox';
import Numeric from './Numeric';
import { InputDialog, PresetDialog } from './OHIFDialogs';
import { AboutModal, ImageModal, UserPreferencesModal } from './OHIFModals';
import { AboutModal, ImageModal, UserPreferencesModal, AppearanceModal } from './OHIFModals';
import Modal from './Modal/Modal';
import { FooterAction } from './FooterAction';
import { InputFilter } from './InputFilter';
Expand Down Expand Up @@ -296,6 +296,7 @@ export {
AboutModal,
ImageModal,
UserPreferencesModal,
AppearanceModal,
FooterAction,
ToolSettings,
InputFilter,
Expand Down
Loading
Loading