ui(components): migrates to new toolbar ui components and Numeric for Input InputRange and InputDoubleRange (#4728)

Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
Dan Rukas 2025-01-29 13:36:52 -05:00 committed by GitHub
parent ac5ae4a29e
commit a8d84443a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
116 changed files with 2930 additions and 1038 deletions

View File

@ -1,6 +1,15 @@
export function getToolbarModule({ servicesManager }: withAppTypes) {
const { segmentationService, toolbarService, toolGroupService } = servicesManager.services;
return [
{
name: 'evaluate.cornerstone.hasSegmentation',
evaluate: ({ viewportId }) => {
const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
return {
disabled: !segmentations?.length,
};
},
},
{
name: 'evaluate.cornerstone.segmentation',
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
@ -12,7 +21,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
if (!segmentations?.length) {
return {
disabled: true,
className: '!text-common-bright !bg-black opacity-50',
disabledText: disabledText ?? 'No segmentations available',
};
}
@ -22,7 +30,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
if (!toolGroup) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
@ -32,7 +39,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
if (!toolGroup.hasTool(toolName) && !toolNames) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
@ -43,12 +49,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
return {
disabled: false,
className: isPrimaryActive
? '!text-black !bg-primary-light hover:bg-primary-light hover-text-black hover:cursor-pointer'
: '!text-common-bright !bg-black hover:bg-primary-light hover:cursor-pointer hover:text-black',
// Todo: isActive right now is used for nested buttons where the primary
// button needs to be fully rounded (vs partial rounded) when active
// otherwise it does not have any other use
isActive: isPrimaryActive,
};
},

View File

@ -1,14 +1,8 @@
import { Enums } from '@cornerstonejs/tools';
const getToggledClassName = (isToggled: boolean) => {
return isToggled
? '!text-primary-active'
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
};
import { utils } from '@ohif/ui-next';
const getDisabledState = (disabledText?: string) => ({
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
});
@ -93,9 +87,6 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
return {
disabled: false,
className: isPrimaryActive
? '!text-black bg-primary-light rounded'
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light rounded',
// Todo: isActive right now is used for nested buttons where the primary
// button needs to be fully rounded (vs partial rounded) when active
// otherwise it does not have any other use
@ -153,9 +144,9 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
},
{
name: 'evaluate.action',
evaluate: ({ viewportId, button }) => {
evaluate: () => {
return {
className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
disabled: false,
};
},
},
@ -190,7 +181,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
if (!synchronizers?.length) {
return {
className: getToggledClassName(false),
className: utils.getToggledClassName(false),
};
}
@ -204,7 +195,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
if (!synchronizers?.length) {
return {
className: getToggledClassName(false),
className: utils.getToggledClassName(false),
};
}
@ -216,7 +207,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
const isEnabled = synchronizer?._enabled;
return {
className: getToggledClassName(isEnabled),
className: utils.getToggledClassName(isEnabled),
};
},
},
@ -239,14 +230,13 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
if (!prop) {
return {
disabled: false,
className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
};
}
const isToggled = prop;
return {
className: getToggledClassName(isToggled),
className: utils.getToggledClassName(isToggled),
};
},
},
@ -275,7 +265,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
return {
disabled: false,
className: getToggledClassName(isMpr),
className: utils.getToggledClassName(isMpr),
};
},
},
@ -304,6 +294,6 @@ function _evaluateToggle({
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);
return {
className: getToggledClassName(!isOff),
className: utils.getToggledClassName(!isOff),
};
}

View File

@ -160,7 +160,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
ViewportActionCornersProvider
);
const { syncGroupService, customizationService } = servicesManager.services;
const { syncGroupService } = servicesManager.services;
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
return init.call(this, props);

View File

@ -3,6 +3,7 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
Icons,
Button,
} from '@ohif/ui-next';
@ -18,6 +19,13 @@ const getMenuItemsDefault = ({ commandsManager, items, servicesManager, ...props
// getMenuItems can also be replaced by providing it to the MoreDropdownMenu
const menuContent = customizationService.getCustomization('ohif.menuContent');
// Default menu item component if none is provided through customization
const DefaultMenuItem = ({ item }) => (
<DropdownMenuItem onClick={item.onClick}>{item.label || item.title}</DropdownMenuItem>
);
const MenuItemComponent = menuContent?.content || DefaultMenuItem;
return (
<DropdownMenuContent
hideWhenDetached
@ -27,15 +35,15 @@ const getMenuItemsDefault = ({ commandsManager, items, servicesManager, ...props
e.preventDefault();
}}
>
{items?.map((item, index) =>
React.createElement(menuContent.content, {
key: item.id || `menu-item-${index}`,
item,
commandsManager,
servicesManager,
...props,
})
)}
{items?.map((item, index) => (
<MenuItemComponent
key={item.id || `menu-item-${index}`}
item={item}
commandsManager={commandsManager}
servicesManager={servicesManager}
{...props}
/>
))}
</DropdownMenuContent>
);
};

View File

@ -0,0 +1,44 @@
import React from 'react';
import classNames from 'classnames';
import { ToolButton } from '@ohif/ui-next';
/**
* Wraps the ToolButtonList component to handle the OHIF toolbar button structure
* @param props - Component props
* @returns Component
*/
export function ToolBoxButtonGroupWrapper({ groupId, items, onInteraction, ...props }) {
if (!items || !groupId) {
return null;
}
return (
<div className="bg-popover flex flex-row space-x-1 rounded-md px-0 py-0">
{items.map(item => (
<ToolButton
{...item}
key={item.id}
size="small"
className={props.disabled && 'text-primary'}
onInteraction={() =>
onInteraction?.({ groupId, itemId: item.id, commands: item.commands })
}
/>
))}
</div>
);
}
export function ToolBoxButtonWrapper({ onInteraction, ...props }) {
return (
<div className="bg-popover flex flex-row rounded-md px-0 py-0">
<ToolButton
{...props}
id={props.id}
size="small"
className={classNames(props.disabled && 'text-primary')}
onInteraction={() => onInteraction?.({ itemId: props.id, commands: props.commands })}
/>
</div>
);
}

View File

@ -0,0 +1,84 @@
import React from 'react';
import {
ToolButtonList,
ToolButton,
ToolButtonListDefault,
ToolButtonListDropDown,
ToolButtonListItem,
ToolButtonListDivider,
} from '@ohif/ui-next';
interface ButtonItem {
id: string;
icon?: string;
label?: string;
tooltip?: string;
isActive?: boolean;
disabledText?: string;
commands?: Record<string, unknown>;
disabled?: boolean;
className?: string;
}
interface ToolButtonListWrapperProps {
groupId: string;
primary: ButtonItem;
items: ButtonItem[];
onInteraction?: (details: {
groupId: string;
itemId: string;
commands?: Record<string, unknown>;
}) => void;
}
/**
* Wraps the ToolButtonList component to handle the OHIF toolbar button structure
* @param props - Component props
* @returns Component
* // test
*/
export default function ToolButtonListWrapper({
groupId,
primary,
items,
onInteraction,
}: ToolButtonListWrapperProps) {
return (
<ToolButtonList>
<ToolButtonListDefault>
<div
data-cy={`${groupId}-split-button-primary`}
data-tool={primary.id}
data-active={primary.isActive}
>
<ToolButton
{...primary}
onInteraction={({ itemId }) =>
onInteraction?.({ groupId, itemId, commands: primary.commands })
}
className={primary.className}
/>
</div>
</ToolButtonListDefault>
<ToolButtonListDivider className={primary.isActive ? 'opacity-0' : 'opacity-100'} />
<div data-cy={`${groupId}-split-button-secondary`}>
<ToolButtonListDropDown>
{items.map(item => (
<ToolButtonListItem
key={item.id}
{...item}
data-cy={item.id}
data-tool={item.id}
data-active={item.isActive}
onSelect={() =>
onInteraction?.({ groupId, itemId: item.id, commands: item.commands })
}
>
<span className="pl-1">{item.label || item.tooltip || item.id}</span>
</ToolButtonListItem>
))}
</ToolButtonListDropDown>
</div>
</ToolButtonList>
);
}

View File

@ -1,6 +1,4 @@
import React from 'react';
import { Tooltip } from '@ohif/ui';
import classnames from 'classnames';
import { useToolbar } from '@ohif/core';
export function Toolbar({ servicesManager, buttonSection = 'primary' }) {
@ -15,7 +13,7 @@ export function Toolbar({ servicesManager, buttonSection = 'primary' }) {
return (
<>
{toolbarButtons.map(toolDef => {
{toolbarButtons?.map(toolDef => {
if (!toolDef) {
return null;
}

View File

@ -1,5 +1,5 @@
import { ToolbarButton, ButtonGroup } from '@ohif/ui';
import React, { useCallback } from 'react';
import { ToolbarButton, ButtonGroup } from '@ohif/ui';
function ToolbarButtonGroupWithServices({ groupId, items, onInteraction, size }) {
const getSplitButtonItems = useCallback(

View File

@ -107,18 +107,18 @@ const useResizablePanels = (
// And by virtue of the dependency on the minimum size state variables, this code
// is executed on the render following an update of the minimum percentage sizes
// for a panel.
if (!resizableLeftPanelAPIRef.current.isCollapsed()) {
if (!resizableLeftPanelAPIRef.current?.isCollapsed()) {
const leftSize = getPercentageSize(
leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize
);
resizableLeftPanelAPIRef.current.resize(leftSize);
resizableLeftPanelAPIRef.current?.resize(leftSize);
}
if (!resizableRightPanelAPIRef.current.isCollapsed()) {
if (!resizableRightPanelAPIRef?.current?.isCollapsed()) {
const rightSize = getPercentageSize(
rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize
);
resizableRightPanelAPIRef.current.resize(rightSize);
resizableRightPanelAPIRef?.current?.resize(rightSize);
}
// This observer kicks in when the ViewportLayout resizable panel group
@ -199,7 +199,7 @@ const useResizablePanels = (
}, [setLeftPanelClosed]);
const onLeftPanelResize = useCallback(size => {
if (!resizablePanelGroupElemRef?.current || resizableLeftPanelAPIRef.current.isCollapsed()) {
if (!resizablePanelGroupElemRef?.current || resizableLeftPanelAPIRef.current?.isCollapsed()) {
return;
}
@ -228,7 +228,7 @@ const useResizablePanels = (
}, [setRightPanelClosed]);
const onRightPanelResize = useCallback(size => {
if (!resizablePanelGroupElemRef?.current || resizableRightPanelAPIRef.current.isCollapsed()) {
if (!resizablePanelGroupElemRef?.current || resizableRightPanelAPIRef?.current?.isCollapsed()) {
return;
}
@ -248,7 +248,7 @@ const useResizablePanels = (
* Note that the width attributed to the handles must be taken into account.
*/
const getPercentageSize = pixelSize => {
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current?.getBoundingClientRect();
return (pixelSize / (panelGroupWidth - resizableHandlesWidth.current)) * 100;
};
@ -257,7 +257,7 @@ const useResizablePanels = (
* Note that the width attributed to the handles must be taken into account.
*/
const getExpandedPixelWidth = percentageSize => {
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current?.getBoundingClientRect();
const expandedWidth =
(percentageSize / 100) * (panelGroupWidth - resizableHandlesWidth.current) -
panelGroupDefinition.shared.expandedInsideBorderSize;

View File

@ -1,42 +1,61 @@
import ToolbarDivider from './Toolbar/ToolbarDivider';
import { ToolbarButton as ToolbarButtonLegacy } from '@ohif/ui';
import { ToolButton, utils } from '@ohif/ui-next';
import ToolbarLayoutSelectorWithServices from './Toolbar/ToolbarLayoutSelector';
import ToolbarSplitButtonWithServices from './Toolbar/ToolbarSplitButtonWithServices';
import ToolbarButtonGroupWithServices from './Toolbar/ToolbarButtonGroupWithServices';
import { ToolbarButton } from '@ohif/ui';
// legacy
import ToolbarDividerLegacy from './Toolbar/ToolbarDivider';
import ToolbarSplitButtonWithServicesLegacy from './Toolbar/ToolbarSplitButtonWithServices';
import ToolbarButtonGroupWithServicesLegacy from './Toolbar/ToolbarButtonGroupWithServices';
import { ProgressDropdownWithService } from './Components/ProgressDropdownWithService';
const getClassName = isToggled => {
return {
className: isToggled
? '!text-primary-active'
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
};
};
// new
import ToolButtonListWrapper from './Toolbar/ToolButtonListWrapper';
import { ToolBoxButtonGroupWrapper, ToolBoxButtonWrapper } from './Toolbar/ToolBoxWrapper';
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
const { cineService } = servicesManager.services;
return [
// new
{
name: 'ohif.toolButton',
defaultComponent: ToolButton,
},
{
name: 'ohif.toolButtonList',
defaultComponent: ToolButtonListWrapper,
},
{
name: 'ohif.toolBoxButtonGroup',
defaultComponent: ToolBoxButtonGroupWrapper,
},
{
name: 'ohif.toolBoxButton',
defaultComponent: ToolBoxButtonWrapper,
},
// legacy
{
name: 'ohif.radioGroup',
defaultComponent: ToolbarButton,
defaultComponent: ToolbarButtonLegacy,
},
{
name: 'ohif.buttonGroup',
defaultComponent: ToolbarButtonGroupWithServicesLegacy,
},
{
name: 'ohif.divider',
defaultComponent: ToolbarDivider,
defaultComponent: ToolbarDividerLegacy,
},
{
name: 'ohif.splitButton',
defaultComponent: ToolbarSplitButtonWithServices,
defaultComponent: ToolbarSplitButtonWithServicesLegacy,
},
// others
{
name: 'ohif.layoutSelector',
defaultComponent: props =>
ToolbarLayoutSelectorWithServices({ ...props, commandsManager, servicesManager }),
},
{
name: 'ohif.buttonGroup',
defaultComponent: ToolbarButtonGroupWithServices,
},
{
name: 'ohif.progressDropdown',
defaultComponent: ProgressDropdownWithService,
@ -66,7 +85,9 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
name: 'evaluate.cine',
evaluate: () => {
const isToggled = cineService.getState().isCineEnabled;
return getClassName(isToggled);
return {
className: utils.getToggledClassName(isToggled),
};
},
},
];

View File

@ -13,9 +13,8 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
PatientDOB: '',
});
const [isMixedPatients, setIsMixedPatients] = useState(false);
const displaySets = displaySetService.getActiveDisplaySets();
const checkMixedPatients = PatientID => {
const checkMixedPatients = (PatientID: string) => {
const displaySets = displaySetService.getActiveDisplaySets();
let isMixedPatients = false;
displaySets.forEach(displaySet => {
@ -30,8 +29,11 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
setIsMixedPatients(isMixedPatients);
};
const updatePatientInfo = () => {
const displaySet = displaySets[0];
const updatePatientInfo = ({ displaySetsAdded }) => {
if (!displaySetsAdded.length) {
return;
}
const displaySet = displaySetsAdded[0];
const instance = displaySet?.instances?.[0] || displaySet?.instance;
if (!instance) {
return;
@ -39,7 +41,7 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
setPatientInfo({
PatientID: instance.PatientID || null,
PatientName: instance.PatientName ? formatPN(instance.PatientName ) : null,
PatientName: instance.PatientName ? formatPN(instance.PatientName) : null,
PatientSex: instance.PatientSex || null,
PatientDOB: formatDate(instance.PatientBirthDate) || null,
});
@ -49,15 +51,11 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
useEffect(() => {
const subscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
() => updatePatientInfo()
props => updatePatientInfo(props)
);
return () => subscription.unsubscribe();
}, []);
useEffect(() => {
updatePatientInfo();
}, [displaySets]);
return { patientInfo, isMixedPatients };
}

View File

@ -39,7 +39,6 @@ import { PanelStudyBrowserHeader } from './Panels/StudyBrowser/PanelStudyBrowser
import * as utils from './utils';
import MoreDropdownMenu from './Components/MoreDropdownMenu';
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
const defaultExtension: Types.Extensions.Extension = {
/**
* Only required property. Should be a unique value across all extensions.

View File

@ -323,7 +323,7 @@ function _getStatusComponent(isTracked, t) {
<Tooltip>
<TooltipTrigger asChild>
<span>
<Icons.StatusTracking className="text-aqua-pale" />
<Icons.StatusTracking className="text-muted-foreground" />
</span>
</TooltipTrigger>
<TooltipContent

View File

@ -35,7 +35,7 @@ function _createSetToolActiveCommands(toolName, toolGroupIds = ['default', 'mpr'
const toolbarButtons: Button[] = [
{
id: 'MeasurementTools',
uiType: 'ohif.splitButton',
uiType: 'ohif.toolButtonList',
props: {
groupId: 'MeasurementTools',
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
@ -81,7 +81,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Zoom',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-zoom',
label: 'Zoom',
@ -91,7 +91,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'WindowLevel',
uiType: 'ohif.splitButton',
uiType: 'ohif.toolButtonList',
props: {
groupId: 'WindowLevel',
primary: ToolbarService.createButton({
@ -118,7 +118,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Pan',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-move',
label: 'Pan',
@ -128,7 +128,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Capture',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-capture',
label: 'Capture',
@ -163,7 +163,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'MoreTools',
uiType: 'ohif.splitButton',
uiType: 'ohif.toolButtonList',
props: {
groupId: 'MoreTools',
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',

View File

@ -14,7 +14,7 @@ const ReferenceLinesListeners: RunCommand = [
const moreTools = [
{
id: 'MoreTools',
uiType: 'ohif.splitButton',
uiType: 'ohif.toolButtonList',
props: {
groupId: 'MoreTools',
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',

View File

@ -43,7 +43,7 @@ export const setToolActiveToolbar = {
const toolbarButtons: Button[] = [
{
id: 'MeasurementTools',
uiType: 'ohif.splitButton',
uiType: 'ohif.toolButtonList',
props: {
groupId: 'MeasurementTools',
// group evaluate to determine which item should move to the top
@ -130,7 +130,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Zoom',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-zoom',
label: 'Zoom',
@ -141,7 +141,7 @@ const toolbarButtons: Button[] = [
// Window Level
{
id: 'WindowLevel',
uiType: 'ohif.splitButton',
uiType: 'ohif.toolButtonList',
props: {
groupId: 'WindowLevel',
primary: createButton({
@ -170,7 +170,7 @@ const toolbarButtons: Button[] = [
// Pan...
{
id: 'Pan',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-move',
@ -181,7 +181,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'MPR',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'icon-mpr',
label: 'MPR',
@ -198,7 +198,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'TrackBallRotate',
type: 'ohif.radioGroup',
type: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-3d-rotate',
@ -208,7 +208,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Capture',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-capture',
label: 'Capture',
@ -234,7 +234,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Crosshairs',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-crosshair',

View File

@ -84,13 +84,7 @@ function modeFactory({ modeConfiguration }) {
* Lifecycle hooks
*/
onModeEnter: function ({ servicesManager, extensionManager, commandsManager }: withAppTypes) {
const {
measurementService,
toolbarService,
toolGroupService,
panelService,
segmentationService,
} = servicesManager.services;
const { measurementService, toolbarService, toolGroupService } = servicesManager.services;
measurementService.clearMeasurements();
@ -112,32 +106,32 @@ function modeFactory({ modeConfiguration }) {
// // ActivatePanel event trigger for when a segmentation or measurement is added.
// // Do not force activation so as to respect the state the user may have left the UI in.
_activatePanelTriggersSubscriptions = [
...panelService.addActivatePanelTriggers(
cornerstone.segmentation,
[
{
sourcePubSubService: segmentationService,
sourceEvents: [segmentationService.EVENTS.SEGMENTATION_ADDED],
},
],
true
),
...panelService.addActivatePanelTriggers(
tracked.measurements,
[
{
sourcePubSubService: measurementService,
sourceEvents: [
measurementService.EVENTS.MEASUREMENT_ADDED,
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
],
},
],
true
),
true,
];
// _activatePanelTriggersSubscriptions = [
// ...panelService.addActivatePanelTriggers(
// cornerstone.segmentation,
// [
// {
// sourcePubSubService: segmentationService,
// sourceEvents: [segmentationService.EVENTS.SEGMENTATION_ADDED],
// },
// ],
// true
// ),
// ...panelService.addActivatePanelTriggers(
// tracked.measurements,
// [
// {
// sourcePubSubService: measurementService,
// sourceEvents: [
// measurementService.EVENTS.MEASUREMENT_ADDED,
// measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
// ],
// },
// ],
// true
// ),
// true,
// ];
},
onModeExit: ({ servicesManager }: withAppTypes) => {
const {

View File

@ -14,7 +14,7 @@ const ReferenceLinesListeners: RunCommand = [
const moreTools = [
{
id: 'MoreTools',
uiType: 'ohif.splitButton',
uiType: 'ohif.toolButtonList',
props: {
groupId: 'MoreTools',
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',

View File

@ -15,7 +15,7 @@ export const setToolActiveToolbar = {
const toolbarButtons: Button[] = [
{
id: 'MeasurementTools',
uiType: 'ohif.splitButton',
uiType: 'ohif.toolButtonList',
props: {
groupId: 'MeasurementTools',
// group evaluate to determine which item should move to the top
@ -110,7 +110,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Zoom',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-zoom',
label: 'Zoom',
@ -121,7 +121,7 @@ const toolbarButtons: Button[] = [
// Window Level
{
id: 'WindowLevel',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-window-level',
label: 'Window Level',
@ -138,7 +138,7 @@ const toolbarButtons: Button[] = [
// Pan...
{
id: 'Pan',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-move',
@ -149,7 +149,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'TrackballRotate',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-3d-rotate',
@ -163,7 +163,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Capture',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-capture',
label: 'Capture',
@ -188,7 +188,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Crosshairs',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-crosshair',

View File

@ -3,7 +3,7 @@ import { ToolbarService } from '@ohif/core';
const toolbarButtons = [
{
id: 'MeasurementTools',
uiType: 'ohif.splitButton',
uiType: 'ohif.toolButtonList',
props: {
groupId: 'MeasurementTools',
// group evaluate to determine which item should move to the top
@ -131,7 +131,7 @@ const toolbarButtons = [
},
{
id: 'dragPan',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-move',
label: 'Pan',
@ -147,7 +147,7 @@ const toolbarButtons = [
},
{
id: 'TagBrowser',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'dicom-tag-browser',
label: 'Dicom Tag Browser',

View File

@ -3,9 +3,10 @@ import type { Button } from '@ohif/core/types';
const toolbarButtons: Button[] = [
{
id: 'BrushTools',
uiType: 'ohif.buttonGroup',
uiType: 'ohif.toolBoxButtonGroup',
props: {
groupId: 'BrushTools',
evaluate: 'evaluate.cornerstone.hasSegmentation',
items: [
{
id: 'Brush',
@ -134,7 +135,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Shapes',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolBoxButton',
props: {
label: 'Shapes',
evaluate: {

View File

@ -13,7 +13,7 @@ const setToolActiveToolbar = {
const toolbarButtons = [
{
id: 'MeasurementTools',
uiType: 'ohif.splitButton',
uiType: 'ohif.toolButtonList',
props: {
groupId: 'MeasurementTools',
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
@ -67,7 +67,7 @@ const toolbarButtons = [
},
{
id: 'Zoom',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-zoom',
label: 'Zoom',
@ -77,7 +77,7 @@ const toolbarButtons = [
},
{
id: 'WindowLevel',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-window-level',
label: 'Window Level',
@ -87,7 +87,7 @@ const toolbarButtons = [
},
{
id: 'Pan',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-move',
@ -98,7 +98,7 @@ const toolbarButtons = [
},
{
id: 'TrackballRotate',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-3d-rotate',
@ -134,7 +134,7 @@ const toolbarButtons = [
},
{
id: 'Crosshairs',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-crosshair',

View File

@ -3,9 +3,10 @@ import type { Button } from '@ohif/core/types';
const toolbarButtons: Button[] = [
{
id: 'BrushTools',
uiType: 'ohif.buttonGroup',
uiType: 'ohif.toolBoxButtonGroup',
props: {
groupId: 'BrushTools',
evaluate: 'evaluate.cornerstone.hasSegmentation',
items: [
{
id: 'Brush',
@ -172,14 +173,16 @@ const toolbarButtons: Button[] = [
},
{
id: 'Shapes',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolBoxButton',
props: {
id: 'Shapes',
icon: 'icon-tool-shape',
label: 'Shapes',
evaluate: {
name: 'evaluate.cornerstone.segmentation',
toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'],
disabledText: 'Create new segmentation to enable shapes tool.',
},
icon: 'icon-tool-shape',
options: [
{
name: 'Shape',

View File

@ -20,7 +20,7 @@ export const setToolActiveToolbar = {
const toolbarButtons: Button[] = [
{
id: 'Zoom',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-zoom',
label: 'Zoom',
@ -30,7 +30,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'WindowLevel',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-window-level',
label: 'Window Level',
@ -40,7 +40,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Pan',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-move',
label: 'Pan',
@ -50,7 +50,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'TrackballRotate',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-3d-rotate',
@ -64,7 +64,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Capture',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-capture',
label: 'Capture',
@ -90,7 +90,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'Crosshairs',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-crosshair',
label: 'Crosshairs',
@ -108,7 +108,7 @@ const toolbarButtons: Button[] = [
},
{
id: 'MoreTools',
uiType: 'ohif.splitButton',
uiType: 'ohif.toolButtonList',
props: {
groupId: 'MoreTools',
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
@ -192,14 +192,6 @@ const toolbarButtons: Button[] = [
commands: 'invertViewport',
evaluate: 'evaluate.viewportProperties.toggle',
}),
createButton({
id: 'Probe',
icon: 'tool-probe',
label: 'Probe',
tooltip: 'Probe',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'Cine',
icon: 'tool-cine',
@ -214,14 +206,6 @@ const toolbarButtons: Button[] = [
},
],
}),
createButton({
id: 'Angle',
icon: 'tool-angle',
label: 'Angle',
tooltip: 'Angle',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'Magnify',
icon: 'tool-magnify',
@ -230,22 +214,6 @@ const toolbarButtons: Button[] = [
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'RectangleROI',
icon: 'tool-rectangle',
label: 'Rectangle',
tooltip: 'Rectangle',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'CalibrationLine',
icon: 'tool-calibration',
label: 'Calibration',
tooltip: 'Calibration Line',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'TagBrowser',
icon: 'dicom-tag-browser',

View File

@ -11,7 +11,7 @@ const setToolActiveToolbar = {
const toolbarButtons = [
{
id: 'MeasurementTools',
uiType: 'ohif.splitButton',
uiType: 'ohif.toolButtonList',
props: {
groupId: 'MeasurementTools',
primary: ToolbarService.createButton({
@ -56,7 +56,7 @@ const toolbarButtons = [
},
{
id: 'Zoom',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-zoom',
label: 'Zoom',
@ -67,7 +67,7 @@ const toolbarButtons = [
// Window Level + Presets
{
id: 'WindowLevel',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-window-level',
label: 'Window Level',
@ -78,7 +78,7 @@ const toolbarButtons = [
// Crosshairs Button
{
id: 'Crosshairs',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-crosshair',
label: 'Crosshairs',
@ -89,7 +89,7 @@ const toolbarButtons = [
// Pan Button
{
id: 'Pan',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-move',
label: 'Pan',
@ -100,7 +100,7 @@ const toolbarButtons = [
// Rectangle ROI Start End Threshold Button
{
id: 'RectangleROIStartEndThreshold',
uiType: 'ohif.radioGroup',
uiType: 'ohif.toolBoxButton',
props: {
icon: 'tool-create-threshold',
label: 'Rectangle ROI Threshold',
@ -119,9 +119,10 @@ const toolbarButtons = [
},
{
id: 'BrushTools',
uiType: 'ohif.buttonGroup',
uiType: 'ohif.toolBoxButtonGroup',
props: {
groupId: 'BrushTools',
evaluate: 'evaluate.cornerstone.hasSegmentation',
items: [
{
id: 'Brush',

View File

@ -72,7 +72,7 @@ describe('OHIF Cornerstone Toolbar', () => {
// Assign an alias to the button element
cy.get('@wwwcBtnPrimary').as('wwwcButton');
cy.get('@wwwcButton').click();
cy.get('@wwwcButton').should('have.class', 'bg-primary-light');
cy.get('@wwwcButton').should('have.attr', 'data-active', 'true');
//drags the mouse inside the viewport to be able to interact with series
cy.get('@viewport')
@ -97,7 +97,7 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('@panButton').click();
// Assert that the button has the 'active' class
cy.get('@panButton').should('have.class', 'bg-primary-light');
cy.get('@panButton').should('have.attr', 'data-active', 'true');
// Trigger the pan actions on the viewport
cy.get('@viewport')

View File

@ -18,7 +18,7 @@ describe('OHIF General Viewer', function () {
cy.get('@zoomBtn')
.click()
.then($zoomBtn => {
cy.wrap($zoomBtn).should('have.class', 'bg-primary-light');
cy.wrap($zoomBtn).should('have.attr', 'data-active', 'true');
});
const zoomLevelInitial = cy.get('@viewportInfoTopLeft').then($viewportInfo => {

View File

@ -7,7 +7,7 @@ describe('OHIF MPR', () => {
});
it('should not go MPR for non reconstructible displaySets', () => {
cy.get('[data-cy="MPR"]').should('have.class', 'ohif-disabled');
cy.get('[data-cy="MPR"]').should('have.class', 'cursor-not-allowed');
});
it('should go MPR for reconstructible displaySets and come back', () => {

View File

@ -1,6 +1,8 @@
//Creating aliases for Cornerstone tools buttons
export function initCornerstoneToolsAliases() {
cy.get('[data-cy="StackScroll"]').as('stackScrollBtn');
// Note: stack scroll is not in the DOM when the study is loaded
// cy.get('[data-cy="StackScroll"]').as('stackScrollBtn');
cy.get('[data-cy="Zoom"]').as('zoomBtn');
cy.get('[data-cy="WindowLevel-split-button-primary"]').as('wwwcBtnPrimary');
cy.get('[data-cy="WindowLevel-split-button-secondary"]').as('wwwcBtnSecondary');
@ -34,7 +36,6 @@ export function initCommonElementsAliases(skipMarkers) {
cy.get('[data-cy="viewport-overlay-bottom-right"]').as('viewportInfoBottomRight');
cy.get('[data-cy="viewport-overlay-bottom-left"]').as('viewportInfoBottomLeft');
console.debug('🚀 ~ skipMarkers:', skipMarkers);
if (skipMarkers) {
return;
}

View File

@ -265,7 +265,7 @@ Cypress.Commands.add(
}
});
cy.get('@lengthButton').should('have.class', 'bg-primary-light');
cy.get('@lengthButton').should('have.attr', 'data-active', 'true');
cy.get('@viewport').then($viewport => {
const [x1, y1] = firstClick;

View File

@ -21,13 +21,13 @@ import {
ViewportDialogProvider,
CineProvider,
UserAuthenticationProvider,
ToolboxProvider,
} from '@ohif/ui';
import {
ThemeWrapper as ThemeWrapperNext,
NotificationProvider,
ViewportGridProvider,
TooltipProvider,
ToolboxProvider,
} from '@ohif/ui-next';
// Viewer Project
// TODO: Should this influence study list?

View File

@ -5,7 +5,7 @@ export function useToolbar({ servicesManager, buttonSection = 'primary' }: withA
const { EVENTS } = toolbarService;
const [toolbarButtons, setToolbarButtons] = useState(
toolbarService.getButtonSection(buttonSection)
toolbarService.getButtonSection(buttonSection as string)
);
// Callback function for handling toolbar interactions
@ -25,7 +25,7 @@ export function useToolbar({ servicesManager, buttonSection = 'primary' }: withA
// Effect to handle toolbar modification events
useEffect(() => {
const handleToolbarModified = () => {
setToolbarButtons(toolbarService.getButtonSection(buttonSection));
setToolbarButtons(toolbarService.getButtonSection(buttonSection as string)?.filter(Boolean));
};
const subs = [EVENTS.TOOL_BAR_MODIFIED, EVENTS.TOOL_BAR_STATE_MODIFIED].map(event => {

View File

@ -236,8 +236,8 @@ export default class ToolbarService extends PubSubService {
const evaluateButtonProps = (button, props, refreshProps) => {
if (evaluationResults.has(button.id)) {
const { disabled, className, isActive } = evaluationResults.get(button.id);
return { ...props, disabled, className, isActive };
const { disabled, disabledText, className, isActive } = evaluationResults.get(button.id);
return { ...props, disabled, disabledText, className, isActive };
} else {
const evaluated = props.evaluate?.({ ...refreshProps, button });
const updatedProps = {
@ -276,7 +276,9 @@ export default class ToolbarService extends PubSubService {
// item in the group
buttonProps = {
...buttonProps,
primary: groupEvaluated?.primary || buttonProps.primary,
primary: groupEvaluated?.primary ?? buttonProps.primary,
disabled: groupEvaluated?.disabled ?? buttonProps.disabled,
disabledText: groupEvaluated?.disabledText ?? buttonProps.disabledText,
};
const { primary, items } = buttonProps;

View File

@ -3,12 +3,28 @@ sidebar_position: 11
sidebar_label: Playwright Testing
---
:::note
You might need to run the `yarn playwright install ` for the first time if you have not
:::
# Running the tests
```bash
# run the tests
yarn test:e2e:ui
```
# Writing PlayWright Tests
Our Playwright tests are written using the Playwright test framework. We use these tests to test our OHIF Viewer and ensure that it is working as expected.
In this guide, we will show you how to write Playwright tests for the OHIF Viewer.
## Using a specific study and mode
If you would like to use a specific study, you can use the `studyInstanceUID` property to reference the study you would like to visit. for example, if you would like to use the study with StudyInstanceUID `2.16.840.1.114362.1.11972228.22789312658.616067305.306.2` and the mode `Basic Viewer`, you can use the following code snippet:

View File

@ -0,0 +1,199 @@
---
title: Input Number, Range, and Double Range
---
# Migration Guide: Moving to `Numeric` Component
This guide explains how to migrate from the existing `Input`, `InputRange`, and `InputDoubleRange` components to the new `Numeric` meta component.
## Why Migrate?
The old components relied heavily on props, making them complex and difficult to maintain and apply custom styles. The new `Numeric` component provides a structured approach with a context-based API, reducing prop clutter and improving reusability.
## `Input` > `Numeric.NumberInput`
### Basic Usage
**Old Usage:**
```tsx
<Input
id="example"
label="Enter a number"
value={value}
onChange={(e) => setValue(e.target.value)}
type="number"
/>
```
**New Usage:**
```tsx
<Numeric.Container mode="number" value={value} onChange={setValue}>
<Numeric.Label>Enter a number</Numeric.Label>
<Numeric.NumberInput />
</Numeric.Container>
```
### `Input` with Custom Classes
#### **Old Usage (with containerClassName, labelClassName, and className)**
In the old implementation, we manually applied `containerClassName`, `labelClassName`, and `className` to style the `Input` component:
```tsx
<Input
id="example"
label="Enter a number"
value={value}
onChange={(e) => setValue(e.target.value)}
type="number"
containerClassName="flex flex-col space-y-2"
labelClassName="text-gray-500 text-sm"
className="border rounded p-2"
/>
```
**New Usage (Migrating to `Numeric.NumberInput`)**
With `Numeric`, you should wrap everything inside `Numeric.Container`, and you can directly apply class names to its subcomponents:
```tsx
<Numeric.Container mode="number" value={value} onChange={setValue} className="flex flex-col space-y-2">
<Numeric.Label className="text-gray-500 text-sm">Enter a number</Numeric.Label>
<Numeric.NumberInput className="border rounded p-2" />
</Numeric.Container>
```
## `InputRange` > `Numeric.SingleRange`
### Basic Usage
**Old Usage:**
```tsx
<InputRange
value={value}
onChange={setValue}
minValue={0}
maxValue={100}
step={1}
showLabel
/>
```
**New Usage:**
```tsx
<Numeric.Container mode="singleRange" value={value} onChange={setValue} min={0} max={100} step={1}>
<Numeric.Label showValue>Range</Numeric.Label>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>
```
### Custom Classes
**Old Usage**
```tsx
<InputRange
value={value}
onChange={setValue}
minValue={0}
maxValue={100}
step={1}
containerClassName="flex flex-col space-y-2"
inputClassName="w-full bg-gray-700"
labelClassName="text-gray-500 text-sm"
labelVariant="body1"
showLabel={true}
labelPosition="left"
/>
```
**New Usage**
```tsx
<Numeric.Container mode="singleRange" value={value} onChange={setValue} min={0} max={100} step={1} className="flex flex-col space-y-2">
<Numeric.Label className="text-gray-500 text-sm">Range</Numeric.Label>
<Numeric.SingleRange sliderClassName="w-full bg-gray-700" />
</Numeric.Container>
```
:::note
You now have more control over the position of the label and the slider. You can use pretty much any layout you want, whether that's flex, grid, or something else. Instead of relying on `labelPosition` to position the label, you're free to use the layout that works best for you.
:::
### AllowNumberEdit
**Old Usage:**
```tsx
<InputRange
value={value}
onChange={setValue}
minValue={0}
maxValue={100}
step={1}
allowNumberEdit={true}
showAdjustmentArrows={true}
/>
```
**New Usage:**
Using `Numeric.SingleRange` and `showNumberInput`
```tsx
<Numeric.Container mode="singleRange" value={value} onChange={setValue} min={0} max={100} step={1}>
<Numeric.Label showValue>Range</Numeric.Label>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>
```
## `InputDoubleRange` > `Numeric.DoubleRange`
### Basic Usage
**Old Usage:**
```tsx
<InputDoubleRange
values={rangeValues}
onChange={setRangeValues}
minValue={0}
maxValue={100}
step={5}
showLabel
/>
```
**New Usage:**
```tsx
<Numeric.Container mode="doubleRange" values={rangeValues} onChange={setRangeValues} min={0} max={100} step={5}>
<Numeric.Label showValue>Range</Numeric.Label>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
```
---
## Summary of Changes
| Old Component | New Component Equivalent |
|--------------------|------------------------|
| `<Input>` | `<Numeric.NumberInput>` |
| `<InputRange>` | `<Numeric.SingleRange>` |
| `<InputDoubleRange>` | `<Numeric.DoubleRange>` |

View File

@ -0,0 +1,68 @@
---
title: Tests
---
## 1. ToolButton `data-active` Attribute
- **Previous**: Checked for a `bg-primary-light` class to determine if a tool was active.
- **Now**: Check for the HTML attribute `data-active="true"`.
### Example
```diff
- cy.get('@wwwcButton').should('have.class', 'bg-primary-light');
+ cy.get('@wwwcButton').should('have.attr', 'data-active', 'true');
```
## 2. Additional Data Attributes
- Each tool button now includes:
- `data-tool="<toolId>"`
- `data-active="<true|false>"`
This makes it easier to identify and assert on specific tools in the DOM.
### Example
```diff
- <span data-cy={id}>
+ <span
+ data-cy={id}
+ data-tool={id}
+ data-active={isActive}
+ >
```
## 3. MPR Button Class Change
If you were targeting the `ohif-disabled` class, you need to update your tests to target the `cursor-not-allowed` class.
- **Previous**: `ohif-disabled`
- **Now**: `cursor-not-allowed`
### Example
```diff
- cy.get('[data-cy="MPR"]').should('have.class', 'ohif-disabled');
+ cy.get('[data-cy="MPR"]').should('have.class', 'cursor-not-allowed');
```
## 4. Removal of Stack Scroll Alias
- The `[data-cy="StackScroll"]` element is no longer reliably in the DOM at study load.
- If needed, reintroduce or conditionally assert its presence when appropriate.
```diff
- cy.get('[data-cy="StackScroll"]').as('stackScrollBtn');
+ // Removed due to absence in DOM at study load
```
---
## Summary
1. **Replace** all checks for `bg-primary-light` with `data-active="true"`.
2. **Use** `data-tool` and `data-active` attributes for more robust DOM selection and assertions.
3. **Update** MPR button checks to `cursor-not-allowed`.
4. **Remove** the `[data-cy="StackScroll"]` alias (or only use it when the element is present).

View File

@ -0,0 +1,101 @@
---
title: Toolbar
---
# Toolbar
## New Toolbar uiType
We have two new toolbar button types: `ohif.toolButtonList` and `ohif.toolButton`, which are intended to replace the `ohif.radioGroup` and `ohif.splitButton` types.
Note that these are backward compatible, so if you are not ready to pick up the new ui types (which are more flexible and powerful), you can continue using the old types.
```js
// Old type
{
uiType: 'ohif.radioGroup',
}
// New type
{
uiType: 'ohif.toolButton',
}
```
and
```js
// Old type
{
uiType: 'ohif.splitButton',
}
// New type
{
uiType: 'ohif.toolButtonList',
}
```
The `ohif.buttonGroup` and `ohif.radioGroup` types used in the Toolbox have been replaced with `ohif.toolBoxButtonGroup` and `ohif.toolBoxButton` to reflect their usage in the Toolbox, which has distinct styling.
```js
// Old type
{
uiType: 'ohif.buttonGroup',
}
// New type
{
uiType: 'ohif.toolBoxButtonGroup',
}
```
```js
// Old type
{
uiType: 'ohif.radioGroup',
}
// New type
{
uiType: 'ohif.toolBoxButton',
}
```
## getToolbarModule
The `getToolbarModule` function previously returned `disabled`, `disabledText`, and `className` as part of its evaluation process for the button state. These properties will still be returned, but common class names are now handled internally by the new UI button components, including `ToolButton`, `ToolButtonList`, `Toolbox`, and `ToolBoxGroup`. You can override the `className` if you need to.
## ToolBox
Previously, the segmentation toolbox was not using an `evaluator` property. This is now taken into account
```js
// old
{
id: 'BrushTools',
uiType: 'ohif.buttonGroup',
props: {
groupId: 'BrushTools',
items: []
}
}
// now
{
id: 'BrushTools',
uiType: 'ohif.buttonGroup',
props: {
groupId: 'BrushTools',
evaluate: 'evaluate.cornerstone.hasSegmentation',
items: []
}
}
```

View File

@ -1,651 +1,57 @@
import React, { useState } from 'react';
import React from 'react';
import '../css/custom.css';
import Layout from '@theme/Layout';
import { Label } from '../../../ui-next/src/components/Label';
import { Input } from '../../../ui-next/src/components/Input';
import { Separator } from '../../../ui-next/src/components/Separator';
import { Tabs, TabsList, TabsTrigger } from '../../../ui-next/src/components/Tabs';
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from '../../../ui-next/src/components/Select';
import { Button } from '../../../ui-next/src/components/Button';
import { Switch } from '../../../ui-next/src/components/Switch';
import { Checkbox } from '../../../ui-next/src/components/Checkbox';
import { Toggle } from '../../../ui-next/src/components/Toggle';
import { Slider } from '../../../ui-next/src/components/Slider';
import { ScrollArea } from '../../../ui-next/src/components/ScrollArea';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from '../../../ui-next/src/components/DropdownMenu';
import { Icons } from '../../../ui-next/src/components/Icons';
import { Toaster, toast } from '../../../ui-next/src/components/Sonner';
import { DataRow } from '../../../ui-next/src/components/DataRow';
import DataRowExample from './patterns/DataRowExample';
import {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
} from '../../../ui-next/src/components/Card';
interface ShowcaseRowProps {
title: string;
description?: string;
children: React.ReactNode;
code: string;
}
export default function ComponentShowcase() {
// Handlers to trigger different types of toasts
const triggerSuccess = () => {
toast.success('This is a success toast!');
};
const triggerError = () => {
toast.error('This is an error toast!');
};
const triggerInfo = () => {
toast.info('This is an info toast!');
};
const triggerWarning = () => {
toast.warning('This is a warning toast!');
};
// Handler to trigger a toast.promise example
const triggerPromiseToast = () => {
const promise = () =>
new Promise<{ name: string }>(resolve =>
setTimeout(() => resolve({ name: 'Segmentation 1' }), 3000)
);
toast.promise(promise(), {
loading: 'Loading Segmentation...',
success: data => `${data.name} has been added`,
error: 'Error',
});
};
// Handler to trigger a toast with description
const triggerDescriptionToast = () => {
toast.success('Completed', {
description: 'This is a detailed description of the success message.',
});
};
// Handler to trigger a toast with an action button
const triggerActionButtonToast = () => {
toast.info('No active segmentation detected', {
description: 'Create a segmentation before using the Brush',
});
};
// Handler to trigger a toast with a cancel button
const triggerCancelButtonToast = () => {
toast.error('No active segmentation detected', {
description: 'Create a segmentation before using the Brush',
});
};
// Handler to trigger a toast with both action and cancel buttons
const triggerCombinedToast = () => {
toast.warning('Warning!', {
description: 'This is a warning with both action and cancel buttons.',
action: (
<Button
size="sm"
variant="ghost"
onClick={() => alert('Retry action clicked')}
>
Retry
</Button>
),
cancel: (
<Button
size="sm"
variant="ghost"
onClick={() => toast.dismiss()}
>
Cancel
</Button>
),
});
};
// Handler to trigger a loading toast using Toaster's default loading icon
const showLoadingToast = () => {
toast.loading('Loading your data...');
};
import { TooltipProvider } from '../../../ui-next/src/components/Tooltip';
// Import all showcase components
import ButtonShowcase from './components/ButtonShowcase';
import CheckboxShowcase from './components/CheckboxShowcase';
import DataRowShowcase from './components/DataRowShowcase';
import DropdownMenuShowcase from './components/DropdownMenuShowcase';
import InputShowcase from './components/InputShowcase';
import ScrollAreaShowcase from './components/ScrollAreaShowcase';
import SelectShowcase from './components/SelectShowcase';
import SliderShowcase from './components/SliderShowcase';
import SwitchShowcase from './components/SwitchShowcase';
import TabsShowcase from './components/TabsShowcase';
import ToastShowcase from './components/ToastShowcase';
import ToolButtonShowcase from './components/ToolButtonShowcase';
import ToolButtonListShowcase from './components/ToolButtonListShowcase';
import NumericMetaShowcase from './components/NumericMetaShowcase';
/**
* Components List page that displays all available UI components
*/
export default function ComponentsList() {
return (
<Layout
title="Components"
description="OHIF Viewer Components"
>
<div className="text-foreground min-h-screen bg-black">
<div className="mx-auto my-4 max-w-5xl pt-6 pb-3">
<div className="grid grid-cols-1 gap-5 md:grid-cols-3">
<a
href="/colors-and-type"
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
>
<Card className="hover:bg-primary/30 w-full transition-colors">
<CardHeader>
<CardTitle className="text-foreground text-xl">
<Icons.ColorChange className="h-12 w-12" />
Colors & Typography
</CardTitle>
<CardDescription className="text-base">
Color Palette and Typography Guidelines
</CardDescription>
</CardHeader>
</Card>
</a>
<a
href="/components-list"
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
>
<Card className="hover:bg-primary/30 w-full transition-colors">
<CardHeader>
<CardTitle className="text-foreground text-xl">
<Icons.ColorChange className="h-12 w-12" />
Components
</CardTitle>
<CardDescription className="text-base">
Essential UI Components with Variants
</CardDescription>
</CardHeader>
</Card>
</a>
<a
href="/patterns"
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
>
<Card className="hover:bg-primary/30 w-full transition-colors">
<CardHeader>
<CardTitle className="text-foreground text-xl">
<Icons.ColorChange className="h-12 w-12" />
Patterns
</CardTitle>
<CardDescription className="text-base">
Component-Based Layout Examples
</CardDescription>
</CardHeader>
</Card>
</a>
<TooltipProvider>
<div className="text-foreground min-h-screen bg-black">
<div className="mx-auto my-4 max-w-5xl pt-4 pb-6">
<div className="ml-6 mb-6 text-base">
<h1 className="text-foreground mb-3 text-5xl">Components</h1>
</div>
<TabsShowcase />
<ButtonShowcase />
<CheckboxShowcase />
<DataRowShowcase />
<DropdownMenuShowcase />
<InputShowcase />
<ScrollAreaShowcase />
<SelectShowcase />
<SliderShowcase />
<SwitchShowcase />
<TabsShowcase />
<ToastShowcase />
<ToolButtonShowcase />
<ToolButtonListShowcase />
<NumericMetaShowcase />
</div>
</div>
<div className="mx-auto my-4 max-w-5xl pt-4 pb-6">
<div className="ml-6 mb-6 text-base">
<h1 className="text-foreground mb-3 text-5xl">Components</h1>
</div>
{/* Alphabetically Sorted ShowcaseRows */}
<ShowcaseRow
title="Buttons"
description="Button components and size variants. Use the primary and secondary buttons in dialogs or screens where one action is required. In the Viewer application, use ghost button in panels where many different actions are available."
code={`
<Button variant="default">Primary Button</Button>
<Button variant="secondary">Secondary Button</Button>
<Button variant="ghost">Ghost Button</Button>
<Button variant="ghost" size="icon">?</Button>
<Button variant="link">Link</Button>
`}
>
<div className="flex flex-wrap gap-4">
<Button variant="default">Primary Button</Button>
<Button variant="secondary">Secondary Button</Button>
<Button variant="ghost">Ghost Button</Button>
<Button
variant="ghost"
size="icon"
>
?
</Button>
<Button variant="link">Link</Button>
</div>
<div className="mt-6 flex flex-wrap gap-4">
<Button
variant="default"
size="lg"
className="w-[107px]"
>
Large Button
</Button>
<Button
variant="default"
size="sm"
>
Small Button
</Button>
</div>
</ShowcaseRow>
<ShowcaseRow
title="Checkbox"
description="When possible use Switch in place of checkbox. If necessary, Checkbox provides a smaller component to change between two states or options."
code={`
<div className="items-top flex space-x-2">
<Checkbox id="terms1" />
<div className="grid gap-1.5 pt-0.5 leading-none">
<Label>Display inactive segmentations</Label>
</div>
</div>
`}
>
<div className="items-top flex space-x-2">
<Checkbox id="terms1" />
<div className="grid gap-1.5 pt-0.5 leading-none">
<Label>Display inactive segmentations</Label>
</div>
</div>
</ShowcaseRow>
<ShowcaseRow
title="Data Row"
description="A selectable row with action menu options and visibility toggle. Color, Secondary details, and Image Series are optional to display."
code={`
Example code coming soon.
`}
>
{/* Render the DataRowExample component */}
<DataRowExample />
</ShowcaseRow>
<ShowcaseRow
title="Dropdown Menu"
description="Dropdown menu provides a flexible list of options that can open from buttons or other elements"
code={`
<DropdownMenu>
<DropdownMenuTrigger>
<Button>Open Basic</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Item 1</DropdownMenuItem>
<DropdownMenuItem>Item 2</DropdownMenuItem>
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>Open Align Start</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem>Item 1</DropdownMenuItem>
<DropdownMenuItem>Item 2</DropdownMenuItem>
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>Open Align End</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Item 1</DropdownMenuItem>
<DropdownMenuItem>Item 2</DropdownMenuItem>
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>Open Align Top</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start">
<DropdownMenuItem onSelect={() => console.debug('Item 1')}>Item 1</DropdownMenuItem>
<DropdownMenuItem onSelect={() => console.debug('Item 2')}>Item 2</DropdownMenuItem>
<DropdownMenuItem onSelect={() => console.debug('Item 3')}>Long name Item 3</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
`}
>
<div className="flex flex-wrap gap-4">
<DropdownMenu>
<DropdownMenuTrigger>
<Button>Open Basic</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Item 1</DropdownMenuItem>
<DropdownMenuItem>Item 2</DropdownMenuItem>
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>Open Align Start</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem>Item 1</DropdownMenuItem>
<DropdownMenuItem>Item 2</DropdownMenuItem>
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>Open Align End</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Item 1</DropdownMenuItem>
<DropdownMenuItem>Item 2</DropdownMenuItem>
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>Open Align Top</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
>
<DropdownMenuItem onSelect={() => console.debug('Item 1')}>
Item 1
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => console.debug('Item 2')}>
Item 2
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => console.debug('Item 3')}>
Long name Item 3
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</ShowcaseRow>
<ShowcaseRow
title="Input"
description="Input fields can be used with or without example text"
code={`
<div className="inline-block">
<div className="mr-4 inline-block">
<Label>Patient Weight</Label>
</div>
<div className="inline-block">
<Input placeholder="(kg)" />
</div>
</div>
`}
>
<div className="inline-block">
<div className="mr-4 inline-block">
<Label>Patient Weight</Label>
</div>
<div className="inline-block">
<Input placeholder="(kg)" />
</div>
</div>
</ShowcaseRow>
<ShowcaseRow
title="Scroll Area"
description="Displays a scroll indicator when hovering within an element."
code={`
<ScrollArea className="border-input bg-background h-[150px] w-[350px] rounded-md border p-2 text-sm text-white">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua.
</ScrollArea>
`}
>
<ScrollArea className="border-input bg-background h-[150px] w-[350px] rounded-md border p-2 text-sm text-white">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</ScrollArea>
</ShowcaseRow>
<ShowcaseRow
title="Select"
description="Switch between a list of options"
code={`
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
`}
>
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
</ShowcaseRow>
<ShowcaseRow
title="Slider"
description="Used to select a value in a predefined range."
code={`
<div className="w-40 px-5">
<Slider
className="w-full"
defaultValue={[50]}
max={100}
step={1}
/>
</div>
`}
>
<div className="w-40 px-5">
<Slider
className="w-full"
defaultValue={[50]}
max={100}
step={1}
/>
</div>
</ShowcaseRow>
<ShowcaseRow
title="Switch"
description="A toggle Switch is used to change between two different states. Use descriptive labels next to Switches that are understandable before interacting."
code={`
<Switch />
`}
>
<Switch defaultChecked />
<Label className="text-foreground mx-2 w-14 flex-none whitespace-nowrap text-sm">
Sync changes in all viewports
</Label>
</ShowcaseRow>
<ShowcaseRow
title="Tabs"
description="Tabs (or segmented controls) can be used to provide navigation options or allow users to switch between multiple options (e.g., tool settings) "
code={`
<Tabs className="w-[400px]">
<TabsList>
<TabsTrigger value="circle">Circle</TabsTrigger>
<Separator orientation="vertical" />
<TabsTrigger value="sphere">Sphere</TabsTrigger>
<Separator orientation="vertical" />
<TabsTrigger value="square">Square</TabsTrigger>
</TabsList>
</Tabs>
`}
>
<Tabs className="w-[400px]">
<TabsList>
<TabsTrigger value="circle">Circle</TabsTrigger>
<Separator orientation="vertical" />
<TabsTrigger value="sphere">Sphere</TabsTrigger>
<Separator orientation="vertical" />
<TabsTrigger value="square">Square</TabsTrigger>
</TabsList>
</Tabs>
</ShowcaseRow>
<ShowcaseRow
title="Toast"
description="A toast notification displays temporary feedback messages to users above the current UI. Notifications stack into one unit after multiple cascading notifications."
code={`
Example code coming soon.
`}
>
{/* Toast Examples Section */}
Simple message:
<div className="mt-2 mb-7 space-x-2">
<Button
variant="default"
onClick={triggerPromiseToast}
>
Loading & Success Toast
</Button>
<Button
variant="default"
onClick={triggerSuccess}
>
Success Toast
</Button>
<Button
variant="default"
onClick={triggerError}
>
Error Toast
</Button>
<Button
variant="default"
onClick={triggerInfo}
>
Info Toast
</Button>
<Button
variant="default"
onClick={triggerWarning}
>
Warning Toast
</Button>
</div>
Message with details:
<div className="mt-2 space-x-2">
<Button
variant="default"
onClick={triggerDescriptionToast}
>
Success Toast
</Button>
<Button
variant="default"
onClick={triggerActionButtonToast}
>
Info Toast
</Button>
<Button
variant="default"
onClick={triggerCancelButtonToast}
>
Error Toast
</Button>
<Button
variant="default"
onClick={triggerCombinedToast}
>
Toast with Buttons
</Button>
</div>
{/* Render the Toaster component */}
<Toaster />
</ShowcaseRow>
</div>
</div>
</TooltipProvider>
</Layout>
);
}
function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
const [showCode, setShowCode] = useState(false);
return (
<div className="bg-background mb-8 rounded-lg p-6">
<div className="mb-4 flex items-start justify-between">
<div>
<h2 className="text-highlight text-2xl">{title}</h2>
</div>
<Button
className="text-primary"
variant="ghost"
size="sm"
onClick={() => setShowCode(!showCode)}
>
{showCode ? 'Hide Code' : 'Show Code'} <Icons.Code className="ml-2 h-4 w-4" />
</Button>
</div>
<div className="grid grid-cols-1 gap-9 md:grid-cols-3">
<div className="text-base md:col-span-1">
{description && <p className="text-secondary-foreground mt-2">{description}</p>}
</div>
<div className="flex min-h-[120px] items-center md:col-span-2">
<div className="showcase-content">{children}</div>
</div>
</div>
{showCode && (
<pre className="border-input mt-4 overflow-x-auto rounded-md border bg-black p-4 text-sm">
<code>{code}</code>
</pre>
)}
</div>
);
}
// function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
// const [showCode, setShowCode] = useState(false);
// return (
// <div className="bg-background mb-8 rounded-lg p-6">
// <div className="mb-4 flex items-start justify-between">
// <div>
// <h2 className="text-2xl font-bold">{title}</h2>
// {description && <p className="text-muted-foreground mt-1">{description}</p>}
// </div>
// <Button
// className="text-primary"
// variant="outline"
// size="sm"
// onClick={() => setShowCode(!showCode)}
// >
// {showCode ? 'Hide Code' : 'Show Code'} <Icons.Code className="ml-2 h-4 w-4" />
// </Button>
// </div>
// <div className="showcase-content mb-4">{children}</div>
// {showCode && (
// <pre className="mt-4 overflow-x-auto rounded-md bg-black p-4 text-sm">
// <code>{code}</code>
// </pre>
// )}
// </div>
// );
// }

View File

@ -0,0 +1,54 @@
import React from 'react';
import { Button } from '../../../../ui-next/src/components/Button';
import ShowcaseRow from './ShowcaseRow';
/**
* ButtonShowcase component displays button variants and examples
*/
export default function ButtonShowcase() {
return (
<ShowcaseRow
title="Buttons"
description="Button components and size variants. Use the primary and secondary buttons in dialogs or screens where one action is required. In the Viewer application, use ghost button in panels where many different actions are available."
code={`
<Button variant="default">Primary Button</Button>
<Button variant="secondary">Secondary Button</Button>
<Button variant="ghost">Ghost Button</Button>
<Button variant="ghost" size="icon">?</Button>
<Button variant="link">Link</Button>
`}
>
<div className="flex flex-wrap gap-4">
<Button variant="default">Primary Button</Button>
<Button variant="secondary">Secondary Button</Button>
<Button variant="ghost">Ghost Button</Button>
<Button
variant="ghost"
size="icon"
>
?
</Button>
<Button variant="link">Link</Button>
</div>
<div className="mt-6 flex flex-wrap gap-4">
<Button
variant="default"
size="lg"
className="w-[107px]"
>
Large Button
</Button>
<Button
variant="default"
size="sm"
>
Small Button
</Button>
</div>
</ShowcaseRow>
);
}

View File

@ -0,0 +1,31 @@
import React from 'react';
import { Checkbox } from '../../../../ui-next/src/components/Checkbox';
import { Label } from '../../../../ui-next/src/components/Label';
import ShowcaseRow from './ShowcaseRow';
/**
* CheckboxShowcase component displays checkbox variants and examples
*/
export default function CheckboxShowcase() {
return (
<ShowcaseRow
title="Checkbox"
description="When possible use Switch in place of checkbox. If necessary, Checkbox provides a smaller component to change between two states or options."
code={`
<div className="items-top flex space-x-2">
<Checkbox id="terms1" />
<div className="grid gap-1.5 pt-0.5 leading-none">
<Label>Display inactive segmentations</Label>
</div>
</div>
`}
>
<div className="items-top flex space-x-2">
<Checkbox id="terms1" />
<div className="grid gap-1.5 pt-0.5 leading-none">
<Label>Display inactive segmentations</Label>
</div>
</div>
</ShowcaseRow>
);
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import DataRowExample from '../patterns/DataRowExample';
import ShowcaseRow from './ShowcaseRow';
/**
* DataRowShowcase component displays DataRow variants and examples
*/
export default function DataRowShowcase() {
return (
<ShowcaseRow
title="Data Row"
description="A selectable row with action menu options and visibility toggle. Color, Secondary details, and Image Series are optional to display."
code={`
Example code coming soon.
`}
>
<DataRowExample />
</ShowcaseRow>
);
}

View File

@ -0,0 +1,81 @@
import React from 'react';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from '../../../../ui-next/src/components/DropdownMenu';
import { Button } from '../../../../ui-next/src/components/Button';
import ShowcaseRow from './ShowcaseRow';
/**
* DropdownMenuShowcase component displays DropdownMenu variants and examples
*/
export default function DropdownMenuShowcase() {
return (
<ShowcaseRow
title="Dropdown Menu"
description="Dropdown menu provides a flexible list of options that can open from buttons or other elements"
code={`
<DropdownMenu>
<DropdownMenuTrigger>
<Button>Open Basic</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Item 1</DropdownMenuItem>
<DropdownMenuItem>Item 2</DropdownMenuItem>
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
`}
>
<div className="flex flex-wrap gap-4">
<DropdownMenu>
<DropdownMenuTrigger>
<Button>Open Basic</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Item 1</DropdownMenuItem>
<DropdownMenuItem>Item 2</DropdownMenuItem>
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>Open Align Start</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem>Item 1</DropdownMenuItem>
<DropdownMenuItem>Item 2</DropdownMenuItem>
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>Open Align End</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Item 1</DropdownMenuItem>
<DropdownMenuItem>Item 2</DropdownMenuItem>
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>Open Align Top</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
>
<DropdownMenuItem onSelect={() => console.debug('Item 1')}>Item 1</DropdownMenuItem>
<DropdownMenuItem onSelect={() => console.debug('Item 2')}>Item 2</DropdownMenuItem>
<DropdownMenuItem onSelect={() => console.debug('Item 3')}>
Long name Item 3
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</ShowcaseRow>
);
}

View File

@ -0,0 +1,35 @@
import React from 'react';
import { Input } from '../../../../ui-next/src/components/Input';
import { Label } from '../../../../ui-next/src/components/Label';
import ShowcaseRow from './ShowcaseRow';
/**
* InputShowcase component displays Input variants and examples
*/
export default function InputShowcase() {
return (
<ShowcaseRow
title="Input"
description="Input fields can be used with or without example text"
code={`
<div className="inline-block">
<div className="mr-4 inline-block">
<Label>Patient Weight</Label>
</div>
<div className="inline-block">
<Input placeholder="(kg)" />
</div>
</div>
`}
>
<div className="inline-block">
<div className="mr-4 inline-block">
<Label>Patient Weight</Label>
</div>
<div className="inline-block">
<Input placeholder="(kg)" />
</div>
</div>
</ShowcaseRow>
);
}

View File

@ -0,0 +1,260 @@
import React from 'react';
import Numeric from '../../../../ui-next/src/components/Numeric';
import Icons from '../../../../ui-next/src/components/Icons';
import ShowcaseRow from './ShowcaseRow';
/**
* NumericShowcase component displays Numeric variants and examples
*/
export default function NumericShowcase() {
return (
<div className="space-y-8">
{/* Basic Number Input */}
<ShowcaseRow
title="Numeric - Number Input"
description="Basic number input with min, max and a label"
code={`
<Numeric.Container mode="number" min={0} max={10} onChange={onChange}>
<div className="flex flex-row items-center space-x-2">
<Numeric.Label>Width</Numeric.Label>
<Numeric.NumberInput />
</div>
</Numeric.Container>
<Numeric.Container mode="number" min={0} max={100} onChange={onChange}>
<Numeric.Label className="text-secondary-foreground text-sm font-bold">Bolder</Numeric.Label>
<Numeric.NumberInput className="w-12" />
</Numeric.Container>
<Numeric.Container mode="number" className="flex flex-row items-center justify-between" onChange={onChange} min={0} value={123465789} max={10000000000000}>
<Numeric.Label className="flex flex-row items-center">
<Icons.Add />
With Icon
</Numeric.Label>
<Numeric.NumberInput className="w-32 text-center" />
</Numeric.Container>`}
>
<div className="bg-popover flex w-[300px] flex-col space-y-4 rounded p-4">
<Numeric.Container
mode="number"
min={0}
max={10}
onChange={val => console.debug('Value changed:', val)}
>
<div className="flex flex-row items-center space-x-2">
<Numeric.Label>Width</Numeric.Label>
<Numeric.NumberInput />
</div>
</Numeric.Container>
<Numeric.Container
mode="number"
className="space-y-1"
min={0}
max={100}
onChange={val => console.debug('Value changed:', val)}
>
<Numeric.Label className="text-muted-foreground text-sm font-bold">
Bolder
</Numeric.Label>
<Numeric.NumberInput className="w-12" />
</Numeric.Container>
<Numeric.Container
mode="number"
className="flex flex-row items-center justify-between"
onChange={val => console.debug('Value changed:', val)}
min={0}
value={123465789}
max={10000000000000}
>
<Numeric.Label className="flex flex-row items-center">
<Icons.Add />
With Icon
</Numeric.Label>
<Numeric.NumberInput className="w-32 text-center" />
</Numeric.Container>
</div>
</ShowcaseRow>
{/* Single Range Slider */}
<ShowcaseRow
title="Numeric - Single Range"
description="Single range slider with optional number input"
code={`
<Numeric.Container mode="singleRange" min={0} max={100} step={1} value={50} onChange={onChange}>
<Numeric.Label>Brightness</Numeric.Label>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>
<Numeric.Container mode="singleRange" min={-50} max={50} step={1} value={0} className="flex flex-row items-center" onChange={onChange}>
<Numeric.Label showValue>Contrast</Numeric.Label>
<Numeric.SingleRange />
</Numeric.Container>
<Numeric.Container mode="singleRange" min={0} max={100} step={1} value={50} className="flex flex-row items-center space-x-2" onChange={onChange}>
<Numeric.Label>Something Else </Numeric.Label>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>`}
>
<div className="bg-popover flex w-[300px] flex-col space-y-4 rounded p-4">
<Numeric.Container
mode="singleRange"
min={0}
max={100}
step={1}
value={50}
onChange={val => console.debug('Value changed:', val)}
>
<Numeric.Label>Brightness</Numeric.Label>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>
<Numeric.Container
mode="singleRange"
min={-50}
max={50}
step={1}
value={0}
className="flex flex-row items-center"
onChange={val => console.debug('Value changed:', val)}
>
<Numeric.Label showValue>Contrast</Numeric.Label>
<Numeric.SingleRange />
</Numeric.Container>
<Numeric.Container
mode="singleRange"
min={0}
max={100}
step={1}
value={50}
className="flex flex-row items-center space-x-2"
onChange={val => console.debug('Value changed:', val)}
>
<Numeric.Label>Something Else </Numeric.Label>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>
</div>
</ShowcaseRow>
{/* Double Range Slider */}
<ShowcaseRow
title="Numeric - Double Range"
description="Double range slider for selecting a range of values"
code={`
<Numeric.Container mode="doubleRange" min={0} max={100} step={1} values={[30, 70]} onChange={onChange}>
<Numeric.Label showValue>Window Width/Level</Numeric.Label>
<Numeric.DoubleRange />
</Numeric.Container>
<Numeric.Container mode="doubleRange" min={0} max={100} step={1} values={[30, 70]}>
<Numeric.Label>Window Width/Level</Numeric.Label>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
<Numeric.Container mode="doubleRange" min={0} max={100} step={1} values={[30, 70]} className="flex flex-row items-center space-x-2" onChange={onChange}>
<Numeric.Label>Inline double slider</Numeric.Label>
<Numeric.DoubleRange />
</Numeric.Container>`}
>
<div className="bg-popover flex w-[300px] flex-col space-y-4 rounded p-4">
<Numeric.Container
mode="doubleRange"
min={0}
max={100}
step={1}
values={[30, 70]}
className="space-y-1"
onChange={vals => console.debug('Values changed:', vals)}
>
<Numeric.Label showValue>Window Width/Level</Numeric.Label>
<Numeric.DoubleRange />
</Numeric.Container>
<Numeric.Container
mode="doubleRange"
min={0}
max={100}
step={1}
values={[30, 70]}
className="space-y-1"
>
<Numeric.Label>Window Width/Level</Numeric.Label>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
<Numeric.Container
mode="doubleRange"
min={0}
max={100}
step={1}
values={[30, 70]}
className="flex flex-row items-center space-x-2"
onChange={vals => console.debug('Values changed:', vals)}
>
<Numeric.Label>Inline double slider</Numeric.Label>
<Numeric.DoubleRange />
</Numeric.Container>
</div>
</ShowcaseRow>
{/* Combined Examples */}
<ShowcaseRow
title="Numeric - Combined Examples"
description="Different modes and configurations working together"
code={`
<Numeric.Container mode="number" min={0} max={10} step={0.1}>
<Numeric.Label>Zoom Factor</Numeric.Label>
<Numeric.NumberInput />
</Numeric.Container>
<Numeric.Container mode="singleRange" min={0} max={360} step={1}>
<Numeric.Label showValue>Rotation</Numeric.Label>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>
<Numeric.Container mode="doubleRange" min={-1000} max={3000} values={[1000, 2000]} step={10}>
<Numeric.Label showValue>CT Window</Numeric.Label>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>`}
>
<div className="bg-popover flex w-[300px] flex-col space-y-4 rounded p-4">
<Numeric.Container
mode="number"
min={0}
max={10}
step={0.1}
className="space-y-1"
>
<Numeric.Label>Zoom Factor</Numeric.Label>
<Numeric.NumberInput />
</Numeric.Container>
<Numeric.Container
mode="singleRange"
min={0}
max={360}
step={1}
className="space-y-1"
>
<Numeric.Label showValue>Rotation</Numeric.Label>
<Numeric.SingleRange showNumberInput />
</Numeric.Container>
<Numeric.Container
mode="doubleRange"
min={-1000}
max={3000}
values={[1000, 2000]}
step={10}
className="space-y-1"
>
<Numeric.Label showValue>CT Window</Numeric.Label>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
</div>
</ShowcaseRow>
</div>
);
}

View File

@ -0,0 +1,33 @@
import React from 'react';
import { ScrollArea } from '../../../../ui-next/src/components/ScrollArea';
import ShowcaseRow from './ShowcaseRow';
/**
* ScrollAreaShowcase component displays ScrollArea variants and examples
*/
export default function ScrollAreaShowcase() {
return (
<ShowcaseRow
title="Scroll Area"
description="Displays a scroll indicator when hovering within an element."
code={`
<ScrollArea className="border-input bg-background h-[150px] w-[350px] rounded-md border p-2 text-sm text-white">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua.
</ScrollArea>
`}
>
<ScrollArea className="border-input bg-background h-[150px] w-[350px] rounded-md border p-2 text-sm text-white">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem
ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
</ScrollArea>
</ShowcaseRow>
);
}

View File

@ -0,0 +1,44 @@
import React from 'react';
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from '../../../../ui-next/src/components/Select';
import ShowcaseRow from './ShowcaseRow';
/**
* SelectShowcase component displays Select variants and examples
*/
export default function SelectShowcase() {
return (
<ShowcaseRow
title="Select"
description="Switch between a list of options"
code={`
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
`}
>
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
</ShowcaseRow>
);
}

View File

@ -0,0 +1,49 @@
import React, { useState } from 'react';
import { Button } from '../../../../ui-next/src/components/Button';
import { Icons } from '../../../../ui-next/src/components/Icons';
interface ShowcaseRowProps {
title: string;
description?: string;
children: React.ReactNode;
code: string;
}
/**
* ShowcaseRow component displays a UI component example with title, description,
* and optional code snippet that can be toggled.
*/
export default function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
const [showCode, setShowCode] = useState(false);
return (
<div className="bg-background mb-8 rounded-lg p-6">
<div className="mb-4 flex items-start justify-between">
<div>
<h2 className="text-highlight text-2xl">{title}</h2>
</div>
<Button
className="text-primary"
variant="ghost"
size="sm"
onClick={() => setShowCode(!showCode)}
>
{showCode ? 'Hide Code' : 'Show Code'} <Icons.Code className="ml-2 h-4 w-4" />
</Button>
</div>
<div className="grid grid-cols-1 gap-9 md:grid-cols-3">
<div className="text-base md:col-span-1">
{description && <p className="text-secondary-foreground mt-2">{description}</p>}
</div>
<div className="flex min-h-[120px] items-center md:col-span-2">
<div className="showcase-content">{children}</div>
</div>
</div>
{showCode && (
<pre className="border-input mt-4 overflow-x-auto rounded-md border bg-black p-4 text-sm">
<code>{code}</code>
</pre>
)}
</div>
);
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import { Slider } from '../../../../ui-next/src/components/Slider';
import ShowcaseRow from './ShowcaseRow';
/**
* SliderShowcase component displays Slider variants and examples
*/
export default function SliderShowcase() {
return (
<ShowcaseRow
title="Slider"
description="Used to select a value in a predefined range."
code={`
<div className="w-40 px-5">
<Slider
className="w-full"
defaultValue={[50]}
max={100}
step={1}
/>
</div>
`}
>
<div className="w-40 px-5">
<Slider
className="w-full"
defaultValue={[50]}
max={100}
step={1}
/>
</div>
</ShowcaseRow>
);
}

View File

@ -0,0 +1,24 @@
import React from 'react';
import { Switch } from '../../../../ui-next/src/components/Switch';
import { Label } from '../../../../ui-next/src/components/Label';
import ShowcaseRow from './ShowcaseRow';
/**
* SwitchShowcase component displays Switch variants and examples
*/
export default function SwitchShowcase() {
return (
<ShowcaseRow
title="Switch"
description="A toggle Switch is used to change between two different states. Use descriptive labels next to Switches that are understandable before interacting."
code={`
<Switch />
`}
>
<Switch defaultChecked />
<Label className="text-foreground mx-2 w-14 flex-none whitespace-nowrap text-sm">
Sync changes in all viewports
</Label>
</ShowcaseRow>
);
}

View File

@ -0,0 +1,40 @@
import React from 'react';
import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs';
import { Separator } from '../../../../ui-next/src/components/Separator';
import ShowcaseRow from './ShowcaseRow';
/**
* TabsShowcase component displays Tabs variants and examples
*/
export default function TabsShowcase() {
return (
<ShowcaseRow
title="Tabs"
description="Tabs (or segmented controls) can be used to provide navigation options or allow users to switch between multiple options (e.g., tool settings) "
code={`
<Tabs className="w-[400px]" onValueChange={newValue => console.log(newValue)}>
<TabsList>
<TabsTrigger value="circle">Circle</TabsTrigger>
<Separator orientation="vertical" />
<TabsTrigger value="sphere">Sphere</TabsTrigger>
<Separator orientation="vertical" />
<TabsTrigger value="square">Square</TabsTrigger>
</TabsList>
</Tabs>
`}
>
<Tabs
className="w-[400px]"
onValueChange={newValue => console.log(newValue)}
>
<TabsList>
<TabsTrigger value="circle">Circle</TabsTrigger>
<Separator orientation="vertical" />
<TabsTrigger value="sphere">Sphere</TabsTrigger>
<Separator orientation="vertical" />
<TabsTrigger value="square">Square</TabsTrigger>
</TabsList>
</Tabs>
</ShowcaseRow>
);
}

View File

@ -0,0 +1,158 @@
import React from 'react';
import { Button } from '../../../../ui-next/src/components/Button';
import { Toaster, toast } from '../../../../ui-next/src/components/Sonner';
import ShowcaseRow from './ShowcaseRow';
/**
* ToastShowcase component displays Toast variants and examples
*/
export default function ToastShowcase() {
// Handlers to trigger different types of toasts
const triggerSuccess = () => {
toast.success('This is a success toast!');
};
const triggerError = () => {
toast.error('This is an error toast!');
};
const triggerInfo = () => {
toast.info('This is an info toast!');
};
const triggerWarning = () => {
toast.warning('This is a warning toast!');
};
// Handler to trigger a toast.promise example
const triggerPromiseToast = () => {
const promise = () =>
new Promise<{ name: string }>(resolve =>
setTimeout(() => resolve({ name: 'Segmentation 1' }), 3000)
);
toast.promise(promise(), {
loading: 'Loading Segmentation...',
success: data => `${data.name} has been added`,
error: 'Error',
});
};
// Handler to trigger a toast with description
const triggerDescriptionToast = () => {
toast.success('Completed', {
description: 'This is a detailed description of the success message.',
});
};
// Handler to trigger a toast with an action button
const triggerActionButtonToast = () => {
toast.info('No active segmentation detected', {
description: 'Create a segmentation before using the Brush',
});
};
// Handler to trigger a toast with a cancel button
const triggerCancelButtonToast = () => {
toast.error('No active segmentation detected', {
description: 'Create a segmentation before using the Brush',
});
};
// Handler to trigger a toast with both action and cancel buttons
const triggerCombinedToast = () => {
toast.warning('Warning!', {
description: 'This is a warning with both action and cancel buttons.',
action: (
<Button
size="sm"
variant="ghost"
onClick={() => console.debug('Retry action clicked')}
>
Retry
</Button>
),
cancel: (
<Button
size="sm"
variant="ghost"
onClick={() => toast.dismiss()}
>
Cancel
</Button>
),
});
};
return (
<ShowcaseRow
title="Toast"
description="A toast notification displays temporary feedback messages to users above the current UI. Notifications stack into one unit after multiple cascading notifications."
code={`
Example code coming soon.
`}
>
Simple message:
<div className="mt-2 mb-7 space-x-2">
<Button
variant="default"
onClick={triggerPromiseToast}
>
Loading & Success Toast
</Button>
<Button
variant="default"
onClick={triggerSuccess}
>
Success Toast
</Button>
<Button
variant="default"
onClick={triggerError}
>
Error Toast
</Button>
<Button
variant="default"
onClick={triggerInfo}
>
Info Toast
</Button>
<Button
variant="default"
onClick={triggerWarning}
>
Warning Toast
</Button>
</div>
Message with details:
<div className="mt-2 space-x-2">
<Button
variant="default"
onClick={triggerDescriptionToast}
>
Success Toast
</Button>
<Button
variant="default"
onClick={triggerActionButtonToast}
>
Info Toast
</Button>
<Button
variant="default"
onClick={triggerCancelButtonToast}
>
Error Toast
</Button>
<Button
variant="default"
onClick={triggerCombinedToast}
>
Toast with Buttons
</Button>
</div>
<Toaster />
</ShowcaseRow>
);
}

View File

@ -0,0 +1,90 @@
import React from 'react';
import {
ToolButtonList,
ToolButton,
ToolButtonListDefault,
ToolButtonListDropDown,
ToolButtonListItem,
ToolButtonListDivider,
} from '../../../../ui-next/src/components/ToolButton';
import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip';
import ShowcaseRow from './ShowcaseRow';
/**
* ToolButtonListShowcase component displays ToolButtonList variants and examples
*/
export default function ToolButtonListShowcase() {
return (
<ShowcaseRow
title="ToolButtonList"
description="A compound component that combines a primary tool button with a dropdown menu of additional related tools"
code={`
// Example usage:
<ToolButtonList>
<ToolButtonListDefault>
<ToolButton
id="Length"
icon="tool-length"
label="Length"
tooltip="Length Tool"
onInteraction={({ itemId }) => console.debug(\`Clicked \${itemId}\`)}
/>
</ToolButtonListDefault>
<ToolButtonListDivider />
<ToolButtonListDropDown>
<ToolButtonListItem
icon="tool-length"
onSelect={() => console.debug('Selected Length')}
>
<span className="pl-1">Length</span>
</ToolButtonListItem>
<ToolButtonListItem
icon="tool-bidirectional"
onSelect={() => console.debug('Selected Bidirectional')}
>
<span className="pl-1">Bidirectional</span>
</ToolButtonListItem>
</ToolButtonListDropDown>
</ToolButtonList>
`}
>
<div className="bg-popover flex h-11 w-[450px] items-center justify-start rounded p-2">
<TooltipProvider>
<ToolButtonList>
<ToolButtonListDefault>
<ToolButton
id="Length"
icon="ToolLength"
label="Length"
tooltip="Length Tool"
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
/>
</ToolButtonListDefault>
<ToolButtonListDivider />
<ToolButtonListDropDown>
<ToolButtonListItem
icon="ToolLength"
onSelect={() => console.debug('Selected Length')}
>
<span className="pl-1">Length</span>
</ToolButtonListItem>
<ToolButtonListItem
icon="ToolBidirectional"
onSelect={() => console.debug('Selected Bidirectional')}
>
<span className="pl-1">Bidirectional</span>
</ToolButtonListItem>
<ToolButtonListItem
icon="ToolAnnotate"
onSelect={() => console.debug('Selected Annotation')}
>
<span className="pl-1">Annotation</span>
</ToolButtonListItem>
</ToolButtonListDropDown>
</ToolButtonList>
</TooltipProvider>
</div>
</ShowcaseRow>
);
}

View File

@ -0,0 +1,54 @@
import React from 'react';
import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip';
import ToolButton from '../../../../ui-next/src/components/ToolButton/ToolButton';
import ShowcaseRow from './ShowcaseRow';
/**
* ToolButtonShowcase component displays ToolButton variants and examples
*/
export default function ToolButtonShowcase() {
return (
<ShowcaseRow
title="ToolButton"
description="Used to activate tools or perform single actions"
code={`
// Example usage:
<ToolButton
id="Zoom"
icon="zoom" // must exist in your Icons or fallback to 'MissingIcon'
label="Zoom"
tooltip="Zoom Tool"
isActive={false}
onInteraction={({ itemId }) => console.debug(\`Clicked \${itemId}\`)}
/>
`}
>
<div className="bg-popover flex h-11 w-[450px] items-center justify-center rounded">
<TooltipProvider>
<ToolButton
id="Zoom"
icon="ToolZoom"
isActive={true}
label="Zoom"
tooltip="Zoom"
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
/>
<ToolButton
id="Zoom"
icon="ToolMove"
label="Pan"
tooltip="Pan"
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
/>
<ToolButton
id="Zoom"
icon="ToolWindowLevel"
label="Window Level"
tooltip="Window Level"
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
/>
</TooltipProvider>
</div>
</ShowcaseRow>
);
}

View File

@ -38,12 +38,12 @@ export interface ButtonProps
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
({ className, variant, size, asChild = false, ...props }, forwardRef) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
ref={forwardRef}
{...props}
/>
);

View File

@ -11,10 +11,22 @@ interface DoubleSliderProps {
step?: number;
defaultValue?: [number, number];
onValueChange?: (value: [number, number]) => void;
showNumberInputs?: boolean;
}
const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
({ className, min, max, step = 1, defaultValue = [min, max], onValueChange }, ref) => {
(
{
className,
min,
max,
onValueChange,
step = 1,
defaultValue = [min, max],
showNumberInputs = false,
},
ref
) => {
const [value, setValue] = React.useState<[number, number]>(defaultValue);
const prevDefaultValueRef = React.useRef<[number, number] | null>(null);
@ -77,16 +89,18 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
ref={ref}
className={cn('flex w-full items-center space-x-2', className)}
>
<Input
type="number"
value={formatValue(value[0])}
onChange={e => handleInputChange(0, e.target.value)}
onBlur={() => handleInputChange(0, value[0].toString())}
className="w-14"
min={min}
max={max}
step={step}
/>
{showNumberInputs && (
<Input
type="number"
value={formatValue(value[0])}
onChange={e => handleInputChange(0, e.target.value)}
onBlur={() => handleInputChange(0, value[0].toString())}
className="w-14"
min={min}
max={max}
step={step}
/>
)}
<SliderPrimitive.Root
className="relative flex h-4 w-full touch-none select-none items-center"
min={min}
@ -101,16 +115,18 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
<SliderPrimitive.Thumb className="border-background bg-primary focus-visible:ring-ring block h-4 w-4 rounded-full border-2 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50" />
<SliderPrimitive.Thumb className="border-background bg-primary focus-visible:ring-ring block h-4 w-4 rounded-full border-2 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
<Input
type="number"
value={formatValue(value[1])}
onChange={e => handleInputChange(1, e.target.value)}
onBlur={() => handleInputChange(1, value[1].toString())}
className="w-14"
min={min}
max={max}
step={step}
/>
{showNumberInputs && (
<Input
type="number"
value={formatValue(value[1])}
onChange={e => handleInputChange(1, e.target.value)}
onBlur={() => handleInputChange(1, value[1].toString())}
className="w-14"
min={min}
max={max}
step={step}
/>
)}
</div>
);
}

View File

@ -689,6 +689,9 @@ export const Icons = {
'icon-status-alert': (props: IconProps) => Alert(props),
'info-link': (props: IconProps) => InfoLink(props),
'launch-info': (props: IconProps) => LaunchInfo(props),
'old-trash': (props: IconProps) => Trash(props),
'tool-point': (props: IconProps) => ToolCircle(props),
'tool-freehand-line': (props: IconProps) => ToolFreehand(props),
clipboard: (props: IconProps) => Clipboard(props),
/** Adds an icon to the set of icons */

View File

@ -2447,8 +2447,6 @@ export const ToolSegBrush = (props: IconProps) => (
width="24px"
height="24px"
viewBox="0 0 24 24"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g
@ -2490,8 +2488,6 @@ export const ToolSegEraser = (props: IconProps) => (
width="24px"
height="24px"
viewBox="0 0 24 24"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g
@ -2592,8 +2588,6 @@ export const ToolSegThreshold = (props: IconProps) => (
width="24px"
height="24px"
viewBox="0 0 24 24"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g
@ -3012,10 +3006,11 @@ export const ToolWindowLevel = (props: IconProps) => (
export const ToolWindowRegion = (props: IconProps) => (
<svg
width="28px"
height="28px"
viewBox="0 0 28 28"
width="24px"
height="24px"
viewBox="0 0 24 24"
version="1.1"
{...props}
xmlns="http://www.w3.org/2000/svg"
>
<g
@ -3029,8 +3024,8 @@ export const ToolWindowRegion = (props: IconProps) => (
id="Rectangle"
x="0"
y="0"
width="28"
height="28"
width="24"
height="24"
></rect>
<path
d="M10.3460449,22 L4,22 C3.44771525,22 3,21.5522847 3,21 L3,4 C3,3.44771525 3.44771525,3 4,3 L21,3 C21.5522847,3 22,3.44771525 22,4 L22,11.3197021"
@ -3110,11 +3105,9 @@ export const ToolZoom = (props: IconProps) => (
export const ToolBrush = (props: IconProps) => (
<svg
width="28px"
height="28px"
viewBox="0 0 28 28"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="24px"
height="24px"
viewBox="0 0 24 24"
{...props}
>
<g
@ -3128,8 +3121,8 @@ export const ToolBrush = (props: IconProps) => (
id="Rectangle"
x="0"
y="0"
width="28"
height="28"
width="24"
height="24"
></rect>
<path
d="M3.24640621,21.8286833 C3.09173375,21.7551472 2.99513748,21.5971513 3.00018895,21.4259625 C3.00524042,21.2547737 3.11098486,21.1027485 3.26972426,21.0384606 C5.3260304,20.2059201 4.66362518,17.8620247 5.27421252,16.6088957 C6.02197747,15.1026514 7.84114758,14.4766383 9.35746132,15.2037675 C13.9485253,17.4422999 8.48346644,24.3211232 3.24640621,21.8286833 Z"
@ -3153,11 +3146,9 @@ export const ToolBrush = (props: IconProps) => (
export const ToolEraser = (props: IconProps) => (
<svg
width="28px"
height="28px"
viewBox="0 0 28 28"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="24px"
height="24px"
viewBox="0 0 24 24"
{...props}
>
<g
@ -3171,8 +3162,8 @@ export const ToolEraser = (props: IconProps) => (
id="Rectangle"
x="0"
y="0"
width="28"
height="28"
width="24"
height="24"
></rect>
<path
d="M8.47826087,18.0030772 C7.48731947,17.9495317 6.54439458,17.5591084 5.80565217,16.8964685 L4.1066087,15.1958598 C3.49763625,14.5834856 3.49763625,13.5942339 4.1066087,12.9818598 L13.6317391,3.45672934 C14.2441133,2.84775689 15.233365,2.84775689 15.8457391,3.45672934 L19.8926087,7.5035989 C20.5015811,8.11597307 20.5015811,9.10522473 19.8926087,9.7175989 L12.7153043,16.8949033 C11.9769855,17.5579476 11.0343414,17.9489213 10.0434783,18.0030772 L8.47826087,18.0030772 Z"
@ -3210,11 +3201,9 @@ export const ToolEraser = (props: IconProps) => (
export const ToolThreshold = (props: IconProps) => (
<svg
width="28px"
height="28px"
viewBox="0 0 28 28"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="24px"
height="24px"
viewBox="0 0 24 24"
{...props}
>
<g
@ -3228,8 +3217,8 @@ export const ToolThreshold = (props: IconProps) => (
id="Rectangle"
x="0"
y="0"
width="28"
height="28"
width="24"
height="24"
></rect>
<g
id="Group"
@ -3295,11 +3284,9 @@ export const ToolThreshold = (props: IconProps) => (
export const ToolShape = (props: IconProps) => (
<svg
width="28px"
height="28px"
viewBox="0 0 28 28"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="24px"
height="24px"
viewBox="0 0 24 24"
{...props}
>
<g
@ -3313,8 +3300,8 @@ export const ToolShape = (props: IconProps) => (
id="Rectangle"
x="0"
y="0"
width="28"
height="28"
width="24"
height="24"
></rect>
<circle
id="Oval"

View File

@ -1,3 +1,4 @@
import { Icons } from './Icons';
export { Icons };
export default Icons;

View File

@ -0,0 +1,277 @@
// Numeric.tsx
import React, { createContext, useContext, useState, useCallback, PropsWithChildren } from 'react';
import { cn } from '../../lib/utils';
import { Input } from '../Input/Input';
import { Slider } from '../Slider/Slider';
import { DoubleSlider } from '../DoubleSlider/DoubleSlider';
interface NumericMetaContextValue {
mode: 'number' | 'singleRange' | 'doubleRange';
singleValue: number;
doubleValue: [number, number];
setSingleValue: (val: number) => void;
setDoubleValue: (vals: [number, number]) => void;
min: number;
max: number;
step: number;
}
const NumericMetaContext = createContext<NumericMetaContextValue | null>(null);
/* -------------------------------------------------------------------------
1) Container
---------------------------------------------------------------------------*/
interface NumericMetaContainerProps {
mode: 'number' | 'singleRange' | 'doubleRange';
value?: number; // for single-value usage
values?: [number, number]; // for double-range usage
onChange?: (val: number | [number, number]) => void;
min?: number;
max?: number;
step?: number;
className?: string;
}
function NumericMetaContainer({
mode,
value = 0,
values = [0, 100],
onChange,
min = 0,
max = 100,
step = 1,
className,
children,
}: PropsWithChildren<NumericMetaContainerProps>) {
// Initialize state with props but don't update automatically
const [internalSingleValue, setInternalSingleValue] = useState<number>(value);
const [internalDoubleValue, setInternalDoubleValue] = useState<[number, number]>(values);
const handleSingleChange = useCallback(
(newVal: number) => {
setInternalSingleValue(newVal);
onChange?.(newVal);
},
[onChange]
);
const handleDoubleChange = useCallback(
(newVals: [number, number]) => {
// Update internal state
setInternalDoubleValue(newVals);
// Notify parent if onChange is provided
onChange?.(newVals);
},
[onChange]
);
return (
<NumericMetaContext.Provider
value={{
mode,
singleValue: internalSingleValue,
doubleValue: internalDoubleValue,
setSingleValue: handleSingleChange,
setDoubleValue: handleDoubleChange,
min,
max,
step,
}}
>
<div className={cn('flex flex-col', className)}>{children}</div>
</NumericMetaContext.Provider>
);
}
/* -------------------------------------------------------------------------
2) Label sub-component
---------------------------------------------------------------------------*/
interface NumericMetaLabelProps {
showValue?: boolean; // optionally show the current numeric value(s)
className?: string;
children: React.ReactNode;
}
function NumericMetaLabel({ children, showValue, className }: NumericMetaLabelProps) {
const ctx = useContext(NumericMetaContext);
if (!ctx) {
throw new Error('NumericMetaLabel must be used inside <Numeric.Container>.');
}
const { mode, singleValue, doubleValue } = ctx;
let displayedValue = '';
let valueClasses = '';
if (mode === 'number' || mode === 'singleRange') {
displayedValue = singleValue.toString();
valueClasses = 'w-10';
} else if (mode === 'doubleRange') {
displayedValue = `[${doubleValue[0]} - ${doubleValue[1]}]`;
}
return (
<div className={cn('text-foreground flex text-base', className)}>
{children}
{showValue && (
<span className={cn('inline-block', valueClasses)}>{`: ${displayedValue}`}</span>
)}
</div>
);
}
/* -------------------------------------------------------------------------
3) SingleRange sub-component
---------------------------------------------------------------------------*/
interface SingleRangeProps {
showNumberInput?: boolean;
sliderClassName?: string;
numberInputClassName?: string;
}
function SingleRange({ showNumberInput, sliderClassName, numberInputClassName }: SingleRangeProps) {
const ctx = useContext(NumericMetaContext);
if (!ctx) {
throw new Error('SingleRange must be used inside <Numeric.Container>.');
}
const { mode, singleValue, setSingleValue, min, max, step } = ctx;
const handleSliderChange = useCallback(
(val: number[]) => {
setSingleValue(val[0]);
},
[setSingleValue]
);
const handleNumberChange = useCallback(
(evt: React.ChangeEvent<HTMLInputElement>) => {
const parsed = parseFloat(evt.target.value);
if (!isNaN(parsed)) {
setSingleValue(Math.max(min, Math.min(parsed, max)));
}
},
[min, max, setSingleValue]
);
if (mode !== 'singleRange') {
return null;
}
return (
<div className="flex flex-1 items-center space-x-2">
<Slider
className={cn('flex-1', sliderClassName)}
value={[singleValue]}
min={min}
max={max}
step={step}
onValueChange={handleSliderChange}
/>
{showNumberInput && (
<Input
type="number"
className={cn('w-[50px] shrink-0', numberInputClassName)}
value={singleValue}
step={step}
min={min}
max={max}
onChange={handleNumberChange}
/>
)}
</div>
);
}
/* -------------------------------------------------------------------------
4) DoubleRange sub-component
---------------------------------------------------------------------------*/
interface DoubleRangeProps {
showNumberInputs?: boolean;
className?: string;
}
function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) {
const ctx = useContext(NumericMetaContext);
if (!ctx) {
throw new Error('DoubleRange must be used inside <Numeric.Container>.');
}
const { mode, doubleValue, setDoubleValue, min, max, step } = ctx;
const handleSliderChange = useCallback(
(values: [number, number]) => {
setDoubleValue(values);
},
[setDoubleValue]
);
if (mode !== 'doubleRange') {
return null;
}
return (
<div className={cn('min-w-0 flex-1', className)}>
<DoubleSlider
min={min}
max={max}
step={step}
defaultValue={doubleValue}
onValueChange={handleSliderChange}
showNumberInputs={showNumberInputs}
/>
</div>
);
}
/* -------------------------------------------------------------------------
5) Basic NumberInput sub-component
---------------------------------------------------------------------------*/
interface NumberInputProps {
className?: string;
}
function NumberInput({ className }: NumberInputProps) {
const ctx = useContext(NumericMetaContext);
if (!ctx) {
throw new Error('NumberInput must be used inside <Numeric.Container>.');
}
const { mode, singleValue, setSingleValue, min, max, step } = ctx;
if (mode !== 'number') {
return null;
}
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
const val = parseFloat(evt.target.value);
if (!isNaN(val)) {
setSingleValue(Math.max(min, Math.min(val, max)));
}
};
// Calculate width based on max value's length, with a minimum of 3 characters
const maxLength = Math.max(3, max?.toString().length ?? 3);
const calculatedWidth = `${maxLength + 1.5}ch`;
return (
<Input
type="number"
value={singleValue}
step={step}
min={min}
max={max}
onChange={handleChange}
className={cn('min-w-[60px]', `w-[${calculatedWidth}]`, className)}
/>
);
}
export const Numeric = {
Container: NumericMetaContainer,
Label: NumericMetaLabel,
SingleRange,
DoubleRange,
NumberInput,
};
export default Numeric;

View File

@ -0,0 +1,3 @@
import Numeric from './Numeric';
export default Numeric;

View File

@ -0,0 +1,42 @@
import React from 'react';
import Numeric from '../Numeric';
import { cn } from '../../lib/utils';
interface RowDoubleRangeProps {
values: [number, number];
onChange: (values: [number, number]) => void;
minValue: number;
maxValue: number;
step: number;
showLabel?: boolean;
label?: string;
className?: string;
}
const RowDoubleRange: React.FC<RowDoubleRangeProps> = ({
values,
onChange,
minValue,
maxValue,
step,
showLabel = false,
label = '',
className,
}) => {
return (
<Numeric.Container
mode="doubleRange"
values={values}
onChange={onChange}
min={minValue}
max={maxValue}
step={step}
className={cn('flex flex-col space-y-2', className)}
>
{showLabel && <Numeric.Label showValue>{label}</Numeric.Label>}
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
);
};
export default RowDoubleRange;

View File

@ -0,0 +1,65 @@
import React from 'react';
import Numeric from '../Numeric';
import { cn } from '../../lib/utils';
interface RowInputRangeProps {
value: number;
onChange: (newValue: number) => void;
minValue?: number;
maxValue?: number;
step?: number;
label?: string;
showLabel?: boolean;
labelPosition?: 'left' | 'right';
allowNumberEdit?: boolean;
showNumberInput?: boolean;
className?: string;
containerClassName?: string;
}
const RowInputRange: React.FC<RowInputRangeProps> = ({
value,
onChange,
minValue = 0,
maxValue = 100,
step = 1,
label = '',
showLabel = false,
labelPosition = 'right',
allowNumberEdit = false,
showNumberInput = true,
className,
containerClassName,
}) => {
const handleChange = (newValue: number | [number, number]) => {
if (typeof newValue === 'number') {
onChange(newValue);
} else {
onChange(newValue[0]);
}
};
const content = (
<Numeric.Container
mode="singleRange"
value={value}
onChange={handleChange}
min={minValue}
max={maxValue}
step={step}
className={cn('flex flex-row items-center space-x-2', className)}
>
{showLabel && label && labelPosition === 'left' && (
<Numeric.Label showValue={showNumberInput}>{label}</Numeric.Label>
)}
<Numeric.SingleRange showNumberInput={allowNumberEdit && showNumberInput} />
{showLabel && label && labelPosition === 'right' && (
<Numeric.Label showValue={showNumberInput}>{label}</Numeric.Label>
)}
</Numeric.Container>
);
return containerClassName ? <div className={containerClassName}>{content}</div> : content;
};
export default RowInputRange;

View File

@ -0,0 +1,58 @@
import React from 'react';
import { Label } from '../Label';
import { Tabs, TabsList, TabsTrigger } from '../Tabs';
import { cn } from '../../lib/utils';
interface RadioValue {
value: string;
label: string;
}
interface RadioOption {
id: string;
name: string;
value: string;
values: RadioValue[];
commands?: (val: string) => void;
}
interface RowSegmentedControlProps {
option: RadioOption;
className?: string;
}
export const RowSegmentedControl: React.FC<RowSegmentedControlProps> = ({ option, className }) => {
const handleValueChange = (newVal: string) => {
if (option.commands) {
option.commands(newVal);
}
};
return (
<div
className={cn('flex items-center justify-between text-[13px]', className)}
key={option.id}
>
<Label className="mr-2">{option.name}</Label>
<div className="max-w-1/2">
<Tabs
value={option.value}
onValueChange={handleValueChange}
>
<TabsList className="inline-flex space-x-1">
{option.values.map(({ label, value: itemValue }, index) => (
<TabsTrigger
value={itemValue}
key={`button-${option.id}-${index}`}
>
{label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
</div>
);
};
export default RowSegmentedControl;

View File

@ -0,0 +1,99 @@
import React from 'react';
import RowInputRange from './RowInputRange';
import RowSegmentedControl from './RowSegmentedControl';
import RowDoubleRange from './RowDoubleRange';
const SETTING_TYPES = {
RANGE: 'range',
RADIO: 'radio',
CUSTOM: 'custom',
DOUBLE_RANGE: 'double-range',
};
function ToolSettings({ options }) {
if (!options) {
return null;
}
if (typeof options === 'function') {
return options();
}
return (
<div className="text-foreground space-y-2 pb-4">
{options?.map(option => {
if (option.condition && option.condition?.({ options }) === false) {
return null;
}
switch (option.type) {
case SETTING_TYPES.RANGE:
return renderRangeSetting(option);
case SETTING_TYPES.RADIO:
return renderRadioSetting(option);
case SETTING_TYPES.DOUBLE_RANGE:
return renderDoubleRangeSetting(option);
case SETTING_TYPES.CUSTOM:
return renderCustomSetting(option);
default:
return null;
}
})}
</div>
);
}
const renderRangeSetting = option => {
return (
<div
className="flex items-center"
key={option.id}
>
<div className="w-1/3 text-[13px]">{option.name}</div>
<div className="w-2/3">
<RowInputRange
minValue={option.min}
maxValue={option.max}
step={option.step}
value={option.value}
onChange={value => option.commands?.(value)}
allowNumberEdit={true}
inputClassName="ml-1 w-4/5 cursor-pointer"
/>
</div>
</div>
);
};
const renderRadioSetting = option => {
return (
<RowSegmentedControl
key={option.id}
option={option}
/>
);
};
function renderDoubleRangeSetting(option) {
return (
<RowDoubleRange
key={option.id}
values={option.value}
onChange={option.commands}
minValue={option.min}
maxValue={option.max}
step={option.step}
showLabel={false}
/>
);
}
const renderCustomSetting = option => {
return (
<div key={option.id}>
{typeof option.children === 'function' ? option.children() : option.children}
</div>
);
};
export default ToolSettings;

View File

@ -0,0 +1 @@
export { default as ToolSettings } from './ToolSettings';

View File

@ -1,8 +1,6 @@
import React, { useEffect, useRef } from 'react';
import { ToolboxUI, useToolbox } from '../../';
import { useToolbar } from '@ohif/core';
import { ToolboxUI } from './';
// Migrate this file to the new UI eventually
import { useToolbox } from '@ohif/ui';
/**
* A toolbox is a collection of buttons and commands that they invoke, used to provide
@ -21,6 +19,8 @@ function Toolbox({
title,
...props
}: withAppTypes) {
// We should move these outside of the platform/ui-next, no file here
// should rely on the managers and services
const { state: toolboxState, api } = useToolbox(buttonSectionId);
const { onInteraction, toolbarButtons } = useToolbar({
servicesManager,
@ -117,6 +117,9 @@ function Toolbox({
if (items?.length) {
items.forEach(({ options, id }) => {
if (!options) {
return;
}
accumulator[id] = createEnhancedOptions(options, id);
});
} else if (options?.length) {
@ -144,17 +147,15 @@ function Toolbox({
}, []);
return (
<>
<ToolboxUI
{...props}
title={title}
toolbarButtons={toolbarButtons}
toolboxState={toolboxState}
handleToolSelect={id => api.handleToolSelect(id)}
handleToolOptionChange={handleToolOptionChange}
onInteraction={onInteraction}
/>
</>
<ToolboxUI
{...props}
title={title}
toolbarButtons={toolbarButtons}
toolboxState={toolboxState}
handleToolSelect={id => api.handleToolSelect(id)}
handleToolOptionChange={handleToolOptionChange}
onInteraction={onInteraction}
/>
);
}

View File

@ -1,9 +1,9 @@
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';
import { PanelSection } from '../../components';
import { ToolSettings } from '../OHIFToolSettings';
const ItemsPerRow = 4;
function usePrevious(value) {
@ -55,8 +55,8 @@ function ToolboxUI(props: withAppTypes) {
const render = () => {
return (
<>
<div className="flex flex-col bg-black">
<div className="bg-primary-dark mt-0.5 flex flex-wrap py-2">
<div className="flex flex-col">
<div className="bg-muted mt-0.5 flex flex-wrap space-x-2 py-2 px-1">
{toolbarButtons.map((toolDef, index) => {
if (!toolDef) {
return null;
@ -81,20 +81,16 @@ function ToolboxUI(props: withAppTypes) {
key={id}
className={classnames({
[toolClasses]: true,
'border-secondary-light flex flex-col items-center justify-center rounded-md border':
true,
})}
>
<div className="flex rounded-md bg-black">
<Component
{...componentProps}
{...props}
id={id}
servicesManager={servicesManager}
onInteraction={onInteraction}
size="toolbox"
/>
</div>
<Component
{...componentProps}
{...props}
id={id}
servicesManager={servicesManager}
onInteraction={onInteraction}
size="toolbox"
/>
</div>
);
})}

View File

@ -101,7 +101,7 @@ const Thumbnail = ({
<Tooltip>
<TooltipTrigger>
<div className="group">
<Icons.StatusTracking className="text-primary-light h-[20px] w-[20px] group-hover:hidden" />
<Icons.StatusTracking className="text-primary-light h-[15px] w-[15px] group-hover:hidden" />
<Icons.Cancel
className="text-primary-light hidden h-[15px] w-[15px] group-hover:block"
onClick={onClickUntrack}
@ -216,7 +216,7 @@ const Thumbnail = ({
<Tooltip>
<TooltipTrigger>
<div className="group">
<Icons.StatusTracking className="text-primary-light h-[20px] w-[20px] group-hover:hidden" />
<Icons.StatusTracking className="text-primary-light h-[20px] w-[15px] group-hover:hidden" />
<Icons.Cancel
className="text-primary-light hidden h-[15px] w-[15px] group-hover:block"
onClick={onClickUntrack}

View File

@ -0,0 +1,110 @@
import React from 'react';
import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip';
import { Icons } from '../Icons';
import { Button } from '../Button';
import { cn } from '../../lib/utils';
const baseClasses = '!rounded-lg inline-flex items-center justify-center';
const defaultClasses = 'bg-transparent text-foreground/80 hover:bg-background hover:text-highlight';
const activeClasses = 'bg-highlight text-background hover:!bg-highlight/80';
const disabledClasses =
'text-common-bright hover:bg-primary-dark hover:text-primary-light opacity-40 cursor-not-allowed';
const sizeClasses = {
default: {
buttonSizeClass: 'w-10 h-10',
iconSizeClass: 'h-7 w-7',
},
small: {
buttonSizeClass: 'w-8 h-8',
iconSizeClass: 'h-6 w-6',
},
};
interface ToolButtonProps {
id: string;
icon?: string;
label?: string;
tooltip?: string;
size?: 'default' | 'small';
isActive?: boolean;
disabled?: boolean;
disabledText?: string;
commands?: Record<string, unknown>;
onInteraction?: (details: { itemId: string; commands?: Record<string, unknown> }) => void;
className?: string;
}
function ToolButton(props: ToolButtonProps) {
const {
id,
icon = 'MissingIcon',
label,
tooltip,
size = 'default',
disabled = false,
isActive = false,
disabledText,
commands,
onInteraction,
className,
} = props;
const { buttonSizeClass, iconSizeClass } = sizeClasses[size];
const buttonClasses = cn(
baseClasses,
buttonSizeClass,
disabled ? disabledClasses : isActive ? activeClasses : defaultClasses,
className
);
const defaultTooltip = tooltip || label;
const disabledTooltip = disabled && disabledText ? disabledText : null;
const hasTooltip = defaultTooltip || disabledTooltip;
return (
<Tooltip>
<TooltipTrigger
asChild
className={cn(disabled && 'cursor-not-allowed')}
>
{/* TooltipTrigger is a span since a disabled button does not fire events and the tooltip
will not show. */}
<span
data-cy={id}
data-tool={id}
data-active={isActive}
>
<Button
className={buttonClasses}
onClick={() => {
if (!disabled) {
onInteraction?.({ itemId: id, commands });
}
}}
variant="ghost"
size="icon"
aria-label={hasTooltip ? defaultTooltip : undefined}
disabled={disabled}
>
<Icons.ByName
name={icon}
className={iconSizeClass}
/>
</Button>
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
{hasTooltip && (
<>
<div>{defaultTooltip}</div>
{disabledTooltip && <div className="text-muted-foreground">{disabledTooltip}</div>}
</>
)}
</TooltipContent>
</Tooltip>
);
}
export default ToolButton;

View File

@ -0,0 +1,207 @@
import React from 'react';
import { Button } from '../Button';
import { Icons } from '../Icons';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../DropdownMenu';
import { cn } from '../../lib/utils';
import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip';
/**
* ToolButtonList Component
* Root component that wraps the default and dropdown sections
* -----------------------------------------------
*/
interface ToolButtonListProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
const ToolButtonList = React.forwardRef<HTMLDivElement, ToolButtonListProps>(
({ className, children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn('flex items-center', className)}
{...props}
>
{children}
</div>
);
}
);
ToolButtonList.displayName = 'ToolButtonList';
/**
* ToolButtonListDefault Component
* Container for the default/primary tool button
* -----------------------------------------------
*/
interface ToolButtonListDefaultProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
tooltip?: string;
disabledText?: string;
disabled?: boolean;
}
const ToolButtonListDefault = React.forwardRef<HTMLDivElement, ToolButtonListDefaultProps>(
({ className, children, tooltip, disabledText, disabled, ...props }, ref) => {
const hasTooltip = tooltip || disabledText;
const defaultContent = (
<div
ref={ref}
className={cn('flex items-center', className)}
{...props}
>
{children}
</div>
);
if (!hasTooltip) {
return defaultContent;
}
return (
<Tooltip>
<TooltipTrigger asChild>
<span>{defaultContent}</span>
</TooltipTrigger>
<TooltipContent side="bottom">
{tooltip && <div>{tooltip}</div>}
{disabledText && disabled && <div className="text-muted-foreground">{disabledText}</div>}
</TooltipContent>
</Tooltip>
);
}
);
ToolButtonListDefault.displayName = 'ToolButtonListDefault';
/**
* ToolButtonListDropDown Component
* Container for the dropdown section with trigger and content
* -----------------------------------------------
*/
interface ToolButtonListDropDownProps {
children: React.ReactNode;
className?: string;
}
const ToolButtonListDropDown = React.forwardRef<HTMLDivElement, ToolButtonListDropDownProps>(
({ children, className, ...props }, ref) => (
<DropdownMenu {...props}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
'text-foreground/80 hover:bg-background hover:text-highlight border-primary',
'inline-flex h-10 w-5 items-center justify-center',
'!rounded-tr-lg !rounded-br-lg !rounded-tl-none !rounded-bl-none',
'bg-transparent',
className
)}
>
<Icons.ByName
name="chevron-down"
className="text-primary h-5 w-5"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
ref={ref}
side="bottom"
align="start"
alignOffset={-40}
>
{children}
</DropdownMenuContent>
</DropdownMenu>
)
);
ToolButtonListDropDown.displayName = 'ToolButtonListDropDown';
/**
* ToolButtonListItem Component
* Individual item in the dropdown menu
* -----------------------------------------------
*/
interface ToolButtonListItemProps extends React.ComponentProps<typeof DropdownMenuItem> {
icon?: string;
children?: React.ReactNode;
className?: string;
disabledText?: string;
tooltip?: string;
}
const ToolButtonListItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuItem>,
ToolButtonListItemProps
>(({ className, children, icon, disabledText, tooltip, disabled, ...props }, ref) => {
const defaultTooltip = tooltip || (typeof children === 'string' ? children : undefined);
const hasTooltip = defaultTooltip || disabledText;
const menuItem = (
<DropdownMenuItem
ref={ref}
className={cn('flex items-center space-x-2', className)}
disabled={disabled}
{...props}
>
{icon && (
<Icons.ByName
name={icon || 'MissingIcon'}
className="h-6 w-6"
/>
)}
{children}
</DropdownMenuItem>
);
// Todo: there is a weird issue where i can't control the duration of the delay
// for the items in this list, causing the tooltip to show up too early in the
// dropdown menu. So i'm just removing the tooltip for list items unless the disabledText is set.
if (!disabled) {
return menuItem;
}
return (
<Tooltip>
<TooltipTrigger asChild>
<span>{menuItem}</span>
</TooltipTrigger>
<TooltipContent side="bottom">
{defaultTooltip && <div>{defaultTooltip}</div>}
{disabledText && disabled && <div className="text-muted-foreground">{disabledText}</div>}
</TooltipContent>
</Tooltip>
);
});
ToolButtonListItem.displayName = 'ToolButtonListItem';
/**
* ToolButtonListDivider Component
* Divider between items in the dropdown menu
* -----------------------------------------------
*/
const ToolButtonListDivider = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('bg-primary h-5 w-px self-center', className)}
{...props}
/>
));
ToolButtonListDivider.displayName = 'ToolButtonListDivider';
export {
ToolButtonList,
ToolButtonListDefault,
ToolButtonListDropDown,
ToolButtonListItem,
ToolButtonListDivider,
};

View File

@ -0,0 +1,8 @@
export { default as ToolButton } from './ToolButton';
export {
ToolButtonList,
ToolButtonListDefault,
ToolButtonListDropDown,
ToolButtonListItem,
ToolButtonListDivider,
} from './ToolButtonList';

View File

@ -52,7 +52,9 @@ import { ThumbnailList } from './ThumbnailList';
import { PanelSection } from './PanelSection';
import { DisplaySetMessageListTooltip } from './DisplaySetMessageListTooltip';
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip';
import { Toolbox, ToolboxUI } from './Toolbox';
import { ToolboxUI, Toolbox } from './OHIFToolbox';
import Numeric from './Numeric';
import {
DropdownMenu,
DropdownMenuTrigger,
@ -91,8 +93,17 @@ import {
ViewportOverlay,
ViewportGrid,
} from './Viewport';
import {
ToolButton,
ToolButtonList,
ToolButtonListDefault,
ToolButtonListDropDown,
ToolButtonListItem,
ToolButtonListDivider,
} from './ToolButton';
export {
Numeric,
ErrorBoundary,
Button,
buttonVariants,
@ -157,8 +168,8 @@ export {
ThumbnailList,
PanelSection,
DisplaySetMessageListTooltip,
Toolbox,
ToolboxUI,
Toolbox,
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
@ -208,4 +219,10 @@ export {
ViewportOverlay,
ViewportGrid,
Clipboard,
ToolButton,
ToolButtonList,
ToolButtonListDefault,
ToolButtonListDropDown,
ToolButtonListItem,
ToolButtonListDivider,
};

View File

@ -0,0 +1,109 @@
import React, { createContext, useContext, useReducer } from 'react';
export const initialState = {};
export const toolboxReducer = (state, action) => {
const { toolbarSectionId } = action.payload;
if (!state[toolbarSectionId]) {
state[toolbarSectionId] = { activeTool: null, toolOptions: {}, selectedEvent: false };
}
switch (action.type) {
case 'SET_ACTIVE_TOOL':
return {
...state,
[toolbarSectionId]: {
...state[toolbarSectionId],
activeTool: action.payload.activeTool,
selectedEvent: true,
},
};
case 'UPDATE_TOOL_OPTION':
const { toolName, optionName, value } = action.payload;
return {
...state,
[toolbarSectionId]: {
...state[toolbarSectionId],
selectedEvent: false,
toolOptions: {
...state[toolbarSectionId].toolOptions,
[toolName]: state[toolbarSectionId].toolOptions[toolName].map(option =>
option.id === optionName ? { ...option, value } : option
),
},
},
};
case 'INITIALIZE_TOOL_OPTIONS':
// Initialize tool options for each toolbarSectionId
return {
...state,
selectedEvent: false,
[action.toolbarSectionId]: {
...state[action.toolbarSectionId],
toolOptions: action.payload,
},
};
default:
return state;
}
};
const ToolboxContext = createContext();
export const ToolboxProvider = ({ children }) => {
const [state, dispatch] = useReducer(toolboxReducer, initialState);
const handleToolSelect = (toolbarSectionId, toolName) => {
dispatch({
type: 'SET_ACTIVE_TOOL',
payload: { toolbarSectionId, activeTool: toolName },
});
};
const handleToolOptionChange = (toolbarSectionId, toolName, optionName, newValue) => {
dispatch({
type: 'UPDATE_TOOL_OPTION',
payload: { toolbarSectionId, toolName, optionName, value: newValue },
});
};
const initializeToolOptions = (toolbarSectionId, toolOptions) => {
dispatch({
type: 'INITIALIZE_TOOL_OPTIONS',
toolbarSectionId,
payload: toolOptions,
});
};
const api = { handleToolSelect, handleToolOptionChange, initializeToolOptions };
const value = { state, api };
return <ToolboxContext.Provider value={value}>{children}</ToolboxContext.Provider>;
};
/**
* Custom hook for accessing toolbox state and actions for a specific toolbar section.
* You can use this hook to access the state and actions for a specific toolbar section (
* defined by the toolbarSectionId) in your custom toolbar components. This hook
* helps to manage the state and actions for the tools and their options in the toolbar.
*/
export const useToolbox = toolbarSectionId => {
const context = useContext(ToolboxContext);
if (context === undefined) {
throw new Error('useToolbox must be used within a ToolboxProvider');
}
const { state, api } = context;
return {
state: state[toolbarSectionId] || { activeTool: null, toolOptions: {} },
api: {
handleToolSelect: toolName => api.handleToolSelect(toolbarSectionId, toolName),
handleToolOptionChange: (toolName, optionName, value) =>
api.handleToolOptionChange(toolbarSectionId, toolName, optionName, value),
initializeToolOptions: toolOptions =>
api.initializeToolOptions(toolbarSectionId, toolOptions),
},
};
};

View File

@ -1,5 +1,7 @@
import NotificationProvider, { useNotification } from './NotificationProvider';
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './ViewportGridProvider';
import { ToolboxProvider, useToolbox } from './ToolboxContext';
export { useNotification, NotificationProvider };
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
export { ToolboxProvider, useToolbox };

View File

@ -32,7 +32,6 @@ import {
ThumbnailList,
PanelSection,
DisplaySetMessageListTooltip,
Toolbox,
ToolboxUI,
DoubleSlider,
Label,
@ -93,11 +92,24 @@ import {
ViewportActionCornersLocations,
ViewportOverlay,
ViewportGrid,
ToolButton,
ToolButtonList,
ToolButtonListDefault,
ToolButtonListDropDown,
ToolButtonListItem,
ToolButtonListDivider,
Toolbox,
} from './components';
import { DataRow } from './components/DataRow';
import { useNotification, NotificationProvider } from './contextProviders';
import {
useNotification,
NotificationProvider,
useToolbox,
ToolboxProvider,
} from './contextProviders';
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './contextProviders';
import * as utils from './utils';
export {
ErrorBoundary,
@ -143,7 +155,6 @@ export {
ThumbnailList,
PanelSection,
DisplaySetMessageListTooltip,
Toolbox,
ToolboxUI,
Label,
Slider,
@ -202,4 +213,14 @@ export {
ViewportActionCornersLocations,
ViewportOverlay,
ViewportGrid,
ToolButton,
ToolButtonList,
ToolButtonListDefault,
ToolButtonListDropDown,
ToolButtonListItem,
ToolButtonListDivider,
ToolboxProvider,
Toolbox,
useToolbox,
utils,
};

View File

@ -0,0 +1,7 @@
const getToggledClassName = isToggled => {
return isToggled
? '!text-primary-active'
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
};
export { getToggledClassName };

View File

@ -0,0 +1,3 @@
import { getToggledClassName } from './getToggledClassName';
export { getToggledClassName };

View File

@ -31,7 +31,7 @@ const classes = {
isActive
? isExpanded
? 'border-primary-dark !bg-primary-dark hover:border-primary-dark !text-primary-light'
: 'border-primary-light bg-primary-light rounded-md'
: 'border-primary-light bg-primary-light !text-black rounded-md'
: `focus:!text-black focus:!rounded-md focus:!border-primary-light focus:!bg-primary-light ${isExpanded ? 'border-primary-dark bg-primary-dark !text-primary-light' : 'border-secondary-dark bg-secondary-dark group-hover/button:border-primary-dark group-hover/button:text-primary-light hover:!bg-primary-dark hover:border-primary-dark focus:!text-black'}`
),
Secondary: ({ isExpanded, primary }) =>

View File

@ -76,6 +76,7 @@ ToolbarButton.propTypes = {
commands: PropTypes.oneOfType([PropTypes.array, PropTypes.object, PropTypes.string]),
onInteraction: PropTypes.func,
icon: PropTypes.string.isRequired,
disabledText: PropTypes.string,
label: PropTypes.string.isRequired,
/** Tooltip content can be replaced for a customized content by passing a node to this value. */
dropdownContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),

View File

@ -31,7 +31,7 @@ function Toolbox({
useEffect(() => {
const currentButtonIdsStr = JSON.stringify(
toolbarButtons.map(button => {
toolbarButtons?.map(button => {
const { id, componentProps } = button;
if (componentProps.items?.length) {
return componentProps.items.map(item => `${item.id}-${item.disabled}`);

View File

@ -55,7 +55,7 @@ function ToolboxUI(props: withAppTypes) {
<>
<div className="flex flex-col bg-black">
<div className="bg-primary-dark mt-0.5 flex flex-wrap py-2">
{toolbarButtons.map((toolDef, index) => {
{toolbarButtons?.map((toolDef, index) => {
if (!toolDef) {
return null;
}

View File

@ -1,52 +0,0 @@
export function toolboxReducer(state, action) {
let newState = { ...state };
const { payload } = action;
switch (action.type) {
case 'SET_ACTIVE_TOOL':
newState = {
...state,
activeTool: payload.activeTool,
};
break;
case 'UPDATE_TOOL_OPTION':
newState = {
...state,
toolOptions: {
...state.toolOptions,
[payload.toolName]: state.toolOptions[payload.toolName].map(option =>
option.id === payload.optionName ? { ...option, value: payload.value } : option
),
},
};
break;
case 'INITIALIZE_TOOL_OPTIONS':
// eslint-disable-next-line no-case-declarations
const newToolOptions = Object.keys(payload?.toolOptions || {}).reduce((acc, toolId) => {
const tool = payload.toolOptions[toolId];
if (state.toolOptions[toolId]) {
// Preserve existing options, potentially merging with new ones if necessary
acc[toolId] = state.toolOptions[toolId].map(existingOption => {
const initialOption = tool.find(option => option.id === existingOption.id);
return initialOption
? { ...initialOption, value: existingOption.value }
: existingOption;
});
} else {
acc[toolId] = tool;
}
return acc;
}, {});
newState = {
...state,
toolOptions: newToolOptions,
};
break;
default:
break;
}
return newState;
}

View File

@ -38,7 +38,7 @@ export default defineConfig({
//},
],
webServer: {
command: 'yarn test:e2e:serve',
command: 'cross-env APP_CONFIG=config/e2e.js yarn start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 360_000,

View File

@ -9,7 +9,7 @@ import {
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

View File

@ -9,7 +9,7 @@ import {
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

View File

@ -9,7 +9,7 @@ import {
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

View File

@ -9,7 +9,7 @@ import {
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

View File

@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

View File

@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

View File

@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

View File

@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

View File

@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

View File

@ -40,7 +40,7 @@ const increaseSlabThickness = async (page: Page, id: string, lineNumber: number,
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
await initilizeMousePositionTracker(page);
});

View File

@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

View File

@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

View File

@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '2.16.840.1.114362.1.11972228.22789312658.616067305.306.2';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

View File

@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'Basic Viewer';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});

Some files were not shown because too many files have changed in this diff Show More