feat(ViewportActionMenu): window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering (#3963)
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
This commit is contained in:
1 parent
07301fc020
commit
b7f90e3951
164 files changed
+5887
-932
No files matched your search
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '@ohif/ui';
|
||||
import DividerItem from './DividerItem';
|
||||
|
||||
type BackItemProps = {
|
||||
backLabel?: string;
|
||||
onBackClick: () => void;
|
||||
};
|
||||
|
||||
const BackItem = ({ backLabel, onBackClick }: BackItemProps) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="all-in-one-menu-item all-in-one-menu-item-effects"
|
||||
onClick={onBackClick}
|
||||
>
|
||||
<Icon name="content-prev"></Icon>
|
||||
|
||||
<div className="pl-2">{backLabel || 'Back to Display Options'}</div>
|
||||
</div>
|
||||
<DividerItem></DividerItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackItem;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
const DividerItem = () => {
|
||||
return (
|
||||
<div className="flex h-3.5 shrink-0 items-center px-2">
|
||||
<div className="bg-primary-dark h-[2px] w-full"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DividerItem;
|
||||
@@ -0,0 +1,13 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
type HeaderItemProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const HeaderItem = ({ children }: HeaderItemProps) => {
|
||||
return (
|
||||
<div className="text-aqua-pale mx-2 flex h-6 shrink-0 items-center text-[11px]">{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderItem;
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import OutsideClickHandler from 'react-outside-click-handler';
|
||||
import { MenuProps } from './Menu';
|
||||
import getIcon from '../Icon/getIcon';
|
||||
import classNames from 'classnames';
|
||||
import { AllInOneMenu } from '..';
|
||||
export interface IconMenuProps extends MenuProps {
|
||||
icon: string;
|
||||
iconClassName?: string;
|
||||
horizontalDirection?: AllInOneMenu.HorizontalDirection;
|
||||
verticalDirection?: AllInOneMenu.VerticalDirection;
|
||||
menuKey?: number | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An IconMenu allows for a div wrapped icon to be clicked to show and hide
|
||||
* an AllInOneMenu.Menu. Based on the direction(s) specified, the menu is
|
||||
* positioned relative to the icon.
|
||||
*
|
||||
* HorizontalDirection.LeftToRight - the left edges of the icon and menu are aligned
|
||||
* HorizontalDirection.RightRoLeft - the right edges of the icon and menu are aligned
|
||||
* VerticalDirection.TopToBottom - the top edge of the menu appears directly below the bottom edge of the icon
|
||||
* VerticalDirection.BottomToTop - the bottom edge of the menu appears directly above the top edge of the icon
|
||||
*
|
||||
* For example, if an IconMenu were situated in the bottom-left corner of a container,
|
||||
* it would be best to use BottomToTop and LeftToRight directions for it.
|
||||
*/
|
||||
export default function IconMenu({
|
||||
icon,
|
||||
iconClassName,
|
||||
horizontalDirection,
|
||||
verticalDirection,
|
||||
children,
|
||||
backLabel,
|
||||
menuClassName,
|
||||
menuStyle,
|
||||
onVisibilityChange,
|
||||
menuKey,
|
||||
}: IconMenuProps) {
|
||||
const [isMenuVisible, setIsMenuVisible] = useState(false);
|
||||
|
||||
const toggleMenuVisibility = useCallback(() => setIsMenuVisible(isVisible => !isVisible), []);
|
||||
|
||||
return (
|
||||
<OutsideClickHandler
|
||||
onOutsideClick={toggleMenuVisibility}
|
||||
disabled={!isMenuVisible}
|
||||
>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={iconClassName}
|
||||
onClick={toggleMenuVisibility}
|
||||
>
|
||||
{getIcon(icon)}
|
||||
</div>
|
||||
<AllInOneMenu.Menu
|
||||
key={menuKey}
|
||||
isVisible={isMenuVisible}
|
||||
backLabel={backLabel}
|
||||
menuClassName={classNames(
|
||||
menuClassName,
|
||||
'absolute',
|
||||
verticalDirection === AllInOneMenu.VerticalDirection.TopToBottom
|
||||
? 'top-[100%]'
|
||||
: 'bottom-[100%]',
|
||||
horizontalDirection === AllInOneMenu.HorizontalDirection.LeftToRight
|
||||
? 'left-0'
|
||||
: 'right-0'
|
||||
)}
|
||||
menuStyle={menuStyle}
|
||||
onVisibilityChange={isVis => {
|
||||
setIsMenuVisible(isVis);
|
||||
onVisibilityChange?.(isVis);
|
||||
}}
|
||||
horizontalDirection={horizontalDirection}
|
||||
>
|
||||
{children}
|
||||
</AllInOneMenu.Menu>
|
||||
</div>
|
||||
</OutsideClickHandler>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { ReactNode, useCallback, useContext } from 'react';
|
||||
import { MenuContext } from './Menu';
|
||||
|
||||
type ItemProps = {
|
||||
label: string;
|
||||
secondaryLabel?: string;
|
||||
icon?: ReactNode;
|
||||
onClick?: () => void;
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
rightIcon?: ReactNode;
|
||||
};
|
||||
|
||||
const Item = ({
|
||||
label,
|
||||
secondaryLabel,
|
||||
icon,
|
||||
rightIcon,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}: ItemProps) => {
|
||||
const { hideMenu } = useContext(MenuContext);
|
||||
|
||||
const onClickHandler = useCallback(() => {
|
||||
hideMenu();
|
||||
onClick?.();
|
||||
}, [hideMenu, onClick]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="all-in-one-menu-item all-in-one-menu-item-effects"
|
||||
onClick={onClickHandler}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{icon && <div className="pr-2">{icon}</div>}
|
||||
<span>{label}</span>
|
||||
{secondaryLabel != null && <span className="text-aqua-pale ml-[1ch]">{secondaryLabel}</span>}
|
||||
{rightIcon && <div className="ml-auto">{rightIcon}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Item;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, { ReactNode, useContext, useEffect } from 'react';
|
||||
import { MenuContext } from './Menu';
|
||||
|
||||
type ItemPanelProps = {
|
||||
label?: string;
|
||||
index?: number;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const ItemPanel = ({ label, index = 0, children }: ItemPanelProps) => {
|
||||
const { addItemPanel, activePanelIndex } = useContext(MenuContext);
|
||||
|
||||
useEffect(() => {
|
||||
addItemPanel(index, label);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
activePanelIndex === index && (
|
||||
<div
|
||||
style={{ scrollbarGutter: 'auto' }}
|
||||
className="ohif-scrollbar flex flex-col overflow-auto"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemPanel;
|
||||
@@ -0,0 +1,188 @@
|
||||
import React, { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import './allInOneMenu.css';
|
||||
import DividerItem from './DividerItem';
|
||||
import PanelSelector from './PanelSelector';
|
||||
import classNames from 'classnames';
|
||||
import BackItem from './BackItem';
|
||||
|
||||
/**
|
||||
* The vertical direction that the menu will be opened/used with.
|
||||
*
|
||||
* A TopToBottom menu would be used for cases where the menu is opened "near"
|
||||
* the top edge of its container. Likewise a BottomToTop menu would be used
|
||||
* for cases where the menu is opened "near" the bottom edge of its container.
|
||||
*
|
||||
* See IconMenu for more information.
|
||||
*/
|
||||
export enum VerticalDirection {
|
||||
TopToBottom,
|
||||
BottomToTop,
|
||||
}
|
||||
|
||||
/**
|
||||
* The horizontal direction that the menu is opened/used with.
|
||||
* This direction dictates the general direction sub-menus and
|
||||
* back-to-menus are opened with. For example, a RightToLeft menu
|
||||
* will have sub-menu items indicated with a left pointing chevron
|
||||
* and aligned with the left edge of the menu. Similarly back-to items of a
|
||||
* RightToLeft menu are indicated with a right pointing chevron and
|
||||
* aligned with the right edge of the menu.
|
||||
*
|
||||
* It is also worth noting that a LeftToRight menu would be used for
|
||||
* cases where a menu is opened "near" the left edge of its container.
|
||||
* Likewise, a RightToLeft menu would be used for cases where a menu is opened
|
||||
* "near" the right edge of its container.
|
||||
*
|
||||
* See IconMenu for more information.
|
||||
*/
|
||||
export enum HorizontalDirection {
|
||||
LeftToRight,
|
||||
RightToLeft,
|
||||
}
|
||||
|
||||
export interface MenuProps {
|
||||
menuStyle?: unknown;
|
||||
menuClassName?: string;
|
||||
isVisible?: boolean;
|
||||
preventHideMenu?: boolean;
|
||||
backLabel?: string;
|
||||
headerComponent?: ReactNode;
|
||||
showHeaderDivider?: boolean;
|
||||
activePanelIndex?: number;
|
||||
onVisibilityChange?: (isVisible: boolean) => void;
|
||||
horizontalDirection?: HorizontalDirection;
|
||||
children: ReactNode;
|
||||
}
|
||||
type MenuContextProps = {
|
||||
showSubMenu: (subMenuProps: MenuProps) => void;
|
||||
hideMenu: () => void;
|
||||
addItemPanel: (index: number, label: string) => void;
|
||||
horizontalDirection: HorizontalDirection;
|
||||
activePanelIndex: number;
|
||||
};
|
||||
|
||||
type MenuPathState = {
|
||||
props: MenuProps;
|
||||
activePanelIndex: number;
|
||||
};
|
||||
|
||||
export const MenuContext = createContext<MenuContextProps>(null);
|
||||
|
||||
const Menu = (props: MenuProps) => {
|
||||
const {
|
||||
isVisible,
|
||||
onVisibilityChange,
|
||||
activePanelIndex,
|
||||
preventHideMenu,
|
||||
menuClassName,
|
||||
menuStyle,
|
||||
horizontalDirection = HorizontalDirection.LeftToRight,
|
||||
} = props;
|
||||
|
||||
const [isMenuVisible, setIsMenuVisible] = useState(isVisible);
|
||||
|
||||
// The menuPath is an array consisting of this top Menu and every SubMenu
|
||||
// that has been traversed/opened by the user with the last item in the array
|
||||
// being the current (sub)menu that is currently visible. This allows for the previously
|
||||
// viewed menus to be returned to via the Back button at the top of the menu.
|
||||
const [menuPath, setMenuPath] = useState<Array<MenuPathState>>([
|
||||
{ props, activePanelIndex: activePanelIndex || 0 },
|
||||
]);
|
||||
const [itemPanelLabels, setItemPanelLabels] = useState<Array<string>>([]);
|
||||
|
||||
const hideMenu = useCallback(() => {
|
||||
if (preventHideMenu) {
|
||||
return;
|
||||
}
|
||||
setMenuPath(path => [path[0]]);
|
||||
setItemPanelLabels([]);
|
||||
setIsMenuVisible(false);
|
||||
onVisibilityChange?.(false);
|
||||
}, [preventHideMenu, onVisibilityChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
setIsMenuVisible(isVisible);
|
||||
onVisibilityChange?.(isVisible);
|
||||
} else {
|
||||
hideMenu();
|
||||
}
|
||||
}, [hideMenu, isVisible, onVisibilityChange]);
|
||||
|
||||
const showSubMenu = useCallback((subMenuProps: MenuProps) => {
|
||||
setMenuPath(path => {
|
||||
return [
|
||||
...path,
|
||||
{ props: subMenuProps, activePanelIndex: subMenuProps.activePanelIndex || 0 },
|
||||
];
|
||||
});
|
||||
setItemPanelLabels([]);
|
||||
}, []);
|
||||
|
||||
const addItemPanel = useCallback((index, label) => {
|
||||
setItemPanelLabels(labels => {
|
||||
return [...labels.slice(0, index), label, ...labels.slice(index + 1, labels.length)];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onActivePanelIndexChange = useCallback(index => {
|
||||
setMenuPath(path => {
|
||||
return [
|
||||
...path.slice(0, path.length - 1),
|
||||
{ ...path[path.length - 1], activePanelIndex: index },
|
||||
];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onBackClick = useCallback(() => {
|
||||
setMenuPath(path => [...path.slice(0, path.length - 1)]);
|
||||
setItemPanelLabels([]);
|
||||
}, []);
|
||||
|
||||
const { props: currentMenuProps, activePanelIndex: currentMenuActivePanelIndex } =
|
||||
menuPath[menuPath.length - 1];
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuContext.Provider
|
||||
value={{
|
||||
showSubMenu,
|
||||
hideMenu,
|
||||
addItemPanel,
|
||||
activePanelIndex: currentMenuActivePanelIndex,
|
||||
horizontalDirection,
|
||||
}}
|
||||
>
|
||||
{isMenuVisible && (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-secondary-dark flex select-none flex-col rounded px-1 py-1.5 text-white opacity-90',
|
||||
menuClassName
|
||||
)}
|
||||
style={menuStyle}
|
||||
>
|
||||
{menuPath.length > 1 && (
|
||||
<BackItem
|
||||
backLabel={menuPath[menuPath.length - 2].props.backLabel}
|
||||
onBackClick={onBackClick}
|
||||
/>
|
||||
)}
|
||||
{itemPanelLabels.length > 1 && (
|
||||
<PanelSelector
|
||||
panelLabels={itemPanelLabels}
|
||||
activeIndex={currentMenuActivePanelIndex}
|
||||
onActiveIndexChange={onActivePanelIndexChange}
|
||||
></PanelSelector>
|
||||
)}
|
||||
{currentMenuProps.headerComponent}
|
||||
{currentMenuProps.showHeaderDivider && <DividerItem />}
|
||||
{currentMenuProps.children}
|
||||
</div>
|
||||
)}
|
||||
</MenuContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { ButtonGroup } from '../../components';
|
||||
|
||||
type PanelSelectorProps = {
|
||||
panelLabels: Array<ReactNode>;
|
||||
onActiveIndexChange: (index: number) => void;
|
||||
activeIndex: number;
|
||||
};
|
||||
|
||||
const PanelSelector = ({ panelLabels, onActiveIndexChange, activeIndex }: PanelSelectorProps) => {
|
||||
const getButtons = () => {
|
||||
return panelLabels.map((panelLabel, index) => {
|
||||
return {
|
||||
children: panelLabel,
|
||||
key: index,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-2 my-1 flex justify-center">
|
||||
<ButtonGroup
|
||||
buttons={getButtons()}
|
||||
onActiveIndexChange={onActiveIndexChange}
|
||||
defaultActiveIndex={activeIndex}
|
||||
></ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PanelSelector;
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { MenuContext, MenuProps } from './Menu';
|
||||
import Icon from '../Icon';
|
||||
|
||||
export interface SubMenuProps extends MenuProps {
|
||||
itemLabel: string;
|
||||
onClick?: () => void;
|
||||
itemIcon?: string;
|
||||
}
|
||||
|
||||
const SubMenu = (props: SubMenuProps) => {
|
||||
const { showSubMenu } = useContext(MenuContext);
|
||||
|
||||
const onClickHandler = useCallback(() => {
|
||||
showSubMenu(props);
|
||||
props.onClick?.();
|
||||
}, [showSubMenu, props]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="all-in-one-menu-item all-in-one-menu-item-effects flex items-center"
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
{props.itemIcon && (
|
||||
<Icon
|
||||
name={props.itemIcon}
|
||||
width="25px"
|
||||
height="25px"
|
||||
className="mr-2"
|
||||
></Icon>
|
||||
)}
|
||||
<div className="mr-auto">{props.itemLabel}</div>
|
||||
<Icon name="content-next"></Icon>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubMenu;
|
||||
@@ -0,0 +1,129 @@
|
||||
import { DividerItem, HeaderItem, Item, ItemPanel, Menu, SubMenu } from '..';
|
||||
import InputRange from '../../InputRange/index.js';
|
||||
import SwitchButton from '../../SwitchButton/index.js';
|
||||
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
|
||||
export const argTypes = {
|
||||
component: Menu,
|
||||
title: 'Components/AllInOneMenu',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/AllInOneMenu"
|
||||
component={Menu}
|
||||
/>
|
||||
|
||||
export const AllInOneMenuTemplate = args => (
|
||||
<div className="w-80">
|
||||
<Menu {...args}>
|
||||
<ItemPanel>
|
||||
<Item
|
||||
label="Item 1"
|
||||
key="0"
|
||||
onClick={() => console.info('Item 1 clicked.')}
|
||||
></Item>
|
||||
<Item
|
||||
label="Item 2"
|
||||
secondaryLabel="Alt item 2"
|
||||
key="1"
|
||||
></Item>
|
||||
<div
|
||||
className="all-in-one-menu-item py-1"
|
||||
style={{ flexBasis: 'content' }}
|
||||
>
|
||||
<div>Arbitrary item component:</div>
|
||||
<InputRange
|
||||
minValue={1}
|
||||
maxValue={10}
|
||||
value={5}
|
||||
onChange={() => {}}
|
||||
></InputRange>
|
||||
</div>
|
||||
<DividerItem key="2"></DividerItem>
|
||||
<SubMenu
|
||||
key="3"
|
||||
backLabel="Back to Level 2"
|
||||
itemLabel="Item 3 opens a sub menu"
|
||||
onClick={() => console.info('Sub menu item clicked.')}
|
||||
showHeaderDivider={true}
|
||||
headerComponent={
|
||||
<div className="all-in-one-menu-item flex w-full justify-center">
|
||||
<SwitchButton label="Switch item in header" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ItemPanel
|
||||
index={0}
|
||||
label="Panel A"
|
||||
>
|
||||
<Item label="Panel A item"></Item>
|
||||
</ItemPanel>
|
||||
<ItemPanel
|
||||
index={1}
|
||||
label="Panel B"
|
||||
>
|
||||
<Item label="Panel B item"></Item>
|
||||
<SubMenu
|
||||
itemLabel="Opens a sub, sub menu"
|
||||
headerComponent={<HeaderItem>Header for scrolling list of items</HeaderItem>}
|
||||
>
|
||||
<ItemPanel label="Sub sub Panel">
|
||||
<Item label="Sub sub menu item 1"></Item>
|
||||
<Item label="Sub sub menu item 2"></Item>
|
||||
<Item label="Sub sub menu item 3"></Item>
|
||||
<Item label="Sub sub menu item 4"></Item>
|
||||
<Item label="Sub sub menu item 5"></Item>
|
||||
<Item label="Sub sub menu item 6"></Item>
|
||||
</ItemPanel>
|
||||
</SubMenu>
|
||||
</ItemPanel>
|
||||
</SubMenu>
|
||||
</ItemPanel>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
|
||||
<Heading
|
||||
title="AllInOneMenu"
|
||||
componentRelativePath="AllInOneMenu/AllInOneMenu.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
AllInOneMenu is a component that renders a menu with various menu items, sub-menus and
|
||||
sub-components. The particular feature of the AllInOneMenu is its ability to render a sub-menu by
|
||||
replacing the parent menu on screen. It also provides the ability to return to the parent menu with
|
||||
a back menu item from the sub-menu. Furthermore, each menu level can be split into item panes - that
|
||||
is several panes of menu items that are switched in and out of view like a tabbed pane.
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
className: 'max-h-[210px]',
|
||||
isVisible: true,
|
||||
preventHideMenu: true,
|
||||
backLabel: 'Back to Level 1',
|
||||
onVisibilityChange: (isVisible) => console.info(`The menu visibility: ${isVisible}`),
|
||||
}}
|
||||
>
|
||||
|
||||
{AllInOneMenuTemplate.bind({})}
|
||||
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={Menu} />
|
||||
|
||||
## Usage
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="AllInOneMenu/__stories__/AllInOneMenu.stories.mdx" />
|
||||
@@ -0,0 +1,11 @@
|
||||
.all-in-one-menu-item {
|
||||
@apply h-8 px-2 text-[14px] w-full;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.all-in-one-menu-item-effects {
|
||||
@apply cursor-pointer hover:bg-primary-dark hover:rounded;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import Menu, { HorizontalDirection, VerticalDirection } from './Menu';
|
||||
import DividerItem from './DividerItem';
|
||||
import HeaderItem from './HeaderItem';
|
||||
import IconMenu from './IconMenu';
|
||||
import Item from './Item';
|
||||
import ItemPanel from './ItemPanel';
|
||||
import SubMenu from './SubMenu';
|
||||
|
||||
export {
|
||||
Menu,
|
||||
DividerItem,
|
||||
HeaderItem,
|
||||
IconMenu,
|
||||
Item,
|
||||
ItemPanel,
|
||||
SubMenu,
|
||||
HorizontalDirection,
|
||||
VerticalDirection,
|
||||
};
|
||||
@@ -41,7 +41,7 @@ const ButtonGroup = ({
|
||||
return cloneElement(child, {
|
||||
key: index,
|
||||
className: classnames(
|
||||
'rounded-[4px] px-2 py-1',
|
||||
'rounded-[4px] px-2 py-1 text-center',
|
||||
index === activeIndex
|
||||
? 'bg-customblue-40 text-white'
|
||||
: 'text-primary-active bg-black',
|
||||
|
||||
@@ -8,6 +8,8 @@ import Svg from '../Svg';
|
||||
import Icon from '../Icon';
|
||||
import IconButton from '../IconButton';
|
||||
import Dropdown from '../Dropdown';
|
||||
import HeaderPatientInfo from '../HeaderPatientInfo';
|
||||
import { PatientInfoVisibility } from '../../types/PatientInfoVisibility';
|
||||
|
||||
function Header({
|
||||
children,
|
||||
@@ -16,6 +18,8 @@ function Header({
|
||||
onClickReturnButton,
|
||||
isSticky,
|
||||
WhiteLabeling,
|
||||
showPatientInfo = PatientInfoVisibility.VISIBLE_COLLAPSED,
|
||||
servicesManager,
|
||||
...props
|
||||
}): ReactNode {
|
||||
const { t } = useTranslation('Header');
|
||||
@@ -34,7 +38,7 @@ function Header({
|
||||
{...props}
|
||||
>
|
||||
<div className="relative h-[48px] items-center ">
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 ">
|
||||
<div className="absolute left-0 top-1/2 flex -translate-y-1/2 items-center">
|
||||
<div
|
||||
className={classNames(
|
||||
'mr-3 inline-flex items-center',
|
||||
@@ -49,7 +53,7 @@ function Header({
|
||||
className="text-primary-active w-8"
|
||||
/>
|
||||
)}
|
||||
<div className="ml-4">
|
||||
<div className="ml-1">
|
||||
{WhiteLabeling?.createLogoComponentFn?.(React, props) || <Svg name="logo-ohif" />}
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,32 +62,30 @@ function Header({
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<div className="flex items-center justify-center space-x-2">{children}</div>
|
||||
</div>
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2 ">
|
||||
<Dropdown
|
||||
id="options"
|
||||
showDropdownIcon={false}
|
||||
list={menuOptions}
|
||||
alignment="right"
|
||||
>
|
||||
<IconButton
|
||||
id={'options-settings-icon'}
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
<div className="absolute right-0 top-1/2 flex -translate-y-1/2 select-none items-center">
|
||||
{(showPatientInfo === PatientInfoVisibility.VISIBLE ||
|
||||
showPatientInfo === PatientInfoVisibility.VISIBLE_COLLAPSED) && (
|
||||
<HeaderPatientInfo servicesManager={servicesManager} />
|
||||
)}
|
||||
<div className="border-primary-dark mx-1.5 h-[25px] border-r"></div>
|
||||
<div className="flex-shrink-0">
|
||||
<Dropdown
|
||||
id="options"
|
||||
showDropdownIcon={false}
|
||||
list={menuOptions}
|
||||
alignment="right"
|
||||
>
|
||||
<Icon name="settings" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
id={'options-chevron-down-icon'}
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
>
|
||||
<Icon name="chevron-down" />
|
||||
</IconButton>
|
||||
</Dropdown>
|
||||
<IconButton
|
||||
id={'options-settings-icon'}
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active hover:bg-primary-dark h-full w-full"
|
||||
>
|
||||
<Icon name="icon-settings" />
|
||||
</IconButton>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NavBar>
|
||||
@@ -103,6 +105,8 @@ Header.propTypes = {
|
||||
isSticky: PropTypes.bool,
|
||||
onClickReturnButton: PropTypes.func,
|
||||
WhiteLabeling: PropTypes.object,
|
||||
showPatientInfo: PropTypes.string,
|
||||
servicesManager: PropTypes.object,
|
||||
};
|
||||
|
||||
Header.defaultProps = {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
const { formatDate, formatPN } = utils;
|
||||
|
||||
const formatWithEllipsis = (str, maxLength) => {
|
||||
if (str.length > maxLength) {
|
||||
return str.substring(0, maxLength) + '...';
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
function usePatientInfo(servicesManager) {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
|
||||
const [patientInfo, setPatientInfo] = useState({
|
||||
PatientName: '',
|
||||
PatientID: '',
|
||||
PatientSex: '',
|
||||
PatientDOB: '',
|
||||
});
|
||||
const [isMixedPatients, setIsMixedPatients] = useState(false);
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
const checkMixedPatients = PatientID => {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
let isMixedPatients = false;
|
||||
displaySets.forEach(displaySet => {
|
||||
const instance = displaySet?.instances?.[0] || displaySet?.instance;
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
if (instance.PatientID !== PatientID) {
|
||||
isMixedPatients = true;
|
||||
}
|
||||
});
|
||||
setIsMixedPatients(isMixedPatients);
|
||||
};
|
||||
|
||||
const updatePatientInfo = () => {
|
||||
const displaySet = displaySets[0];
|
||||
const instance = displaySet?.instances?.[0] || displaySet?.instance;
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
setPatientInfo({
|
||||
PatientID: instance.PatientID || '',
|
||||
PatientName: instance.PatientName ? formatPN(instance.PatientName.Alphabetic) : '',
|
||||
PatientSex: instance.PatientSex || '',
|
||||
PatientDOB: formatDate(instance.PatientBirthDate) || '',
|
||||
});
|
||||
checkMixedPatients(instance.PatientID || '');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
() => updatePatientInfo()
|
||||
);
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
updatePatientInfo();
|
||||
}, [displaySets]);
|
||||
|
||||
return { patientInfo, isMixedPatients };
|
||||
}
|
||||
|
||||
function HeaderPatientInfo({ servicesManager }) {
|
||||
const [appConfig] = useAppConfig();
|
||||
const initialExpandedState = appConfig.showPatientInfo === 'visible';
|
||||
const [expanded, setExpanded] = useState(initialExpandedState);
|
||||
const { patientInfo, isMixedPatients } = usePatientInfo(servicesManager);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMixedPatients && expanded) {
|
||||
setExpanded(false);
|
||||
}
|
||||
}, [isMixedPatients, expanded]);
|
||||
|
||||
const handleOnClick = () => {
|
||||
if (!isMixedPatients) {
|
||||
setExpanded(!expanded);
|
||||
}
|
||||
};
|
||||
|
||||
const formattedPatientName = formatWithEllipsis(patientInfo.PatientName, 27);
|
||||
const formattedPatientID = formatWithEllipsis(patientInfo.PatientID, 15);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="align-items-center hover:bg-primary-dark flex cursor-pointer justify-center gap-1 rounded-lg"
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
<Icon
|
||||
name={isMixedPatients ? 'icon-multiple-patients' : 'icon-patient'}
|
||||
className="text-primary-active"
|
||||
/>
|
||||
<div className="flex flex-col justify-center">
|
||||
{expanded ? (
|
||||
<>
|
||||
<div className="self-start text-[13px] font-bold text-white">
|
||||
{formattedPatientName}
|
||||
</div>
|
||||
<div className="text-aqua-pale flex gap-2 text-[11px]">
|
||||
<div>{formattedPatientID}</div>
|
||||
<div>{patientInfo.PatientSex}</div>
|
||||
<div>{patientInfo.PatientDOB}</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-primary-active self-center text-[13px]">
|
||||
{' '}
|
||||
{isMixedPatients ? 'Multiple Patients' : 'Patient'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Icon
|
||||
name="icon-chevron-patient"
|
||||
className={`text-primary-active ${expanded ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
HeaderPatientInfo.propTypes = {
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default HeaderPatientInfo;
|
||||
@@ -0,0 +1,3 @@
|
||||
import HeaderPatientInfo from './HeaderPatientInfo';
|
||||
|
||||
export default HeaderPatientInfo;
|
||||
@@ -17,6 +17,8 @@ import chevronMenu from './../../assets/icons/chevron-menu.svg';
|
||||
import chevronNext from './../../assets/icons/chevron-next.svg';
|
||||
import chevronPrev from './../../assets/icons/chevron-prev.svg';
|
||||
import chevronRight from './../../assets/icons/chevron-right.svg';
|
||||
import contentNext from './../../assets/icons/content-next.svg';
|
||||
import contentPrev from './../../assets/icons/content-prev.svg';
|
||||
import eyeVisible from './../../assets/icons/eye-visible.svg';
|
||||
import eyeHidden from './../../assets/icons/eye-hidden.svg';
|
||||
import exclamation from './../../assets/icons/exclamation.svg';
|
||||
@@ -83,6 +85,9 @@ import notificationsInfo from './../../assets/icons/notifications-info.svg';
|
||||
import notificationsWarning from './../../assets/icons/notifications-warning.svg';
|
||||
import notificationsError from './../../assets/icons/notifications-error.svg';
|
||||
import notificationsSuccess from './../../assets/icons/notifications-success.svg';
|
||||
import nextArrow from './../../assets/icons/next-arrow.svg';
|
||||
import prevArrow from './../../assets/icons/prev-arrow.svg';
|
||||
import viewportStatusTracked from './../../assets/icons/viewport-status-tracked.svg';
|
||||
|
||||
/** Tools */
|
||||
import toggleDicomOverlay from './../../assets/icons/toggle-dicom-overlay.svg';
|
||||
@@ -139,12 +144,53 @@ import iconToolEraser from './../../assets/icons/icon-tool-eraser.svg';
|
||||
import iconToolScissor from './../../assets/icons/icon-tool-scissor.svg';
|
||||
import iconToolShape from './../../assets/icons/icon-tool-shape.svg';
|
||||
import iconToolThreshold from './../../assets/icons/icon-tool-threshold.svg';
|
||||
import viewportWindowLevel from './../../assets/icons/viewport-window-level.svg';
|
||||
|
||||
/** Old OHIF */
|
||||
import oldTrash from './../../assets/icons/old-trash.svg';
|
||||
import oldPlay from './../../assets/icons/old-play.svg';
|
||||
import oldStop from './../../assets/icons/old-stop.svg';
|
||||
|
||||
/** ColorLut */
|
||||
import iconColorLUT from './../../assets/icons/icon-color-lut.svg';
|
||||
|
||||
/** New Patient Info Toolbar */
|
||||
import iconChevronPatient from './../../assets/icons/icon-chevron-patient.svg';
|
||||
import iconPatient from './../../assets/icons/icon-patient.svg';
|
||||
import iconSettings from './../../assets/icons/icon-settings.svg';
|
||||
import iconToolbarBack from './../../assets/icons/icon-toolbar-back.svg';
|
||||
import iconMultiplePatients from './../../assets/icons/icon-multiple-patients.svg';
|
||||
|
||||
/** Volume Rendering */
|
||||
import CTAAA from './../../assets/icons/CT-AAA.png';
|
||||
import CTAAA2 from './../../assets/icons/CT-AAA2.png';
|
||||
import CTAir from './../../assets/icons/CT-Air.png';
|
||||
import CTBone from './../../assets/icons/CT-Bone.png';
|
||||
import CTBones from './../../assets/icons/CT-Bones.png';
|
||||
import CTCardiac from './../../assets/icons/CT-Cardiac.png';
|
||||
import CTCardiac2 from './../../assets/icons/CT-Cardiac2.png';
|
||||
import CTCardiac3 from './../../assets/icons/CT-Cardiac3.png';
|
||||
import CTChestContrastEnhanced from './../../assets/icons/CT-Chest-Contrast-Enhanced.png';
|
||||
import CTChestVessels from './../../assets/icons/CT-Chest-Vessels.png';
|
||||
import CTCoronaryArteries from './../../assets/icons/CT-Coronary-Arteries.png';
|
||||
import CTCoronaryArteries2 from './../../assets/icons/CT-Coronary-Arteries-2.png';
|
||||
import CTCoronaryArteries3 from './../../assets/icons/CT-Coronary-Arteries-3.png';
|
||||
import CTCroppedVolumeBone from './../../assets/icons/CT-Cropped-Volume-Bone.png';
|
||||
import CTFat from './../../assets/icons/CT-Fat.png';
|
||||
import CTLiverVasculature from './../../assets/icons/CT-Liver-Vasculature.png';
|
||||
import CTLung from './../../assets/icons/CT-Lung.png';
|
||||
import CTMIP from './../../assets/icons/CT-MIP.png';
|
||||
import CTMuscle from './../../assets/icons/CT-Muscle.png';
|
||||
import CTPulmonaryArteries from './../../assets/icons/CT-Pulmonary-Arteries.png';
|
||||
import CTSoftTissue from './../../assets/icons/CT-Soft-Tissue.png';
|
||||
import DTIFABrain from './../../assets/icons/DTI-FA-Brain.png';
|
||||
import MRAngio from './../../assets/icons/MR-Angio.png';
|
||||
import MRDefault from './../../assets/icons/MR-Default.png';
|
||||
import MRMIP from './../../assets/icons/MR-MIP.png';
|
||||
import MRT2Brain from './../../assets/icons/MR-T2-Brain.png';
|
||||
import VolumeRendering from './../../assets/icons/VolumeRendering.png';
|
||||
import actionNewDialog from './../../assets/icons/action-new-dialog.svg';
|
||||
|
||||
/** LAYOUT */
|
||||
|
||||
import layoutAdvanced3DFourUp from './../../assets/icons/layout-advanced-3d-four-up.svg';
|
||||
@@ -160,7 +206,6 @@ import layoutCommon2x3 from './../../assets/icons/layout-common-2x3.svg';
|
||||
import iconToolRotate from './../../assets/icons/tool-3d-rotate.svg';
|
||||
|
||||
/** New investigational use */
|
||||
|
||||
import investigationalUse from './../../assets/icons/illustration-investigational-use.svg';
|
||||
|
||||
const ICONS = {
|
||||
@@ -180,6 +225,8 @@ const ICONS = {
|
||||
'chevron-next': chevronNext,
|
||||
'chevron-prev': chevronPrev,
|
||||
'chevron-right': chevronRight,
|
||||
'content-next': contentNext,
|
||||
'content-prev': contentPrev,
|
||||
'eye-visible': eyeVisible,
|
||||
'eye-hidden': eyeHidden,
|
||||
'external-link': externalLink,
|
||||
@@ -304,12 +351,51 @@ const ICONS = {
|
||||
'icon-tool-scissor': iconToolScissor,
|
||||
'icon-tool-shape': iconToolShape,
|
||||
'icon-tool-threshold': iconToolThreshold,
|
||||
|
||||
'next-arrow': nextArrow,
|
||||
'prev-arrow': prevArrow,
|
||||
'viewport-status-tracked': viewportStatusTracked,
|
||||
'viewport-window-level': viewportWindowLevel,
|
||||
/** Old OHIF */
|
||||
'old-trash': oldTrash,
|
||||
'old-play': oldPlay,
|
||||
'old-stop': oldStop,
|
||||
|
||||
/** ColorLut */
|
||||
'icon-color-lut': iconColorLUT,
|
||||
/** New Patient Info Toolbar */
|
||||
'icon-chevron-patient': iconChevronPatient,
|
||||
'icon-patient': iconPatient,
|
||||
'icon-settings': iconSettings,
|
||||
'icon-toolbar-back': iconToolbarBack,
|
||||
'icon-multiple-patients': iconMultiplePatients,
|
||||
/** Volume Rendering */
|
||||
'CT-AAA': CTAAA,
|
||||
'CT-AAA2': CTAAA2,
|
||||
'CT-Air': CTAir,
|
||||
'CT-Bone': CTBone,
|
||||
'CT-Bones': CTBones,
|
||||
'CT-Cardiac': CTCardiac,
|
||||
'CT-Cardiac2': CTCardiac2,
|
||||
'CT-Cardiac3': CTCardiac3,
|
||||
'CT-Chest-Contrast-Enhanced': CTChestContrastEnhanced,
|
||||
'CT-Chest-Vessels': CTChestVessels,
|
||||
'CT-Coronary-Arteries': CTCoronaryArteries,
|
||||
'CT-Coronary-Arteries-2': CTCoronaryArteries2,
|
||||
'CT-Coronary-Arteries-3': CTCoronaryArteries3,
|
||||
'CT-Cropped-Volume-Bone': CTCroppedVolumeBone,
|
||||
'CT-Fat': CTFat,
|
||||
'CT-Liver-Vasculature': CTLiverVasculature,
|
||||
'CT-Lung': CTLung,
|
||||
'CT-MIP': CTMIP,
|
||||
'CT-Muscle': CTMuscle,
|
||||
'CT-Pulmonary-Arteries': CTPulmonaryArteries,
|
||||
'CT-Soft-Tissue': CTSoftTissue,
|
||||
'DTI-FA-Brain': DTIFABrain,
|
||||
'MR-Angio': MRAngio,
|
||||
'MR-Default': MRDefault,
|
||||
'MR-MIP': MRMIP,
|
||||
'MR-T2-Brain': MRT2Brain,
|
||||
VolumeRendering: VolumeRendering,
|
||||
'action-new-dialog': actionNewDialog,
|
||||
/** LAYOUT */
|
||||
'layout-advanced-3d-four-up': layoutAdvanced3DFourUp,
|
||||
'layout-advanced-3d-main': layoutAdvanced3DMain,
|
||||
@@ -340,11 +426,17 @@ function addIcon(iconName, iconSVG) {
|
||||
* return `null`
|
||||
*/
|
||||
export default function getIcon(key, props) {
|
||||
if (!key || !ICONS[key]) {
|
||||
const icon = ICONS[key];
|
||||
|
||||
if (!key || !icon) {
|
||||
return React.createElement('div', null, 'Missing Icon');
|
||||
}
|
||||
|
||||
return React.createElement(ICONS[key], props);
|
||||
if (typeof icon === 'string' && icon.endsWith('.png')) {
|
||||
return React.createElement('img', { src: icon, ...props });
|
||||
} else {
|
||||
return React.createElement(icon, props);
|
||||
}
|
||||
}
|
||||
|
||||
export { getIcon, ICONS, addIcon };
|
||||
@@ -1,13 +1,13 @@
|
||||
.scroll {
|
||||
height: 100%;
|
||||
height: calc(100% - 30px);
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
top: 30px;
|
||||
}
|
||||
.scroll .scroll-holder {
|
||||
height: calc(100% - 20px);
|
||||
margin-top: 5px;
|
||||
height: calc(100%);
|
||||
margin-bottom: 5px;
|
||||
position: relative;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ import getMaxDigits from '../../utils/getMaxDigits';
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
type Label = {
|
||||
text: string;
|
||||
position: number;
|
||||
};
|
||||
|
||||
type InputRangeProps = {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
@@ -28,6 +34,7 @@ type InputRangeProps = {
|
||||
trackColor?: string;
|
||||
allowNumberEdit?: boolean;
|
||||
showAdjustmentArrows?: boolean;
|
||||
labels?: Label[];
|
||||
};
|
||||
|
||||
const InputRange: React.FC<InputRangeProps> = ({
|
||||
@@ -46,6 +53,7 @@ const InputRange: React.FC<InputRangeProps> = ({
|
||||
trackColor,
|
||||
allowNumberEdit = false,
|
||||
showAdjustmentArrows = true,
|
||||
labels = [],
|
||||
}) => {
|
||||
const [rangeValue, setRangeValue] = useState(value);
|
||||
|
||||
@@ -114,6 +122,22 @@ const InputRange: React.FC<InputRangeProps> = ({
|
||||
{showLabel && labelPosition === 'right' && (
|
||||
<div style={{ width: labelWidth }}>{LabelOrEditableNumber}</div>
|
||||
)}
|
||||
{labels.length > 0 && (
|
||||
<>
|
||||
{labels.map((label, index) => {
|
||||
const position = label.position;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute !m-0"
|
||||
style={{ left: `calc(${position}%)`, bottom: '-20px' }}
|
||||
>
|
||||
<span className="text-aqua-pale text-xs">{label.text}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactModal from 'react-modal';
|
||||
import Draggable from 'react-draggable';
|
||||
import { useModal } from '../../contextProviders';
|
||||
|
||||
import Icon from '../Icon';
|
||||
@@ -20,6 +21,9 @@ const Modal = ({
|
||||
onClose,
|
||||
children,
|
||||
shouldCloseOnOverlayClick,
|
||||
movable = false,
|
||||
containerDimensions = null,
|
||||
contentDimensions = null,
|
||||
}) => {
|
||||
const { hide } = useModal();
|
||||
|
||||
@@ -27,44 +31,70 @@ const Modal = ({
|
||||
hide();
|
||||
};
|
||||
|
||||
const renderHeader = () => {
|
||||
return (
|
||||
title && (
|
||||
<header className="bg-primary-dark flex items-center rounded-tl rounded-tr px-[20px] py-[13px]">
|
||||
<Typography
|
||||
variant="h6"
|
||||
color="primaryLight"
|
||||
className="flex grow !leading-[1.2]"
|
||||
data-cy="modal-header"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{closeButton && (
|
||||
<Icon
|
||||
onClick={onClose}
|
||||
name="close"
|
||||
className="text-primary-active cursor-pointer"
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
const renderHeader = () =>
|
||||
title && (
|
||||
<header className="bg-primary-dark drag-handle flex items-center rounded-tl rounded-tr px-[20px] py-[13px]">
|
||||
<Typography
|
||||
variant="h6"
|
||||
color="primaryLight"
|
||||
className="flex grow !leading-[1.2]"
|
||||
data-cy="modal-header"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{closeButton && (
|
||||
<Icon
|
||||
onClick={onClose}
|
||||
name="close"
|
||||
className="text-primary-active cursor-pointer"
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
const modalContent = (
|
||||
<>
|
||||
{renderHeader()}
|
||||
<section
|
||||
className={
|
||||
contentDimensions
|
||||
? `ohif-scrollbar bg-primary-dark overflow-y-auto ${contentDimensions}`
|
||||
: 'ohif-scrollbar modal-content bg-primary-dark overflow-y-auto rounded-bl rounded-br px-[20px] pt-2 pb-[20px]'
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
className="relative max-h-full w-11/12 text-white outline-none lg:w-10/12 xl:w-9/12"
|
||||
overlayClassName="fixed top-0 left-0 right-0 bottom-0 z-50 bg-overlay flex items-start justify-center py-16"
|
||||
className={
|
||||
containerDimensions
|
||||
? `relative text-white outline-none ${containerDimensions}`
|
||||
: 'relative max-h-full w-11/12 text-white outline-none lg:w-10/12 xl:w-9/12'
|
||||
}
|
||||
overlayClassName={
|
||||
movable
|
||||
? 'fixed top-0 left-0 right-0 bottom-0 z-50 flex items-center justify-center py-16 pointer-events-none'
|
||||
: 'fixed top-0 left-0 right-0 bottom-0 z-50 bg-overlay flex items-center justify-center py-16'
|
||||
}
|
||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||
onRequestClose={handleClose}
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
|
||||
>
|
||||
{renderHeader()}
|
||||
<section className="ohif-scrollbar modal-content bg-primary-dark overflow-y-auto rounded-bl rounded-br px-[20px] pt-2 pb-[20px]">
|
||||
{children}
|
||||
</section>
|
||||
{movable ? (
|
||||
<Draggable
|
||||
handle=".drag-handle"
|
||||
defaultClassName="bg-primary-dark pointer-events-auto"
|
||||
>
|
||||
<div>{modalContent}</div>
|
||||
</Draggable>
|
||||
) : (
|
||||
modalContent
|
||||
)}
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
@@ -72,6 +102,9 @@ const Modal = ({
|
||||
Modal.defaultProps = {
|
||||
shouldCloseOnEsc: true,
|
||||
shouldCloseOnOverlayClick: true,
|
||||
movable: false,
|
||||
containerDimensions: null,
|
||||
contentDimensions: null,
|
||||
};
|
||||
|
||||
Modal.propTypes = {
|
||||
@@ -80,9 +113,11 @@ Modal.propTypes = {
|
||||
isOpen: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
/** The modal's content */
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
|
||||
shouldCloseOnOverlayClick: PropTypes.bool,
|
||||
movable: PropTypes.bool,
|
||||
containerDimensions: PropTypes.string,
|
||||
contentDimensions: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
@@ -149,7 +149,6 @@ StudyBrowser.propTypes = {
|
||||
componentType: PropTypes.oneOf(['thumbnail', 'thumbnailTracked', 'thumbnailNoImage'])
|
||||
.isRequired,
|
||||
isTracked: PropTypes.bool,
|
||||
viewportIdentificator: PropTypes.arrayOf(PropTypes.string),
|
||||
/**
|
||||
* Data the thumbnail should expose to a receiving drop target. Use a matching
|
||||
* `dragData.type` to identify which targets can receive this draggable item.
|
||||
|
||||
@@ -99,7 +99,6 @@ With Studies
|
||||
displaySetInstanceUID: '0b66675a-a4e3-a718-21f4-035d2d178d62',
|
||||
},
|
||||
isTracked: false,
|
||||
viewportIdentificator: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
// Svgs
|
||||
import logoOhif from './../../assets/svgs/logo-ohif.svg';
|
||||
import logoOhif from './../../assets/svgs/ohif-logo.svg';
|
||||
|
||||
const SVGS = {
|
||||
'logo-ohif': logoOhif,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import './switchButton.css';
|
||||
|
||||
export enum SwitchLabelLocation {
|
||||
left,
|
||||
right,
|
||||
}
|
||||
|
||||
export type SwitchButtonProps = {
|
||||
checked?: boolean;
|
||||
label?: string;
|
||||
labelLocation?: SwitchLabelLocation;
|
||||
onChange?: (checked: boolean) => void;
|
||||
};
|
||||
|
||||
const SwitchButton = ({
|
||||
label,
|
||||
checked = false,
|
||||
onChange,
|
||||
labelLocation = SwitchLabelLocation.left,
|
||||
}: SwitchButtonProps) => {
|
||||
const [isInputChecked, setIsInputChecked] = useState(checked);
|
||||
|
||||
const onHandleChange = useCallback(
|
||||
event => {
|
||||
setIsInputChecked(event.target.checked);
|
||||
onChange?.(event.target.checked);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
// Thanks goes to https://codepen.io/lhermann/pen/EBGZRZ for the inspiration to the code below.
|
||||
return (
|
||||
<label className="switch-button flex w-full cursor-pointer items-center justify-between text-[14px]">
|
||||
{label && labelLocation === SwitchLabelLocation.left && <div>{label}</div>}
|
||||
<div className="relative">
|
||||
<input
|
||||
className="absolute hidden"
|
||||
type="checkbox"
|
||||
onChange={onHandleChange}
|
||||
checked={isInputChecked}
|
||||
/>
|
||||
<div className="switch-button-outer border-common-bright bg-primary-dark block h-[16px] w-[30px] rounded-full border"></div>
|
||||
<div className="switch-button-dot bg-common-bright absolute left-[4px] top-[3px] h-[10px] w-[10px] rounded-full transition duration-150 ease-in-out"></div>
|
||||
</div>
|
||||
{label && labelLocation === SwitchLabelLocation.right && <div>{label}</div>}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwitchButton;
|
||||
@@ -0,0 +1,58 @@
|
||||
import SwitchButton, { SwitchLabelLocation } from '../SwitchButton';
|
||||
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
|
||||
|
||||
export const argTypes = {
|
||||
component: SwitchButton,
|
||||
title: 'Components/SwitchButton',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/SwitchButton"
|
||||
component={SwitchButton}
|
||||
/>
|
||||
|
||||
export const SwitchButtonTemplate = args => (
|
||||
<div className="w-[200px]">
|
||||
<SwitchButton {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
<Heading
|
||||
title="SwitchButton"
|
||||
componentRelativePath="SwitchButton/SwitchButton.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
A SwitchButton is a checkbox type input/button that can either be on or off (like a light switch).
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
label: 'Switch Button Label',
|
||||
labelLocation: SwitchLabelLocation.left,
|
||||
onChange: (checked) => console.info(`${checked ? 'The switch is on.' : 'The switch is off.'}`)
|
||||
}}
|
||||
>
|
||||
|
||||
{SwitchButtonTemplate.bind({})}
|
||||
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={SwitchButton} />
|
||||
|
||||
## Usage
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="SwitchButton/__stories__/SwitchButton.stories.mdx" />
|
||||
@@ -0,0 +1,3 @@
|
||||
import SwitchButton, { SwitchLabelLocation } from './SwitchButton';
|
||||
export default SwitchButton;
|
||||
export { SwitchLabelLocation };
|
||||
@@ -0,0 +1,7 @@
|
||||
.switch-button input:checked ~ .switch-button-outer {
|
||||
@apply border-primary-light;
|
||||
}
|
||||
.switch-button input:checked ~ .switch-button-dot {
|
||||
@apply bg-primary-light;
|
||||
transform: translateX(12px);
|
||||
}
|
||||
@@ -29,7 +29,6 @@ const ThumbnailList = ({
|
||||
componentType,
|
||||
seriesDate,
|
||||
countIcon,
|
||||
viewportIdentificator,
|
||||
isTracked,
|
||||
canReject,
|
||||
onReject,
|
||||
@@ -53,7 +52,6 @@ const ThumbnailList = ({
|
||||
imageSrc={imageSrc}
|
||||
imageAltText={imageAltText}
|
||||
messages={messages}
|
||||
viewportIdentificator={viewportIdentificator}
|
||||
isActive={isActive}
|
||||
onClick={() => onThumbnailClick(displaySetInstanceUID)}
|
||||
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
|
||||
@@ -72,7 +70,6 @@ const ThumbnailList = ({
|
||||
imageSrc={imageSrc}
|
||||
imageAltText={imageAltText}
|
||||
messages={messages}
|
||||
viewportIdentificator={viewportIdentificator}
|
||||
isTracked={isTracked}
|
||||
isActive={isActive}
|
||||
onClick={() => onThumbnailClick(displaySetInstanceUID)}
|
||||
@@ -96,7 +93,6 @@ const ThumbnailList = ({
|
||||
onReject={onReject}
|
||||
onClick={() => onThumbnailClick(displaySetInstanceUID)}
|
||||
onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID)}
|
||||
viewportIdentificator={viewportIdentificator}
|
||||
isHydratedForDerivedDisplaySet={isHydratedForDerivedDisplaySet}
|
||||
/>
|
||||
);
|
||||
@@ -120,7 +116,6 @@ ThumbnailList.propTypes = {
|
||||
numInstances: PropTypes.number,
|
||||
description: PropTypes.string,
|
||||
componentType: Types.ThumbnailType.isRequired,
|
||||
viewportIdentificator: Types.StringArray,
|
||||
isTracked: PropTypes.bool,
|
||||
/**
|
||||
* Data the thumbnail should expose to a receiving drop target. Use a matching
|
||||
|
||||
@@ -24,43 +24,11 @@ function ThumbnailTracked({
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
onClickUntrack,
|
||||
viewportIdentificator,
|
||||
isTracked,
|
||||
isActive,
|
||||
}) {
|
||||
const { t } = useTranslation('ThumbnailTracked');
|
||||
const trackedIcon = isTracked ? 'circled-checkmark' : 'dotted-circle';
|
||||
const viewportIdentificatorLabel = viewportIdentificator.join(', ');
|
||||
const renderViewportLabels = () => {
|
||||
const MAX_LABELS_PER_COL = 3;
|
||||
const shouldShowStack = viewportIdentificator.length > MAX_LABELS_PER_COL;
|
||||
if (shouldShowStack) {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{viewportIdentificator.slice(0, MAX_LABELS_PER_COL).map(label => (
|
||||
<div key={label}>{label}</div>
|
||||
))}
|
||||
</div>
|
||||
<Tooltip
|
||||
position="right"
|
||||
content={
|
||||
<div className="max-w-40 text-left">
|
||||
{`${t('Viewport')}: ${viewportIdentificatorLabel}`}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="tool-more-menu"
|
||||
className="py-2 text-white"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return viewportIdentificator.map(label => <div key={label}>{label}</div>);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -93,9 +61,6 @@ function ThumbnailTracked({
|
||||
{isTracked ? t('Series is tracked') : t('Series is untracked')}
|
||||
</span>
|
||||
</span>
|
||||
{!!viewportIdentificator.length && (
|
||||
<span>{`${t('Viewport')}: ${viewportIdentificatorLabel}`}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -105,13 +70,6 @@ function ThumbnailTracked({
|
||||
className="text-primary-light mb-2 w-4"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
className="text-center text-xl leading-tight text-white"
|
||||
data-cy={'thumbnail-viewport-labels'}
|
||||
>
|
||||
{renderViewportLabels()}
|
||||
</div>
|
||||
</div>
|
||||
{isTracked && (
|
||||
<div onClick={onClickUntrack}>
|
||||
@@ -162,7 +120,6 @@ ThumbnailTracked.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onDoubleClick: PropTypes.func.isRequired,
|
||||
onClickUntrack: PropTypes.func.isRequired,
|
||||
viewportIdentificator: PropTypes.array,
|
||||
isTracked: PropTypes.bool,
|
||||
messages: PropTypes.object,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const arrowClasses =
|
||||
'cursor-pointer flex items-center justify-center shrink-0 text-primary-light active:text-white hover:bg-secondary-light/60 rounded';
|
||||
|
||||
type ViewportActionArrowsProps = {
|
||||
onArrowsClick: (direction: number) => void;
|
||||
className: string;
|
||||
};
|
||||
|
||||
function ViewportActionArrows({ onArrowsClick, className }: ViewportActionArrowsProps) {
|
||||
return (
|
||||
// The arrows are only visible when hover over the viewport.
|
||||
<div className={classNames(className, 'flex')}>
|
||||
<div className={arrowClasses}>
|
||||
<Icon
|
||||
name="prev-arrow"
|
||||
onClick={() => onArrowsClick(-1)}
|
||||
/>
|
||||
</div>
|
||||
<div className={arrowClasses}>
|
||||
<Icon
|
||||
name="next-arrow"
|
||||
onClick={() => onArrowsClick(1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewportActionArrows;
|
||||
@@ -0,0 +1,2 @@
|
||||
import ViewportActionArrows from './ViewportActionArrows';
|
||||
export default ViewportActionArrows;
|
||||
@@ -0,0 +1,76 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { ViewportActionCornersComponentInfo } from '../../types/ViewportActionCornersTypes';
|
||||
|
||||
export enum ViewportActionCornersLocations {
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
}
|
||||
|
||||
export type ViewportActionCornersProps = {
|
||||
cornerComponents: Record<
|
||||
ViewportActionCornersLocations,
|
||||
Array<ViewportActionCornersComponentInfo>
|
||||
>;
|
||||
};
|
||||
|
||||
const commonClasses = 'pointer-events-auto flex items-center gap-1';
|
||||
const classes = {
|
||||
[ViewportActionCornersLocations.topLeft]: classNames(
|
||||
commonClasses,
|
||||
'absolute top-[4px] left-[0px] pl-[4px]'
|
||||
),
|
||||
[ViewportActionCornersLocations.topRight]: classNames(
|
||||
commonClasses,
|
||||
'absolute top-[4px] right-[4px] right-viewport-scrollbar'
|
||||
),
|
||||
[ViewportActionCornersLocations.bottomLeft]: classNames(
|
||||
commonClasses,
|
||||
'absolute bottom-[4px] left-[0px] pl-[4px]'
|
||||
),
|
||||
[ViewportActionCornersLocations.bottomRight]: classNames(
|
||||
commonClasses,
|
||||
'absolute bottom-[4px] right-[0px] right-viewport-scrollbar'
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* A component that renders various action items/components to each corner of a viewport.
|
||||
* The position of each corner's components is such that a single row of components are
|
||||
* rendered absolutely without intersecting the ViewportOverlay component.
|
||||
* Note that corner components are passed as an object mapping each corner location
|
||||
* to an array of components for that location. The components in each array are
|
||||
* rendered from left to right in the order that they appear in the array.
|
||||
*/
|
||||
function ViewportActionCorners({ cornerComponents }: ViewportActionCornersProps) {
|
||||
if (!cornerComponents) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute h-full w-full select-none"
|
||||
onDoubleClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{Object.entries(cornerComponents).map(([location, locationComponents]) => {
|
||||
return (
|
||||
<div
|
||||
key={location}
|
||||
className={classNames(classes[location])}
|
||||
>
|
||||
{locationComponents.map(componentInfo => {
|
||||
return <div key={componentInfo.id}>{componentInfo.component}</div>;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewportActionCorners;
|
||||
@@ -0,0 +1,4 @@
|
||||
import ViewportActionCorners, { ViewportActionCornersLocations } from './ViewportActionCorners';
|
||||
|
||||
export { ViewportActionCornersLocations };
|
||||
export default ViewportActionCorners;
|
||||
@@ -10,40 +10,13 @@
|
||||
text-shadow: 1px 1px #000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ViewportOverlay .top-left {
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
.overlay-top {
|
||||
top: 28px;
|
||||
}
|
||||
.ViewportOverlay .top-center {
|
||||
top: 20px;
|
||||
padding-top: 20px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
.overlay-bottom {
|
||||
bottom: 3px;
|
||||
}
|
||||
.ViewportOverlay .top-right {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
.ViewportOverlay .bottom-left {
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
.ViewportOverlay .bottom-right {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
.ViewportOverlay.controlsVisible .topright,
|
||||
.ViewportOverlay.controlsVisible .bottomright {
|
||||
right: calc(20px + 19px);
|
||||
}
|
||||
.ViewportOverlay svg {
|
||||
color: #9ccef9;
|
||||
fill: #9ccef9;
|
||||
stroke: #9ccef9;
|
||||
background-color: transparent;
|
||||
margin: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
.overlay-text {
|
||||
text-shadow: 0.8px 0.8px 0.5px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
@@ -3,11 +3,16 @@ import classnames from 'classnames';
|
||||
|
||||
import './ViewportOverlay.css';
|
||||
|
||||
// The overlay-top and overlay-bottom classes are explicitly needed to offset
|
||||
// the overlays (i.e. via absolute positioning) such the ViewportActionCorners
|
||||
// have space for its child components.
|
||||
// ToDo: offset the ViewportOverlay automatically via css to account for the
|
||||
// space needed for ViewportActionCorners.
|
||||
const classes = {
|
||||
topLeft: 'top-viewport left-viewport',
|
||||
topRight: 'top-viewport right-viewport-scrollbar',
|
||||
bottomRight: 'bottom-viewport right-viewport-scrollbar',
|
||||
bottomLeft: 'bottom-viewport left-viewport',
|
||||
topLeft: 'overlay-top left-viewport',
|
||||
topRight: 'overlay-top right-viewport-scrollbar',
|
||||
bottomRight: 'overlay-bottom right-viewport-scrollbar',
|
||||
bottomLeft: 'overlay-bottom left-viewport',
|
||||
};
|
||||
|
||||
export type ViewportOverlayProps = {
|
||||
@@ -27,7 +32,14 @@ const ViewportOverlay = ({
|
||||
}: ViewportOverlayProps) => {
|
||||
const overlay = 'absolute pointer-events-none viewport-overlay';
|
||||
return (
|
||||
<div className={color}>
|
||||
<div
|
||||
className={classnames(
|
||||
color ? color : 'text-aqua-pale',
|
||||
'text-[13px]',
|
||||
'leading-5',
|
||||
'overlay-text'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-cy={'viewport-overlay-top-left'}
|
||||
className={classnames(overlay, classes.topLeft)}
|
||||
@@ -37,12 +49,14 @@ const ViewportOverlay = ({
|
||||
<div
|
||||
data-cy={'viewport-overlay-top-right'}
|
||||
className={classnames(overlay, classes.topRight)}
|
||||
style={{ transform: 'translateX(-8px)' }} // shift right side overlays by 4px for better alignment with ViewportActionCorners' icons
|
||||
>
|
||||
{topRight}
|
||||
</div>
|
||||
<div
|
||||
data-cy={'viewport-overlay-bottom-right'}
|
||||
className={classnames(overlay, classes.bottomRight)}
|
||||
style={{ transform: 'translateX(-8px)' }} // shift right side overlays by 4px for better alignment with ViewportActionCorners' icons
|
||||
>
|
||||
{bottomRight}
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,7 @@ function ViewportPane({
|
||||
onScroll={onInteractionHandler}
|
||||
onWheel={onInteractionHandler}
|
||||
className={classnames(
|
||||
'hover:border-primary-light group h-full w-full overflow-hidden rounded-md transition duration-300',
|
||||
'group h-full w-full overflow-hidden rounded-md transition duration-300',
|
||||
{
|
||||
'border-primary-light border-2': isActive,
|
||||
'border-2 border-transparent': !isActive,
|
||||
@@ -77,10 +77,10 @@ function ViewportPane({
|
||||
>
|
||||
<div
|
||||
className={classnames(
|
||||
'h-full w-full overflow-hidden rounded-md group-hover:border-transparent',
|
||||
'h-full w-full overflow-hidden rounded-md',
|
||||
{
|
||||
'border border-transparent': isActive,
|
||||
'border-secondary-light border': !isActive,
|
||||
'border-secondary-light group-hover:border-primary-light/70 border': !isActive,
|
||||
},
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -73,22 +73,29 @@ import CheckBox from './CheckBox';
|
||||
import LoadingIndicatorProgress from './LoadingIndicatorProgress';
|
||||
import LoadingIndicatorTotalPercent from './LoadingIndicatorTotalPercent';
|
||||
import ViewportActionBar from './ViewportActionBar';
|
||||
import ViewportActionCorners, { ViewportActionCornersLocations } from './ViewportActionCorners';
|
||||
import ProgressLoadingBar from './ProgressLoadingBar';
|
||||
import LegacySidePanel from './LegacySidePanel';
|
||||
import PanelSection from './PanelSection';
|
||||
import AdvancedToolbox from './AdvancedToolbox';
|
||||
import InputDoubleRange from './InputDoubleRange';
|
||||
import LegacyButtonGroup from './LegacyButtonGroup';
|
||||
import SwitchButton, { SwitchLabelLocation } from './SwitchButton';
|
||||
import * as AllInOneMenu from './AllInOneMenu';
|
||||
import ViewportActionArrows from './ViewportActionArrows';
|
||||
import HeaderPatientInfo from './HeaderPatientInfo';
|
||||
import LegacySplitButton from './LegacySplitButton';
|
||||
import { ToolSettings } from './AdvancedToolbox';
|
||||
import { Toolbox } from './Toolbox';
|
||||
import InvestigationalUseDialog from './InvestigationalUseDialog';
|
||||
import MeasurementItem from './MeasurementTable/MeasurementItem';
|
||||
|
||||
import LayoutPreset from './LayoutPreset';
|
||||
|
||||
export {
|
||||
AboutModal,
|
||||
AdvancedToolbox,
|
||||
AllInOneMenu,
|
||||
HotkeyField,
|
||||
Header,
|
||||
UserPreferences,
|
||||
@@ -148,6 +155,8 @@ export {
|
||||
StudyListTableRow,
|
||||
StudySummary,
|
||||
Svg,
|
||||
SwitchButton,
|
||||
SwitchLabelLocation,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -163,12 +172,16 @@ export {
|
||||
TooltipClipboard,
|
||||
Typography,
|
||||
Viewport,
|
||||
ViewportActionArrows,
|
||||
ViewportActionBar,
|
||||
ViewportActionCorners,
|
||||
ViewportActionCornersLocations,
|
||||
ViewportDownloadForm,
|
||||
ViewportGrid,
|
||||
ViewportPane,
|
||||
ViewportOverlay,
|
||||
WindowLevelMenuItem,
|
||||
HeaderPatientInfo,
|
||||
LayoutPreset,
|
||||
LegacySplitButton,
|
||||
ToolSettings,
|
||||
|
||||
Reference in new issue
Block a user