feat(ui-next): Adds appearance dialog with theme presets and custom theme support (#6041)
This commit is contained in:
1 parent
0fcebe05c6
commit
0dfd321326
22 files changed
+868
-3
No files matched your search
@@ -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'),
|
||||
|
||||
@@ -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<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,
|
||||
};
|
||||
@@ -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';
|
||||
@@ -39,6 +40,10 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
||||
name: 'helloPage',
|
||||
value: helloPageCustomization,
|
||||
},
|
||||
{
|
||||
name: 'theme',
|
||||
value: appearanceModalCustomization,
|
||||
},
|
||||
{
|
||||
name: 'datasources',
|
||||
value: datasourcesCustomization,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DicomMetadataStore, classes } from '@ohif/core';
|
||||
import { calculateSUVScalingFactors } from '@cornerstonejs/calculate-suv';
|
||||
import { ActiveThemeProvider } from '@ohif/ui-next';
|
||||
|
||||
import getPTImageIdInstanceMetadata from './getPTImageIdInstanceMetadata';
|
||||
import { registerHangingProtocolAttributes } from './hangingprotocols';
|
||||
@@ -16,7 +17,18 @@ export default function init({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
hotkeysManager,
|
||||
serviceProvidersManager,
|
||||
appConfig,
|
||||
}: withAppTypes): void {
|
||||
const hasThemeModule =
|
||||
Array.isArray(appConfig.customizationService) &&
|
||||
appConfig.customizationService.some(
|
||||
ref => typeof ref === 'string' && ref.includes('customizationModule.theme')
|
||||
);
|
||||
|
||||
if (hasThemeModule) {
|
||||
serviceProvidersManager.registerProvider('activeTheme', ActiveThemeProvider);
|
||||
}
|
||||
const { toolbarService, cineService, viewportGridService } = servicesManager.services;
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(cineService, [
|
||||
|
||||
Reference in new issue
Block a user