ui(components): Viewport components updated to ui-next with design updates (#4886)

This commit is contained in:
Dan Rukas 2025-04-07 16:45:05 -04:00 committed by GitHub
parent 4e09f85d5d
commit a8ac297e16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 933 additions and 326 deletions

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ViewportActionArrows } from '@ohif/ui'; import { ViewportActionArrows } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next'; import { useViewportGrid } from '@ohif/ui-next';
import { utils } from '@ohif/extension-cornerstone'; import { utils } from '@ohif/extension-cornerstone';

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ViewportActionButton } from '@ohif/ui'; import { ViewportActionButton } from '@ohif/ui-next';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next'; import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
export default function _getStatusComponent({ isHydrated, onStatusClick }) { export default function _getStatusComponent({ isHydrated, onStatusClick }) {

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ViewportActionArrows } from '@ohif/ui'; import { ViewportActionArrows } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next'; import { useViewportGrid } from '@ohif/ui-next';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup'; import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG'; import promptHydrateSEG from '../utils/promptHydrateSEG';

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ViewportActionButton } from '@ohif/ui'; import { ViewportActionButton } from '@ohif/ui-next';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next'; import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
export default function _getStatusComponent({ isHydrated, onStatusClick }) { export default function _getStatusComponent({ isHydrated, onStatusClick }) {

View File

@ -5,7 +5,7 @@ import { ExtensionManager, useToolbar } from '@ohif/core';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule'; import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import { ViewportActionArrows } from '@ohif/ui'; import { ViewportActionArrows } from '@ohif/ui-next';
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet'; import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone'; import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { useViewportGrid } from '@ohif/ui-next'; import { useViewportGrid } from '@ohif/ui-next';

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SOPClassHandlerId, SOPClassHandlerId3D } from './id'; import { SOPClassHandlerId, SOPClassHandlerId3D } from './id';
import { ViewportActionButton } from '@ohif/ui'; import { ViewportActionButton } from '@ohif/ui-next';
import i18n from '@ohif/i18n'; import i18n from '@ohif/i18n';
export default function onModeEnter({ servicesManager }) { export default function onModeEnter({ servicesManager }) {

View File

@ -3,7 +3,8 @@ import { useResizeDetector } from 'react-resize-detector';
import * as cs3DTools from '@cornerstonejs/tools'; import * as cs3DTools from '@cornerstonejs/tools';
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core'; import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core'; import { MeasurementService } from '@ohif/core';
import { AllInOneMenu, useViewportDialog } from '@ohif/ui'; import { AllInOneMenu } from '@ohif/ui-next';
import { useViewportDialog } from '@ohif/ui';
import type { Types as csTypes } from '@cornerstonejs/core'; import type { Types as csTypes } from '@cornerstonejs/core';
import { setEnabledElement } from '../state'; import { setEnabledElement } from '../state';
@ -99,7 +100,7 @@ const OHIFCornerstoneViewport = React.memo(
const [viewportDialogState] = useViewportDialog(); const [viewportDialogState] = useViewportDialog();
// useCallback for scroll bar height calculation // useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => { const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`; const scrollbarHeight = `${elementRef.current.clientHeight - 10}px`;
setScrollbarHeight(scrollbarHeight); setScrollbarHeight(scrollbarHeight);
}, [elementRef]); }, [elementRef]);

View File

@ -3,7 +3,7 @@ import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core'; import { metaData, Enums, utilities } from '@cornerstonejs/core';
import type { ImageSliceData } from '@cornerstonejs/core/types'; import type { ImageSliceData } from '@cornerstonejs/core/types';
import { ViewportOverlay } from '@ohif/ui'; import { ViewportOverlay } from '@ohif/ui-next';
import type { InstanceMetadata } from '@ohif/core/src/types'; import type { InstanceMetadata } from '@ohif/core/src/types';
import { formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils'; import { formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { utils } from '@ohif/core'; import { utils } from '@ohif/core';
@ -368,7 +368,7 @@ function OverlayItem(props) {
title={title} title={title}
> >
{label ? <span className="mr-1 shrink-0">{label}</span> : null} {label ? <span className="mr-1 shrink-0">{label}</span> : null}
<span className="ml-1 mr-2 shrink-0">{value}</span> <span className="ml-0 mr-2 shrink-0">{value}</span>
</div> </div>
); );
} }
@ -387,10 +387,10 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
className="overlay-item flex flex-row" className="overlay-item flex flex-row"
style={{ color: customization?.color }} style={{ color: customization?.color }}
> >
<span className="mr-1 shrink-0">W:</span> <span className="mr-0.5 shrink-0 opacity-[0.67]">W:</span>
<span className="ml-1 mr-2 shrink-0">{windowWidth.toFixed(0)}</span> <span className="mr-2.5 shrink-0">{windowWidth.toFixed(0)}</span>
<span className="mr-1 shrink-0">L:</span> <span className="mr-0.5 shrink-0 opacity-[0.67]">L:</span>
<span className="ml-1 shrink-0">{windowCenter.toFixed(0)}</span> <span className="shrink-0">{windowCenter.toFixed(0)}</span>
</div> </div>
); );
} }
@ -404,7 +404,7 @@ function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
className="overlay-item flex flex-row" className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }} style={{ color: (customization && customization.color) || undefined }}
> >
<span className="mr-1 shrink-0">Zoom:</span> <span className="mr-0.5 shrink-0 opacity-[0.67]">Zoom:</span>
<span>{scale.toFixed(2)}x</span> <span>{scale.toFixed(2)}x</span>
</div> </div>
); );
@ -428,7 +428,7 @@ function InstanceNumberOverlayItem({
<span> <span>
{instanceNumber !== undefined && instanceNumber !== null ? ( {instanceNumber !== undefined && instanceNumber !== null ? (
<> <>
<span className="mr-1 shrink-0">I:</span> <span className="mr-0.5 shrink-0 opacity-[0.67]">I:</span>
<span>{`${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`}</span> <span>{`${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`}</span>
</> </>
) : ( ) : (

View File

@ -1,7 +1,7 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core'; import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
import { ImageScrollbar } from '@ohif/ui'; import { ImageScrollbar } from '@ohif/ui-next';
function CornerstoneImageScrollbar({ function CornerstoneImageScrollbar({
viewportData, viewportData,

View File

@ -3,19 +3,18 @@
--marker-height: 100px; --marker-height: 100px;
--scrollbar-width: 20px; --scrollbar-width: 20px;
pointer-events: none; pointer-events: none;
font-size: 15px;
line-height: 18px; line-height: 18px;
} }
.ViewportOrientationMarkers .orientation-marker { .ViewportOrientationMarkers .orientation-marker {
position: absolute; position: absolute;
} }
.ViewportOrientationMarkers .top-mid { .ViewportOrientationMarkers .top-mid {
top: 0.6rem; top: 0.38rem;
left: 50%; left: 50%;
} }
.ViewportOrientationMarkers .left-mid { .ViewportOrientationMarkers .left-mid {
top: 47%; top: 47%;
left: 5px; left: 0.38rem;
} }
.ViewportOrientationMarkers .right-mid { .ViewportOrientationMarkers .right-mid {
top: 47%; top: 47%;

View File

@ -119,8 +119,8 @@ function ViewportOrientationMarkers({
className={classNames( className={classNames(
'overlay-text', 'overlay-text',
`${m}-mid orientation-marker`, `${m}-mid orientation-marker`,
'text-aqua-pale', 'text-highlight/65',
'text-[13px]', 'text-base',
'leading-5' 'leading-5'
)} )}
key={`${m}-mid orientation-marker`} key={`${m}-mid orientation-marker`}

View File

@ -1,5 +1,5 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react'; import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { SwitchButton } from '@ohif/ui'; import { Switch } from '@ohif/ui-next';
import { StackViewport, VolumeViewport } from '@cornerstonejs/core'; import { StackViewport, VolumeViewport } from '@cornerstonejs/core';
import { ColorbarProps } from '../../types/Colorbar'; import { ColorbarProps } from '../../types/Colorbar';
import { utilities } from '@cornerstonejs/core'; import { utilities } from '@cornerstonejs/core';
@ -22,7 +22,7 @@ export function setViewportColorbar(
colorbarOptions.ticks = { colorbarOptions.ticks = {
position: 'left', position: 'left',
style: { style: {
font: '12px Arial', font: '13px Inter',
color: '#000000', color: '#000000',
maxNumTicks: 8, maxNumTicks: 8,
tickSize: 5, tickSize: 5,
@ -101,12 +101,18 @@ export function Colorbar({
}, [viewportId]); }, [viewportId]);
return ( return (
<div className="all-in-one-menu-item flex w-full justify-center"> <div
<div className="mr-2 w-[28px]"></div> className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
<SwitchButton onClick={() => {
label="Display Color bar" onSetColorbar();
}}
>
<div className="flex w-7 flex-shrink-0 items-center justify-center"></div>
<span className="flex-grow">Display Color bar</span>
<Switch
className="ml-2 flex-shrink-0"
checked={showColorbar} checked={showColorbar}
onChange={() => { onCheckedChange={() => {
onSetColorbar(); onSetColorbar();
}} }}
/> />

View File

@ -1,5 +1,6 @@
import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react'; import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui'; import { AllInOneMenu, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
import { StackViewport, Types } from '@cornerstonejs/core'; import { StackViewport, Types } from '@cornerstonejs/core';
import { ColormapProps } from '../../types/Colormap'; import { ColormapProps } from '../../types/Colormap';
@ -26,6 +27,7 @@ export function Colormap({
const onSetColorLUT = useCallback( const onSetColorLUT = useCallback(
props => { props => {
debugger;
// TODO: Better way to check if it's a fusion // TODO: Better way to check if it's a fusion
const oneOpacityColormaps = ['Grayscale', 'X Ray']; const oneOpacityColormaps = ['Grayscale', 'X Ray'];
const opacity = const opacity =
@ -64,20 +66,9 @@ export function Colormap({
return colormap; return colormap;
}; };
const buttons = useMemo(() => { const activeIndex = displaySets.findIndex(
return displaySets.map((displaySet, index) => ({ ds => ds.displaySetInstanceUID === activeDisplaySetRef.current.displaySetInstanceUID
children: displaySet.Modality, );
key: index,
style: {
minWidth: `calc(100% / ${displaySets.length})`,
fontSize: '0.8rem',
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
}));
}, [displaySets]);
useEffect(() => { useEffect(() => {
setActiveDisplaySet(displaySets[displaySets.length - 1]); setActiveDisplaySet(displaySets[displaySets.length - 1]);
@ -85,46 +76,52 @@ export function Colormap({
return ( return (
<> <>
{buttons.length > 1 && ( {displaySets.length > 1 && (
<div className="all-in-one-menu-item flex w-full justify-center"> <div className="flex h-8 w-full flex-shrink-0 items-center justify-center px-2 text-base">
<ButtonGroup <Tabs
onActiveIndexChange={index => { value={String(activeIndex)}
onValueChange={val => {
const index = parseInt(val, 10);
setActiveDisplaySet(displaySets[index]); setActiveDisplaySet(displaySets[index]);
setPrePreviewColormap(null); setPrePreviewColormap(null);
}} }}
activeIndex={
displaySets.findIndex(
ds => ds.displaySetInstanceUID === activeDisplaySetRef.current.displaySetInstanceUID
) || 1
}
className="w-[70%] text-[10px]"
> >
{buttons.map(({ children, key, style }) => ( <TabsList>
<div {displaySets.map((ds, i) => (
key={key} <TabsTrigger
style={style} key={i}
> value={String(i)}
{children} >
</div> {ds.Modality}
))} </TabsTrigger>
</ButtonGroup> ))}
</TabsList>
</Tabs>
</div> </div>
)} )}
<div className="all-in-one-menu-item flex w-full justify-center"> <div
<SwitchButton className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
label="Preview in viewport" onClick={() => setShowPreview(!showPreview)}
>
<span className="flex-shrink-0">Preview in viewport</span>
<Switch
className="ml-auto flex-shrink-0"
checked={showPreview} checked={showPreview}
onChange={checked => { onCheckedChange={checked => {
setShowPreview(checked); setShowPreview(checked);
}} }}
/> />
</div> </div>
<AllInOneMenu.DividerItem /> <AllInOneMenu.DividerItem />
<AllInOneMenu.ItemPanel> <AllInOneMenu.ItemPanel
maxHeight="calc(100vh - 250px)"
className="min-h-[200px] flex-grow"
>
{colormaps.map((colormap, index) => ( {colormaps.map((colormap, index) => (
<AllInOneMenu.Item <AllInOneMenu.Item
key={index} key={index}
label={colormap.description} label={colormap.description}
useIconSpace={false}
onClick={() => { onClick={() => {
onSetColorLUT({ onSetColorLUT({
viewportId, viewportId,

View File

@ -1,5 +1,6 @@
import React, { ReactElement, useState, useEffect, useCallback } from 'react'; import React, { ReactElement, useState, useEffect, useCallback } from 'react';
import { VolumeLightingProps } from '../../types/ViewportPresets'; import { VolumeLightingProps } from '../../types/ViewportPresets';
import { Numeric } from '@ohif/ui-next';
export function VolumeLighting({ export function VolumeLighting({
servicesManager, servicesManager,
@ -8,135 +9,75 @@ export function VolumeLighting({
hasShade, hasShade,
}: VolumeLightingProps): ReactElement { }: VolumeLightingProps): ReactElement {
const { cornerstoneViewportService } = servicesManager.services; const { cornerstoneViewportService } = servicesManager.services;
const [ambient, setAmbient] = useState(null); const [lightingValues, setLightingValues] = useState({
const [diffuse, setDiffuse] = useState(null); ambient: null,
const [specular, setSpecular] = useState(null); diffuse: null,
specular: null,
});
const onAmbientChange = useCallback(() => { // Single callback to handle all lighting property changes
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { ambient } }); const onLightingChange = useCallback(
}, [ambient, commandsManager, viewportId]); (property, value) => {
commandsManager.runCommand('setVolumeLighting', {
const onDiffuseChange = useCallback(() => { viewportId,
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { diffuse } }); options: { [property]: value },
}, [diffuse, commandsManager, viewportId]); });
setLightingValues(prev => ({
const onSpecularChange = useCallback(() => { ...prev,
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { specular } }); [property]: value,
}, [specular, commandsManager, viewportId]); }));
},
const calculateBackground = value => { [commandsManager, viewportId]
const percentage = ((value - 0) / (1 - 0)) * 100; );
return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`;
};
useEffect(() => { useEffect(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0]; const { actor } = viewport.getActors()[0];
const ambient = actor.getProperty().getAmbient(); const property = actor.getProperty();
const diffuse = actor.getProperty().getDiffuse();
const specular = actor.getProperty().getSpecular();
setAmbient(ambient);
setDiffuse(diffuse);
setSpecular(specular);
}, [viewportId, cornerstoneViewportService]);
const disableOption = hasShade ? '' : 'ohif-disabled !opacity-40';
const disableSlider = !hasShade;
return (
<>
<div
className={`all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px] ${disableOption}`}
>
<label
className="block text-white"
htmlFor="ambient"
>
Ambient
</label>
{ambient !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={ambient}
onChange={e => {
setAmbient(e.target.value);
onAmbientChange();
}}
id="ambient"
disabled={disableSlider}
max={1}
min={0}
type="range"
step={0.1}
style={{
background: calculateBackground(ambient),
'--thumb-inner-color': '#5acce6',
'--thumb-outer-color': '#090c29',
}}
/>
)}
</div>
<div
className={`all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px] ${disableOption}`}
>
<label
className="block text-white"
htmlFor="diffuse"
>
Diffuse
</label>
{diffuse !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={diffuse}
onChange={e => {
setDiffuse(e.target.value);
onDiffuseChange();
}}
disabled={disableSlider}
id="diffuse"
max={1}
min={0}
type="range"
step={0.1}
style={{
background: calculateBackground(diffuse),
'--thumb-inner-color': '#5acce6',
'--thumb-outer-color': '#090c29',
}}
/>
)}
</div>
<div const values = {
className={`all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px] ${disableOption}`} ambient: property.getAmbient(),
> diffuse: property.getDiffuse(),
<label specular: property.getSpecular(),
className="block text-white" };
htmlFor="specular"
> setLightingValues(values);
Specular }, [viewportId, cornerstoneViewportService]);
</label>
{specular !== null && ( const disableOption = hasShade ? '' : 'ohif-disabled !opacity-40';
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg" // Configuration for our lighting properties
value={specular} const lightingProperties = [
disabled={disableSlider} { key: 'ambient', label: 'Ambient' },
onChange={e => { { key: 'diffuse', label: 'Diffuse' },
setSpecular(e.target.value); { key: 'specular', label: 'Specular' },
onSpecularChange(); ];
}}
id="specular" return (
max={1} <div className="my-1 mt-2 flex flex-col space-y-2">
min={0} {lightingProperties.map(
type="range" ({ key, label }) =>
step={0.1} lightingValues[key] !== null && (
style={{ <div
background: calculateBackground(specular), key={key}
'--thumb-inner-color': '#5acce6', className={`w-full pl-2 pr-1 ${disableOption}`}
'--thumb-outer-color': '#090c29', >
}} <Numeric.Container
/> mode="singleRange"
)} min={0}
</div> max={1}
</> step={0.1}
value={lightingValues[key]}
onChange={value => onLightingChange(key, value)}
>
<div className="flex flex-row items-center">
<Numeric.Label className="w-16">{label}</Numeric.Label>
<Numeric.SingleRange sliderClassName="mx-2 flex-grow" />
</div>
</Numeric.Container>
</div>
)
)}
</div>
); );
} }

View File

@ -1,5 +1,5 @@
import React, { ReactElement, useState } from 'react'; import React, { ReactElement, useState } from 'react';
import { AllInOneMenu } from '@ohif/ui'; import { AllInOneMenu } from '@ohif/ui-next';
import { VolumeRenderingOptionsProps } from '../../types/ViewportPresets'; import { VolumeRenderingOptionsProps } from '../../types/ViewportPresets';
import { VolumeRenderingQuality } from './VolumeRenderingQuality'; import { VolumeRenderingQuality } from './VolumeRenderingQuality';
import { VolumeShift } from './VolumeShift'; import { VolumeShift } from './VolumeShift';
@ -26,11 +26,11 @@ export function VolumeRenderingOptions({
commandsManager={commandsManager} commandsManager={commandsManager}
servicesManager={servicesManager} servicesManager={servicesManager}
/> />
<div className="all-in-one-menu-item mt-2 flex !h-[20px] w-full justify-start"> <div className="mt-2 flex h-8 !h-[20px] w-full flex-shrink-0 items-center justify-start px-2 text-base">
<div className="text-aqua-pale text-[13px]">LIGHTING</div> <div className="text-muted-foreground text-sm">Lighting</div>
</div> </div>
<div className="bg-primary-dark mt-1 mb-1 h-[2px] w-full"></div> <div className="bg-background mt-1 mb-1 h-px w-full"></div>
<div className="all-in-one-menu-item flex w-full justify-center"> <div className="hover:bg-accent flex h-8 w-full flex-shrink-0 items-center px-2 text-base hover:rounded">
<VolumeShade <VolumeShade
commandsManager={commandsManager} commandsManager={commandsManager}
servicesManager={servicesManager} servicesManager={servicesManager}

View File

@ -1,4 +1,4 @@
import { AllInOneMenu } from '@ohif/ui'; import { AllInOneMenu } from '@ohif/ui-next';
import { Icons } from '@ohif/ui-next'; import { Icons } from '@ohif/ui-next';
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import { VolumeRenderingPresetsProps } from '../../types/ViewportPresets'; import { VolumeRenderingPresetsProps } from '../../types/ViewportPresets';

View File

@ -1,5 +1,6 @@
import React, { ReactElement, useCallback, useState, useEffect } from 'react'; import React, { ReactElement, useCallback, useState, useEffect } from 'react';
import { VolumeRenderingQualityProps } from '../../types/ViewportPresets'; import { VolumeRenderingQualityProps } from '../../types/ViewportPresets';
import { Numeric } from '@ohif/ui-next';
export function VolumeRenderingQuality({ export function VolumeRenderingQuality({
volumeRenderingQualityRange, volumeRenderingQualityRange,
@ -22,11 +23,6 @@ export function VolumeRenderingQuality({
[commandsManager, viewportId] [commandsManager, viewportId]
); );
const calculateBackground = value => {
const percentage = ((value - 0) / (1 - 0)) * 100;
return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`;
};
useEffect(() => { useEffect(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0]; const { actor } = viewport.getActors()[0];
@ -41,33 +37,26 @@ export function VolumeRenderingQuality({
setQuality(Math.sqrt(averageSpacing / (sampleDistance * 0.5))); setQuality(Math.sqrt(averageSpacing / (sampleDistance * 0.5)));
} }
}, [cornerstoneViewportService, viewportId]); }, [cornerstoneViewportService, viewportId]);
return ( return (
<> <div className="my-1 mt-2 flex flex-col space-y-2">
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]"> {quality !== null && (
<label <div className="w-full pl-2 pr-1">
className="block text-white" <Numeric.Container
htmlFor="volume" mode="singleRange"
>
Quality
</label>
{quality !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={quality}
id="volume"
max={max}
min={min} min={min}
type="range" max={max}
step={step} step={step}
onChange={e => onChange(parseInt(e.target.value, 10))} value={quality}
style={{ onChange={onChange}
background: calculateBackground((quality - min) / (max - min)), >
'--thumb-inner-color': '#5acce6', <div className="flex flex-row items-center">
'--thumb-outer-color': '#090c29', <Numeric.Label className="w-16">Quality</Numeric.Label>
}} <Numeric.SingleRange sliderClassName="mx-2 flex-grow" />
/> </div>
)} </Numeric.Container>
</div> </div>
</> )}
</div>
); );
} }

View File

@ -1,5 +1,5 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react'; import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { SwitchButton } from '@ohif/ui'; import { Switch } from '@ohif/ui-next';
import { VolumeShadeProps } from '../../types/ViewportPresets'; import { VolumeShadeProps } from '../../types/ViewportPresets';
export function VolumeShade({ export function VolumeShade({
@ -28,15 +28,18 @@ export function VolumeShade({
}, [viewportId, cornerstoneViewportService]); }, [viewportId, cornerstoneViewportService]);
return ( return (
<SwitchButton <>
key={key} <span className="flex-grow">Shade</span>
label="Shade" <Switch
checked={shade} className="ml-2 flex-shrink-0"
onChange={() => { key={key}
setShade(!shade); checked={shade}
onClickShade(!shade); onCheckedChange={() => {
onShadeChange(!shade); setShade(!shade);
}} onClickShade(!shade);
/> onShadeChange(!shade);
}}
/>
</>
); );
} }

View File

@ -1,5 +1,6 @@
import React, { ReactElement, useCallback, useEffect, useState, useRef } from 'react'; import React, { ReactElement, useCallback, useEffect, useState, useRef } from 'react';
import { VolumeShiftProps } from '../../types/ViewportPresets'; import { VolumeShiftProps } from '../../types/ViewportPresets';
import { Numeric } from '@ohif/ui-next';
export function VolumeShift({ export function VolumeShift({
viewportId, viewportId,
@ -46,48 +47,32 @@ export function VolumeShift({
viewportId, viewportId,
shift: shiftDifference, shift: shiftDifference,
}); });
setShift(newShift);
}, },
[commandsManager, viewportId, viewport] [commandsManager, viewportId, viewport]
); );
const calculateBackground = value => {
const percentage = ((value - 0) / (1 - 0)) * 100;
return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`;
};
return ( return (
<> <div className="my-1 mt-2 flex flex-col space-y-2">
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]"> {step !== null && minShift !== null && maxShift !== null && (
<label <div className="w-full pl-2 pr-1">
className="block text-white" <Numeric.Container
htmlFor="shift" mode="singleRange"
> min={minShift}
Shift max={maxShift}
</label> step={step}
{step !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={shift} value={shift}
onChange={e => { onChange={onChangeRange}
const shiftValue = parseInt(e.target.value, 10);
setShift(shiftValue);
onChangeRange(shiftValue);
}}
id="shift"
onMouseDown={() => setIsBlocking(true)} onMouseDown={() => setIsBlocking(true)}
onMouseUp={() => setIsBlocking(false)} onMouseUp={() => setIsBlocking(false)}
max={maxShift} >
min={minShift} <div className="flex flex-row items-center">
type="range" <Numeric.Label className="w-16">Shift</Numeric.Label>
step={step} <Numeric.SingleRange sliderClassName="mx-2 flex-grow" />
style={{ </div>
background: calculateBackground((shift - minShift) / (maxShift - minShift)), </Numeric.Container>
'--thumb-inner-color': '#5acce6', </div>
'--thumb-outer-color': '#090c29', )}
}} </div>
/>
)}
</div>
</>
); );
} }

View File

@ -1,5 +1,5 @@
import React, { ReactElement, useCallback } from 'react'; import React, { ReactElement, useCallback } from 'react';
import { AllInOneMenu } from '@ohif/ui'; import { AllInOneMenu } from '@ohif/ui-next';
import { WindowLevelPreset } from '../../types/WindowLevel'; import { WindowLevelPreset } from '../../types/WindowLevel';
import { CommandsManager } from '@ohif/core'; import { CommandsManager } from '@ohif/core';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -45,6 +45,7 @@ export function WindowLevel({
key={`${modality}-${index}`} key={`${modality}-${index}`}
label={preset.description} label={preset.description}
secondaryLabel={`${preset.window} / ${preset.level}`} secondaryLabel={`${preset.window} / ${preset.level}`}
useIconSpace={false}
onClick={() => onSetWindowLevel(preset)} onClick={() => onSetWindowLevel(preset)}
/> />
))} ))}

View File

@ -1,7 +1,7 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react'; import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import classNames from 'classnames'; import classNames from 'classnames';
import { AllInOneMenu } from '@ohif/ui'; import { AllInOneMenu } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next'; import { useViewportGrid } from '@ohif/ui-next';
import { Colormap } from './Colormap'; import { Colormap } from './Colormap';
import { Colorbar } from './Colorbar'; import { Colorbar } from './Colorbar';
@ -119,8 +119,8 @@ export function WindowLevelActionMenu({
iconClassName={classNames( iconClassName={classNames(
// Visible on hover and for the active viewport // Visible on hover and for the active viewport
activeViewportId === viewportId ? 'visible' : 'invisible group-hover/pane:visible', activeViewportId === viewportId ? 'visible' : 'invisible group-hover/pane:visible',
'flex shrink-0 cursor-pointer rounded active:text-white text-primary-light', 'flex shrink-0 cursor-pointer rounded active:text-foreground text-highlight',
isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60' isLight ? ' hover:bg-primary/30' : 'hover:bg-primary/30'
)} )}
menuStyle={{ maxHeight: vpHeight - 32, minWidth: 218 }} menuStyle={{ maxHeight: vpHeight - 32, minWidth: 218 }}
onVisibilityChange={() => { onVisibilityChange={() => {
@ -144,8 +144,10 @@ export function WindowLevelActionMenu({
key="colorLUTPresets" key="colorLUTPresets"
itemLabel="Color LUT" itemLabel="Color LUT"
itemIcon="icon-color-lut" itemIcon="icon-color-lut"
className="flex h-[calc(100%-32px)] flex-col"
> >
<Colormap <Colormap
className="flex h-full w-full flex-col"
colormaps={colormaps} colormaps={colormaps}
viewportId={viewportId} viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))} displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}

View File

@ -8,7 +8,7 @@ import React, {
} from 'react'; } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Types, ViewportActionCornersLocations } from '@ohif/ui'; import { Types, ViewportActionCornersLocations } from '@ohif/ui-next';
import ViewportActionCornersService, { import ViewportActionCornersService, {
ActionComponentInfo, ActionComponentInfo,
} from '../services/ViewportActionCornersService/ViewportActionCornersService'; } from '../services/ViewportActionCornersService/ViewportActionCornersService';

View File

@ -1,5 +1,5 @@
import { PubSubService } from '@ohif/core'; import { PubSubService } from '@ohif/core';
import { ViewportActionCornersLocations } from '@ohif/ui'; import { ViewportActionCornersLocations } from '@ohif/ui-next';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
export type ActionComponentInfo = { export type ActionComponentInfo = {

View File

@ -1,4 +1,4 @@
import { ViewportActionCorners } from '@ohif/ui'; import { ViewportActionCorners } from '@ohif/ui-next';
export default { export default {
'ui.viewportActionCorner': ViewportActionCorners, 'ui.viewportActionCorner': ViewportActionCorners,

View File

@ -4,7 +4,7 @@ import getContextModule from './getContextModule';
import getPanelModule from './getPanelModule'; import getPanelModule from './getPanelModule';
import getViewportModule from './getViewportModule'; import getViewportModule from './getViewportModule';
import { id } from './id.js'; import { id } from './id.js';
import { ViewportActionButton } from '@ohif/ui'; import { ViewportActionButton } from '@ohif/ui-next';
import i18n from '@ohif/i18n'; import i18n from '@ohif/i18n';
import { measurementTrackingMode } from './contexts/TrackedMeasurementsContext/promptBeginTracking'; import { measurementTrackingMode } from './contexts/TrackedMeasurementsContext/promptBeginTracking';
import getCustomizationModule from './getCustomizationModule'; import getCustomizationModule from './getCustomizationModule';

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ViewportActionArrows } from '@ohif/ui'; import { ViewportActionArrows } from '@ohif/ui-next';
import { useViewportGrid, Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next'; import { useViewportGrid, Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
import { annotation } from '@cornerstonejs/tools'; import { annotation } from '@cornerstonejs/tools';
@ -325,7 +325,7 @@ function _getStatusComponent(isTracked, t) {
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span> <span>
<Icons.StatusTracking className="text-muted-foreground" /> <Icons.StatusTracking className="text-muted-foreground mt-0.5 ml-0.5" />
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent

View File

@ -376,11 +376,11 @@ function ViewerViewportGrid(props: withAppTypes) {
const tolerance = 0.01; const tolerance = 0.01;
if (x + width < 1 - tolerance) { if (x + width < 1 - tolerance) {
style.borderRight = '1px solid #3a3f99'; style.borderRight = '1px solid hsl(var(--input))';
} }
if (y + height < 1 - tolerance) { if (y + height < 1 - tolerance) {
style.borderBottom = '1px solid #3a3f99'; style.borderBottom = '1px solid hsl(var(--input))';
} }
return style; return style;
@ -446,7 +446,7 @@ function ViewerViewportGrid(props: withAppTypes) {
return ( return (
<div <div
ref={resizeRef} ref={resizeRef}
className="border-secondary-light h-full w-full border" className="border-input h-[calc(100%-0.25rem)] w-full border"
> >
<ViewportGrid <ViewportGrid
numRows={numRows} numRows={numRows}

View File

@ -56,7 +56,6 @@ declare global {
export type UIViewportDialogService = UIViewportDialogServiceType; export type UIViewportDialogService = UIViewportDialogServiceType;
export type PanelService = PanelServiceType; export type PanelService = PanelServiceType;
export type StudyPrefetcherService = StudyPrefetcherServiceType; export type StudyPrefetcherService = StudyPrefetcherServiceType;
export type MultiMonitorService;
export interface Managers { export interface Managers {
servicesManager?: ServicesManager; servicesManager?: ServicesManager;

View File

@ -26,7 +26,7 @@
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%; --border: 0 0% 14.9%;
--input: 236 45% 21%; --input: 236 52% 30%;
--ring: 214 98% 60%; --ring: 214 98% 60%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;
@ -57,7 +57,7 @@
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%; --border: 0 0% 14.9%;
--input: 236 45% 21%; --input: 236 52% 30%;
--ring: 214 98% 60%; --ring: 214 98% 60%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;

View File

@ -0,0 +1,29 @@
import React from 'react';
import { Icons } from '@ohif/ui-next';
import DividerItem from './DividerItem';
type BackItemProps = {
backLabel?: string;
onBackClick: () => void;
};
const BackItem = ({ backLabel, onBackClick }: BackItemProps) => {
return (
<>
<div
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center pl-1 pr-2 text-base hover:rounded"
onClick={onBackClick}
>
<Icons.ByName
name="content-prev"
className="ml-2 mr-2"
/>
<span>{backLabel || 'Back to Display Options'}</span>
</div>
<DividerItem></DividerItem>
</>
);
};
export default BackItem;

View File

@ -0,0 +1,11 @@
import React from 'react';
const DividerItem = () => {
return (
<div className="flex h-3.5 shrink-0 items-center px-2">
<div className="bg-primary-dark h-[2px] w-full"></div>
</div>
);
};
export default DividerItem;

View File

@ -0,0 +1,15 @@
import React, { ReactNode } from 'react';
type HeaderItemProps = {
children: ReactNode;
};
const HeaderItem = ({ children }: HeaderItemProps) => {
return (
<div className="text-muted-foreground mx-2 flex h-6 shrink-0 items-center text-sm">
{children}
</div>
);
};
export default HeaderItem;

View File

@ -0,0 +1,83 @@
import React, { useCallback, useState } from 'react';
import OutsideClickHandler from 'react-outside-click-handler';
import { MenuProps } from './Menu';
import classNames from 'classnames';
import { AllInOneMenu } from '..';
import { Icons } from '@ohif/ui-next';
export interface IconMenuProps extends MenuProps {
icon: string;
iconClassName?: string;
horizontalDirection?: AllInOneMenu.HorizontalDirection;
verticalDirection?: AllInOneMenu.VerticalDirection;
menuKey?: number | string;
}
/**
* An IconMenu allows for a div wrapped icon to be clicked to show and hide
* an AllInOneMenu.Menu. Based on the direction(s) specified, the menu is
* positioned relative to the icon.
*
* HorizontalDirection.LeftToRight - the left edges of the icon and menu are aligned
* HorizontalDirection.RightRoLeft - the right edges of the icon and menu are aligned
* VerticalDirection.TopToBottom - the top edge of the menu appears directly below the bottom edge of the icon
* VerticalDirection.BottomToTop - the bottom edge of the menu appears directly above the top edge of the icon
*
* For example, if an IconMenu were situated in the bottom-left corner of a container,
* it would be best to use BottomToTop and LeftToRight directions for it.
*/
export default function IconMenu({
icon,
iconClassName,
horizontalDirection,
verticalDirection,
children,
backLabel,
menuClassName,
menuStyle,
onVisibilityChange,
menuKey,
}: IconMenuProps) {
const [isMenuVisible, setIsMenuVisible] = useState(false);
const toggleMenuVisibility = useCallback(() => setIsMenuVisible(isVisible => !isVisible), []);
return (
<OutsideClickHandler
onOutsideClick={toggleMenuVisibility}
disabled={!isMenuVisible}
>
<div className="relative">
<div
className={iconClassName}
onClick={toggleMenuVisibility}
>
<Icons.ByName name={icon} />
</div>
<AllInOneMenu.Menu
key={menuKey}
isVisible={isMenuVisible}
backLabel={backLabel}
menuClassName={classNames(
menuClassName,
'absolute',
verticalDirection === AllInOneMenu.VerticalDirection.TopToBottom
? 'top-[100%]'
: 'bottom-[100%]',
horizontalDirection === AllInOneMenu.HorizontalDirection.LeftToRight
? 'left-0'
: 'right-0'
)}
menuStyle={menuStyle}
onVisibilityChange={isVis => {
setIsMenuVisible(isVis);
onVisibilityChange?.(isVis);
}}
horizontalDirection={horizontalDirection}
>
{children}
</AllInOneMenu.Menu>
</div>
</OutsideClickHandler>
);
}

View File

@ -0,0 +1,51 @@
import React, { ReactNode, useCallback, useContext } from 'react';
import { MenuContext } from './Menu';
type ItemProps = {
label: string;
secondaryLabel?: string;
icon?: ReactNode;
rightIcon?: ReactNode;
onClick?: () => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
useIconSpace?: boolean;
};
const Item = ({
label,
secondaryLabel,
icon,
rightIcon,
onClick,
onMouseEnter,
onMouseLeave,
useIconSpace = false,
}: ItemProps) => {
const { hideMenu } = useContext(MenuContext);
const onClickHandler = useCallback(() => {
hideMenu();
onClick?.();
}, [hideMenu, onClick]);
return (
<div
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base leading-[18px] hover:rounded"
onClick={onClickHandler}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{(icon || useIconSpace) && (
<div className="flex w-7 flex-shrink-0 items-center justify-center">{icon}</div>
)}
<span className="flex-grow">{label}</span>
{secondaryLabel && (
<span className="text-muted-foreground ml-2 flex-shrink-0">{secondaryLabel}</span>
)}
{rightIcon && <div className="ml-2 flex-shrink-0">{rightIcon}</div>}
</div>
);
};
export default Item;

View File

@ -0,0 +1,38 @@
import React, { ReactNode, useContext, useEffect } from 'react';
import { MenuContext } from './Menu';
import { ScrollArea } from '@ohif/ui-next';
type ItemPanelProps = {
label?: string;
index?: number;
children: ReactNode;
maxHeight?: string;
className?: string;
};
const ItemPanel = ({
label,
index = 0,
children,
maxHeight = '250px',
className,
}: ItemPanelProps) => {
const { addItemPanel, activePanelIndex } = useContext(MenuContext);
useEffect(() => {
addItemPanel(index, label);
}, [addItemPanel, index, label]);
return (
activePanelIndex === index && (
<ScrollArea
style={{ scrollbarGutter: 'auto', maxHeight, height: '100%' }}
className={`flex flex-col ${className || ''}`}
>
{children}
</ScrollArea>
)
);
};
export default ItemPanel;

View File

@ -0,0 +1,187 @@
import React, { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
import DividerItem from './DividerItem';
import PanelSelector from './PanelSelector';
import classNames from 'classnames';
import BackItem from './BackItem';
/**
* The vertical direction that the menu will be opened/used with.
*
* A TopToBottom menu would be used for cases where the menu is opened "near"
* the top edge of its container. Likewise a BottomToTop menu would be used
* for cases where the menu is opened "near" the bottom edge of its container.
*
* See IconMenu for more information.
*/
export enum VerticalDirection {
TopToBottom,
BottomToTop,
}
/**
* The horizontal direction that the menu is opened/used with.
* This direction dictates the general direction sub-menus and
* back-to-menus are opened with. For example, a RightToLeft menu
* will have sub-menu items indicated with a left pointing chevron
* and aligned with the left edge of the menu. Similarly back-to items of a
* RightToLeft menu are indicated with a right pointing chevron and
* aligned with the right edge of the menu.
*
* It is also worth noting that a LeftToRight menu would be used for
* cases where a menu is opened "near" the left edge of its container.
* Likewise, a RightToLeft menu would be used for cases where a menu is opened
* "near" the right edge of its container.
*
* See IconMenu for more information.
*/
export enum HorizontalDirection {
LeftToRight,
RightToLeft,
}
export interface MenuProps {
menuStyle?: unknown;
menuClassName?: string;
isVisible?: boolean;
preventHideMenu?: boolean;
backLabel?: string;
headerComponent?: ReactNode;
showHeaderDivider?: boolean;
activePanelIndex?: number;
onVisibilityChange?: (isVisible: boolean) => void;
horizontalDirection?: HorizontalDirection;
children: ReactNode;
}
type MenuContextProps = {
showSubMenu: (subMenuProps: MenuProps) => void;
hideMenu: () => void;
addItemPanel: (index: number, label: string) => void;
horizontalDirection: HorizontalDirection;
activePanelIndex: number;
};
type MenuPathState = {
props: MenuProps;
activePanelIndex: number;
};
export const MenuContext = createContext<MenuContextProps>(null);
const Menu = (props: MenuProps) => {
const {
isVisible,
onVisibilityChange,
activePanelIndex,
preventHideMenu,
menuClassName,
menuStyle,
horizontalDirection = HorizontalDirection.LeftToRight,
} = props;
const [isMenuVisible, setIsMenuVisible] = useState(isVisible);
// The menuPath is an array consisting of this top Menu and every SubMenu
// that has been traversed/opened by the user with the last item in the array
// being the current (sub)menu that is currently visible. This allows for the previously
// viewed menus to be returned to via the Back button at the top of the menu.
const [menuPath, setMenuPath] = useState<Array<MenuPathState>>([
{ props, activePanelIndex: activePanelIndex || 0 },
]);
const [itemPanelLabels, setItemPanelLabels] = useState<Array<string>>([]);
const hideMenu = useCallback(() => {
if (preventHideMenu) {
return;
}
setMenuPath(path => [path[0]]);
setItemPanelLabels([]);
setIsMenuVisible(false);
onVisibilityChange?.(false);
}, [preventHideMenu, onVisibilityChange]);
useEffect(() => {
if (isVisible) {
setIsMenuVisible(isVisible);
onVisibilityChange?.(isVisible);
} else {
hideMenu();
}
}, [hideMenu, isVisible, onVisibilityChange]);
const showSubMenu = useCallback((subMenuProps: MenuProps) => {
setMenuPath(path => {
return [
...path,
{ props: subMenuProps, activePanelIndex: subMenuProps.activePanelIndex || 0 },
];
});
setItemPanelLabels([]);
}, []);
const addItemPanel = useCallback((index, label) => {
setItemPanelLabels(labels => {
return [...labels.slice(0, index), label, ...labels.slice(index + 1, labels.length)];
});
}, []);
const onActivePanelIndexChange = useCallback(index => {
setMenuPath(path => {
return [
...path.slice(0, path.length - 1),
{ ...path[path.length - 1], activePanelIndex: index },
];
});
}, []);
const onBackClick = useCallback(() => {
setMenuPath(path => [...path.slice(0, path.length - 1)]);
setItemPanelLabels([]);
}, []);
const { props: currentMenuProps, activePanelIndex: currentMenuActivePanelIndex } =
menuPath[menuPath.length - 1];
return (
<>
<MenuContext.Provider
value={{
showSubMenu,
hideMenu,
addItemPanel,
activePanelIndex: currentMenuActivePanelIndex,
horizontalDirection,
}}
>
{isMenuVisible && (
<div
className={classNames(
'bg-popover/90 text-foreground flex select-none flex-col rounded px-1 py-1.5',
menuClassName
)}
style={menuStyle}
>
{menuPath.length > 1 && (
<BackItem
backLabel={menuPath[menuPath.length - 2].props.backLabel}
onBackClick={onBackClick}
/>
)}
{itemPanelLabels.length > 1 && (
<PanelSelector
panelLabels={itemPanelLabels}
activeIndex={currentMenuActivePanelIndex}
onActiveIndexChange={onActivePanelIndexChange}
></PanelSelector>
)}
{currentMenuProps.headerComponent}
{currentMenuProps.showHeaderDivider && <DividerItem />}
{currentMenuProps.children}
</div>
)}
</MenuContext.Provider>
</>
);
};
export default Menu;

View File

@ -0,0 +1,32 @@
import React, { ReactNode } from 'react';
import { Tabs, TabsList, TabsTrigger } from '../Tabs';
type PanelSelectorProps = {
panelLabels: Array<ReactNode>;
onActiveIndexChange: (index: number) => void;
activeIndex: number;
};
const PanelSelector = ({ panelLabels, onActiveIndexChange, activeIndex }: PanelSelectorProps) => {
return (
<div className="mx-2 my-1 flex justify-center">
<Tabs
value={String(activeIndex)}
onValueChange={val => onActiveIndexChange(parseInt(val, 10))}
>
<TabsList>
{panelLabels.map((panelLabel, index) => (
<TabsTrigger
key={index}
value={String(index)}
>
{panelLabel}
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
);
};
export default PanelSelector;

View File

@ -0,0 +1,36 @@
import React, { useCallback, useContext } from 'react';
import { MenuContext, MenuProps } from './Menu';
import { Icons } from '@ohif/ui-next';
export interface SubMenuProps extends MenuProps {
itemLabel: string;
onClick?: () => void;
itemIcon?: string;
}
const SubMenu = (props: SubMenuProps) => {
const { showSubMenu } = useContext(MenuContext);
const onClickHandler = useCallback(() => {
showSubMenu(props);
props.onClick?.();
}, [showSubMenu, props]);
return (
<div
className="hover:bg-accent flex h-8 w-full cursor-pointer items-center px-2 text-base hover:rounded"
onClick={onClickHandler}
>
{props.itemIcon && (
<div className="flex w-7 flex-shrink-0 items-center justify-center">
<Icons.ByName name={props.itemIcon}></Icons.ByName>
</div>
)}
<span className="flex-grow">{props.itemLabel}</span>
<div className="ml-2 flex-shrink-0">
<Icons.ByName name="content-next"></Icons.ByName>
</div>
</div>
);
};
export default SubMenu;

View File

@ -0,0 +1,42 @@
import Menu, { VerticalDirection, HorizontalDirection } from './Menu';
import SubMenu from './SubMenu';
import BackItem from './BackItem';
import HeaderItem from './HeaderItem';
import DividerItem from './DividerItem';
import IconMenu from './IconMenu';
import ItemPanel from './ItemPanel';
import PanelSelector from './PanelSelector';
import Item from './Item';
// Export enums
export { VerticalDirection, HorizontalDirection };
// Export components
export {
Menu,
SubMenu,
BackItem,
HeaderItem,
DividerItem,
IconMenu,
ItemPanel,
PanelSelector,
Item,
};
// Namespace for components and enums
const AllInOneMenu = {
Menu,
SubMenu,
BackItem,
HeaderItem,
DividerItem,
IconMenu,
ItemPanel,
PanelSelector,
Item,
VerticalDirection,
HorizontalDirection,
};
export default AllInOneMenu;

View File

@ -46,7 +46,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 border-input z-50 min-w-[8rem] overflow-hidden rounded border p-1 shadow-lg', 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 border-input/50 z-50 min-w-[8rem] overflow-hidden rounded border p-1 shadow-lg',
className className
)} )}
{...props} {...props}
@ -68,7 +68,7 @@ const DropdownMenuContent = React.forwardRef<
props.onKeyDown?.(e); props.onKeyDown?.(e);
}} }}
className={cn( className={cn(
'bg-popover text-popover-foreground border-input z-50 min-w-[8rem] overflow-hidden rounded border p-1 shadow-md', 'bg-popover text-popover-foreground border-input/50 z-50 min-w-[8rem] overflow-hidden rounded border p-1 shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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', 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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',
className className
)} )}

View File

@ -11,6 +11,7 @@ import { Icons } from '../Icons';
interface ScrollAreaProps extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> { interface ScrollAreaProps extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {
/** Flag to show/hide scroll indicator arrows at top and bottom */ /** Flag to show/hide scroll indicator arrows at top and bottom */
showArrows?: boolean; showArrows?: boolean;
type?: 'auto' | 'always' | 'scroll';
} }
/** /**
@ -56,6 +57,7 @@ const ScrollArea = React.forwardRef<
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
ref={ref} ref={ref}
className={cn('relative h-full overflow-hidden', className)} className={cn('relative h-full overflow-hidden', className)}
type={props.type || 'auto'}
{...props} {...props}
> >
<ScrollAreaPrimitive.Viewport <ScrollAreaPrimitive.Viewport

View File

@ -0,0 +1,85 @@
/* Scrollbar container */
.scrollbarContainer {
position: absolute;
right: 0;
top: 0;
height: 100%;
padding: 5px 5px 0 5px;
z-index: 10;
}
.scrollbarInner {
position: relative;
height: 100%;
width: 11px;
}
/* Apply these styles to any range input inside our component */
.scrollbarInput {
position: absolute;
left: 12px;
top: 0;
height: 12px;
transform: rotate(90deg);
transform-origin: top left;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: transparent;
padding: 0;
}
/* Thumb styles for WebKit browsers - more specific selector */
input[type='range'].scrollbarInput::-webkit-slider-thumb {
-webkit-appearance: none !important;
background-color: hsl(var(--input));
border: none;
border-radius: 57px;
cursor: -webkit-grab;
height: 8px;
margin-top: -4px;
width: 32px;
}
input[type='range'].scrollbarInput::-webkit-slider-thumb:active,
input[type='range'].scrollbarInput::-webkit-slider-thumb:hover {
background-color: hsl(var(--primary));
cursor: -webkit-grabbing;
}
/* Thumb styles for Firefox - more specific selector */
input[type='range'].scrollbarInput::-moz-range-thumb {
background-color: hsl(var(--input));
border: none;
border-radius: 57px;
cursor: -moz-grab;
height: 12px;
width: 39px;
}
input[type='range'].scrollbarInput::-moz-range-thumb:active,
input[type='range'].scrollbarInput::-moz-range-thumb:hover {
background-color: hsl(var(--primary));
cursor: -moz-grabbing;
}
/* Track styles for WebKit browsers - more specific selector */
input[type='range'].scrollbarInput::-webkit-slider-runnable-track {
background-color: transparent;
border: none;
cursor: pointer;
height: 5px;
}
/* Track styles for Firefox - more specific selector */
input[type='range'].scrollbarInput::-moz-range-track {
background-color: transparent;
border: none;
cursor: pointer;
height: 2px;
}
/* Ensure focus doesn't show an outline */
.scrollbarInput:focus {
outline: none;
}

View File

@ -0,0 +1,72 @@
import React, { useCallback } from 'react';
import { cn } from '../../lib/utils';
import styles from './ImageScrollbar.module.css';
export interface ImageScrollbarProps {
value: number;
max: number;
height: string;
onChange: (value: number) => void;
onContextMenu?: (e: React.MouseEvent) => void;
className?: string;
}
export const ImageScrollbar: React.FC<ImageScrollbarProps> = ({
value,
max,
height,
onChange,
onContextMenu = e => e.preventDefault(),
className = '',
}) => {
if (max === 0) {
return null;
}
const style = {
width: height, // This is intentional for the rotation
};
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const intValue = parseInt(event.target.value, 10);
onChange(intValue);
},
[onChange]
);
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
// We don't allow direct keyboard navigation (arrow keys)
const keys = {
DOWN: 40,
UP: 38,
};
if (event.which === keys.DOWN || event.which === keys.UP) {
event.preventDefault();
}
}, []);
return (
<div
className={cn(styles.scrollbarContainer, className)}
onContextMenu={onContextMenu}
>
<div className={styles.scrollbarInner}>
<input
className={cn(styles.scrollbarInput, 'mousetrap')}
style={style}
type="range"
min="0"
max={max}
step="1"
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
aria-label="Image navigation scrollbar"
data-testid="image-scrollbar-input"
/>
</div>
</div>
);
};

View File

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import { Icons } from '@ohif/ui-next'; import { Icons } from '@ohif/ui-next';
const arrowClasses = const arrowClasses =
'cursor-pointer flex items-center justify-center shrink-0 text-primary-light active:text-white hover:bg-secondary-light/60 rounded'; 'cursor-pointer flex items-center justify-center shrink-0 text-highlight active:text-foreground hover:bg-primary/30 rounded';
/** /**
* A small set of left/right arrow icons for stepping through slices or series. * A small set of left/right arrow icons for stepping through slices or series.

View File

@ -13,7 +13,7 @@ export enum ViewportActionCornersLocations {
bottomRight, bottomRight,
} }
const commonClasses = 'pointer-events-auto flex items-center gap-1'; const commonClasses = 'pointer-events-auto flex items-center';
const locationClasses = { const locationClasses = {
[ViewportActionCornersLocations.topLeft]: classNames( [ViewportActionCornersLocations.topLeft]: classNames(
commonClasses, commonClasses,
@ -21,7 +21,7 @@ const locationClasses = {
), ),
[ViewportActionCornersLocations.topRight]: classNames( [ViewportActionCornersLocations.topRight]: classNames(
commonClasses, commonClasses,
'absolute top-[4px] right-[4px] right-viewport-scrollbar' 'absolute top-[4px] right-[16px] right-viewport-scrollbar'
), ),
[ViewportActionCornersLocations.bottomLeft]: classNames( [ViewportActionCornersLocations.bottomLeft]: classNames(
commonClasses, commonClasses,

View File

@ -2,7 +2,7 @@
display: none; display: none;
} }
.ViewportOverlay { .ViewportOverlay {
color: #9ccef9; color: hsl(var(--highlight));
} }
.ViewportOverlay .overlay-element { .ViewportOverlay .overlay-element {
position: absolute; position: absolute;
@ -11,12 +11,12 @@
pointer-events: none; pointer-events: none;
} }
.overlay-top { .overlay-top {
top: 28px; top: 2.15rem;
} }
.overlay-bottom { .overlay-bottom {
bottom: 3px; bottom: 1rem;
} }
.overlay-text { .overlay-text {
text-shadow: 0.8px 0.8px 0.5px rgba(0, 0, 0, 0.75); text-shadow: 0.8px 0.8px 0.5px rgba(0, 0, 0, 0.65);
} }

View File

@ -17,17 +17,11 @@ const classes = {
bottomLeft: 'overlay-bottom left-viewport', bottomLeft: 'overlay-bottom left-viewport',
}; };
function ViewportOverlay({ function ViewportOverlay({ topLeft, topRight, bottomRight, bottomLeft, color = 'text-highlight' }) {
topLeft,
topRight,
bottomRight,
bottomLeft,
color = 'text-primary-light',
}) {
const overlay = 'absolute pointer-events-none viewport-overlay'; const overlay = 'absolute pointer-events-none viewport-overlay';
return ( return (
<div className={classNames(color, 'overlay-text text-[13px] leading-5')}> <div className={classNames(color, 'overlay-text text-base leading-5')}>
<div <div
data-cy="viewport-overlay-top-left" data-cy="viewport-overlay-top-left"
className={classNames(overlay, classes.topLeft)} className={classNames(overlay, classes.topLeft)}
@ -37,14 +31,14 @@ function ViewportOverlay({
<div <div
data-cy="viewport-overlay-top-right" data-cy="viewport-overlay-top-right"
className={classNames(overlay, classes.topRight)} className={classNames(overlay, classes.topRight)}
style={{ transform: 'translateX(-8px)' }} style={{ transform: 'translateX(9px)' }}
> >
{topRight} {topRight}
</div> </div>
<div <div
data-cy="viewport-overlay-bottom-right" data-cy="viewport-overlay-bottom-right"
className={classNames(overlay, classes.bottomRight)} className={classNames(overlay, classes.bottomRight)}
style={{ transform: 'translateX(-8px)' }} style={{ transform: 'translateX(6px)' }}
> >
{bottomRight} {bottomRight}
</div> </div>

View File

@ -68,8 +68,8 @@ function ViewportPane({
{/* Border overlay */} {/* Border overlay */}
<div <div
className={classNames('pointer-events-none absolute inset-0', { className={classNames('pointer-events-none absolute inset-0', {
'border-primary-light border': isActive, 'border-highlight rounded-md border': isActive,
'group-hover:border-primary-light/70 border border-transparent': !isActive, 'group-hover:border-highlight/50 rounded-md border border-transparent': !isActive,
})} })}
/> />
</div> </div>

View File

@ -6,3 +6,4 @@ export { ViewportPane } from './ViewportPane';
export { ViewportActionCorners, ViewportActionCornersLocations } from './ViewportActionCorners'; export { ViewportActionCorners, ViewportActionCornersLocations } from './ViewportActionCorners';
export { ViewportOverlay } from './ViewportOverlay'; export { ViewportOverlay } from './ViewportOverlay';
export { ViewportGrid } from './ViewportGrid'; export { ViewportGrid } from './ViewportGrid';
export { ImageScrollbar } from './ImageScrollbar';

View File

@ -81,8 +81,6 @@ import {
} from './DropdownMenu'; } from './DropdownMenu';
import { Onboarding } from './Onboarding'; import { Onboarding } from './Onboarding';
import { DoubleSlider } from './DoubleSlider'; import { DoubleSlider } from './DoubleSlider';
export { DataRow } from './DataRow';
export { MeasurementTable } from './MeasurementTable';
import { import {
SegmentationTable, SegmentationTable,
useSegmentationTableContext, useSegmentationTableContext,
@ -92,7 +90,6 @@ import {
import { Toaster, toast } from './Sonner'; import { Toaster, toast } from './Sonner';
import { StudySummary } from './StudySummary'; import { StudySummary } from './StudySummary';
import { ErrorBoundary } from './Errorboundary'; import { ErrorBoundary } from './Errorboundary';
export * from './ColorCircle';
import { Header } from './Header'; import { Header } from './Header';
import { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card'; import { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card';
import { import {
@ -105,6 +102,7 @@ import {
ViewportActionCornersLocations, ViewportActionCornersLocations,
ViewportOverlay, ViewportOverlay,
ViewportGrid, ViewportGrid,
ImageScrollbar,
} from './Viewport'; } from './Viewport';
import { import {
ToolButton, ToolButton,
@ -115,6 +113,11 @@ import {
ToolButtonListDivider, ToolButtonListDivider,
} from './ToolButton'; } from './ToolButton';
import { ToolSettings } from './OHIFToolSettings'; import { ToolSettings } from './OHIFToolSettings';
export { DataRow } from './DataRow';
export { MeasurementTable } from './MeasurementTable';
export * from './ColorCircle';
export { default as AllInOneMenu } from './AllInOneMenu';
export * from './AllInOneMenu';
// Segmentation Context Exports // Segmentation Context Exports
export { useSegmentationTableContext, useSegmentationExpanded, useSegmentStatistics }; export { useSegmentationTableContext, useSegmentationExpanded, useSegmentStatistics };
@ -232,6 +235,7 @@ export {
ViewportActionCornersLocations, ViewportActionCornersLocations,
ViewportOverlay, ViewportOverlay,
ViewportGrid, ViewportGrid,
ImageScrollbar,
Clipboard, Clipboard,
ToolButton, ToolButton,
ToolButtonList, ToolButtonList,

View File

@ -26,7 +26,7 @@
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%; --border: 0 0% 14.9%;
--input: 236 45% 21%; --input: 236 52% 30%;
--ring: 214 98% 60%; --ring: 214 98% 60%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;
@ -69,7 +69,7 @@
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%; --border: 0 0% 14.9%;
--input: 236 45% 21%; --input: 236 52% 30%;
--ring: 214 98% 60%; --ring: 214 98% 60%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;

View File

@ -2,8 +2,8 @@ import React from 'react';
const DividerItem = () => { const DividerItem = () => {
return ( return (
<div className="flex h-3.5 shrink-0 items-center px-2"> <div className="flex h-3.5 shrink-0 items-center px-0">
<div className="bg-primary-dark h-[2px] w-full"></div> <div className="bg-background h-px w-full"></div>
</div> </div>
); );
}; };

View File

@ -11,6 +11,8 @@
} }
} }
/* Truncate 2 lines utility */ /* Truncate 2 lines utility */
.truncate-2-lines { .truncate-2-lines {
overflow: hidden; overflow: hidden;
@ -49,7 +51,7 @@
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%; --border: 0 0% 14.9%;
--input: 236 45% 21%; --input: 236 52% 30%;
--ring: 214 98% 60%; --ring: 214 98% 60%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;
@ -92,7 +94,7 @@
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%; --border: 0 0% 14.9%;
--input: 236 45% 21%; --input: 236 52% 30%;
--ring: 214 98% 60%; --ring: 214 98% 60%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;