diff --git a/extensions/cornerstone-dicom-seg/src/getPanelModule.tsx b/extensions/cornerstone-dicom-seg/src/getPanelModule.tsx index 42c13fff3..3dc802124 100644 --- a/extensions/cornerstone-dicom-seg/src/getPanelModule.tsx +++ b/extensions/cornerstone-dicom-seg/src/getPanelModule.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { useAppConfig } from '@state'; -import { Toolbox } from '@ohif/ui'; +import { Toolbox as NewToolbox } from '@ohif/ui-next'; +import { Toolbox as OldToolbox } from '@ohif/ui'; import PanelSegmentation from './panels/PanelSegmentation'; const getPanelModule = ({ @@ -13,7 +14,27 @@ const getPanelModule = ({ }: withAppTypes) => { const { customizationService } = servicesManager.services; - const wrappedPanelSegmentation = configuration => { + const wrappedPanelSegmentation = ({ configuration, renderHeader, getCloseIcon, tab }) => { + const [appConfig] = useAppConfig(); + + return ( + + ); + }; + + const wrappedPanelSegmentationNoHeader = ({ configuration }) => { const [appConfig] = useAppConfig(); return ( @@ -30,9 +51,16 @@ const getPanelModule = ({ ); }; - const wrappedPanelSegmentationWithTools = configuration => { + const wrappedPanelSegmentationWithTools = ({ + configuration, + renderHeader, + getCloseIcon, + tab, + }) => { const [appConfig] = useAppConfig(); + const Toolbox = appConfig.useExperimentalUI ? NewToolbox : OldToolbox; + return ( <> - _setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value) - } - setOutlineOpacityActive={value => - _setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value) - } - setRenderFill={value => - _setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value) - } - setRenderInactiveSegmentations={value => - _setSegmentationConfiguration(selectedSegmentationId, 'renderInactiveSegmentations', value) - } - setOutlineWidthActive={value => - _setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value) - } - setFillAlpha={value => - _setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value) - } - setFillAlphaInactive={value => - _setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value) - } - /> + <> + {renderHeader && ( + <> +
+
+
+ {tab.label} +
+
+ + {getCloseIcon()} +
+ + + )} + + _setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value) + } + setOutlineOpacityActive={value => + _setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value) + } + setRenderFill={value => + _setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value) + } + setRenderInactiveSegmentations={value => + _setSegmentationConfiguration( + selectedSegmentationId, + 'renderInactiveSegmentations', + value + ) + } + setOutlineWidthActive={value => + _setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value) + } + setFillAlpha={value => + _setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value) + } + setFillAlphaInactive={value => + _setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value) + } + /> + ); } diff --git a/extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx b/extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx index 41f453847..3e70d559d 100644 --- a/extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx +++ b/extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx @@ -1,20 +1,28 @@ import React from 'react'; import { DynamicDataPanel } from './panels'; -import { Toolbox } from '@ohif/ui'; +import { Toolbox as NewToolbox } from '@ohif/ui-next'; +import { Toolbox as OldToolbox } from '@ohif/ui'; +import { useAppConfig } from '@state'; import DynamicExport from './panels/DynamicExport'; function getPanelModule({ commandsManager, extensionManager, servicesManager }) { - const wrappedDynamicDataPanel = () => { + const wrappedDynamicDataPanel = ({ renderHeader, getCloseIcon, tab }) => { return ( ); }; - const wrappedDynamicToolbox = () => { + const wrappedDynamicToolbox = ({ renderHeader, getCloseIcon, tab }) => { + const [appConfig] = useAppConfig(); + + const Toolbox = appConfig.useExperimentalUI ? NewToolbox : OldToolbox; return ( <> ); diff --git a/extensions/cornerstone-dynamic-volume/src/panels/DynamicDataPanel.tsx b/extensions/cornerstone-dynamic-volume/src/panels/DynamicDataPanel.tsx index d885d2e95..2e6c86615 100644 --- a/extensions/cornerstone-dynamic-volume/src/panels/DynamicDataPanel.tsx +++ b/extensions/cornerstone-dynamic-volume/src/panels/DynamicDataPanel.tsx @@ -1,17 +1,44 @@ import React from 'react'; import PanelGenerateImage from './PanelGenerateImage'; +import { Separator } from '@ohif/ui-next'; -function DynamicDataPanel({ servicesManager, commandsManager }) { +function DynamicDataPanel({ + servicesManager, + commandsManager, + renderHeader, + getCloseIcon, + tab, +}: withAppTypes) { return ( -
- -
+ <> + {renderHeader && ( + <> +
+
+
+ {tab.label} +
+
+ + {getCloseIcon()} +
+ + + )} +
+ +
+ ); } diff --git a/extensions/default/src/Components/SidePanelWithServices.tsx b/extensions/default/src/Components/SidePanelWithServices.tsx index 48fb98652..145f97df9 100644 --- a/extensions/default/src/Components/SidePanelWithServices.tsx +++ b/extensions/default/src/Components/SidePanelWithServices.tsx @@ -1,5 +1,7 @@ import React, { useEffect, useState, useCallback } from 'react'; -import { SidePanel } from '@ohif/ui'; +import { SidePanel as NewSidePanel } from '@ohif/ui-next'; +import { SidePanel as OldSidePanel } from '@ohif/ui'; +import { useAppConfig } from '@state'; import { Types } from '@ohif/core'; export type SidePanelWithServicesProps = { @@ -26,6 +28,7 @@ const SidePanelWithServices = ({ const [hasBeenOpened, setHasBeenOpened] = useState(false); const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp); const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side)); + const [appConfig] = useAppConfig(); const handleSidePanelOpen = useCallback(() => { setHasBeenOpened(true); @@ -75,6 +78,8 @@ const SidePanelWithServices = ({ }; }, [tabs, hasBeenOpened, panelService]); + const SidePanel = appConfig?.useExperimentalUI ? NewSidePanel : OldSidePanel; + return ( + {renderHeader && ( + <> +
+
+
+ {tab.label} +
+
+ + {getCloseIcon()} +
+ + + )}
{ + actionIcon.value = !actionIcon.value; + const newActionIcons = [...actionIcons]; + setActionIcons(newActionIcons); + }; + + // only one is true at a time + const updateViewPresetValue = viewPreset => { + if (!viewPreset) { + return; + } + const newViewPresets = viewPresets.map(preset => { + preset.selected = preset.id === viewPreset.id; + return preset; + }); + setViewPresets(newViewPresets); + }; + const onDoubleClickThumbnailHandler = displaySetInstanceUID => { let updatedViewports = []; const viewportId = activeViewportId; @@ -164,9 +199,10 @@ function PanelStudyBrowser({ const SubscriptionDisplaySetsAdded = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => { - if (!hasLoadedViewports) { - return; - } + // for some reason this breaks thumbnail loading + // if (!hasLoadedViewports) { + // return; + // } const { displaySetsAdded, options } = data; displaySetsAdded.forEach(async dSet => { const newImageSrcEntry = {}; @@ -236,7 +272,7 @@ function PanelStudyBrowser({ const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? // eslint-disable-next-line prettier/prettier - [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] + [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); @@ -249,19 +285,42 @@ function PanelStudyBrowser({ const activeDisplaySetInstanceUIDs = viewports.get(activeViewportId)?.displaySetInstanceUIDs; + const StudyBrowser = appConfig?.useExperimentalUI ? NewStudyBrowser : OldStudyBrowser; + return ( - { - setActiveTabName(clickedTabName); - }} - /> + <> + {renderHeader && ( + <> + + + + )} + { + setActiveTabName(clickedTabName); + }} + showSettings={actionIcons.find(icon => icon.id === 'settings').value} + viewPresets={viewPresets} + /> + ); } diff --git a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowserHeader.tsx b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowserHeader.tsx new file mode 100644 index 000000000..0a89e79da --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowserHeader.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { ToggleGroup, ToggleGroupItem } from '@ohif/ui-next'; +import { Icons } from '@ohif/ui-next'; +import { actionIcon, viewPreset } from './types'; + +function PanelStudyBrowserHeader({ + tab, + getCloseIcon, + viewPresets, + updateViewPresetValue, + actionIcons, + updateActionIconValue, +}: { + tab: any; + getCloseIcon: () => JSX.Element; + viewPresets: viewPreset[]; + updateViewPresetValue: (viewPreset: viewPreset) => void; + actionIcons: actionIcon[]; + updateActionIconValue: (actionIcon: actionIcon) => void; +}) { + return ( + <> +
+
+
+
+ preset.selected)[0].id} + onValueChange={value => { + const selectedViewPreset = viewPresets.find(preset => preset.id === value); + updateViewPresetValue(selectedViewPreset); + }} + > + {viewPresets.map((viewPreset: viewPreset, index) => ( + + {React.createElement(Icons[viewPreset.iconName] || Icons.MissingIcon)} + + ))} + +
+ +
+ {' '} + {tab.label}{' '} +
+ +
+
+ {actionIcons.map((icon: actionIcon, index) => + React.createElement(Icons[icon.iconName] || Icons.MissingIcon, { + key: index, + onClick: () => updateActionIconValue(icon), + className: `cursor-pointer`, + }) + )} +
+
+
+
+ {getCloseIcon()} +
+ + ); +} + +export { PanelStudyBrowserHeader }; diff --git a/extensions/default/src/Panels/StudyBrowser/constants/actionIcons.ts b/extensions/default/src/Panels/StudyBrowser/constants/actionIcons.ts new file mode 100644 index 000000000..f94a7a9a3 --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/constants/actionIcons.ts @@ -0,0 +1,11 @@ +import type { actionIcon } from '../types/actionsIcon'; + +const defaultActionIcons = [ + { + id: 'settings', + iconName: 'Settings', + value: false, + }, +] as actionIcon[]; + +export { defaultActionIcons }; diff --git a/extensions/default/src/Panels/StudyBrowser/constants/index.ts b/extensions/default/src/Panels/StudyBrowser/constants/index.ts new file mode 100644 index 000000000..fb47a5ea4 --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/constants/index.ts @@ -0,0 +1,4 @@ +import { defaultActionIcons } from './actionIcons'; +import { defaultViewPresets } from './viewPresets'; + +export { defaultActionIcons, defaultViewPresets }; diff --git a/extensions/default/src/Panels/StudyBrowser/constants/viewPresets.ts b/extensions/default/src/Panels/StudyBrowser/constants/viewPresets.ts new file mode 100644 index 000000000..e0e391d48 --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/constants/viewPresets.ts @@ -0,0 +1,16 @@ +import type { viewPreset } from '../types/viewPreset'; + +const defaultViewPresets = [ + { + id: 'list', + iconName: 'ListView', + selected: false, + }, + { + id: 'thumbnails', + iconName: 'ThumbnailView', + selected: true, + }, +] as viewPreset[]; + +export { defaultViewPresets }; diff --git a/extensions/default/src/Panels/StudyBrowser/types/actionIcon.ts b/extensions/default/src/Panels/StudyBrowser/types/actionIcon.ts new file mode 100644 index 000000000..0b522d5b2 --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/types/actionIcon.ts @@ -0,0 +1,7 @@ +type actionIcon = { + id: string; + iconName: string; + value: boolean; +}; + +export type { actionIcon }; diff --git a/extensions/default/src/Panels/StudyBrowser/types/index.ts b/extensions/default/src/Panels/StudyBrowser/types/index.ts new file mode 100644 index 000000000..d2b31d4ad --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/types/index.ts @@ -0,0 +1,4 @@ +import type { actionIcon } from './actionIcon'; +import type { viewPreset } from './viewPreset'; + +export type { actionIcon, viewPreset }; diff --git a/extensions/default/src/Panels/StudyBrowser/types/viewPreset.ts b/extensions/default/src/Panels/StudyBrowser/types/viewPreset.ts new file mode 100644 index 000000000..ff7f5b48a --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/types/viewPreset.ts @@ -0,0 +1,7 @@ +type viewPreset = { + id: string; + iconName: string; + selected: boolean; +}; + +export type { viewPreset }; diff --git a/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx b/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx index 9be92e25e..3931a4313 100644 --- a/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; // -import PanelStudyBrowser from './PanelStudyBrowser'; +import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser'; import getImageSrcFromImageId from './getImageSrcFromImageId'; import getStudiesForPatientByMRN from './getStudiesForPatientByMRN'; import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStudy'; @@ -13,7 +13,14 @@ import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStu * @param {object} commandsManager * @param {object} extensionManager */ -function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesManager }) { +function WrappedPanelStudyBrowser({ + commandsManager, + extensionManager, + servicesManager, + renderHeader, + getCloseIcon, + tab, +}) { // TODO: This should be made available a different way; route should have // already determined our datasource const dataSource = extensionManager.getDataSources()[0]; @@ -34,6 +41,9 @@ function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesM getImageSrc={_getImageSrcFromImageId} getStudiesForPatientByMRN={_getStudiesForPatientByMRN} requestDisplaySetCreationForStudy={_requestDisplaySetCreationForStudy} + renderHeader={renderHeader} + getCloseIcon={getCloseIcon} + tab={tab} /> ); } diff --git a/extensions/default/src/Panels/index.js b/extensions/default/src/Panels/index.js index e81fdc304..f83eb873b 100644 --- a/extensions/default/src/Panels/index.js +++ b/extensions/default/src/Panels/index.js @@ -1,4 +1,4 @@ -import PanelStudyBrowser from './PanelStudyBrowser'; +import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser'; import WrappedPanelStudyBrowser from './WrappedPanelStudyBrowser'; import PanelMeasurementTable from './PanelMeasurementTable'; import createReportDialogPrompt from './createReportDialogPrompt'; diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx index 75de9b03d..bceeefdaf 100644 --- a/extensions/default/src/getCustomizationModule.tsx +++ b/extensions/default/src/getCustomizationModule.tsx @@ -184,6 +184,22 @@ export default function getCustomizationModule({ servicesManager, extensionManag }, ], }, + { + id: 'studyBrowser.viewPresets', + // change your default selected preset here + value: [ + { + id: 'list', + iconName: 'ListView', + selected: false, + }, + { + id: 'thumbnails', + iconName: 'ThumbnailView', + selected: true, + }, + ], + }, ], }, ]; diff --git a/extensions/default/src/getPanelModule.tsx b/extensions/default/src/getPanelModule.tsx index 138a8d193..ae548e0df 100644 --- a/extensions/default/src/getPanelModule.tsx +++ b/extensions/default/src/getPanelModule.tsx @@ -8,12 +8,15 @@ import i18n from 'i18next'; // - show errors in UI for thumbnails if promise fails function getPanelModule({ commandsManager, extensionManager, servicesManager }) { - const wrappedMeasurementPanel = () => { + const wrappedMeasurementPanel = ({ renderHeader, getCloseIcon, tab }) => { return ( ); }; @@ -24,11 +27,14 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }) iconName: 'tab-studies', iconLabel: 'Studies', label: i18n.t('SidePanel:Studies'), - component: WrappedPanelStudyBrowser.bind(null, { - commandsManager, - extensionManager, - servicesManager, - }), + component: props => ( + + ), }, { name: 'measurements', diff --git a/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx b/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx index c0c983a4c..160e89e62 100644 --- a/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx +++ b/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx @@ -8,6 +8,7 @@ import dcmjs from 'dcmjs'; import callInputDialog from '../../utils/callInputDialog'; import constructSR from '../../utils/constructSR'; import { saveByteArray } from '../../utils/saveByteArray'; +import { Separator } from '@ohif/ui-next'; let saving = false; const { datasetToBuffer } = dcmjs.data; @@ -56,6 +57,9 @@ interface IMicroscopyPanelProps extends WithTranslation { servicesManager: AppTypes.ServicesManager; extensionManager: ExtensionManager; commandsManager: CommandsManager; + renderHeader?: boolean; + getCloseIcon?: PropTypes.func; + tab: PropTypes.object; } /** @@ -328,6 +332,20 @@ function MicroscopyPanel(props: IMicroscopyPanelProps) { return ( <> + {props.renderHeader && ( + <> +
+
+
+ {props.tab.label} +
+
+ + {props.getCloseIcon()} +
+ + + )}{' '}
{ + const wrappedMeasurementPanel = ({ renderHeader, getCloseIcon, tab }) => { const [{ activeViewportId, viewports }] = useViewportGrid(); return ( @@ -25,6 +25,9 @@ export default function getPanelModule({ commandsManager={commandsManager} servicesManager={servicesManager} extensionManager={extensionManager} + renderHeader={renderHeader} + getCloseIcon={getCloseIcon} + tab={tab} /> ); }; diff --git a/extensions/measurement-tracking/src/getPanelModule.tsx b/extensions/measurement-tracking/src/getPanelModule.tsx index a49a44a5b..ef09f59ad 100644 --- a/extensions/measurement-tracking/src/getPanelModule.tsx +++ b/extensions/measurement-tracking/src/getPanelModule.tsx @@ -1,11 +1,13 @@ import { Types } from '@ohif/core'; import { PanelMeasurementTableTracking, PanelStudyBrowserTracking } from './panels'; import i18n from 'i18next'; +import React from 'react'; // TODO: // - No loading UI exists yet // - cancel promises when component is destroyed // - show errors in UI for thumbnails if promise fails + function getPanelModule({ commandsManager, extensionManager, servicesManager }): Types.Panel[] { return [ { @@ -13,11 +15,14 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }): iconName: 'tab-studies', iconLabel: 'Studies', label: i18n.t('SidePanel:Studies'), - component: PanelStudyBrowserTracking.bind(null, { - commandsManager, - extensionManager, - servicesManager, - }), + component: props => ( + + ), }, { @@ -25,11 +30,14 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager }): iconName: 'tab-linear', iconLabel: 'Measure', label: i18n.t('SidePanel:Measurements'), - component: PanelMeasurementTableTracking.bind(null, { - commandsManager, - extensionManager, - servicesManager, - }), + component: props => ( + + ), }, ]; } diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx index 182a9e63d..5fc3cd65c 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.tsx @@ -7,6 +7,7 @@ import { useAppConfig } from '@state'; import { useTrackedMeasurements } from '../../getContextModule'; import debounce from 'lodash.debounce'; import { useTranslation } from 'react-i18next'; +import { Separator } from '@ohif/ui-next'; const { downloadCSVReport } = utils; const { formatDate } = utils; @@ -18,7 +19,13 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { description: '', // 'CHEST/ABD/PELVIS W CONTRAST', }; -function PanelMeasurementTableTracking({ servicesManager, extensionManager }: withAppTypes) { +function PanelMeasurementTableTracking({ + servicesManager, + extensionManager, + renderHeader, + getCloseIcon, + tab, +}: withAppTypes) { const [viewportGrid] = useViewportGrid(); const { t } = useTranslation('MeasurementTable'); const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(Date.now().toString()); @@ -176,6 +183,24 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }: wi return ( <> + {renderHeader && ( + <> +
+
+
+ {tab.label} +
+
+ + {getCloseIcon()} +
+ + + )}
{ + actionIcon.value = !actionIcon.value; + const newActionIcons = [...actionIcons]; + setActionIcons(newActionIcons); + }; + + const updateViewPresetValue = viewPreset => { + if (!viewPreset) { + return; + } + const newViewPresets = viewPresets.map(preset => { + preset.selected = preset.id === viewPreset.id; + return preset; + }); + setViewPresets(newViewPresets); + }; + const onDoubleClickThumbnailHandler = displaySetInstanceUID => { let updatedViewports = []; const viewportId = activeViewportId; @@ -431,23 +466,46 @@ function PanelStudyBrowserTracking({ }); }; + const StudyBrowser = appConfig.useExperimentalUI ? NewStudyBrowser : OldStudyBrowser; + return ( - { - setActiveTabName(clickedTabName); - }} - onClickUntrack={displaySetInstanceUID => { - onClickUntrack(displaySetInstanceUID); - }} - onClickThumbnail={() => {}} - onDoubleClickThumbnail={onDoubleClickThumbnailHandler} - activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs} - /> + <> + {renderHeader && ( + <> + + + + )} + { + setActiveTabName(clickedTabName); + }} + onClickUntrack={displaySetInstanceUID => { + onClickUntrack(displaySetInstanceUID); + }} + onClickThumbnail={() => {}} + onDoubleClickThumbnail={onDoubleClickThumbnailHandler} + activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs} + showSettings={actionIcons.find(icon => icon.id === 'settings').value} + viewPresets={viewPresets} + /> + ); } diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTrackingHeader.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTrackingHeader.tsx new file mode 100644 index 000000000..a9053727c --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTrackingHeader.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { ToggleGroup, ToggleGroupItem } from '@ohif/ui-next'; +import { Icons } from '@ohif/ui-next'; +import { actionIcon, viewPreset } from './types'; + +function PanelStudyBrowserTrackingHeader({ + tab, + getCloseIcon, + viewPresets, + updateViewPresetValue, + actionIcons, + updateActionIconValue, +}: { + tab: any; + getCloseIcon: () => JSX.Element; + viewPresets: viewPreset[]; + updateViewPresetValue: (viewPreset: viewPreset) => void; + actionIcons: actionIcon[]; + updateActionIconValue: (actionIcon: actionIcon) => void; +}) { + return ( + <> +
+
+
+
+ preset.selected)[0].id} + onValueChange={value => { + const selectedViewPreset = viewPresets.find(preset => preset.id === value); + updateViewPresetValue(selectedViewPreset); + }} + > + {viewPresets.map((viewPreset: viewPreset, index) => ( + + {React.createElement(Icons[viewPreset.iconName] || Icons.MissingIcon)} + + ))} + +
+ +
+ {' '} + {tab.label}{' '} +
+ +
+
+ {actionIcons.map((icon: actionIcon, index) => + React.createElement(Icons[icon.iconName] || Icons.MissingIcon, { + key: index, + onClick: () => updateActionIconValue(icon), + className: `cursor-pointer`, + }) + )} +
+
+
+
+ {getCloseIcon()} +
+ + ); +} + +export { PanelStudyBrowserTrackingHeader }; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/actionIcons.ts b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/actionIcons.ts new file mode 100644 index 000000000..26b1b62c4 --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/actionIcons.ts @@ -0,0 +1,11 @@ +import type { actionIcon } from '../PanelStudyBrowserTracking/types/actionsIcon'; + +const defaultActionIcons = [ + { + id: 'settings', + iconName: 'Settings', + value: false, + }, +] as actionIcon[]; + +export { defaultActionIcons }; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/index.ts b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/index.ts new file mode 100644 index 000000000..fb47a5ea4 --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/index.ts @@ -0,0 +1,4 @@ +import { defaultActionIcons } from './actionIcons'; +import { defaultViewPresets } from './viewPresets'; + +export { defaultActionIcons, defaultViewPresets }; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/viewPresets.ts b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/viewPresets.ts new file mode 100644 index 000000000..4ecba95e8 --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/viewPresets.ts @@ -0,0 +1,16 @@ +import type { viewPreset } from '../PanelStudyBrowserTracking/types/viewPreset'; + +const defaultViewPresets = [ + { + id: 'list', + iconName: 'ListView', + selected: false, + }, + { + id: 'thumbnails', + iconName: 'ThumbnailView', + selected: true, + }, +] as viewPreset[]; + +export { defaultViewPresets }; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx index b276579db..8f7a0fdc4 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import PropTypes from 'prop-types'; // import PanelStudyBrowserTracking from './PanelStudyBrowserTracking'; @@ -25,6 +25,9 @@ function WrappedPanelStudyBrowserTracking({ commandsManager, extensionManager, servicesManager, + renderHeader, + getCloseIcon, + tab, }: withAppTypes) { const dataSource = extensionManager.getActiveDataSource()[0]; @@ -46,6 +49,9 @@ function WrappedPanelStudyBrowserTracking({ getImageSrc={_getImageSrcFromImageId} getStudiesForPatientByMRN={_getStudiesForPatientByMRN} requestDisplaySetCreationForStudy={_requestDisplaySetCreationForStudy} + renderHeader={renderHeader} + getCloseIcon={getCloseIcon} + tab={tab} /> ); } diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/actionIcon.ts b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/actionIcon.ts new file mode 100644 index 000000000..0b522d5b2 --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/actionIcon.ts @@ -0,0 +1,7 @@ +type actionIcon = { + id: string; + iconName: string; + value: boolean; +}; + +export type { actionIcon }; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/index.ts b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/index.ts new file mode 100644 index 000000000..d2b31d4ad --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/index.ts @@ -0,0 +1,4 @@ +import type { actionIcon } from './actionIcon'; +import type { viewPreset } from './viewPreset'; + +export type { actionIcon, viewPreset }; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/viewPreset.ts b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/viewPreset.ts new file mode 100644 index 000000000..ff7f5b48a --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/viewPreset.ts @@ -0,0 +1,7 @@ +type viewPreset = { + id: string; + iconName: string; + selected: boolean; +}; + +export type { viewPreset }; diff --git a/extensions/tmtv/src/Panels/PanelPetSUV.tsx b/extensions/tmtv/src/Panels/PanelPetSUV.tsx index 07c3e4578..8a368f49a 100644 --- a/extensions/tmtv/src/Panels/PanelPetSUV.tsx +++ b/extensions/tmtv/src/Panels/PanelPetSUV.tsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { PanelSection, Input, Button } from '@ohif/ui'; import { DicomMetadataStore } from '@ohif/core'; import { useTranslation } from 'react-i18next'; +import { Separator } from '@ohif/ui-next'; const DEFAULT_MEATADATA = { PatientWeight: null, @@ -22,7 +23,13 @@ const DEFAULT_MEATADATA = { * @param param0 * @returns */ -export default function PanelPetSUV({ servicesManager, commandsManager }: withAppTypes) { +export default function PanelPetSUV({ + servicesManager, + commandsManager, + renderHeader, + getCloseIcon, + tab, +}: withAppTypes) { const { t } = useTranslation('PanelSUV'); const { displaySetService, toolGroupService, toolbarService, hangingProtocolService } = servicesManager.services; @@ -125,104 +132,127 @@ export default function PanelPetSUV({ servicesManager, commandsManager }: withAp }, 0); } return ( -
-
- -
-
- { - handleMetadataChange({ - PatientSex: e.target.value, - }); - }} - /> - kg} - labelClassName="text-[13px] font-inter text-white" - className="!m-0 !h-[26px] !w-[117px]" - value={metadata.PatientWeight || ''} - onChange={e => { - handleMetadataChange({ - PatientWeight: e.target.value, - }); - }} - id="weight-input" - /> - bq} - labelClassName="text-[13px] font-inter text-white" - className="!m-0 !h-[26px] !w-[117px]" - value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideTotalDose || ''} - onChange={e => { - handleMetadataChange({ - RadiopharmaceuticalInformationSequence: { - RadionuclideTotalDose: e.target.value, - }, - }); - }} - /> - s} - labelClassName="text-[13px] font-inter text-white" - className="!m-0 !h-[26px] !w-[117px]" - value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideHalfLife || ''} - onChange={e => { - handleMetadataChange({ - RadiopharmaceuticalInformationSequence: { - RadionuclideHalfLife: e.target.value, - }, - }); - }} - /> - s} - labelClassName="text-[13px] font-inter text-white" - className="!m-0 !h-[26px] !w-[117px]" - value={ - metadata.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartTime || '' - } - onChange={e => { - handleMetadataChange({ - RadiopharmaceuticalInformationSequence: { - RadiopharmaceuticalStartTime: e.target.value, - }, - }); - }} - /> - s} - labelClassName="text-[13px] font-inter text-white" - className="!m-0 !h-[26px] !w-[117px]" - value={metadata.SeriesTime || ''} - onChange={() => {}} - /> - + <> + {renderHeader && ( + <> +
+
+
+ {tab.label} +
+ + {getCloseIcon()}
- + + + )} +
+
+ +
+
+ { + handleMetadataChange({ + PatientSex: e.target.value, + }); + }} + /> + kg} + labelClassName="text-[13px] font-inter text-white" + className="!m-0 !h-[26px] !w-[117px]" + value={metadata.PatientWeight || ''} + onChange={e => { + handleMetadataChange({ + PatientWeight: e.target.value, + }); + }} + id="weight-input" + /> + bq} + labelClassName="text-[13px] font-inter text-white" + className="!m-0 !h-[26px] !w-[117px]" + value={ + metadata.RadiopharmaceuticalInformationSequence.RadionuclideTotalDose || '' + } + onChange={e => { + handleMetadataChange({ + RadiopharmaceuticalInformationSequence: { + RadionuclideTotalDose: e.target.value, + }, + }); + }} + /> + s} + labelClassName="text-[13px] font-inter text-white" + className="!m-0 !h-[26px] !w-[117px]" + value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideHalfLife || ''} + onChange={e => { + handleMetadataChange({ + RadiopharmaceuticalInformationSequence: { + RadionuclideHalfLife: e.target.value, + }, + }); + }} + /> + s} + labelClassName="text-[13px] font-inter text-white" + className="!m-0 !h-[26px] !w-[117px]" + value={ + metadata.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartTime || + '' + } + onChange={e => { + handleMetadataChange({ + RadiopharmaceuticalInformationSequence: { + RadiopharmaceuticalStartTime: e.target.value, + }, + }); + }} + /> + s} + labelClassName="text-[13px] font-inter text-white" + className="!m-0 !h-[26px] !w-[117px]" + value={metadata.SeriesTime || ''} + onChange={() => {}} + /> + +
+
+
+
-
+ ); } diff --git a/extensions/tmtv/src/getPanelModule.tsx b/extensions/tmtv/src/getPanelModule.tsx index 239233978..113170993 100644 --- a/extensions/tmtv/src/getPanelModule.tsx +++ b/extensions/tmtv/src/getPanelModule.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { PanelPetSUV, PanelROIThresholdExport } from './Panels'; -import { Toolbox } from '@ohif/ui'; +import { Toolbox as NewToolbox } from '@ohif/ui-next'; +import { Toolbox as OldToolbox } from '@ohif/ui'; +import { useAppConfig } from '@state'; // TODO: // - No loading UI exists yet @@ -8,16 +10,24 @@ import { Toolbox } from '@ohif/ui'; // - show errors in UI for thumbnails if promise fails function getPanelModule({ commandsManager, extensionManager, servicesManager }) { - const wrappedPanelPetSuv = () => { + const wrappedPanelPetSuv = ({ renderHeader, getCloseIcon, tab }) => { return ( ); }; - const wrappedROIThresholdToolbox = () => { + const wrappedROIThresholdToolbox = ({ renderHeader, getCloseIcon, tab }: withAppTypes) => { + const [appConfig] = useAppConfig(); + + const Toolbox = appConfig.useExperimentalUI ? NewToolbox : OldToolbox; + return ( <> ); diff --git a/modes/preclinical-4d/src/getWorkflowSettings.ts b/modes/preclinical-4d/src/getWorkflowSettings.ts index efaab9b1f..a33571644 100644 --- a/modes/preclinical-4d/src/getWorkflowSettings.ts +++ b/modes/preclinical-4d/src/getWorkflowSettings.ts @@ -7,7 +7,7 @@ const dynamicVolume = { }; const cornerstone = { - segmentation: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation', + segmentation: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentationNoHeader', activeViewportWindowLevel: '@ohif/extension-cornerstone.panelModule.activeViewportWindowLevel', }; diff --git a/modes/tmtv/src/index.ts b/modes/tmtv/src/index.ts index 1ef04ca73..37281486e 100644 --- a/modes/tmtv/src/index.ts +++ b/modes/tmtv/src/index.ts @@ -17,7 +17,7 @@ const ohif = { const cs3d = { viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', - segPanel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation', + segPanel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentationNoHeader', }; const tmtv = { diff --git a/platform/app/public/config/default.js b/platform/app/public/config/default.js index 3fec05a88..03a4821b8 100644 --- a/platform/app/public/config/default.js +++ b/platform/app/public/config/default.js @@ -16,6 +16,7 @@ window.config = { experimentalStudyBrowserSort: false, strictZSpacingForVolumeViewport: true, groupEnabledModesFirst: true, + useExperimentalUI: false, maxNumRequests: { interaction: 100, thumbnail: 75, diff --git a/platform/app/public/config/netlify.js b/platform/app/public/config/netlify.js index ed0ca71a6..75c7ad331 100644 --- a/platform/app/public/config/netlify.js +++ b/platform/app/public/config/netlify.js @@ -8,6 +8,7 @@ window.config = { // below flag is for performance reasons, but it might not work for all servers showWarningMessageForCrossOrigin: true, showCPUFallbackMessage: true, + useExperimentalUI: false, showLoadingIndicator: true, experimentalStudyBrowserSort: false, strictZSpacingForVolumeViewport: true, diff --git a/platform/core/src/services/PanelService/PanelService.tsx b/platform/core/src/services/PanelService/PanelService.tsx index 0368b1a6c..4900283e9 100644 --- a/platform/core/src/services/PanelService/PanelService.tsx +++ b/platform/core/src/services/PanelService/PanelService.tsx @@ -105,10 +105,13 @@ export default class PanelService extends PubSubService { entry = panelsData[0].entry; // stack the content of the panels in one react component - content = () => ( + content = props => ( <> {panelsData.map(({ content: PanelContent }, index) => ( - + ))} ); diff --git a/platform/ui-next/package.json b/platform/ui-next/package.json index b1a9f65c5..e70ce54b7 100644 --- a/platform/ui-next/package.json +++ b/platform/ui-next/package.json @@ -27,6 +27,7 @@ ".": "./src/index.ts" }, "dependencies": { + "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-icons": "^1.3.0", diff --git a/platform/ui-next/src/components/Accordion/Accordion.tsx b/platform/ui-next/src/components/Accordion/Accordion.tsx new file mode 100644 index 000000000..2a8d8622e --- /dev/null +++ b/platform/ui-next/src/components/Accordion/Accordion.tsx @@ -0,0 +1,57 @@ +'use client'; + +import * as React from 'react'; +import * as AccordionPrimitive from '@radix-ui/react-accordion'; +import { ChevronDownIcon } from '@radix-ui/react-icons'; + +import { cn } from '../../lib/utils'; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = 'AccordionItem'; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-90', + className + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/platform/ui-next/src/components/Accordion/index.ts b/platform/ui-next/src/components/Accordion/index.ts new file mode 100644 index 000000000..2a755dd53 --- /dev/null +++ b/platform/ui-next/src/components/Accordion/index.ts @@ -0,0 +1,3 @@ +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './Accordion'; + +export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }; diff --git a/platform/ui-next/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx b/platform/ui-next/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx new file mode 100644 index 000000000..d0e9721b9 --- /dev/null +++ b/platform/ui-next/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Icons } from '../Icons'; +import { useTranslation } from 'react-i18next'; +import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../Tooltip'; + +/** + * Displays a tooltip with a list of messages of a displaySet + * @param param0 + * @returns + */ +const DisplaySetMessageListTooltip = ({ messages, id }): React.ReactNode => { + const { t } = useTranslation('Messages'); + if (messages?.size()) { + return ( + <> + + + + + + +
+
+ {t('Display Set Messages')} +
+
    + {messages.messages.map((message, index) => ( +
  1. + {index + 1}. {t(message.id)} +
  2. + ))} +
+
+
+
+
+ + ); + } + return <>; +}; + +DisplaySetMessageListTooltip.propTypes = { + messages: PropTypes.object, + id: PropTypes.string, +}; + +export default DisplaySetMessageListTooltip; diff --git a/platform/ui-next/src/components/DisplaySetMessageListTooltip/index.ts b/platform/ui-next/src/components/DisplaySetMessageListTooltip/index.ts new file mode 100644 index 000000000..8655260a0 --- /dev/null +++ b/platform/ui-next/src/components/DisplaySetMessageListTooltip/index.ts @@ -0,0 +1,3 @@ +import DisplaySetMessageListTooltip from './DisplaySetMessageListTooltip'; + +export default DisplaySetMessageListTooltip; diff --git a/platform/ui-next/src/components/Icons/Icons.tsx b/platform/ui-next/src/components/Icons/Icons.tsx new file mode 100644 index 000000000..ec3aa6d62 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Icons.tsx @@ -0,0 +1,1242 @@ +import React from 'react'; + +type IconProps = React.HTMLAttributes; + +export const Icons = { + // Usage example: + + ChevronClosed: (props: IconProps) => ( + + icon-chevron-closed + + + + + + ), + ChevronOpen: (props: IconProps) => ( + + icon-chevron-open + + + + + + ), + Download: (props: IconProps) => ( + + icon-download + + + + + + + + + ), + ListView: (props: IconProps) => ( + + icon-list-view + + + + + + + + + + + ), + More: (props: IconProps) => ( + + icon-more + + + + + + + + ), + PinFill: (props: IconProps) => ( + + icon-pin-fill + + + + + + + ), + Pin: (props: IconProps) => ( + + icon-pin + + + + + + + ), + ThumbnailView: (props: IconProps) => ( + + icon-thumbnail-view + + + + + + + + + ), + IconMPR: (props: IconProps) => ( + + info-mpr + + + + + + + + + + ), + InfoSeries: (props: IconProps) => ( + + info-series + + + + + + + + + ), + StatusTracking: (props: IconProps) => ( + + status-tracking + + + + + + + + + ), + StatusWarning: (props: IconProps) => ( + + status-warning + + + + + + + + + + + + ), + SortingAscending: (props: IconProps) => ( + + + + + + + ), + SortingDescending: (props: IconProps) => ( + + + + + + + ), + Trash: (props: IconProps) => ( + + Trash + + + ), + Cancel: (props: IconProps) => ( + + + + + + + + + ), + InfoLink: (props: IconProps) => ( + + + + + + + + ), + NavigationPanelReveal: (props: IconProps) => ( + + + + + + ), + MissingIcon: (props: IconProps) =>
Missing icon
, + Settings: (props: IconProps) => ( + + icon-display-settings + + + + + + + + + + + + + + + + + + + ), + SidePanelCloseLeft: (props: IconProps) => ( + + icon-panel-close-left + + + + + + + + + + + + ), + SidePanelCloseRight: (props: IconProps) => ( + + icon-panel-close-right + + + + + + + + + + + + ), + TabSegmentation: (props: IconProps) => ( + + + + + + + + + + + + ), + TabLinear: (props: IconProps) => ( + + + + + + + + + ), + TabStudies: (props: IconProps) => ( + + tab-studies + + + + + + + + + + + ), + Tab4D: (props: IconProps) => ( + + tab-4d + + + + + + + + + + + ), + TabPatientInfo: (props: IconProps) => ( + + + + + + + + ), + TabRoiThreshold: (props: IconProps) => ( + + + + + + + + + + + ), + + // Aliases + 'tab-segmentation': (props: IconProps) => Icons.TabSegmentation(props), + 'tab-studies': (props: IconProps) => Icons.TabStudies(props), + 'tab-linear': (props: IconProps) => Icons.TabLinear(props), + 'tab-4d': (props: IconProps) => Icons.Tab4D(props), + 'tab-patient-info': (props: IconProps) => Icons.TabPatientInfo(props), + 'tab-roi-threshold': (props: IconProps) => Icons.TabRoiThreshold(props), + 'icon-mpr': (props: IconProps) => Icons.IconMPR(props), +}; diff --git a/platform/ui-next/src/components/Icons/icon-disclosure-close.svg b/platform/ui-next/src/components/Icons/icon-disclosure-close.svg new file mode 100644 index 000000000..dd6078c55 --- /dev/null +++ b/platform/ui-next/src/components/Icons/icon-disclosure-close.svg @@ -0,0 +1,12 @@ + + + icon-disclosure-close + + + + + + + + + \ No newline at end of file diff --git a/platform/ui-next/src/components/Icons/icon-disclosure-open.svg b/platform/ui-next/src/components/Icons/icon-disclosure-open.svg new file mode 100644 index 000000000..7dae7c9ec --- /dev/null +++ b/platform/ui-next/src/components/Icons/icon-disclosure-open.svg @@ -0,0 +1,12 @@ + + + icon-disclosure-open + + + + + + + + + \ No newline at end of file diff --git a/platform/ui-next/src/components/Icons/index.ts b/platform/ui-next/src/components/Icons/index.ts new file mode 100644 index 000000000..757c39e0d --- /dev/null +++ b/platform/ui-next/src/components/Icons/index.ts @@ -0,0 +1,3 @@ +import { Icons } from './Icons'; + +export { Icons }; diff --git a/platform/ui-next/src/components/PanelSection/PanelSection.tsx b/platform/ui-next/src/components/PanelSection/PanelSection.tsx new file mode 100644 index 000000000..3e58011de --- /dev/null +++ b/platform/ui-next/src/components/PanelSection/PanelSection.tsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { Icons } from '../Icons'; +import PropTypes from 'prop-types'; + +const PanelSection = ({ title, children, actionIcons = [], childrenClassName }) => { + const [areChildrenVisible, setChildrenVisible] = useState(true); + + const handleHeaderClick = () => { + setChildrenVisible(!areChildrenVisible); + }; + + return ( + <> +
+
{title}
+
+ {actionIcons.map((icon, index) => { + const Icon = Icons[icon.name]; + return ( + { + e.stopPropagation(); + if (!areChildrenVisible) { + setChildrenVisible(true); + } + icon.onClick(); + }} + /> + ); + })} +
+ {areChildrenVisible ? : } +
+
+
+ {areChildrenVisible && ( + <> +
+
+ {children} +
+ + )} + + ); +}; + +PanelSection.propTypes = { + title: PropTypes.string, + children: PropTypes.node, + childrenClassName: PropTypes.string, + actionIcons: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + onClick: PropTypes.func, + }) + ), +}; + +export default PanelSection; diff --git a/platform/ui-next/src/components/PanelSection/index.ts b/platform/ui-next/src/components/PanelSection/index.ts new file mode 100644 index 000000000..32189f310 --- /dev/null +++ b/platform/ui-next/src/components/PanelSection/index.ts @@ -0,0 +1,3 @@ +import PanelSection from './PanelSection'; + +export default PanelSection; diff --git a/platform/ui-next/src/components/Separator/Separator.tsx b/platform/ui-next/src/components/Separator/Separator.tsx index 742159b11..c9a836779 100644 --- a/platform/ui-next/src/components/Separator/Separator.tsx +++ b/platform/ui-next/src/components/Separator/Separator.tsx @@ -1,24 +1,33 @@ -import * as React from 'react'; -import * as SeparatorPrimitive from '@radix-ui/react-separator'; +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" import { cn } from '../../lib/utils'; +type SeparatorProps = React.ComponentPropsWithoutRef & { + thickness?: string; +}; + const Separator = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => ( - -)); -Separator.displayName = SeparatorPrimitive.Root.displayName; + SeparatorProps +>( + ( + { className, orientation = "horizontal", decorative = true, thickness = "1px", ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName -export { Separator }; +export { Separator } \ No newline at end of file diff --git a/platform/ui-next/src/components/Separator/index.ts b/platform/ui-next/src/components/Separator/index.ts new file mode 100644 index 000000000..43685ffac --- /dev/null +++ b/platform/ui-next/src/components/Separator/index.ts @@ -0,0 +1,3 @@ +import { Separator } from "./Separator"; + +export default Separator; diff --git a/platform/ui-next/src/components/SidePanel/SidePanel.tsx b/platform/ui-next/src/components/SidePanel/SidePanel.tsx new file mode 100644 index 000000000..bd3cc95b1 --- /dev/null +++ b/platform/ui-next/src/components/SidePanel/SidePanel.tsx @@ -0,0 +1,422 @@ +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Icons } from '../Icons'; +import { TooltipTrigger, TooltipContent, TooltipProvider, Tooltip } from '../Tooltip'; +import Separator from '../Separator'; + +type StyleMap = { + open: { + left: { marginLeft: string }; + right: { marginRight: string }; + }; + closed: { + left: { marginLeft: string }; + right: { marginRight: string }; + }; +}; +const borderSize = 4; +const collapsedWidth = 25; +const closeIconWidth = 30; +const gridHorizontalPadding = 10; +const tabSpacerWidth = 2; + +const baseClasses = + 'transition-all duration-300 ease-in-out bg-black border-black justify-start box-content flex flex-col'; + +const classesMap = { + open: { + left: `mr-1`, + right: `ml-1`, + }, + closed: { + left: `mr-2 items-end`, + right: `ml-2 items-start`, + }, +}; + +const openStateIconName = { + left: 'SidePanelCloseLeft', + right: 'SidePanelCloseRight', +}; + +const getTabWidth = (numTabs: number) => { + if (numTabs < 3) { + return 68; + } else { + return 40; + } +}; + +const getGridWidth = (numTabs: number, gridAvailableWidth: number) => { + const spacersWidth = (numTabs - 1) * tabSpacerWidth; + const tabsWidth = getTabWidth(numTabs) * numTabs; + + if (gridAvailableWidth > tabsWidth + spacersWidth) { + return tabsWidth + spacersWidth; + } + + return gridAvailableWidth; +}; + +const getNumGridColumns = (numTabs: number, gridWidth: number) => { + if (numTabs === 1) { + return 1; + } + + // Start by calculating the number of tabs assuming each tab was accompanied by a spacer. + const tabWidth = getTabWidth(numTabs); + const numTabsWithOneSpacerEach = Math.floor(gridWidth / (tabWidth + tabSpacerWidth)); + + // But there is always one less spacer than tabs, so now check if an extra tab with one less spacer fits. + if ( + (numTabsWithOneSpacerEach + 1) * tabWidth + numTabsWithOneSpacerEach * tabSpacerWidth <= + gridWidth + ) { + return numTabsWithOneSpacerEach + 1; + } + + return numTabsWithOneSpacerEach; +}; + +const getTabClassNames = ( + numColumns: number, + numTabs: number, + tabIndex: number, + isActiveTab: boolean, + isTabDisabled: boolean +) => + classnames('h-[28px] mb-[2px] cursor-pointer text-white bg-black', { + 'hover:text-primary-active': !isActiveTab && !isTabDisabled, + 'rounded-l': tabIndex % numColumns === 0, + 'rounded-r': (tabIndex + 1) % numColumns === 0 || tabIndex === numTabs - 1, + }); + +const getTabStyle = (numTabs: number) => { + return { + width: `${getTabWidth(numTabs)}px`, + }; +}; + +const getTabIconClassNames = (numTabs: number, isActiveTab: boolean) => { + return classnames('h-full w-full flex items-center justify-center', { + 'bg-customblue-40': isActiveTab, + rounded: isActiveTab, + }); +}; +const createStyleMap = ( + expandedWidth: number, + borderSize: number, + collapsedWidth: number +): StyleMap => { + const collapsedHideWidth = expandedWidth - collapsedWidth - borderSize; + + return { + open: { + left: { marginLeft: '0px' }, + right: { marginRight: '0px' }, + }, + closed: { + left: { marginLeft: `-${collapsedHideWidth}px` }, + right: { marginRight: `-${collapsedHideWidth}px` }, + }, + }; +}; + +const getToolTipContent = (label: string, disabled: boolean) => { + return ( + <> +
{label}
+ {disabled &&
{'Not available based on current context'}
} + + ); +}; + +const createBaseStyle = (expandedWidth: number) => { + return { + maxWidth: `${expandedWidth}px`, + width: `${expandedWidth}px`, + // To align the top of the side panel with the top of the viewport grid, use position relative and offset the + // top by the same top offset as the viewport grid. Also adjust the height so that there is no overflow. + position: 'relative', + top: '0.2%', + height: '99.8%', + }; +}; +const SidePanel = ({ + side, + className, + activeTabIndex: activeTabIndexProp = null, + tabs, + onOpen, + expandedWidth = 280, + onActiveTabIndexChange, +}) => { + const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null); + const [renderHeader, setRenderHeader] = useState(false); + const [activeTabIndex, setActiveTabIndex] = useState(0); + + const styleMap = createStyleMap(expandedWidth, borderSize, collapsedWidth); + const baseStyle = createBaseStyle(expandedWidth); + const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding; + const gridWidth = getGridWidth(tabs.length, gridAvailableWidth); + const openStatus = panelOpen ? 'open' : 'closed'; + const style = Object.assign({}, styleMap[openStatus][side], baseStyle); + + const updatePanelOpen = useCallback( + (panelOpen: boolean) => { + setPanelOpen(panelOpen); + if (panelOpen && onOpen) { + onOpen(); + } + }, + [onOpen] + ); + + const updateActiveTabIndex = useCallback( + (activeTabIndex: number) => { + if (activeTabIndex === null) { + updatePanelOpen(false); + return; + } + + setActiveTabIndex(activeTabIndex); + updatePanelOpen(true); + + if (onActiveTabIndexChange) { + onActiveTabIndexChange({ activeTabIndex }); + } + }, + [onActiveTabIndexChange, updatePanelOpen] + ); + + useEffect(() => { + setRenderHeader(tabs.length === 1); + }, [tabs]); + useEffect(() => { + updateActiveTabIndex(activeTabIndexProp); + }, [activeTabIndexProp, updateActiveTabIndex]); + + const getCloseStateComponent = () => { + const _childComponents = Array.isArray(tabs) ? tabs : [tabs]; + return ( + <> +
{ + updatePanelOpen(!panelOpen); + }} + data-cy={`side-panel-header-${side}`} + > + +
+
+ + {_childComponents.map((childComponent, index) => ( + + +
{ + return childComponent.disabled ? null : updateActiveTabIndex(index); + }} + > + {React.createElement(Icons[childComponent.iconName] || Icons.MissingIcon, { + className: classnames({ + 'text-primary-active': true, + 'ohif-disabled': childComponent.disabled, + }), + style: { + width: '22px', + height: '22px', + }, + })} +
+
+ +
+ {getToolTipContent(childComponent.label, childComponent.disabled)} +
+
+
+ ))} +
+
+ + ); + }; + + const getCloseIcon = () => { + return ( +
{ + updatePanelOpen(!panelOpen); + }} + data-cy={`side-panel-header-${side}`} + > + {React.createElement(Icons[openStateIconName[side]] || Icons.MissingIcon, { + className: 'text-primary-active', + })} +
+ ); + }; + + const getTabGridComponent = () => { + const numCols = getNumGridColumns(tabs.length, gridWidth); + + return ( + <> + {getCloseIcon()} + +
+
+ {tabs.map((tab, tabIndex) => { + const { disabled } = tab; + return ( + + {tabIndex % numCols !== 0 && ( +
+
+
+ )} + + + +
{ + return disabled ? null : updateActiveTabIndex(tabIndex); + }} + data-cy={`${tab.name}-btn`} + > +
+ {React.createElement(Icons[tab.iconName] || Icons.MissingIcon, { + className: classnames({ + 'text-primary-active': true, + 'ohif-disabled': disabled, + }), + style: { + width: '22px', + height: '22px', + }, + })} +
+
+
+ + {getToolTipContent(tab.label, disabled)} + +
+
+
+ ); + })} +
+
+ + ); + }; + + const getOpenStateComponent = () => { + if (tabs.length === 1) { + return null; + } + + return ( + <> +
+ {getTabGridComponent()} +
+ + + ); + }; + + return ( +
+ {panelOpen ? ( + <> + {getOpenStateComponent()} + {tabs.map((tab, tabIndex) => { + if (tabIndex === activeTabIndex) { + return ( + + ); + } + return null; + })} + + ) : ( + {getCloseStateComponent()} + )} +
+ ); +}; + +SidePanel.propTypes = { + side: PropTypes.oneOf(['left', 'right']).isRequired, + className: PropTypes.string, + activeTabIndex: PropTypes.number, + tabs: PropTypes.oneOfType([ + PropTypes.arrayOf( + PropTypes.shape({ + iconName: PropTypes.string.isRequired, + iconLabel: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + content: PropTypes.func, // TODO: Should be node, but it keeps complaining? + }) + ), + ]), + onOpen: PropTypes.func, + onActiveTabIndexChange: PropTypes.func, + expandedWidth: PropTypes.number, +}; + +export default SidePanel; diff --git a/platform/ui-next/src/components/SidePanel/index.ts b/platform/ui-next/src/components/SidePanel/index.ts new file mode 100644 index 000000000..68034ac14 --- /dev/null +++ b/platform/ui-next/src/components/SidePanel/index.ts @@ -0,0 +1,2 @@ +import SidePanel from './SidePanel'; +export default SidePanel; diff --git a/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx b/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx new file mode 100644 index 000000000..a419107e1 --- /dev/null +++ b/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx @@ -0,0 +1,143 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import StudyItem from '../StudyItem'; +import StudyBrowserSort from '../StudyBrowserSort'; +import StudyBrowserViewOptions from '../StudyBrowserViewOptions'; + +const getTrackedSeries = displaySets => { + let trackedSeries = 0; + displaySets.forEach(displaySet => { + if (displaySet.isTracked) { + trackedSeries++; + } + }); + + return trackedSeries; +}; + +const noop = () => {}; + +const StudyBrowser = ({ + tabs, + activeTabName, + expandedStudyInstanceUIDs, + onClickTab = noop, + onClickStudy = noop, + onClickThumbnail = noop, + onDoubleClickThumbnail = noop, + onClickUntrack = noop, + activeDisplaySetInstanceUIDs, + servicesManager, + showSettings, + viewPresets, +}: withAppTypes) => { + const getTabContent = () => { + const tabData = tabs.find(tab => tab.name === activeTabName); + const viewPreset = viewPresets + ? viewPresets.filter(preset => preset.selected)[0]?.id + : 'thumbnails'; + return tabData.studies.map( + ({ studyInstanceUid, date, description, numInstances, modalities, displaySets }) => { + const isExpanded = expandedStudyInstanceUIDs.includes(studyInstanceUid); + return ( + + { + onClickStudy(studyInstanceUid); + }} + onClickThumbnail={onClickThumbnail} + onDoubleClickThumbnail={onDoubleClickThumbnail} + onClickUntrack={onClickUntrack} + activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs} + data-cy="thumbnail-list" + viewPreset={viewPreset} + /> + + ); + } + ); + }; + + return ( + + {showSettings && ( +
+ + +
+ )} +
+ {getTabContent()} +
+
+ ); +}; + +StudyBrowser.propTypes = { + onClickTab: PropTypes.func.isRequired, + onClickStudy: PropTypes.func, + onClickThumbnail: PropTypes.func, + onDoubleClickThumbnail: PropTypes.func, + onClickUntrack: PropTypes.func, + activeTabName: PropTypes.string.isRequired, + expandedStudyInstanceUIDs: PropTypes.arrayOf(PropTypes.string).isRequired, + activeDisplaySetInstanceUIDs: PropTypes.arrayOf(PropTypes.string), + tabs: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + studies: PropTypes.arrayOf( + PropTypes.shape({ + studyInstanceUid: PropTypes.string.isRequired, + date: PropTypes.string, + numInstances: PropTypes.number, + modalities: PropTypes.string, + description: PropTypes.string, + displaySets: PropTypes.arrayOf( + PropTypes.shape({ + displaySetInstanceUID: PropTypes.string.isRequired, + imageSrc: PropTypes.string, + imageAltText: PropTypes.string, + seriesDate: PropTypes.string, + seriesNumber: PropTypes.any, + numInstances: PropTypes.number, + description: PropTypes.string, + componentType: PropTypes.oneOf(['thumbnail', 'thumbnailTracked', 'thumbnailNoImage']) + .isRequired, + isTracked: PropTypes.bool, + /** + * Data the thumbnail should expose to a receiving drop target. Use a matching + * `dragData.type` to identify which targets can receive this draggable item. + * If this is not set, drag-n-drop will be disabled for this thumbnail. + * + * Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members + */ + dragData: PropTypes.shape({ + /** Must match the "type" a dropTarget expects */ + type: PropTypes.string.isRequired, + }), + }) + ), + }) + ).isRequired, + }) + ), +}; + +export default StudyBrowser; diff --git a/platform/ui-next/src/components/StudyBrowser/index.ts b/platform/ui-next/src/components/StudyBrowser/index.ts new file mode 100644 index 000000000..aa85fa066 --- /dev/null +++ b/platform/ui-next/src/components/StudyBrowser/index.ts @@ -0,0 +1,2 @@ +import StudyBrowser from './StudyBrowser'; +export default StudyBrowser; diff --git a/platform/ui-next/src/components/StudyBrowserSort/StudyBrowserSort.tsx b/platform/ui-next/src/components/StudyBrowserSort/StudyBrowserSort.tsx new file mode 100644 index 000000000..f1d38ad93 --- /dev/null +++ b/platform/ui-next/src/components/StudyBrowserSort/StudyBrowserSort.tsx @@ -0,0 +1,75 @@ +import React, { useEffect, useState } from 'react'; +import { Icons } from '../Icons'; + +export default function StudyBrowserSort({ servicesManager }: withAppTypes) { + const { customizationService, displaySetService } = servicesManager.services; + const { values: sortFunctions } = customizationService.get('studyBrowser.sortFunctions'); + + const [selectedSort, setSelectedSort] = useState(sortFunctions[0]); + const [sortDirection, setSortDirection] = useState('ascending'); + + const handleSortChange = event => { + const selectedSortFunction = sortFunctions.find(sort => sort.label === event.target.value); + setSelectedSort(selectedSortFunction); + }; + + const toggleSortDirection = e => { + e.stopPropagation(); + setSortDirection(prevDirection => (prevDirection === 'ascending' ? 'descending' : 'ascending')); + }; + + useEffect(() => { + displaySetService.sortDisplaySets(selectedSort.sortFunction, sortDirection); + }, [displaySetService, selectedSort, sortDirection]); + + useEffect(() => { + const SubscriptionDisplaySetsChanged = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_CHANGED, + () => { + displaySetService.sortDisplaySets(selectedSort.sortFunction, sortDirection, true); + } + ); + const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, + () => { + displaySetService.sortDisplaySets(selectedSort.sortFunction, sortDirection, true); + } + ); + + return () => { + SubscriptionDisplaySetsChanged.unsubscribe(); + SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); + }; + }, [displaySetService, selectedSort, sortDirection]); + + return ( +
+ + +
+ ); +} diff --git a/platform/ui-next/src/components/StudyBrowserSort/index.ts b/platform/ui-next/src/components/StudyBrowserSort/index.ts new file mode 100644 index 000000000..1f9a4601e --- /dev/null +++ b/platform/ui-next/src/components/StudyBrowserSort/index.ts @@ -0,0 +1,3 @@ +import StudyBrowserSort from './StudyBrowserSort'; + +export default StudyBrowserSort; diff --git a/platform/ui-next/src/components/StudyBrowserViewOptions/StudyBrowserViewOptions.tsx b/platform/ui-next/src/components/StudyBrowserViewOptions/StudyBrowserViewOptions.tsx new file mode 100644 index 000000000..05dfd0be4 --- /dev/null +++ b/platform/ui-next/src/components/StudyBrowserViewOptions/StudyBrowserViewOptions.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +export default function StudyBrowserViewOptions({ + tabs, + onSelectTab, + activeTabName, +}: withAppTypes) { + const handleTabChange = (tabName: string) => { + onSelectTab(tabName); + }; + + return ( +
+ +
+ ); +} diff --git a/platform/ui-next/src/components/StudyBrowserViewOptions/index.ts b/platform/ui-next/src/components/StudyBrowserViewOptions/index.ts new file mode 100644 index 000000000..d2193c6ad --- /dev/null +++ b/platform/ui-next/src/components/StudyBrowserViewOptions/index.ts @@ -0,0 +1,3 @@ +import StudyBrowserViewOptions from './StudyBrowserViewOptions'; + +export default StudyBrowserViewOptions; diff --git a/platform/ui-next/src/components/StudyItem/StudyItem.tsx b/platform/ui-next/src/components/StudyItem/StudyItem.tsx new file mode 100644 index 000000000..977db0871 --- /dev/null +++ b/platform/ui-next/src/components/StudyItem/StudyItem.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import ThumbnailList from '../ThumbnailList'; + +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../Accordion'; + +const StudyItem = ({ + date, + description, + numInstances, + modalities, + isActive, + onClick, + isExpanded, + displaySets, + activeDisplaySetInstanceUIDs, + onClickThumbnail, + onDoubleClickThumbnail, + onClickUntrack, + viewPreset = 'thumbnails', +}) => { + return ( + + + +
+
+
+
{date}
+
+ {description} +
+
+
+
{modalities}
+
{numInstances}
+
+
+
+
+ { + event.stopPropagation(); + }} + > + {isExpanded && displaySets && ( + + )} + +
+
+ ); +}; + +StudyItem.propTypes = { + date: PropTypes.string.isRequired, + description: PropTypes.string, + modalities: PropTypes.string.isRequired, + numInstances: PropTypes.number.isRequired, + trackedSeries: PropTypes.number, + isActive: PropTypes.bool, + onClick: PropTypes.func.isRequired, + isExpanded: PropTypes.bool, + displaySets: PropTypes.array, + activeDisplaySetInstanceUIDs: PropTypes.array, + onClickThumbnail: PropTypes.func, + onDoubleClickThumbnail: PropTypes.func, + onClickUntrack: PropTypes.func, + viewPreset: PropTypes.string, +}; + +export default StudyItem; diff --git a/platform/ui-next/src/components/StudyItem/index.ts b/platform/ui-next/src/components/StudyItem/index.ts new file mode 100644 index 000000000..0c9680c4f --- /dev/null +++ b/platform/ui-next/src/components/StudyItem/index.ts @@ -0,0 +1,2 @@ +import StudyItem from './StudyItem'; +export default StudyItem; diff --git a/platform/ui-next/src/components/Tabs/index.ts b/platform/ui-next/src/components/Tabs/index.ts new file mode 100644 index 000000000..f5be4be6d --- /dev/null +++ b/platform/ui-next/src/components/Tabs/index.ts @@ -0,0 +1,3 @@ +import { Tabs, TabsList, TabsTrigger, TabsContent } from "./Tabs" + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/platform/ui-next/src/components/Thumbnail/Thumbnail.tsx b/platform/ui-next/src/components/Thumbnail/Thumbnail.tsx new file mode 100644 index 000000000..387036236 --- /dev/null +++ b/platform/ui-next/src/components/Thumbnail/Thumbnail.tsx @@ -0,0 +1,294 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { useDrag } from 'react-dnd'; +import { Icons } from '../Icons'; +import DisplaySetMessageListTooltip from '../DisplaySetMessageListTooltip'; +import { TooltipTrigger, TooltipContent, TooltipProvider, Tooltip } from '../Tooltip'; + +/** + * Display a thumbnail for a display set. + */ +const Thumbnail = ({ + displaySetInstanceUID, + className, + imageSrc, + imageAltText, + description, + seriesNumber, + numInstances, + loadingProgress, + countIcon, + messages, + dragData = {}, + isActive, + onClick, + onDoubleClick, + viewPreset = 'thumbnails', + modality, + isHydratedForDerivedDisplaySet = false, + canReject = false, + onReject = () => {}, + isTracked = false, + onClickUntrack = () => {}, +}): React.ReactNode => { + // TODO: We should wrap our thumbnail to create a "DraggableThumbnail", as + // this will still allow for "drag", even if there is no drop target for the + // specified item. + const [collectedProps, drag, dragPreview] = useDrag({ + type: 'displayset', + item: { ...dragData }, + canDrag: function (monitor) { + return Object.keys(dragData).length !== 0; + }, + }); + + const [lastTap, setLastTap] = useState(0); + + const handleTouchEnd = e => { + const currentTime = new Date().getTime(); + const tapLength = currentTime - lastTap; + if (tapLength < 300 && tapLength > 0) { + onDoubleClick(e); + } else { + onClick(e); + } + setLastTap(currentTime); + }; + + const renderThumbnailPreset = () => { + return ( +
+
+
+ {imageSrc ? ( + {imageAltText} + ) : ( +
+ )} + + {/* bottom left */} +
+
+
{modality}
+
+ + {/* top right */} +
+ + {canReject && ( + + )} + {isTracked && ( + + + +
+ + +
+
+ +
+
+ +
+
+ + + {isTracked ? 'Series is tracked' : 'Series is untracked'} + + +
+
+
+
+
+ )} +
+
+
+
+
{description}
+
+
S:{seriesNumber}
+
+
+ {' '} + {countIcon ? ( + React.createElement(Icons[countIcon] || Icons.MissingIcon, { className: 'w-3' }) + ) : ( + + )} +
{numInstances}
+
+
+
+
+
+ ); + }; + + const renderListPreset = () => { + return ( +
+
+
+
+
+
{modality}
+ +
+ {description} +
+
+ +
+
S:{seriesNumber}
+
+
+ {' '} + {countIcon ? ( + React.createElement(Icons[countIcon] || Icons.MissingIcon, { className: 'w-3' }) + ) : ( + + )} +
{numInstances}
+
+
+
+
+
+
+ + {canReject && ( + + )} + {isTracked && ( + + + +
+ + +
+
+ +
+
+ +
+
+ + + {isTracked ? 'Series is tracked' : 'Series is untracked'} + + +
+
+
+
+
+ )} +
+
+ ); + }; + + return ( +
+
+ {viewPreset === 'thumbnails' && renderThumbnailPreset()} + {viewPreset === 'list' && renderListPreset()} +
+
+ ); +}; + +Thumbnail.propTypes = { + displaySetInstanceUID: PropTypes.string.isRequired, + className: PropTypes.string, + imageSrc: PropTypes.string, + /** + * Data the thumbnail should expose to a receiving drop target. Use a matching + * `dragData.type` to identify which targets can receive this draggable item. + * If this is not set, drag-n-drop will be disabled for this thumbnail. + * + * Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members + */ + dragData: PropTypes.shape({ + /** Must match the "type" a dropTarget expects */ + type: PropTypes.string.isRequired, + }), + imageAltText: PropTypes.string, + description: PropTypes.string.isRequired, + seriesNumber: PropTypes.any, + numInstances: PropTypes.number.isRequired, + loadingProgress: PropTypes.number, + messages: PropTypes.object, + isActive: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func.isRequired, + viewPreset: PropTypes.string, + modality: PropTypes.string, + isHydratedForDerivedDisplaySet: PropTypes.bool, + canReject: PropTypes.bool, + onReject: PropTypes.func, + isTracked: PropTypes.bool, + onClickUntrack: PropTypes.func, + countIcon: PropTypes.string, +}; + +export default Thumbnail; diff --git a/platform/ui-next/src/components/Thumbnail/index.ts b/platform/ui-next/src/components/Thumbnail/index.ts new file mode 100644 index 000000000..275561d01 --- /dev/null +++ b/platform/ui-next/src/components/Thumbnail/index.ts @@ -0,0 +1,2 @@ +import Thumbnail from './Thumbnail'; +export default Thumbnail; diff --git a/platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx b/platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx new file mode 100644 index 000000000..f0b0179f0 --- /dev/null +++ b/platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Thumbnail from '../Thumbnail'; + +const ThumbnailList = ({ + thumbnails, + onThumbnailClick, + onThumbnailDoubleClick, + onClickUntrack, + activeDisplaySetInstanceUIDs = [], + viewPreset, +}) => { + return ( +
+ {thumbnails.map( + ({ + displaySetInstanceUID, + description, + dragData, + seriesNumber, + numInstances, + loadingProgress, + modality, + componentType, + seriesDate, + countIcon, + isTracked, + canReject, + onReject, + imageSrc, + messages, + imageAltText, + isHydratedForDerivedDisplaySet, + }) => { + const isActive = activeDisplaySetInstanceUIDs.includes(displaySetInstanceUID); + switch (componentType) { + case 'thumbnail': + return ( + onThumbnailClick(displaySetInstanceUID)} + onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)} + viewPreset={viewPreset} + modality={modality} + /> + ); + case 'thumbnailTracked': + return ( + onThumbnailClick(displaySetInstanceUID)} + onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)} + onClickUntrack={() => onClickUntrack(displaySetInstanceUID)} + viewPreset={viewPreset} + modality={modality} + /> + ); + case 'thumbnailNoImage': + return ( + onThumbnailClick(displaySetInstanceUID)} + onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)} + viewPreset={viewPreset} + countIcon={countIcon} + seriesNumber={seriesNumber} + numInstances={numInstances || 1} + isHydratedForDerivedDisplaySet={isHydratedForDerivedDisplaySet} + canReject={canReject} + onReject={onReject} + /> + ); + default: + return <>; + } + } + )} +
+ ); +}; + +ThumbnailList.propTypes = { + thumbnails: PropTypes.arrayOf( + PropTypes.shape({ + displaySetInstanceUID: PropTypes.string.isRequired, + imageSrc: PropTypes.string, + imageAltText: PropTypes.string, + seriesDate: PropTypes.string, + seriesNumber: PropTypes.any, + numInstances: PropTypes.number, + description: PropTypes.string, + componentType: PropTypes.any, + isTracked: PropTypes.bool, + /** + * Data the thumbnail should expose to a receiving drop target. Use a matching + * `dragData.type` to identify which targets can receive this draggable item. + * If this is not set, drag-n-drop will be disabled for this thumbnail. + * + * Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members + */ + dragData: PropTypes.shape({ + /** Must match the "type" a dropTarget expects */ + type: PropTypes.string.isRequired, + }), + }) + ), + activeDisplaySetInstanceUIDs: PropTypes.arrayOf(PropTypes.string), + onThumbnailClick: PropTypes.func.isRequired, + onThumbnailDoubleClick: PropTypes.func.isRequired, + onClickUntrack: PropTypes.func.isRequired, + viewPreset: PropTypes.string, +}; + +// TODO: Support "Viewport Identificator"? +function _getModalityTooltip(modality) { + if (_modalityTooltips.hasOwnProperty(modality)) { + return _modalityTooltips[modality]; + } + + return 'Unknown'; +} + +const _modalityTooltips = { + SR: 'Structured Report', + SEG: 'Segmentation', + OT: 'Other', + RTSTRUCT: 'RT Structure Set', +}; + +export default ThumbnailList; diff --git a/platform/ui-next/src/components/ThumbnailList/index.ts b/platform/ui-next/src/components/ThumbnailList/index.ts new file mode 100644 index 000000000..e2cfade78 --- /dev/null +++ b/platform/ui-next/src/components/ThumbnailList/index.ts @@ -0,0 +1,2 @@ +import ThumbnailList from './ThumbnailList'; +export default ThumbnailList; diff --git a/platform/ui-next/src/components/Toggle/Toggle.tsx b/platform/ui-next/src/components/Toggle/Toggle.tsx index 01a4bfd1b..d6e8d0d9b 100644 --- a/platform/ui-next/src/components/Toggle/Toggle.tsx +++ b/platform/ui-next/src/components/Toggle/Toggle.tsx @@ -9,12 +9,12 @@ const toggleVariants = cva( { variants: { variant: { - default: 'bg-transparent', + default: 'bg-transparent hover:text-primary', outline: 'border border-input bg-transparent shadow-sm hover:bg-primary/20 hover:text-primary-foreground', }, size: { - default: 'h-9 px-3 mx-0.5', + default: 'h-[24px] w-[28px]', sm: 'h-8 px-2', lg: 'h-10 px-3', }, diff --git a/platform/ui-next/src/components/Toggle/index.ts b/platform/ui-next/src/components/Toggle/index.ts new file mode 100644 index 000000000..e96de617a --- /dev/null +++ b/platform/ui-next/src/components/Toggle/index.ts @@ -0,0 +1,3 @@ +import { Toggle, toggleVariants } from "./Toggle"; + +export { Toggle, toggleVariants }; diff --git a/platform/ui-next/src/components/ToggleGroup/ToggleGroup.tsx b/platform/ui-next/src/components/ToggleGroup/ToggleGroup.tsx new file mode 100644 index 000000000..b2d878969 --- /dev/null +++ b/platform/ui-next/src/components/ToggleGroup/ToggleGroup.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'; +import { VariantProps } from 'class-variance-authority'; + +import { cn } from '../../lib/utils'; +import { toggleVariants } from '../Toggle'; + +const ToggleGroupContext = React.createContext>({ + size: 'default', + variant: 'default', +}); + +const ToggleGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, size, children, ...props }, ref) => ( + + {children} + +)); + +ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName; + +const ToggleGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, children, variant, size, ...props }, ref) => { + const context = React.useContext(ToggleGroupContext); + + return ( + + {children} + + ); +}); + +ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName; + +export { ToggleGroup, ToggleGroupItem }; diff --git a/platform/ui-next/src/components/ToggleGroup/index.ts b/platform/ui-next/src/components/ToggleGroup/index.ts new file mode 100644 index 000000000..8e54b5957 --- /dev/null +++ b/platform/ui-next/src/components/ToggleGroup/index.ts @@ -0,0 +1,3 @@ +import { ToggleGroup, ToggleGroupItem } from "./ToggleGroup"; + +export { ToggleGroup, ToggleGroupItem }; diff --git a/platform/ui-next/src/components/Toolbox/Toolbox.tsx b/platform/ui-next/src/components/Toolbox/Toolbox.tsx new file mode 100644 index 000000000..726de8a41 --- /dev/null +++ b/platform/ui-next/src/components/Toolbox/Toolbox.tsx @@ -0,0 +1,183 @@ +import React, { useEffect, useRef } from 'react'; +import { useToolbar } from '@ohif/core'; +import { ToolboxUI } from './'; +// Migrate this file to the new UI eventually +import { useToolbox } from '@ohif/ui'; +import Separator from '../Separator'; + +/** + * A toolbox is a collection of buttons and commands that they invoke, used to provide + * custom control panels to users. This component is a generic UI component that + * interacts with services and commands in a generic fashion. While it might + * seem unconventional to import it from the UI and integrate it into the JSX, + * it belongs in the UI components as there isn't anything in this component that + * couldn't be used for a completely different type of app. It plays a crucial + * role in enhancing the app with a toolbox by providing a way to integrate + * and display various tools and their corresponding options + */ +function Toolbox({ + servicesManager, + buttonSectionId, + commandsManager, + title, + renderHeader, + getCloseIcon, + tab, + ...props +}: withAppTypes) { + const { state: toolboxState, api } = useToolbox(buttonSectionId); + const { onInteraction, toolbarButtons } = useToolbar({ + servicesManager, + buttonSection: buttonSectionId, + }); + + const prevButtonIdsRef = useRef(''); + const prevToolboxStateRef = useRef(''); + + useEffect(() => { + const currentButtonIdsStr = JSON.stringify( + toolbarButtons.map(button => { + const { id, componentProps } = button; + if (componentProps.items?.length) { + return componentProps.items.map(item => `${item.id}-${item.disabled}`); + } + return `${id}-${componentProps.disabled}`; + }) + ); + + const currentToolBoxStateStr = JSON.stringify( + Object.keys(toolboxState.toolOptions).map(tool => { + const options = toolboxState.toolOptions[tool]; + if (Array.isArray(options)) { + return options?.map(option => `${option.id}-${option.value}`); + } + }) + ); + + if ( + prevButtonIdsRef.current === currentButtonIdsStr && + prevToolboxStateRef.current === currentToolBoxStateStr + ) { + return; + } + + prevButtonIdsRef.current = currentButtonIdsStr; + prevToolboxStateRef.current = currentToolBoxStateStr; + + const initializeOptionsWithEnhancements = toolbarButtons.reduce( + (accumulator, toolbarButton) => { + const { id: buttonId, componentProps } = toolbarButton; + + const createEnhancedOptions = (options, parentId) => { + const optionsToUse = Array.isArray(options) ? options : [options]; + + return optionsToUse.map(option => { + if (typeof option.optionComponent === 'function') { + return option; + } + + const value = + toolboxState.toolOptions?.[parentId]?.find(prop => prop.id === option.id)?.value ?? + option.value; + + const updatedOptions = toolboxState.toolOptions?.[parentId]; + + return { + ...option, + value, + commands: value => { + api.handleToolOptionChange(parentId, option.id, value); + + const { isArray } = Array; + const cmds = isArray(option.commands) ? option.commands : [option.commands]; + + cmds.forEach(command => { + const isString = typeof command === 'string'; + const isObject = typeof command === 'object'; + const isFunction = typeof command === 'function'; + + if (isString) { + commandsManager.run(command, { value }); + } else if (isObject) { + commandsManager.run({ + ...command, + commandOptions: { + ...command.commandOptions, + ...option, + value, + options: updatedOptions, + }, + }); + } else if (isFunction) { + command({ value, commandsManager, servicesManager, options: updatedOptions }); + } + }); + }, + }; + }); + }; + + const { items, options } = componentProps; + + if (items?.length) { + items.forEach(({ options, id }) => { + accumulator[id] = createEnhancedOptions(options, id); + }); + } else if (options?.length) { + accumulator[buttonId] = createEnhancedOptions(options, buttonId); + } else if (options?.optionComponent) { + accumulator[buttonId] = options.optionComponent; + } + + return accumulator; + }, + {} + ); + + api.initializeToolOptions(initializeOptionsWithEnhancements); + }, [toolbarButtons, api, toolboxState, commandsManager, servicesManager]); + + const handleToolOptionChange = (toolName, optionName, newValue) => { + api.handleToolOptionChange(toolName, optionName, newValue); + }; + + useEffect(() => { + return () => { + api.handleToolSelect(null); + }; + }, []); + + return ( + <> + {renderHeader && ( + <> +
+
+
+ {tab.label} +
+
+ + {getCloseIcon()} +
+ + + )} + api.handleToolSelect(id)} + handleToolOptionChange={handleToolOptionChange} + onInteraction={onInteraction} + /> + + ); +} + +export default Toolbox; diff --git a/platform/ui-next/src/components/Toolbox/ToolboxUI.tsx b/platform/ui-next/src/components/Toolbox/ToolboxUI.tsx new file mode 100644 index 000000000..00c381b4e --- /dev/null +++ b/platform/ui-next/src/components/Toolbox/ToolboxUI.tsx @@ -0,0 +1,126 @@ +import React, { useEffect, useRef } from 'react'; +import { PanelSection } from '../../components'; +// Migrate this file to the new UI eventually +import { ToolSettings } from '@ohif/ui'; +import classnames from 'classnames'; + +const ItemsPerRow = 4; + +function usePrevious(value) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +} + +/** + * Just refactoring from the toolbox component to make it more readable + */ +function ToolboxUI(props: withAppTypes) { + const { + toolbarButtons, + handleToolSelect, + toolboxState, + numRows, + servicesManager, + title, + useCollapsedPanel = true, + } = props; + + const { activeTool, toolOptions, selectedEvent } = toolboxState; + const activeToolOptions = toolOptions?.[activeTool]; + + const prevToolOptions = usePrevious(activeToolOptions); + + useEffect(() => { + if (!activeToolOptions || Array.isArray(activeToolOptions) === false) { + return; + } + + activeToolOptions.forEach((option, index) => { + const prevOption = prevToolOptions ? prevToolOptions[index] : undefined; + if (!prevOption || option.value !== prevOption.value || selectedEvent) { + const isOptionValid = option.condition + ? option.condition({ options: activeToolOptions }) + : true; + if (isOptionValid) { + const { commands } = option; + commands(option.value); + } + } + }); + }, [activeToolOptions, selectedEvent]); + + const render = () => { + return ( + <> +
+
+ {toolbarButtons.map((toolDef, index) => { + if (!toolDef) { + return null; + } + + const { id, Component, componentProps } = toolDef; + const isLastRow = Math.floor(index / ItemsPerRow) + 1 === numRows; + + const toolClasses = `ml-1 ${isLastRow ? '' : 'mb-2'}`; + + const onInteraction = ({ itemId, id, commands }) => { + const idToUse = itemId || id; + handleToolSelect(idToUse); + props.onInteraction({ + itemId, + commands, + }); + }; + + return ( +
+
+ +
+
+ ); + })} +
+
+
+ {activeToolOptions && } +
+ + ); + }; + + return ( + <> + {useCollapsedPanel ? ( + + {render()} + + ) : ( + render() + )} + + ); +} + +export { ToolboxUI }; diff --git a/platform/ui-next/src/components/Toolbox/index.ts b/platform/ui-next/src/components/Toolbox/index.ts new file mode 100644 index 000000000..e5c0d5de7 --- /dev/null +++ b/platform/ui-next/src/components/Toolbox/index.ts @@ -0,0 +1,3 @@ +import { ToolboxUI } from './ToolboxUI'; +import Toolbox from './Toolbox'; +export { ToolboxUI, Toolbox }; diff --git a/platform/ui-next/src/components/Tooltip/Tooltip.tsx b/platform/ui-next/src/components/Tooltip/Tooltip.tsx index 8f3cb69b2..0b24596bf 100644 --- a/platform/ui-next/src/components/Tooltip/Tooltip.tsx +++ b/platform/ui-next/src/components/Tooltip/Tooltip.tsx @@ -17,7 +17,7 @@ const TooltipContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - 'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + 'bg-primary-dark border-secondary-light text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-xs', className )} {...props} diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts index 1306c1f74..c8951c2d7 100644 --- a/platform/ui-next/src/components/index.ts +++ b/platform/ui-next/src/components/index.ts @@ -6,15 +6,28 @@ import Combobox from './Combobox'; import Popover from './Popover'; import Calendar from './Calendar'; import DatePickerWithRange from './DateRange'; +import Separator from './Separator'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs'; +import { Toggle, toggleVariants } from './Toggle'; +import { ToggleGroup, ToggleGroupItem } from './ToggleGroup'; import { Input } from './Input'; import { Label } from './Label'; -import { Tabs, TabsList, TabsTrigger, TabsContent } from './Tabs'; -import { Separator } from './Separator'; import { Switch } from './Switch'; import { Checkbox } from './Checkbox'; -import { Toggle, toggleVariants } from './Toggle'; import { Slider } from './Slider'; import { ScrollArea, ScrollBar } from './ScrollArea'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './Accordion'; +import { Icons } from './Icons'; +import SidePanel from './SidePanel'; +import StudyItem from './StudyItem'; +import StudyBrowser from './StudyBrowser'; +import StudyBrowserSort from './StudyBrowserSort'; +import StudyBrowserViewOptions from './StudyBrowserViewOptions'; +import Thumbnail from './Thumbnail'; +import ThumbnailList from './ThumbnailList'; +import PanelSection from './PanelSection'; +import DisplaySetMessageListTooltip from './DisplaySetMessageListTooltip'; +import { Toolbox, ToolboxUI } from './Toolbox'; export { Button, @@ -39,4 +52,23 @@ export { toggleVariants, Slider, ScrollArea, + ToggleGroup, + ToggleGroupItem, + ScrollBar, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Icons, + SidePanel, + StudyItem, + StudyBrowser, + StudyBrowserSort, + StudyBrowserViewOptions, + Thumbnail, + ThumbnailList, + PanelSection, + DisplaySetMessageListTooltip, + Toolbox, + ToolboxUI, }; diff --git a/platform/ui-next/src/index.ts b/platform/ui-next/src/index.ts index 0c140faba..3a3dfff9f 100644 --- a/platform/ui-next/src/index.ts +++ b/platform/ui-next/src/index.ts @@ -8,9 +8,34 @@ import { Combobox, Calendar, DatePickerWithRange, + Separator, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Toggle, + toggleVariants, + ToggleGroup, + ToggleGroupItem, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Icons, + SidePanel, + StudyItem, + StudyBrowser, + StudyBrowserSort, + StudyBrowserViewOptions, + Thumbnail, + ThumbnailList, + PanelSection, + DisplaySetMessageListTooltip, + Toolbox, + ToolboxUI, } from './components'; + import { useNotification, NotificationProvider } from './contextProviders'; -import { Toggle, toggleVariants } from './Toggle'; export { // components @@ -26,4 +51,29 @@ export { // contextProviders NotificationProvider, useNotification, + Separator, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Toggle, + toggleVariants, + ToggleGroup, + ToggleGroupItem, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Icons, + SidePanel, + StudyItem, + StudyBrowser, + StudyBrowserSort, + StudyBrowserViewOptions, + Thumbnail, + ThumbnailList, + PanelSection, + DisplaySetMessageListTooltip, + Toolbox, + ToolboxUI, }; diff --git a/platform/ui-next/tailwind.config.js b/platform/ui-next/tailwind.config.js index 54c59f431..e89be1e46 100644 --- a/platform/ui-next/tailwind.config.js +++ b/platform/ui-next/tailwind.config.js @@ -90,6 +90,20 @@ module.exports = { 'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out', }, + bkg: { + low: '#050615', + med: '#090C29', + full: '#041C4A', + }, + info: { + primary: '#FFFFFF', + secondary: '#7BB2CE', + }, + actions: { + primary: '#348CFD', + highlight: '#5ACCE6', + hover: 'rgba(52, 140, 253, 0.2)', + }, }, }, plugins: [require('tailwindcss-animate')], diff --git a/platform/ui-next/tsconfig.json b/platform/ui-next/tsconfig.json index e63e92ef4..adea64ef2 100644 --- a/platform/ui-next/tsconfig.json +++ b/platform/ui-next/tsconfig.json @@ -3,7 +3,9 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@ui/*": ["./src/*"] + "@ui/*": ["./src/*"], + "@ohif/core": ["../core/src"], + "@ohif/ui": ["../ui/src"] } }, "include": ["src"], diff --git a/platform/ui/tailwind.config.js b/platform/ui/tailwind.config.js index 29e859d8f..ad6fb046e 100644 --- a/platform/ui/tailwind.config.js +++ b/platform/ui/tailwind.config.js @@ -59,7 +59,20 @@ module.exports = { dark: '#726f7e', active: '#2c3074', }, - + bkg: { + low: '#050615', + med: '#090C29', + full: '#041C4A', + }, + info: { + primary: '#FFFFFF', + secondary: '#7BB2CE', + }, + actions: { + primary: '#348CFD', + highlight: '#5ACCE6', + hover: 'rgba(52, 140, 253, 0.2)', + }, customgreen: { 100: '#05D97C', 200: '#0FD97C', diff --git a/yarn.lock b/yarn.lock index ef1edd8e8..8bd793ce8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3527,9 +3527,9 @@ graceful-fs "4.2.10" "@pnpm/npm-conf@^2.1.0": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz#0058baf1c26cbb63a828f0193795401684ac86f0" - integrity sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz#bb375a571a0bd63ab0a23bece33033c683e9b6b0" + integrity sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw== dependencies: "@pnpm/config.env-replace" "^1.1.0" "@pnpm/network.ca-file" "^1.0.1" @@ -3564,6 +3564,21 @@ resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2" integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA== +"@radix-ui/react-accordion@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.2.0.tgz#aed0770fcb16285db992d81873ccd7a014c7f17d" + integrity sha512-HJOzSX8dQqtsp/3jVxCU3CXEONF7/2jlGAB28oX8TTw1Dz8JYbEI1UcL8355PuLBE41/IRRMvCw7VkiK/jcUOQ== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-collapsible" "1.1.0" + "@radix-ui/react-collection" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-arrow@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d" @@ -3593,6 +3608,20 @@ "@radix-ui/react-use-previous" "1.1.0" "@radix-ui/react-use-size" "1.1.0" +"@radix-ui/react-collapsible@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz#4d49ddcc7b7d38f6c82f1fd29674f6fab5353e77" + integrity sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-collection@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.3.tgz#9595a66e09026187524a36c6e7e9c7d286469159"