ui(components): Viewport components updated to ui-next with design updates (#4886)
This commit is contained in:
1 parent
4e09f85d5d
commit
a8ac297e16
53 files changed
+933
-326
No files matched your search
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import DividerItem from './DividerItem';
|
||||
|
||||
type BackItemProps = {
|
||||
backLabel?: string;
|
||||
onBackClick: () => void;
|
||||
};
|
||||
|
||||
const BackItem = ({ backLabel, onBackClick }: BackItemProps) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center pl-1 pr-2 text-base hover:rounded"
|
||||
onClick={onBackClick}
|
||||
>
|
||||
<Icons.ByName
|
||||
name="content-prev"
|
||||
className="ml-2 mr-2"
|
||||
/>
|
||||
<span>{backLabel || 'Back to Display Options'}</span>
|
||||
</div>
|
||||
<DividerItem></DividerItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackItem;
|
||||
@@ -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,15 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
type HeaderItemProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const HeaderItem = ({ children }: HeaderItemProps) => {
|
||||
return (
|
||||
<div className="text-muted-foreground mx-2 flex h-6 shrink-0 items-center text-sm">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderItem;
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import OutsideClickHandler from 'react-outside-click-handler';
|
||||
import { MenuProps } from './Menu';
|
||||
import classNames from 'classnames';
|
||||
import { AllInOneMenu } from '..';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
|
||||
export interface IconMenuProps extends MenuProps {
|
||||
icon: string;
|
||||
iconClassName?: string;
|
||||
horizontalDirection?: AllInOneMenu.HorizontalDirection;
|
||||
verticalDirection?: AllInOneMenu.VerticalDirection;
|
||||
menuKey?: number | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An IconMenu allows for a div wrapped icon to be clicked to show and hide
|
||||
* an AllInOneMenu.Menu. Based on the direction(s) specified, the menu is
|
||||
* positioned relative to the icon.
|
||||
*
|
||||
* HorizontalDirection.LeftToRight - the left edges of the icon and menu are aligned
|
||||
* HorizontalDirection.RightRoLeft - the right edges of the icon and menu are aligned
|
||||
* VerticalDirection.TopToBottom - the top edge of the menu appears directly below the bottom edge of the icon
|
||||
* VerticalDirection.BottomToTop - the bottom edge of the menu appears directly above the top edge of the icon
|
||||
*
|
||||
* For example, if an IconMenu were situated in the bottom-left corner of a container,
|
||||
* it would be best to use BottomToTop and LeftToRight directions for it.
|
||||
*/
|
||||
export default function IconMenu({
|
||||
icon,
|
||||
iconClassName,
|
||||
horizontalDirection,
|
||||
verticalDirection,
|
||||
children,
|
||||
backLabel,
|
||||
menuClassName,
|
||||
menuStyle,
|
||||
onVisibilityChange,
|
||||
menuKey,
|
||||
}: IconMenuProps) {
|
||||
const [isMenuVisible, setIsMenuVisible] = useState(false);
|
||||
|
||||
const toggleMenuVisibility = useCallback(() => setIsMenuVisible(isVisible => !isVisible), []);
|
||||
|
||||
return (
|
||||
<OutsideClickHandler
|
||||
onOutsideClick={toggleMenuVisibility}
|
||||
disabled={!isMenuVisible}
|
||||
>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={iconClassName}
|
||||
onClick={toggleMenuVisibility}
|
||||
>
|
||||
<Icons.ByName name={icon} />
|
||||
</div>
|
||||
<AllInOneMenu.Menu
|
||||
key={menuKey}
|
||||
isVisible={isMenuVisible}
|
||||
backLabel={backLabel}
|
||||
menuClassName={classNames(
|
||||
menuClassName,
|
||||
'absolute',
|
||||
verticalDirection === AllInOneMenu.VerticalDirection.TopToBottom
|
||||
? 'top-[100%]'
|
||||
: 'bottom-[100%]',
|
||||
horizontalDirection === AllInOneMenu.HorizontalDirection.LeftToRight
|
||||
? 'left-0'
|
||||
: 'right-0'
|
||||
)}
|
||||
menuStyle={menuStyle}
|
||||
onVisibilityChange={isVis => {
|
||||
setIsMenuVisible(isVis);
|
||||
onVisibilityChange?.(isVis);
|
||||
}}
|
||||
horizontalDirection={horizontalDirection}
|
||||
>
|
||||
{children}
|
||||
</AllInOneMenu.Menu>
|
||||
</div>
|
||||
</OutsideClickHandler>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { ReactNode, useCallback, useContext } from 'react';
|
||||
import { MenuContext } from './Menu';
|
||||
|
||||
type ItemProps = {
|
||||
label: string;
|
||||
secondaryLabel?: string;
|
||||
icon?: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
onClick?: () => void;
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
useIconSpace?: boolean;
|
||||
};
|
||||
|
||||
const Item = ({
|
||||
label,
|
||||
secondaryLabel,
|
||||
icon,
|
||||
rightIcon,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
useIconSpace = false,
|
||||
}: ItemProps) => {
|
||||
const { hideMenu } = useContext(MenuContext);
|
||||
|
||||
const onClickHandler = useCallback(() => {
|
||||
hideMenu();
|
||||
onClick?.();
|
||||
}, [hideMenu, onClick]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base leading-[18px] hover:rounded"
|
||||
onClick={onClickHandler}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{(icon || useIconSpace) && (
|
||||
<div className="flex w-7 flex-shrink-0 items-center justify-center">{icon}</div>
|
||||
)}
|
||||
<span className="flex-grow">{label}</span>
|
||||
{secondaryLabel && (
|
||||
<span className="text-muted-foreground ml-2 flex-shrink-0">{secondaryLabel}</span>
|
||||
)}
|
||||
{rightIcon && <div className="ml-2 flex-shrink-0">{rightIcon}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Item;
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { ReactNode, useContext, useEffect } from 'react';
|
||||
import { MenuContext } from './Menu';
|
||||
import { ScrollArea } from '@ohif/ui-next';
|
||||
|
||||
type ItemPanelProps = {
|
||||
label?: string;
|
||||
index?: number;
|
||||
children: ReactNode;
|
||||
maxHeight?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const ItemPanel = ({
|
||||
label,
|
||||
index = 0,
|
||||
children,
|
||||
maxHeight = '250px',
|
||||
className,
|
||||
}: ItemPanelProps) => {
|
||||
const { addItemPanel, activePanelIndex } = useContext(MenuContext);
|
||||
|
||||
useEffect(() => {
|
||||
addItemPanel(index, label);
|
||||
}, [addItemPanel, index, label]);
|
||||
|
||||
return (
|
||||
activePanelIndex === index && (
|
||||
<ScrollArea
|
||||
style={{ scrollbarGutter: 'auto', maxHeight, height: '100%' }}
|
||||
className={`flex flex-col ${className || ''}`}
|
||||
>
|
||||
{children}
|
||||
</ScrollArea>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemPanel;
|
||||
@@ -0,0 +1,187 @@
|
||||
import React, { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import DividerItem from './DividerItem';
|
||||
import PanelSelector from './PanelSelector';
|
||||
import classNames from 'classnames';
|
||||
import BackItem from './BackItem';
|
||||
|
||||
/**
|
||||
* The vertical direction that the menu will be opened/used with.
|
||||
*
|
||||
* A TopToBottom menu would be used for cases where the menu is opened "near"
|
||||
* the top edge of its container. Likewise a BottomToTop menu would be used
|
||||
* for cases where the menu is opened "near" the bottom edge of its container.
|
||||
*
|
||||
* See IconMenu for more information.
|
||||
*/
|
||||
export enum VerticalDirection {
|
||||
TopToBottom,
|
||||
BottomToTop,
|
||||
}
|
||||
|
||||
/**
|
||||
* The horizontal direction that the menu is opened/used with.
|
||||
* This direction dictates the general direction sub-menus and
|
||||
* back-to-menus are opened with. For example, a RightToLeft menu
|
||||
* will have sub-menu items indicated with a left pointing chevron
|
||||
* and aligned with the left edge of the menu. Similarly back-to items of a
|
||||
* RightToLeft menu are indicated with a right pointing chevron and
|
||||
* aligned with the right edge of the menu.
|
||||
*
|
||||
* It is also worth noting that a LeftToRight menu would be used for
|
||||
* cases where a menu is opened "near" the left edge of its container.
|
||||
* Likewise, a RightToLeft menu would be used for cases where a menu is opened
|
||||
* "near" the right edge of its container.
|
||||
*
|
||||
* See IconMenu for more information.
|
||||
*/
|
||||
export enum HorizontalDirection {
|
||||
LeftToRight,
|
||||
RightToLeft,
|
||||
}
|
||||
|
||||
export interface MenuProps {
|
||||
menuStyle?: unknown;
|
||||
menuClassName?: string;
|
||||
isVisible?: boolean;
|
||||
preventHideMenu?: boolean;
|
||||
backLabel?: string;
|
||||
headerComponent?: ReactNode;
|
||||
showHeaderDivider?: boolean;
|
||||
activePanelIndex?: number;
|
||||
onVisibilityChange?: (isVisible: boolean) => void;
|
||||
horizontalDirection?: HorizontalDirection;
|
||||
children: ReactNode;
|
||||
}
|
||||
type MenuContextProps = {
|
||||
showSubMenu: (subMenuProps: MenuProps) => void;
|
||||
hideMenu: () => void;
|
||||
addItemPanel: (index: number, label: string) => void;
|
||||
horizontalDirection: HorizontalDirection;
|
||||
activePanelIndex: number;
|
||||
};
|
||||
|
||||
type MenuPathState = {
|
||||
props: MenuProps;
|
||||
activePanelIndex: number;
|
||||
};
|
||||
|
||||
export const MenuContext = createContext<MenuContextProps>(null);
|
||||
|
||||
const Menu = (props: MenuProps) => {
|
||||
const {
|
||||
isVisible,
|
||||
onVisibilityChange,
|
||||
activePanelIndex,
|
||||
preventHideMenu,
|
||||
menuClassName,
|
||||
menuStyle,
|
||||
horizontalDirection = HorizontalDirection.LeftToRight,
|
||||
} = props;
|
||||
|
||||
const [isMenuVisible, setIsMenuVisible] = useState(isVisible);
|
||||
|
||||
// The menuPath is an array consisting of this top Menu and every SubMenu
|
||||
// that has been traversed/opened by the user with the last item in the array
|
||||
// being the current (sub)menu that is currently visible. This allows for the previously
|
||||
// viewed menus to be returned to via the Back button at the top of the menu.
|
||||
const [menuPath, setMenuPath] = useState<Array<MenuPathState>>([
|
||||
{ props, activePanelIndex: activePanelIndex || 0 },
|
||||
]);
|
||||
const [itemPanelLabels, setItemPanelLabels] = useState<Array<string>>([]);
|
||||
|
||||
const hideMenu = useCallback(() => {
|
||||
if (preventHideMenu) {
|
||||
return;
|
||||
}
|
||||
setMenuPath(path => [path[0]]);
|
||||
setItemPanelLabels([]);
|
||||
setIsMenuVisible(false);
|
||||
onVisibilityChange?.(false);
|
||||
}, [preventHideMenu, onVisibilityChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
setIsMenuVisible(isVisible);
|
||||
onVisibilityChange?.(isVisible);
|
||||
} else {
|
||||
hideMenu();
|
||||
}
|
||||
}, [hideMenu, isVisible, onVisibilityChange]);
|
||||
|
||||
const showSubMenu = useCallback((subMenuProps: MenuProps) => {
|
||||
setMenuPath(path => {
|
||||
return [
|
||||
...path,
|
||||
{ props: subMenuProps, activePanelIndex: subMenuProps.activePanelIndex || 0 },
|
||||
];
|
||||
});
|
||||
setItemPanelLabels([]);
|
||||
}, []);
|
||||
|
||||
const addItemPanel = useCallback((index, label) => {
|
||||
setItemPanelLabels(labels => {
|
||||
return [...labels.slice(0, index), label, ...labels.slice(index + 1, labels.length)];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onActivePanelIndexChange = useCallback(index => {
|
||||
setMenuPath(path => {
|
||||
return [
|
||||
...path.slice(0, path.length - 1),
|
||||
{ ...path[path.length - 1], activePanelIndex: index },
|
||||
];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onBackClick = useCallback(() => {
|
||||
setMenuPath(path => [...path.slice(0, path.length - 1)]);
|
||||
setItemPanelLabels([]);
|
||||
}, []);
|
||||
|
||||
const { props: currentMenuProps, activePanelIndex: currentMenuActivePanelIndex } =
|
||||
menuPath[menuPath.length - 1];
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuContext.Provider
|
||||
value={{
|
||||
showSubMenu,
|
||||
hideMenu,
|
||||
addItemPanel,
|
||||
activePanelIndex: currentMenuActivePanelIndex,
|
||||
horizontalDirection,
|
||||
}}
|
||||
>
|
||||
{isMenuVisible && (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-popover/90 text-foreground flex select-none flex-col rounded px-1 py-1.5',
|
||||
menuClassName
|
||||
)}
|
||||
style={menuStyle}
|
||||
>
|
||||
{menuPath.length > 1 && (
|
||||
<BackItem
|
||||
backLabel={menuPath[menuPath.length - 2].props.backLabel}
|
||||
onBackClick={onBackClick}
|
||||
/>
|
||||
)}
|
||||
{itemPanelLabels.length > 1 && (
|
||||
<PanelSelector
|
||||
panelLabels={itemPanelLabels}
|
||||
activeIndex={currentMenuActivePanelIndex}
|
||||
onActiveIndexChange={onActivePanelIndexChange}
|
||||
></PanelSelector>
|
||||
)}
|
||||
{currentMenuProps.headerComponent}
|
||||
{currentMenuProps.showHeaderDivider && <DividerItem />}
|
||||
{currentMenuProps.children}
|
||||
</div>
|
||||
)}
|
||||
</MenuContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../Tabs';
|
||||
|
||||
type PanelSelectorProps = {
|
||||
panelLabels: Array<ReactNode>;
|
||||
onActiveIndexChange: (index: number) => void;
|
||||
activeIndex: number;
|
||||
};
|
||||
|
||||
const PanelSelector = ({ panelLabels, onActiveIndexChange, activeIndex }: PanelSelectorProps) => {
|
||||
return (
|
||||
<div className="mx-2 my-1 flex justify-center">
|
||||
<Tabs
|
||||
value={String(activeIndex)}
|
||||
onValueChange={val => onActiveIndexChange(parseInt(val, 10))}
|
||||
>
|
||||
<TabsList>
|
||||
{panelLabels.map((panelLabel, index) => (
|
||||
<TabsTrigger
|
||||
key={index}
|
||||
value={String(index)}
|
||||
>
|
||||
{panelLabel}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PanelSelector;
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { MenuContext, MenuProps } from './Menu';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
export interface SubMenuProps extends MenuProps {
|
||||
itemLabel: string;
|
||||
onClick?: () => void;
|
||||
itemIcon?: string;
|
||||
}
|
||||
|
||||
const SubMenu = (props: SubMenuProps) => {
|
||||
const { showSubMenu } = useContext(MenuContext);
|
||||
|
||||
const onClickHandler = useCallback(() => {
|
||||
showSubMenu(props);
|
||||
props.onClick?.();
|
||||
}, [showSubMenu, props]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hover:bg-accent flex h-8 w-full cursor-pointer items-center px-2 text-base hover:rounded"
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
{props.itemIcon && (
|
||||
<div className="flex w-7 flex-shrink-0 items-center justify-center">
|
||||
<Icons.ByName name={props.itemIcon}></Icons.ByName>
|
||||
</div>
|
||||
)}
|
||||
<span className="flex-grow">{props.itemLabel}</span>
|
||||
<div className="ml-2 flex-shrink-0">
|
||||
<Icons.ByName name="content-next"></Icons.ByName>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubMenu;
|
||||
@@ -0,0 +1,42 @@
|
||||
import Menu, { VerticalDirection, HorizontalDirection } from './Menu';
|
||||
import SubMenu from './SubMenu';
|
||||
import BackItem from './BackItem';
|
||||
import HeaderItem from './HeaderItem';
|
||||
import DividerItem from './DividerItem';
|
||||
import IconMenu from './IconMenu';
|
||||
import ItemPanel from './ItemPanel';
|
||||
import PanelSelector from './PanelSelector';
|
||||
import Item from './Item';
|
||||
|
||||
// Export enums
|
||||
export { VerticalDirection, HorizontalDirection };
|
||||
|
||||
// Export components
|
||||
export {
|
||||
Menu,
|
||||
SubMenu,
|
||||
BackItem,
|
||||
HeaderItem,
|
||||
DividerItem,
|
||||
IconMenu,
|
||||
ItemPanel,
|
||||
PanelSelector,
|
||||
Item,
|
||||
};
|
||||
|
||||
// Namespace for components and enums
|
||||
const AllInOneMenu = {
|
||||
Menu,
|
||||
SubMenu,
|
||||
BackItem,
|
||||
HeaderItem,
|
||||
DividerItem,
|
||||
IconMenu,
|
||||
ItemPanel,
|
||||
PanelSelector,
|
||||
Item,
|
||||
VerticalDirection,
|
||||
HorizontalDirection,
|
||||
};
|
||||
|
||||
export default AllInOneMenu;
|
||||
@@ -46,7 +46,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-input z-50 min-w-[8rem] overflow-hidden rounded border p-1 shadow-lg',
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-input/50 z-50 min-w-[8rem] overflow-hidden rounded border p-1 shadow-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -68,7 +68,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
props.onKeyDown?.(e);
|
||||
}}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground border-input z-50 min-w-[8rem] overflow-hidden rounded border p-1 shadow-md',
|
||||
'bg-popover text-popover-foreground border-input/50 z-50 min-w-[8rem] overflow-hidden rounded border p-1 shadow-md',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Icons } from '../Icons';
|
||||
interface ScrollAreaProps extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {
|
||||
/** Flag to show/hide scroll indicator arrows at top and bottom */
|
||||
showArrows?: boolean;
|
||||
type?: 'auto' | 'always' | 'scroll';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +57,7 @@ const ScrollArea = React.forwardRef<
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative h-full overflow-hidden', className)}
|
||||
type={props.type || 'auto'}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/* Scrollbar container */
|
||||
.scrollbarContainer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
padding: 5px 5px 0 5px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.scrollbarInner {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
/* Apply these styles to any range input inside our component */
|
||||
.scrollbarInput {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 0;
|
||||
height: 12px;
|
||||
transform: rotate(90deg);
|
||||
transform-origin: top left;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Thumb styles for WebKit browsers - more specific selector */
|
||||
input[type='range'].scrollbarInput::-webkit-slider-thumb {
|
||||
-webkit-appearance: none !important;
|
||||
background-color: hsl(var(--input));
|
||||
border: none;
|
||||
border-radius: 57px;
|
||||
cursor: -webkit-grab;
|
||||
height: 8px;
|
||||
margin-top: -4px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
input[type='range'].scrollbarInput::-webkit-slider-thumb:active,
|
||||
input[type='range'].scrollbarInput::-webkit-slider-thumb:hover {
|
||||
background-color: hsl(var(--primary));
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
/* Thumb styles for Firefox - more specific selector */
|
||||
input[type='range'].scrollbarInput::-moz-range-thumb {
|
||||
background-color: hsl(var(--input));
|
||||
border: none;
|
||||
border-radius: 57px;
|
||||
cursor: -moz-grab;
|
||||
height: 12px;
|
||||
width: 39px;
|
||||
}
|
||||
|
||||
input[type='range'].scrollbarInput::-moz-range-thumb:active,
|
||||
input[type='range'].scrollbarInput::-moz-range-thumb:hover {
|
||||
background-color: hsl(var(--primary));
|
||||
cursor: -moz-grabbing;
|
||||
}
|
||||
|
||||
/* Track styles for WebKit browsers - more specific selector */
|
||||
input[type='range'].scrollbarInput::-webkit-slider-runnable-track {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
/* Track styles for Firefox - more specific selector */
|
||||
input[type='range'].scrollbarInput::-moz-range-track {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/* Ensure focus doesn't show an outline */
|
||||
.scrollbarInput:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import styles from './ImageScrollbar.module.css';
|
||||
|
||||
export interface ImageScrollbarProps {
|
||||
value: number;
|
||||
max: number;
|
||||
height: string;
|
||||
onChange: (value: number) => void;
|
||||
onContextMenu?: (e: React.MouseEvent) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ImageScrollbar: React.FC<ImageScrollbarProps> = ({
|
||||
value,
|
||||
max,
|
||||
height,
|
||||
onChange,
|
||||
onContextMenu = e => e.preventDefault(),
|
||||
className = '',
|
||||
}) => {
|
||||
if (max === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = {
|
||||
width: height, // This is intentional for the rotation
|
||||
};
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const intValue = parseInt(event.target.value, 10);
|
||||
onChange(intValue);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
|
||||
// We don't allow direct keyboard navigation (arrow keys)
|
||||
const keys = {
|
||||
DOWN: 40,
|
||||
UP: 38,
|
||||
};
|
||||
|
||||
if (event.which === keys.DOWN || event.which === keys.UP) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(styles.scrollbarContainer, className)}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
<div className={styles.scrollbarInner}>
|
||||
<input
|
||||
className={cn(styles.scrollbarInput, 'mousetrap')}
|
||||
style={style}
|
||||
type="range"
|
||||
min="0"
|
||||
max={max}
|
||||
step="1"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-label="Image navigation scrollbar"
|
||||
data-testid="image-scrollbar-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
|
||||
const arrowClasses =
|
||||
'cursor-pointer flex items-center justify-center shrink-0 text-primary-light active:text-white hover:bg-secondary-light/60 rounded';
|
||||
'cursor-pointer flex items-center justify-center shrink-0 text-highlight active:text-foreground hover:bg-primary/30 rounded';
|
||||
|
||||
/**
|
||||
* A small set of left/right arrow icons for stepping through slices or series.
|
||||
|
||||
@@ -13,7 +13,7 @@ export enum ViewportActionCornersLocations {
|
||||
bottomRight,
|
||||
}
|
||||
|
||||
const commonClasses = 'pointer-events-auto flex items-center gap-1';
|
||||
const commonClasses = 'pointer-events-auto flex items-center';
|
||||
const locationClasses = {
|
||||
[ViewportActionCornersLocations.topLeft]: classNames(
|
||||
commonClasses,
|
||||
@@ -21,7 +21,7 @@ const locationClasses = {
|
||||
),
|
||||
[ViewportActionCornersLocations.topRight]: classNames(
|
||||
commonClasses,
|
||||
'absolute top-[4px] right-[4px] right-viewport-scrollbar'
|
||||
'absolute top-[4px] right-[16px] right-viewport-scrollbar'
|
||||
),
|
||||
[ViewportActionCornersLocations.bottomLeft]: classNames(
|
||||
commonClasses,
|
||||
@@ -64,4 +64,4 @@ ViewportActionCorners.propTypes = {
|
||||
cornerComponents: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export { ViewportActionCorners };
|
||||
export { ViewportActionCorners };
|
||||
@@ -2,7 +2,7 @@
|
||||
display: none;
|
||||
}
|
||||
.ViewportOverlay {
|
||||
color: #9ccef9;
|
||||
color: hsl(var(--highlight));
|
||||
}
|
||||
.ViewportOverlay .overlay-element {
|
||||
position: absolute;
|
||||
@@ -11,12 +11,12 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
.overlay-top {
|
||||
top: 28px;
|
||||
top: 2.15rem;
|
||||
}
|
||||
.overlay-bottom {
|
||||
bottom: 3px;
|
||||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.overlay-text {
|
||||
text-shadow: 0.8px 0.8px 0.5px rgba(0, 0, 0, 0.75);
|
||||
text-shadow: 0.8px 0.8px 0.5px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
@@ -17,17 +17,11 @@ const classes = {
|
||||
bottomLeft: 'overlay-bottom left-viewport',
|
||||
};
|
||||
|
||||
function ViewportOverlay({
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomRight,
|
||||
bottomLeft,
|
||||
color = 'text-primary-light',
|
||||
}) {
|
||||
function ViewportOverlay({ topLeft, topRight, bottomRight, bottomLeft, color = 'text-highlight' }) {
|
||||
const overlay = 'absolute pointer-events-none viewport-overlay';
|
||||
|
||||
return (
|
||||
<div className={classNames(color, 'overlay-text text-[13px] leading-5')}>
|
||||
<div className={classNames(color, 'overlay-text text-base leading-5')}>
|
||||
<div
|
||||
data-cy="viewport-overlay-top-left"
|
||||
className={classNames(overlay, classes.topLeft)}
|
||||
@@ -37,14 +31,14 @@ function ViewportOverlay({
|
||||
<div
|
||||
data-cy="viewport-overlay-top-right"
|
||||
className={classNames(overlay, classes.topRight)}
|
||||
style={{ transform: 'translateX(-8px)' }}
|
||||
style={{ transform: 'translateX(9px)' }}
|
||||
>
|
||||
{topRight}
|
||||
</div>
|
||||
<div
|
||||
data-cy="viewport-overlay-bottom-right"
|
||||
className={classNames(overlay, classes.bottomRight)}
|
||||
style={{ transform: 'translateX(-8px)' }}
|
||||
style={{ transform: 'translateX(6px)' }}
|
||||
>
|
||||
{bottomRight}
|
||||
</div>
|
||||
|
||||
@@ -68,8 +68,8 @@ function ViewportPane({
|
||||
{/* Border overlay */}
|
||||
<div
|
||||
className={classNames('pointer-events-none absolute inset-0', {
|
||||
'border-primary-light border': isActive,
|
||||
'group-hover:border-primary-light/70 border border-transparent': !isActive,
|
||||
'border-highlight rounded-md border': isActive,
|
||||
'group-hover:border-highlight/50 rounded-md border border-transparent': !isActive,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,3 +6,4 @@ export { ViewportPane } from './ViewportPane';
|
||||
export { ViewportActionCorners, ViewportActionCornersLocations } from './ViewportActionCorners';
|
||||
export { ViewportOverlay } from './ViewportOverlay';
|
||||
export { ViewportGrid } from './ViewportGrid';
|
||||
export { ImageScrollbar } from './ImageScrollbar';
|
||||
@@ -81,8 +81,6 @@ import {
|
||||
} from './DropdownMenu';
|
||||
import { Onboarding } from './Onboarding';
|
||||
import { DoubleSlider } from './DoubleSlider';
|
||||
export { DataRow } from './DataRow';
|
||||
export { MeasurementTable } from './MeasurementTable';
|
||||
import {
|
||||
SegmentationTable,
|
||||
useSegmentationTableContext,
|
||||
@@ -92,7 +90,6 @@ import {
|
||||
import { Toaster, toast } from './Sonner';
|
||||
import { StudySummary } from './StudySummary';
|
||||
import { ErrorBoundary } from './Errorboundary';
|
||||
export * from './ColorCircle';
|
||||
import { Header } from './Header';
|
||||
import { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card';
|
||||
import {
|
||||
@@ -105,6 +102,7 @@ import {
|
||||
ViewportActionCornersLocations,
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
ImageScrollbar,
|
||||
} from './Viewport';
|
||||
import {
|
||||
ToolButton,
|
||||
@@ -115,6 +113,11 @@ import {
|
||||
ToolButtonListDivider,
|
||||
} from './ToolButton';
|
||||
import { ToolSettings } from './OHIFToolSettings';
|
||||
export { DataRow } from './DataRow';
|
||||
export { MeasurementTable } from './MeasurementTable';
|
||||
export * from './ColorCircle';
|
||||
export { default as AllInOneMenu } from './AllInOneMenu';
|
||||
export * from './AllInOneMenu';
|
||||
|
||||
// Segmentation Context Exports
|
||||
export { useSegmentationTableContext, useSegmentationExpanded, useSegmentStatistics };
|
||||
@@ -232,6 +235,7 @@ export {
|
||||
ViewportActionCornersLocations,
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
ImageScrollbar,
|
||||
Clipboard,
|
||||
ToolButton,
|
||||
ToolButtonList,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 236 45% 21%;
|
||||
--input: 236 52% 30%;
|
||||
--ring: 214 98% 60%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
@@ -69,7 +69,7 @@
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 236 45% 21%;
|
||||
--input: 236 52% 30%;
|
||||
--ring: 214 98% 60%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
@@ -252,4 +252,4 @@ input[type='number']::-webkit-outer-spin-button {
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield; /* For Firefox */
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user