diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx
index 73bb70375..c03e51be8 100644
--- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx
+++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx
@@ -13,11 +13,11 @@ import CinePlayer from '../components/CinePlayer';
import type { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
+import ViewportColorbarsContainer from '../components/ViewportColorbar';
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
import { WITH_NAVIGATION } from '../services/ViewportService/CornerstoneViewportService';
-import { useViewportActionCorners } from '../hooks';
const STACK = 'stack';
@@ -38,7 +38,6 @@ const OHIFCornerstoneViewport = React.memo(
viewportOptions,
displaySetOptions,
servicesManager,
- commandsManager,
onElementEnabled,
// eslint-disable-next-line react/prop-types
onElementDisabled,
@@ -319,16 +318,6 @@ const OHIFCornerstoneViewport = React.memo(
};
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
- // Set up the window level action menu in the viewport action corners using the custom hook
- useViewportActionCorners({
- viewportId,
- elementRef,
- displaySets,
- viewportActionCornersService,
- customizationService,
- commandsManager,
- });
-
const Notification = customizationService.getCustomization('ui.notificationComponent');
return (
@@ -357,6 +346,10 @@ const OHIFCornerstoneViewport = React.memo(
viewportId={viewportId}
servicesManager={servicesManager}
/>
+
{/* top offset of 24px to account for ViewportActionCorners. */}
diff --git a/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx b/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx
index ab60510c0..e5bafac42 100644
--- a/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx
+++ b/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx
@@ -1,31 +1,54 @@
import React from 'react';
-import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider';
-import { useSystem } from '@ohif/core';
-import { useViewportGrid } from '@ohif/ui-next';
+import {
+ useViewportActionCorners,
+ ViewportActionCorners,
+ ViewportActionCornersLocations,
+} from '@ohif/ui-next';
export type OHIFViewportActionCornersProps = {
viewportId: string;
};
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
- const { servicesManager } = useSystem();
- const [viewportActionCornersState] = useViewportActionCornersContext();
+ const [state] = useViewportActionCorners();
- const [viewportGrid] = useViewportGrid();
- const isActiveViewport = viewportGrid.activeViewportId === viewportId;
-
- const ViewportActionCorners =
- servicesManager.services.customizationService.getCustomization('ui.viewportActionCorner');
-
- if (!viewportActionCornersState[viewportId]) {
+ if (!state.viewports[viewportId]) {
return null;
}
+ const components = state.viewports[viewportId];
+
+ const renderCorner = (location: ViewportActionCornersLocations, CornerComponent) => {
+ const cornerComponents = components[location];
+ if (!cornerComponents?.length) {
+ return null;
+ }
+
+ return (
+
+ {cornerComponents
+ .filter(componentInfo => componentInfo.isVisible !== false)
+ .map(componentInfo => (
+
+ {componentInfo.component}
+
+ ))}
+
+ );
+ };
+
return (
-
+
+ {renderCorner(ViewportActionCornersLocations.topLeft, ViewportActionCorners.TopLeft)}
+ {renderCorner(ViewportActionCornersLocations.topRight, ViewportActionCorners.TopRight)}
+ {renderCorner(ViewportActionCornersLocations.bottomLeft, ViewportActionCorners.BottomLeft)}
+ {renderCorner(ViewportActionCornersLocations.bottomRight, ViewportActionCorners.BottomRight)}
+
);
}
diff --git a/extensions/cornerstone/src/components/ViewportColorbar/AdvancedColorbarWithControls.tsx b/extensions/cornerstone/src/components/ViewportColorbar/AdvancedColorbarWithControls.tsx
new file mode 100644
index 000000000..a6f2e00ac
--- /dev/null
+++ b/extensions/cornerstone/src/components/ViewportColorbar/AdvancedColorbarWithControls.tsx
@@ -0,0 +1,117 @@
+import React from 'react';
+import { Button, Icons } from '@ohif/ui-next';
+import ViewportColorbar from './ViewportColorbar';
+import {
+ ColorbarCustomization,
+ ColorbarPositionType,
+ TickPositionType,
+} from '../../types/Colorbar';
+import type { ColorMapPreset } from '../../types/Colormap';
+
+type ColorbarData = {
+ colorbar: {
+ activeColormapName: string;
+ colormaps: ColorMapPreset[];
+ volumeId?: string;
+ };
+ displaySetInstanceUID: string;
+};
+
+type AdvancedColorbarWithControlsProps = {
+ viewportId: string;
+ colorbars: ColorbarData[];
+ position: string;
+ tickPosition: string;
+ colorbarCustomization: ColorbarCustomization;
+ onClose: (displaySetInstanceUID?: string) => void;
+ viewportElementRef?: React.RefObject
;
+};
+
+/**
+ * AdvancedColorbarWithControls Component
+ * A specialized colorbar component with additional control buttons for advanced window level controls
+ * This component handles the rendering portion of the ViewportColorbarsContainer
+ */
+const AdvancedColorbarWithControls = ({
+ viewportId,
+ colorbars,
+ position,
+ tickPosition,
+ colorbarCustomization,
+ onClose,
+ viewportElementRef,
+}: AdvancedColorbarWithControlsProps) => {
+ const handleClose = (displaySetInstanceUID?: string) => {
+ onClose(displaySetInstanceUID);
+ };
+
+ // Get bottom position styles from customization
+ const positionStyles = colorbarCustomization?.positionStyles || {};
+ const bottomPositionStyles = positionStyles.bottom || {};
+ const heightStyle = bottomPositionStyles.height;
+
+ return (
+
+
+
+
+
+
+
+
+ {colorbars.map((colorbarInfo, index) => {
+ const { colorbar, displaySetInstanceUID } = colorbarInfo;
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+ );
+};
+
+export default AdvancedColorbarWithControls;
diff --git a/extensions/cornerstone/src/components/ViewportColorbar/ViewportColorbar.tsx b/extensions/cornerstone/src/components/ViewportColorbar/ViewportColorbar.tsx
new file mode 100644
index 000000000..58c1d9baf
--- /dev/null
+++ b/extensions/cornerstone/src/components/ViewportColorbar/ViewportColorbar.tsx
@@ -0,0 +1,126 @@
+import React, { useEffect, useRef } from 'react';
+import { utilities } from '@cornerstonejs/tools';
+import { useSystem } from '@ohif/core';
+import {
+ ColorbarPositionType,
+ TickPositionType,
+ ColorbarCustomization,
+ TickStyleType,
+ ContainerStyleType,
+} from '../../types/Colorbar';
+import { ColorbarRangeTextPosition } from '@cornerstonejs/tools/utilities/voi/colorbar/enums/ColorbarRangeTextPosition';
+
+const { ViewportColorbar: CornerstoneViewportColorbar } = utilities.voi.colorbar;
+
+type ColorbarProps = {
+ viewportId: string;
+ displaySetInstanceUID: string;
+ colormap?: any;
+ colormaps: any[];
+ activeColormapName: string;
+ volumeId?: string;
+ position: ColorbarPositionType;
+ tickPosition: TickPositionType;
+ tickStyles?: TickStyleType;
+ containerStyles?: ContainerStyleType;
+ viewportElementRef?: React.RefObject;
+};
+
+/**
+ * ViewportColorbar Component
+ * A React wrapper for the cornerstone ViewportColorbar that adds a close button
+ * positioned appropriately based on the colorbar position.
+ */
+const ViewportColorbar = ({
+ viewportId,
+ displaySetInstanceUID,
+ colormaps,
+ activeColormapName,
+ volumeId,
+ position,
+ tickPosition,
+ tickStyles,
+ viewportElementRef,
+}: ColorbarProps) => {
+ const containerRef = useRef(null);
+ const { servicesManager } = useSystem();
+ const { customizationService } = servicesManager.services;
+
+ useEffect(() => {
+ if (!containerRef.current || !colormaps || !activeColormapName) {
+ return;
+ }
+
+ const viewportElement = viewportElementRef?.current;
+
+ if (!viewportElement) {
+ return;
+ }
+
+ const colorbarCustomization = customizationService.getCustomization(
+ 'cornerstone.colorbar'
+ ) as unknown as ColorbarCustomization;
+
+ const positionTickStyles = colorbarCustomization?.positionTickStyles?.[position];
+ const csColorbar = new CornerstoneViewportColorbar({
+ id: `Colorbar-${viewportId}-${displaySetInstanceUID}`,
+ element: viewportElement,
+ container: containerRef.current,
+ colormaps: colormaps,
+ activeColormapName: activeColormapName,
+ volumeId,
+ ticks: {
+ position: tickPosition as ColorbarRangeTextPosition,
+ style: {
+ ...(colorbarCustomization?.tickStyles || {}),
+ ...(positionTickStyles?.style || {}),
+ ...(tickStyles || {}),
+ },
+ },
+ });
+
+ return () => {
+ if (csColorbar) {
+ csColorbar.destroy();
+ }
+ };
+ }, [
+ viewportId,
+ displaySetInstanceUID,
+ colormaps,
+ activeColormapName,
+ volumeId,
+ position,
+ tickPosition,
+ tickStyles,
+ viewportElementRef,
+ customizationService,
+ ]);
+
+ // Get position styles from customization service
+ const colorbarCustomization = customizationService.getCustomization(
+ 'cornerstone.colorbar'
+ ) as unknown as ColorbarCustomization;
+
+ const positionStylesFromConfig = colorbarCustomization?.positionStyles?.[position] || {};
+
+ return (
+
+ );
+};
+
+export default ViewportColorbar;
diff --git a/extensions/cornerstone/src/components/ViewportColorbar/ViewportColorbarsContainer.tsx b/extensions/cornerstone/src/components/ViewportColorbar/ViewportColorbarsContainer.tsx
new file mode 100644
index 000000000..953c47fbb
--- /dev/null
+++ b/extensions/cornerstone/src/components/ViewportColorbar/ViewportColorbarsContainer.tsx
@@ -0,0 +1,140 @@
+import React, { useEffect, useState } from 'react';
+import { useSystem } from '@ohif/core';
+import AdvancedColorbarWithControls from './AdvancedColorbarWithControls';
+import { ColorbarCustomization } from '../../types/Colorbar';
+import type { ColorMapPreset } from '../../types/Colormap';
+import ViewportColorbar from './ViewportColorbar';
+import { deepMerge } from '@cornerstonejs/core/utilities';
+
+type ViewportColorbarsContainerProps = {
+ viewportId: string;
+ viewportElementRef?: React.RefObject;
+};
+
+type ColorbarData = {
+ colorbar: {
+ activeColormapName: string;
+ colormaps: ColorMapPreset[];
+ volumeId?: string;
+ };
+ displaySetInstanceUID: string;
+};
+
+/**
+ * Container component that manages multiple colorbars for a viewport
+ * It interacts with the colorbarService to get/set colorbar states
+ */
+const ViewportColorbarsContainer = ({
+ viewportId,
+ viewportElementRef,
+}: ViewportColorbarsContainerProps) => {
+ const [colorbars, setColorbars] = useState([]);
+ const { servicesManager } = useSystem();
+ const { colorbarService, customizationService } = servicesManager.services;
+
+ useEffect(() => {
+ setColorbars(colorbarService.getViewportColorbar(viewportId) || []);
+ }, [viewportId, colorbarService]);
+
+ useEffect(() => {
+ const { unsubscribe } = colorbarService.subscribe(
+ colorbarService.EVENTS.STATE_CHANGED,
+ (event: { viewportId: string; displaySetInstanceUID?: string; changeType: string }) => {
+ if (event.viewportId === viewportId) {
+ setColorbars(colorbarService.getViewportColorbar(viewportId) || []);
+ }
+ }
+ );
+
+ return () => {
+ unsubscribe();
+ };
+ }, [viewportId, colorbarService]);
+
+ const handleClose = (displaySetInstanceUID?: string): void => {
+ if (displaySetInstanceUID) {
+ colorbarService.removeColorbar(viewportId, displaySetInstanceUID);
+ } else {
+ colorbarService.removeColorbar(viewportId);
+ }
+ };
+
+ if (!colorbars.length) {
+ return null;
+ }
+
+ const colorbarCustomization = customizationService.getCustomization(
+ 'cornerstone.colorbar'
+ ) as unknown as ColorbarCustomization;
+
+ const defaultPosition = colorbarCustomization?.colorbarContainerPosition;
+ const defaultTickPosition = colorbarCustomization?.colorbarTickPosition;
+
+ const position = colorbarCustomization?.colorbarContainerPosition || defaultPosition;
+ const tickPosition = colorbarCustomization?.colorbarTickPosition || defaultTickPosition;
+ const positionStyles = colorbarCustomization?.positionStyles;
+
+ const positionStyle = positionStyles?.[position];
+
+ const defaultPositionStyle =
+ position !== 'bottom'
+ ? {
+ width: '20px',
+ height: colorbars.length === 1 ? '55%' : '75%',
+ top: '50%',
+ transform: 'translateY(-50%)',
+ }
+ : {
+ width: '100%',
+ height: '20px',
+ };
+
+ const finalPositionStyle = deepMerge(defaultPositionStyle, positionStyle);
+
+ return (
+
+ {position !== 'bottom' ? (
+
+ {colorbars.map((colorbarInfo, index) => {
+ const { colorbar, displaySetInstanceUID } = colorbarInfo;
+ return (
+
+ );
+ })}
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default ViewportColorbarsContainer;
diff --git a/extensions/cornerstone/src/components/ViewportColorbar/index.ts b/extensions/cornerstone/src/components/ViewportColorbar/index.ts
new file mode 100644
index 000000000..ae84d89bf
--- /dev/null
+++ b/extensions/cornerstone/src/components/ViewportColorbar/index.ts
@@ -0,0 +1,6 @@
+import ViewportColorbar from './ViewportColorbar';
+import ViewportColorbarsContainer from './ViewportColorbarsContainer';
+import AdvancedColorbarWithControls from './AdvancedColorbarWithControls';
+
+export { ViewportColorbar, ViewportColorbarsContainer, AdvancedColorbarWithControls };
+export default ViewportColorbarsContainer;
diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenuWrapper.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenuWrapper.tsx
index efa155203..a2087774c 100644
--- a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenuWrapper.tsx
+++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenuWrapper.tsx
@@ -7,10 +7,11 @@ import {
PopoverContent,
PopoverTrigger,
useViewportGrid,
+ useViewportActionCorners,
} from '@ohif/ui-next';
import ViewportDataOverlayMenu from './ViewportDataOverlayMenu';
import classNames from 'classnames';
-import { useSystem } from '@ohif/core';
+import { MENU_IDS } from '../menus/menu-ids';
export function ViewportDataOverlayMenuWrapper({
viewportId,
@@ -19,15 +20,39 @@ export function ViewportDataOverlayMenuWrapper({
}: withAppTypes<{
viewportId: string;
element: HTMLElement;
+ location: string;
}>): ReactNode {
- const { servicesManager } = useSystem();
- const { viewportActionCornersService } = servicesManager.services;
const [viewportGrid] = useViewportGrid();
+ const [actionCornerState, viewportActionCornersAPI] = useViewportActionCorners();
- const { align, side } = viewportActionCornersService.getAlignAndSide(location);
+ const isMenuOpen =
+ actionCornerState.viewports[viewportId]?.[location]?.find(
+ item => item.id === MENU_IDS.DATA_OVERLAY_MENU
+ )?.isOpen ?? false;
+
+ const handleOpenChange = (openState: boolean) => {
+ if (openState) {
+ viewportActionCornersAPI.openItem?.(viewportId, MENU_IDS.DATA_OVERLAY_MENU);
+ } else {
+ viewportActionCornersAPI.closeItem?.(viewportId, MENU_IDS.DATA_OVERLAY_MENU);
+ }
+ };
+
+ // Get proper alignment and side based on the location
+ let align = 'center';
+ let side = 'bottom';
+
+ if (location !== undefined) {
+ const positioning = viewportActionCornersAPI.getAlignAndSide(location);
+ align = positioning.align;
+ side = positioning.side;
+ }
return (
-
+
): ReactNode {
return ;
diff --git a/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx b/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx
index 0ea4160f7..1abe593b5 100644
--- a/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx
+++ b/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Icons, useViewportGrid } from '@ohif/ui-next';
+import { Icons, useViewportActionCorners, useViewportGrid } from '@ohif/ui-next';
import { useSystem } from '@ohif/core';
import { Enums } from '@cornerstonejs/core';
import {
@@ -10,14 +10,21 @@ import {
DropdownMenuLabel,
Button,
} from '@ohif/ui-next';
+import { MENU_IDS } from '../menus/menu-ids';
function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string }>) {
const { servicesManager, commandsManager } = useSystem();
const [viewportGridState, viewportGridService] = useViewportGrid();
+ const [actionCornerState, viewportActionCornersServiceAPI] = useViewportActionCorners();
const { cornerstoneViewportService, displaySetService } = servicesManager.services;
const viewportId = viewportGridState.activeViewportId;
+ const isMenuOpen =
+ actionCornerState.viewports[viewportId]?.[location]?.find(
+ item => item.id === MENU_IDS.ORIENTATION_MENU
+ )?.isOpen ?? false;
+
const handleOrientationChange = (orientation: string) => {
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const currentViewportType = viewportInfo?.getViewportType();
@@ -80,8 +87,19 @@ function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string
orientation: orientationEnum,
});
}
+
+ // Close the menu after selection
+ viewportActionCornersServiceAPI.closeItem?.(viewportId, MENU_IDS.ORIENTATION_MENU);
};
- const { viewportActionCornersService } = servicesManager.services;
+
+ const handleOpenChange = (openState: boolean) => {
+ if (openState) {
+ viewportActionCornersServiceAPI.openItem?.(viewportId, MENU_IDS.ORIENTATION_MENU);
+ } else {
+ viewportActionCornersServiceAPI.closeItem?.(viewportId, MENU_IDS.ORIENTATION_MENU);
+ }
+ };
+
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
const displaySets = displaySetUIDs
.map(uid => displaySetService.getDisplaySetByUID(uid))
@@ -98,13 +116,16 @@ function ViewportOrientationMenu({ location }: withAppTypes<{ location?: string
let side = 'bottom';
if (location !== undefined) {
- const positioning = viewportActionCornersService.getAlignAndSide(location);
+ const positioning = viewportActionCornersServiceAPI.getAlignAndSide(location);
align = positioning.align;
side = positioning.side;
}
return (
-
+
@@ -425,7 +433,7 @@ function ViewerViewportGrid(props: withAppTypes) {
}
return viewportPanes;
- }, [viewports, activeViewportId, viewportComponents, dataSource]);
+ }, [viewports, activeViewportId, viewportComponents, dataSource, initializeViewportCorners]);
/**
* Loading indicator until numCols and numRows are gotten from the HangingProtocolService
diff --git a/platform/app/src/hooks/index.js b/platform/app/src/hooks/index.js
index b2e97562e..44163c188 100644
--- a/platform/app/src/hooks/index.js
+++ b/platform/app/src/hooks/index.js
@@ -1,4 +1,5 @@
-import useDebounce from './useDebounce.js';
+import useDebounce from './useDebounce';
import useSearchParams from './useSearchParams';
+import useViewportActionCornersWithGrid from './useViewportActionCornersWithGrid';
-export { useDebounce, useSearchParams };
+export { useDebounce, useSearchParams, useViewportActionCornersWithGrid };
diff --git a/platform/app/src/hooks/index.ts b/platform/app/src/hooks/index.ts
new file mode 100644
index 000000000..2eb9011e0
--- /dev/null
+++ b/platform/app/src/hooks/index.ts
@@ -0,0 +1,5 @@
+import useViewportActionCornersWithGrid from './useViewportActionCornersWithGrid';
+import useDebounce from './useDebounce';
+import useSearchParams from './useSearchParams';
+
+export { useViewportActionCornersWithGrid, useDebounce, useSearchParams };
diff --git a/platform/app/src/hooks/useViewportActionCornersWithGrid.ts b/platform/app/src/hooks/useViewportActionCornersWithGrid.ts
new file mode 100644
index 000000000..d0f75092c
--- /dev/null
+++ b/platform/app/src/hooks/useViewportActionCornersWithGrid.ts
@@ -0,0 +1,112 @@
+import { useCallback, MutableRefObject, useRef } from 'react';
+import { useViewportActionCorners, ViewportActionCornersLocations } from '@ohif/ui-next';
+import { useSystem } from '@ohif/core';
+
+/**
+ * Hook that manages viewport action corner components for all viewports in the grid
+ * @returns A function that can be called to initialize action corners for a viewport
+ */
+export default function useViewportActionCornersWithGrid() {
+ const [, api] = useViewportActionCorners();
+ const { servicesManager } = useSystem();
+ const { customizationService } = servicesManager.services;
+
+ // Keep a ref to track processed viewports to avoid duplicates
+ const processedViewports = useRef>(new Set());
+
+ // Map of customization keys to their corresponding enum values
+ const locationMap = {
+ 'viewportActionMenu.topLeft': ViewportActionCornersLocations.topLeft,
+ 'viewportActionMenu.topRight': ViewportActionCornersLocations.topRight,
+ 'viewportActionMenu.bottomLeft': ViewportActionCornersLocations.bottomLeft,
+ 'viewportActionMenu.bottomRight': ViewportActionCornersLocations.bottomRight,
+ };
+
+ // Function to process customizations for a viewport
+ const initializeViewportCorners = useCallback(
+ (
+ viewportId: string,
+ elementRef: MutableRefObject,
+ displaySets: any[],
+ commandsManager: any
+ ) => {
+ if (!viewportId || !elementRef?.current) {
+ return;
+ }
+
+ // Prevent duplicate processing
+ if (processedViewports.current.has(viewportId)) {
+ return;
+ }
+
+ // Mark this viewport as processed
+ processedViewports.current.add(viewportId);
+
+ // Clear any existing components for this viewport
+ api.clear(viewportId);
+
+ // Process each location
+ Object.entries(locationMap).forEach(([locationKey, locationValue]) => {
+ const items = customizationService.getCustomization(locationKey);
+ if (!items || !items.length) {
+ return;
+ }
+
+ items.forEach(item => {
+ try {
+ if (typeof item.component === 'function') {
+ // Use the component renderer provided directly in the item
+ const component = item.component({
+ viewportId,
+ element: elementRef.current,
+ displaySets,
+ location: locationValue,
+ commandsManager,
+ });
+
+ if (component) {
+ api.addComponent({
+ viewportId,
+ id: item.id,
+ component,
+ location: locationValue,
+ indexPriority: item.indexPriority,
+ });
+ }
+ } else if (item.component) {
+ // Handle static components
+ api.addComponent({
+ viewportId,
+ id: item.id,
+ component: item.component,
+ location: locationValue,
+ indexPriority: item.indexPriority,
+ });
+ }
+ } catch (error) {
+ console.error(`Error adding component ${item.id} to viewport corner:`, error);
+ }
+ });
+ });
+ },
+ [api, customizationService]
+ );
+
+ // Cleanup function for unmounting viewports
+ const cleanupViewportCorners = useCallback(
+ (viewportId: string) => {
+ if (!viewportId) {
+ return;
+ }
+
+ // Remove from processed set
+ processedViewports.current.delete(viewportId);
+
+ // Clear from the store
+ api.clear(viewportId);
+ },
+ [api]
+ );
+
+ return { initializeViewportCorners, cleanupViewportCorners };
+}
diff --git a/platform/app/src/routes/Mode/Mode.tsx b/platform/app/src/routes/Mode/Mode.tsx
index 11efb98c6..94f1a1e34 100644
--- a/platform/app/src/routes/Mode/Mode.tsx
+++ b/platform/app/src/routes/Mode/Mode.tsx
@@ -3,7 +3,7 @@ import { useParams, useLocation, useNavigate } from 'react-router';
import PropTypes from 'prop-types';
import { utils } from '@ohif/core';
import { ImageViewerProvider, DragAndDropProvider } from '@ohif/ui-next';
-import { useSearchParams } from '@hooks';
+import { useSearchParams } from '../../hooks';
import { useAppConfig } from '@state';
import ViewportGrid from '@components/ViewportGrid';
import Compose from './Compose';
diff --git a/platform/app/src/routes/WorkList/WorkList.tsx b/platform/app/src/routes/WorkList/WorkList.tsx
index a37027595..0ea85fa38 100644
--- a/platform/app/src/routes/WorkList/WorkList.tsx
+++ b/platform/app/src/routes/WorkList/WorkList.tsx
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
//
import filtersMeta from './filtersMeta.js';
import { useAppConfig } from '@state';
-import { useDebounce, useSearchParams } from '@hooks';
+import { useDebounce, useSearchParams } from '../../hooks';
import { utils, Types as coreTypes } from '@ohif/core';
import {
diff --git a/platform/core/src/types/DisplaySet.ts b/platform/core/src/types/DisplaySet.ts
index 4bda89ac5..2c410d8ab 100644
--- a/platform/core/src/types/DisplaySet.ts
+++ b/platform/core/src/types/DisplaySet.ts
@@ -16,6 +16,8 @@ export type DisplaySet = {
label?: string;
/** Flag indicating if this is an overlay display set (e.g., SEG, RTSTRUCT) */
isOverlayDisplaySet?: boolean;
+ /** flag indicating if it supports window level */
+ supportsWindowLevel?: boolean;
// Details about how to display:
/**
diff --git a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx
index 4b70c0252..1bb0b3d10 100644
--- a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx
+++ b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx
@@ -201,6 +201,31 @@ window.config = {
],
`,
},
+ {
+ id: 'viewportActionMenu.windowLevelActionMenu',
+ description:
+ 'Configures the display and location of the window level action menu in the viewport.',
+ image: windowLevelActionMenu,
+ default: null,
+ configuration: `
+ window.config = {
+ // rest of window config
+ customizationService: [
+ {
+ 'viewportActionMenu.windowLevelActionMenu': {
+ $merge: {
+ location: 0, // Set the location of the menu in the viewport.
+ // 0: topLeft
+ // 1: topRight
+ // 2: bottomLeft
+ // 3: bottomRight
+ }
+ },
+ },
+ ],
+ };
+ `,
+ },
{
id: 'measurementLabels',
description: 'Labels for measurement tools in the viewer that are automatically asked for.',
@@ -937,7 +962,7 @@ window.config = {
`,
},
{
- id: 'viewportActionMenu.dataOverlay',
+ id: 'viewportActionMenu.dataOverlayMenu',
description: 'Configures the display and location of the data overlay in the viewport.',
image: segmentationOverlay,
default: null,
@@ -946,7 +971,7 @@ window.config = {
// rest of window config
customizationService: [
{
- 'viewportActionMenu.dataOverlay': {
+ 'viewportActionMenu.dataOverlayMenu': {
$merge: {
enabled: true,
location: 1, // Set the location of the overlay in the viewport.
diff --git a/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx b/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx
index 4e44953e1..79738127b 100644
--- a/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx
+++ b/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx
@@ -1,11 +1,13 @@
-import React from 'react';
+import React, {
+ createContext,
+ useContext,
+ ReactNode,
+ useState,
+ useEffect,
+ useCallback,
+} from 'react';
import classNames from 'classnames';
-import PropTypes from 'prop-types';
-/**
- * A small container that can render multiple "corner" items (like icons, status)
- * in each corner of the viewport: top-left, top-right, bottom-left, bottom-right.
- */
export enum ViewportActionCornersLocations {
topLeft,
topRight,
@@ -33,35 +35,102 @@ const locationClasses = {
),
};
-function ViewportActionCorners({ cornerComponents }) {
- if (!cornerComponents) {
- return null;
- }
+const ViewportActionCornersContext = createContext<{
+ registerCorner: (location: ViewportActionCornersLocations, children: ReactNode) => void;
+} | null>(null);
+
+function Container({ children }: { children: ReactNode }) {
+ const [corners, setCorners] = useState({
+ [ViewportActionCornersLocations.topLeft]: null,
+ [ViewportActionCornersLocations.topRight]: null,
+ [ViewportActionCornersLocations.bottomLeft]: null,
+ [ViewportActionCornersLocations.bottomRight]: null,
+ });
+
+ const registerCorner = useCallback(
+ (location: ViewportActionCornersLocations, children: ReactNode) => {
+ setCorners(prev => {
+ // Only update if the children are different to avoid unnecessary renders
+ if (prev[location] === children) {
+ return prev;
+ }
+ return {
+ ...prev,
+ [location]: children,
+ };
+ });
+ },
+ []
+ );
return (
- {
- event.preventDefault();
- event.stopPropagation();
- }}
- >
- {Object.entries(cornerComponents).map(([location, locationArray]) => (
-
- {locationArray.map(componentInfo => (
-
{componentInfo.component}
- ))}
-
- ))}
-
+
+ {children}
+ {
+ event.preventDefault();
+ event.stopPropagation();
+ }}
+ >
+ {Object.entries(corners).map(([location, children]) => {
+ if (!children) {
+ return null;
+ }
+ return (
+
+ {children}
+
+ );
+ })}
+
+
);
}
-ViewportActionCorners.propTypes = {
- cornerComponents: PropTypes.object.isRequired,
-};
+function Corner({
+ location,
+ children,
+}: {
+ location: ViewportActionCornersLocations;
+ children: ReactNode;
+}) {
+ const context = useContext(ViewportActionCornersContext);
-export { ViewportActionCorners };
+ if (!context) {
+ throw new Error('Corner component must be used within a ViewportActionCorners.Container');
+ }
+
+ useEffect(() => {
+ context.registerCorner(location, children);
+ }, [context, location, children]);
+
+ return null;
+}
+
+function TopLeft({ children }: { children: ReactNode }) {
+ return {children};
+}
+
+function TopRight({ children }: { children: ReactNode }) {
+ return {children};
+}
+
+function BottomLeft({ children }: { children: ReactNode }) {
+ return {children};
+}
+
+function BottomRight({ children }: { children: ReactNode }) {
+ return {children};
+}
+
+export const ViewportActionCorners = {
+ Container,
+ TopLeft,
+ TopRight,
+ BottomLeft,
+ BottomRight,
+};
diff --git a/platform/ui-next/src/contextProviders/ViewportActionCornersProvider.tsx b/platform/ui-next/src/contextProviders/ViewportActionCornersProvider.tsx
new file mode 100644
index 000000000..55fe7dc2d
--- /dev/null
+++ b/platform/ui-next/src/contextProviders/ViewportActionCornersProvider.tsx
@@ -0,0 +1,550 @@
+import React, {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useReducer,
+ ReactNode,
+ useMemo,
+} from 'react';
+import { ViewportActionCornersLocations } from '../components/Viewport/ViewportActionCorners';
+import { ActionComponentInfo, AlignAndSide } from '../types';
+
+const DEFAULT_STATE = {
+ viewports: {} as Record>,
+};
+
+/**
+ * API for managing viewport action corner items.
+ * These methods control the state and behavior of the corner items themselves,
+ * not the content they might affect in the viewport.
+ */
+interface ViewportActionCornersApi {
+ getState: () => typeof DEFAULT_STATE;
+ addComponent: (component: ActionComponentInfo) => void;
+ addComponents: (components: Array) => void;
+ clear: (viewportId: string) => void;
+ getAlignAndSide: (location: ViewportActionCornersLocations) => AlignAndSide;
+ lockItem: (viewportId: string, itemId: string) => void;
+ unlockItem: (viewportId: string, itemId: string) => void;
+ toggleLock: (viewportId: string, itemId: string) => void;
+ isItemLocked: (viewportId: string, itemId: string) => boolean;
+ showItem: (viewportId: string, itemId: string) => void;
+ hideItem: (viewportId: string, itemId: string) => void;
+ toggleVisibility: (viewportId: string, itemId: string) => void;
+ isItemVisible: (viewportId: string, itemId: string) => boolean;
+ openItem: (viewportId: string, itemId: string) => void;
+ closeItem: (viewportId: string, itemId: string) => void;
+ closeAllItems: (viewportId: string) => void;
+ isItemOpen: (viewportId: string, itemId: string) => boolean;
+}
+
+export const ViewportActionCornersContext = createContext<
+ [typeof DEFAULT_STATE, ViewportActionCornersApi]
+>([DEFAULT_STATE, {} as ViewportActionCornersApi]);
+
+interface ViewportActionCornersProviderProps {
+ children: ReactNode;
+ service: any;
+}
+
+export function ViewportActionCornersProvider({
+ children,
+ service,
+}: ViewportActionCornersProviderProps) {
+ const viewportActionCornersReducer = (state = DEFAULT_STATE, action) => {
+ switch (action.type) {
+ case 'ADD_COMPONENT': {
+ const { viewportId, id, component, location, indexPriority = 0 } = action.payload;
+
+ const newState = { ...state };
+
+ if (!newState.viewports[viewportId]) {
+ newState.viewports[viewportId] = {
+ [ViewportActionCornersLocations.topLeft]: [],
+ [ViewportActionCornersLocations.topRight]: [],
+ [ViewportActionCornersLocations.bottomLeft]: [],
+ [ViewportActionCornersLocations.bottomRight]: [],
+ };
+ }
+
+ if (!newState.viewports[viewportId][location]) {
+ newState.viewports[viewportId][location] = [];
+ }
+
+ const componentInfo = {
+ id,
+ component,
+ indexPriority,
+ isOpen: false,
+ isVisible: true,
+ isLocked: false,
+ };
+
+ const components = [...newState.viewports[viewportId][location]];
+ const index = components.findIndex(item => item.indexPriority > indexPriority);
+
+ if (index === -1) {
+ components.push(componentInfo);
+ } else {
+ components.splice(index, 0, componentInfo);
+ }
+
+ newState.viewports[viewportId][location] = components;
+
+ return newState;
+ }
+
+ case 'ADD_COMPONENTS': {
+ const components = action.payload;
+ let newState = { ...state };
+
+ components.forEach(component => {
+ newState = viewportActionCornersReducer(newState, {
+ type: 'ADD_COMPONENT',
+ payload: component,
+ });
+ });
+
+ return newState;
+ }
+
+ case 'CLEAR': {
+ const viewportId = action.payload;
+ const newState = { ...state };
+
+ if (newState.viewports[viewportId]) {
+ const newViewports = { ...newState.viewports };
+ delete newViewports[viewportId];
+ newState.viewports = newViewports;
+ }
+
+ return newState;
+ }
+
+ case 'SET_LOCKED': {
+ const { viewportId, itemId, lockedStatus } = action.payload;
+ const newState = { ...state };
+
+ if (!newState.viewports[viewportId]) {
+ return state;
+ }
+
+ const viewportCopy = { ...newState.viewports[viewportId] };
+
+ Object.keys(viewportCopy).forEach(locationKey => {
+ const location = Number(locationKey) as ViewportActionCornersLocations;
+ const components = [...viewportCopy[location]];
+
+ const itemIndex = components.findIndex(item => item.id === itemId);
+ if (itemIndex !== -1) {
+ const updatedItem = { ...components[itemIndex], isLocked: lockedStatus };
+ components[itemIndex] = updatedItem;
+ viewportCopy[location] = components;
+ }
+ });
+
+ newState.viewports[viewportId] = viewportCopy;
+ return newState;
+ }
+
+ case 'SET_VISIBLE': {
+ const { viewportId, itemId, visibleStatus } = action.payload;
+ const newState = { ...state };
+
+ if (!newState.viewports[viewportId]) {
+ return state;
+ }
+
+ const viewportCopy = { ...newState.viewports[viewportId] };
+
+ Object.keys(viewportCopy).forEach(locationKey => {
+ const location = Number(locationKey) as ViewportActionCornersLocations;
+ const components = [...viewportCopy[location]];
+
+ const itemIndex = components.findIndex(item => item.id === itemId);
+ if (itemIndex !== -1) {
+ const updatedItem = { ...components[itemIndex], isVisible: visibleStatus };
+ components[itemIndex] = updatedItem;
+ viewportCopy[location] = components;
+ }
+ });
+
+ newState.viewports[viewportId] = viewportCopy;
+ return newState;
+ }
+
+ case 'OPEN_ITEM': {
+ const { viewportId, itemId } = action.payload;
+ const newState = { ...state };
+
+ if (!newState.viewports[viewportId]) {
+ return state;
+ }
+
+ const viewportCopy = { ...newState.viewports[viewportId] };
+
+ // Update isOpen flag for the component with matching id in any location
+ Object.keys(viewportCopy).forEach(locationKey => {
+ const location = Number(locationKey) as ViewportActionCornersLocations;
+ const components = [...viewportCopy[location]];
+
+ const itemIndex = components.findIndex(item => item.id === itemId);
+ if (itemIndex !== -1) {
+ const updatedItem = { ...components[itemIndex], isOpen: true };
+ components[itemIndex] = updatedItem;
+ viewportCopy[location] = components;
+ }
+ });
+
+ newState.viewports[viewportId] = viewportCopy;
+ return newState;
+ }
+
+ case 'CLOSE_ITEM': {
+ const { viewportId, itemId } = action.payload;
+ const newState = { ...state };
+
+ if (!newState.viewports[viewportId]) {
+ return state;
+ }
+
+ const viewportCopy = { ...newState.viewports[viewportId] };
+
+ // Update isOpen flag for the component with matching id in any location
+ Object.keys(viewportCopy).forEach(locationKey => {
+ const location = Number(locationKey) as ViewportActionCornersLocations;
+ const components = [...viewportCopy[location]];
+
+ const itemIndex = components.findIndex(item => item.id === itemId);
+ if (itemIndex !== -1) {
+ const updatedItem = { ...components[itemIndex], isOpen: false };
+ components[itemIndex] = updatedItem;
+ viewportCopy[location] = components;
+ }
+ });
+
+ newState.viewports[viewportId] = viewportCopy;
+ return newState;
+ }
+
+ case 'CLOSE_ALL_ITEMS': {
+ const viewportId = action.payload;
+ const newState = { ...state };
+
+ if (!newState.viewports[viewportId]) {
+ return state;
+ }
+
+ const viewportCopy = { ...newState.viewports[viewportId] };
+
+ // Set isOpen to false for all components in the viewport
+ Object.keys(viewportCopy).forEach(locationKey => {
+ const location = Number(locationKey) as ViewportActionCornersLocations;
+ const components = viewportCopy[location].map(item => ({
+ ...item,
+ isOpen: false,
+ }));
+
+ viewportCopy[location] = components;
+ });
+
+ newState.viewports[viewportId] = viewportCopy;
+ return newState;
+ }
+
+ default:
+ return state;
+ }
+ };
+
+ const [state, dispatch] = useReducer(viewportActionCornersReducer, DEFAULT_STATE);
+
+ const getState = useCallback(() => {
+ return state;
+ }, [state]);
+
+ const addComponent = useCallback(
+ (component: ActionComponentInfo) => {
+ dispatch({
+ type: 'ADD_COMPONENT',
+ payload: component,
+ });
+ },
+ [dispatch]
+ );
+
+ const addComponents = useCallback(
+ (components: Array) => {
+ dispatch({
+ type: 'ADD_COMPONENTS',
+ payload: components,
+ });
+ },
+ [dispatch]
+ );
+
+ const clear = useCallback(
+ (viewportId: string) => {
+ dispatch({
+ type: 'CLEAR',
+ payload: viewportId,
+ });
+ },
+ [dispatch]
+ );
+
+ const lockItem = useCallback(
+ (viewportId: string, itemId: string) => {
+ dispatch({
+ type: 'SET_LOCKED',
+ payload: { viewportId, itemId, lockedStatus: true },
+ });
+ },
+ [dispatch]
+ );
+
+ const unlockItem = useCallback(
+ (viewportId: string, itemId: string) => {
+ dispatch({
+ type: 'SET_LOCKED',
+ payload: { viewportId, itemId, lockedStatus: false },
+ });
+ },
+ [dispatch]
+ );
+
+ const toggleLock = useCallback(
+ (viewportId: string, itemId: string) => {
+ const currentLocked = isItemLocked(viewportId, itemId);
+ dispatch({
+ type: 'SET_LOCKED',
+ payload: { viewportId, itemId, lockedStatus: !currentLocked },
+ });
+ },
+ [dispatch]
+ );
+
+ const isItemLocked = useCallback(
+ (viewportId: string, itemId: string) => {
+ if (!state.viewports[viewportId]) {
+ return false; // Default to unlocked if viewport doesn't exist
+ }
+
+ for (const locationKey in state.viewports[viewportId]) {
+ const location = Number(locationKey) as ViewportActionCornersLocations;
+ const components = state.viewports[viewportId][location];
+ const item = components.find(item => item.id === itemId);
+
+ if (item && item.isLocked === true) {
+ return true;
+ }
+ }
+
+ return false; // Default to unlocked if item not found or isLocked is undefined
+ },
+ [state.viewports]
+ );
+
+ const showItem = useCallback(
+ (viewportId: string, itemId: string) => {
+ dispatch({
+ type: 'SET_VISIBLE',
+ payload: { viewportId, itemId, visibleStatus: true },
+ });
+ },
+ [dispatch]
+ );
+
+ const hideItem = useCallback(
+ (viewportId: string, itemId: string) => {
+ dispatch({
+ type: 'SET_VISIBLE',
+ payload: { viewportId, itemId, visibleStatus: false },
+ });
+ },
+ [dispatch]
+ );
+
+ const toggleVisibility = useCallback(
+ (viewportId: string, itemId: string) => {
+ const currentVisible = isItemVisible(viewportId, itemId);
+ dispatch({
+ type: 'SET_VISIBLE',
+ payload: { viewportId, itemId, visibleStatus: !currentVisible },
+ });
+ },
+ [dispatch]
+ );
+
+ const isItemVisible = useCallback(
+ (viewportId: string, itemId: string) => {
+ if (!state.viewports[viewportId]) {
+ return true; // Default to visible if viewport doesn't exist
+ }
+
+ for (const locationKey in state.viewports[viewportId]) {
+ const location = Number(locationKey) as ViewportActionCornersLocations;
+ const components = state.viewports[viewportId][location];
+ const item = components.find(item => item.id === itemId);
+
+ if (item && item.isVisible === false) {
+ return false;
+ }
+ }
+
+ return true; // Default to visible if item not found or isVisible is undefined
+ },
+ [state.viewports]
+ );
+
+ const openItem = useCallback(
+ (viewportId: string, itemId: string) => {
+ dispatch({
+ type: 'OPEN_ITEM',
+ payload: { viewportId, itemId },
+ });
+ },
+ [dispatch]
+ );
+
+ const closeItem = useCallback(
+ (viewportId: string, itemId: string) => {
+ dispatch({
+ type: 'CLOSE_ITEM',
+ payload: { viewportId, itemId },
+ });
+ },
+ [dispatch]
+ );
+
+ const closeAllItems = useCallback(
+ (viewportId: string) => {
+ dispatch({
+ type: 'CLOSE_ALL_ITEMS',
+ payload: viewportId,
+ });
+ },
+ [dispatch]
+ );
+
+ const isItemOpen = useCallback(
+ (viewportId: string, itemId: string) => {
+ if (!state.viewports[viewportId]) {
+ return false;
+ }
+
+ for (const locationKey in state.viewports[viewportId]) {
+ const location = Number(locationKey) as ViewportActionCornersLocations;
+ const components = state.viewports[viewportId][location];
+ const item = components.find(item => item.id === itemId);
+
+ if (item && item.isOpen === true) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+ [state.viewports]
+ );
+
+ const getAlignAndSide = useCallback(
+ (location: ViewportActionCornersLocations) => {
+ return service.getAlignAndSide(location);
+ },
+ [service]
+ );
+
+ const api = useMemo(() => {
+ return {
+ getState,
+ addComponent,
+ addComponents,
+ clear,
+ getAlignAndSide,
+ lockItem,
+ unlockItem,
+ toggleLock,
+ isItemLocked,
+ showItem,
+ hideItem,
+ toggleVisibility,
+ isItemVisible,
+ openItem,
+ closeItem,
+ closeAllItems,
+ isItemOpen,
+ };
+ }, [
+ getState,
+ addComponent,
+ addComponents,
+ clear,
+ getAlignAndSide,
+ lockItem,
+ unlockItem,
+ toggleLock,
+ isItemLocked,
+ showItem,
+ hideItem,
+ toggleVisibility,
+ isItemVisible,
+ openItem,
+ closeItem,
+ closeAllItems,
+ isItemOpen,
+ ]);
+
+ useEffect(() => {
+ if (service && service.setServiceImplementation) {
+ const implementation = {
+ getState,
+ addComponent,
+ addComponents,
+ clear,
+ lockItem,
+ unlockItem,
+ toggleLock,
+ isItemLocked,
+ showItem,
+ hideItem,
+ toggleVisibility,
+ isItemVisible,
+ openItem,
+ closeItem,
+ closeAllItems,
+ isItemOpen,
+ };
+
+ service.setServiceImplementation(implementation);
+ }
+ }, [
+ service,
+ getState,
+ addComponent,
+ addComponents,
+ clear,
+ lockItem,
+ unlockItem,
+ toggleLock,
+ isItemLocked,
+ showItem,
+ hideItem,
+ toggleVisibility,
+ isItemVisible,
+ openItem,
+ closeItem,
+ closeAllItems,
+ isItemOpen,
+ ]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+// Custom hook to use the ViewportActionCorners context
+export const useViewportActionCorners = () => useContext(ViewportActionCornersContext);
diff --git a/platform/ui-next/src/contextProviders/index.ts b/platform/ui-next/src/contextProviders/index.ts
index 178f1d953..72a4726c3 100644
--- a/platform/ui-next/src/contextProviders/index.ts
+++ b/platform/ui-next/src/contextProviders/index.ts
@@ -9,6 +9,10 @@ import { UserAuthenticationProvider, useUserAuthentication } from './UserAuthent
import { ImageViewerContext, ImageViewerProvider, useImageViewer } from './ImageViewerProvider';
import DragAndDropProvider from './DragAndDropProvider';
import CineProvider, { useCine } from './CineProvider';
+import {
+ ViewportActionCornersProvider,
+ useViewportActionCorners,
+} from './ViewportActionCornersProvider';
export { useNotification, NotificationProvider };
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
@@ -20,3 +24,4 @@ export { UserAuthenticationProvider, useUserAuthentication };
export { ImageViewerContext, ImageViewerProvider, useImageViewer };
export { DragAndDropProvider };
export { CineProvider, useCine };
+export { ViewportActionCornersProvider, useViewportActionCorners };
diff --git a/platform/ui-next/src/types/ActionCorners.ts b/platform/ui-next/src/types/ActionCorners.ts
new file mode 100644
index 000000000..06a4c5b38
--- /dev/null
+++ b/platform/ui-next/src/types/ActionCorners.ts
@@ -0,0 +1,18 @@
+import { ReactNode } from 'react';
+import { ViewportActionCornersLocations } from '../components/Viewport/ViewportActionCorners';
+
+export type ActionComponentInfo = {
+ viewportId: string;
+ id: string;
+ component: ReactNode;
+ location: ViewportActionCornersLocations;
+ indexPriority?: number;
+ isLocked?: boolean;
+ isOpen?: boolean;
+ isVisible?: boolean;
+};
+
+export type AlignAndSide = {
+ align: 'start' | 'end' | 'center';
+ side: 'top' | 'bottom' | 'left' | 'right';
+};
diff --git a/platform/ui-next/src/types/index.ts b/platform/ui-next/src/types/index.ts
index 14053383d..8e18f5e7f 100644
--- a/platform/ui-next/src/types/index.ts
+++ b/platform/ui-next/src/types/index.ts
@@ -5,6 +5,7 @@ import ThumbnailType from './ThumbnailType';
export * from './Predicate';
export * from './ContextMenuItem';
export * from './ViewportActionCornersTypes';
+export * from './ActionCorners';
/**
* StringNumber often comes back from DICOMweb for integer valued items.