ui(components): migrates to new toolbar ui components and Numeric for Input InputRange and InputDoubleRange (#4728)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
parent
ac5ae4a29e
commit
a8d84443a9
@ -1,6 +1,15 @@
|
||||
export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
const { segmentationService, toolbarService, toolGroupService } = servicesManager.services;
|
||||
return [
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentation',
|
||||
evaluate: ({ viewportId }) => {
|
||||
const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
|
||||
return {
|
||||
disabled: !segmentations?.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
|
||||
@ -12,7 +21,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
if (!segmentations?.length) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright !bg-black opacity-50',
|
||||
disabledText: disabledText ?? 'No segmentations available',
|
||||
};
|
||||
}
|
||||
@ -22,7 +30,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
if (!toolGroup) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
@ -32,7 +39,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
if (!toolGroup.hasTool(toolName) && !toolNames) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
@ -43,12 +49,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
className: isPrimaryActive
|
||||
? '!text-black !bg-primary-light hover:bg-primary-light hover-text-black hover:cursor-pointer'
|
||||
: '!text-common-bright !bg-black hover:bg-primary-light hover:cursor-pointer hover:text-black',
|
||||
// Todo: isActive right now is used for nested buttons where the primary
|
||||
// button needs to be fully rounded (vs partial rounded) when active
|
||||
// otherwise it does not have any other use
|
||||
isActive: isPrimaryActive,
|
||||
};
|
||||
},
|
||||
|
||||
@ -1,14 +1,8 @@
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
|
||||
const getToggledClassName = (isToggled: boolean) => {
|
||||
return isToggled
|
||||
? '!text-primary-active'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
|
||||
};
|
||||
import { utils } from '@ohif/ui-next';
|
||||
|
||||
const getDisabledState = (disabledText?: string) => ({
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
});
|
||||
|
||||
@ -93,9 +87,6 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
className: isPrimaryActive
|
||||
? '!text-black bg-primary-light rounded'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light rounded',
|
||||
// Todo: isActive right now is used for nested buttons where the primary
|
||||
// button needs to be fully rounded (vs partial rounded) when active
|
||||
// otherwise it does not have any other use
|
||||
@ -153,9 +144,9 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
},
|
||||
{
|
||||
name: 'evaluate.action',
|
||||
evaluate: ({ viewportId, button }) => {
|
||||
evaluate: () => {
|
||||
return {
|
||||
className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
disabled: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
@ -190,7 +181,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
|
||||
if (!synchronizers?.length) {
|
||||
return {
|
||||
className: getToggledClassName(false),
|
||||
className: utils.getToggledClassName(false),
|
||||
};
|
||||
}
|
||||
|
||||
@ -204,7 +195,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
|
||||
if (!synchronizers?.length) {
|
||||
return {
|
||||
className: getToggledClassName(false),
|
||||
className: utils.getToggledClassName(false),
|
||||
};
|
||||
}
|
||||
|
||||
@ -216,7 +207,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
const isEnabled = synchronizer?._enabled;
|
||||
|
||||
return {
|
||||
className: getToggledClassName(isEnabled),
|
||||
className: utils.getToggledClassName(isEnabled),
|
||||
};
|
||||
},
|
||||
},
|
||||
@ -239,14 +230,13 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
if (!prop) {
|
||||
return {
|
||||
disabled: false,
|
||||
className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
};
|
||||
}
|
||||
|
||||
const isToggled = prop;
|
||||
|
||||
return {
|
||||
className: getToggledClassName(isToggled),
|
||||
className: utils.getToggledClassName(isToggled),
|
||||
};
|
||||
},
|
||||
},
|
||||
@ -275,7 +265,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
className: getToggledClassName(isMpr),
|
||||
className: utils.getToggledClassName(isMpr),
|
||||
};
|
||||
},
|
||||
},
|
||||
@ -304,6 +294,6 @@ function _evaluateToggle({
|
||||
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);
|
||||
|
||||
return {
|
||||
className: getToggledClassName(!isOff),
|
||||
className: utils.getToggledClassName(!isOff),
|
||||
};
|
||||
}
|
||||
|
||||
@ -160,7 +160,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
ViewportActionCornersProvider
|
||||
);
|
||||
|
||||
const { syncGroupService, customizationService } = servicesManager.services;
|
||||
const { syncGroupService } = servicesManager.services;
|
||||
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
|
||||
|
||||
return init.call(this, props);
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuItem,
|
||||
Icons,
|
||||
Button,
|
||||
} from '@ohif/ui-next';
|
||||
@ -18,6 +19,13 @@ const getMenuItemsDefault = ({ commandsManager, items, servicesManager, ...props
|
||||
// getMenuItems can also be replaced by providing it to the MoreDropdownMenu
|
||||
const menuContent = customizationService.getCustomization('ohif.menuContent');
|
||||
|
||||
// Default menu item component if none is provided through customization
|
||||
const DefaultMenuItem = ({ item }) => (
|
||||
<DropdownMenuItem onClick={item.onClick}>{item.label || item.title}</DropdownMenuItem>
|
||||
);
|
||||
|
||||
const MenuItemComponent = menuContent?.content || DefaultMenuItem;
|
||||
|
||||
return (
|
||||
<DropdownMenuContent
|
||||
hideWhenDetached
|
||||
@ -27,15 +35,15 @@ const getMenuItemsDefault = ({ commandsManager, items, servicesManager, ...props
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{items?.map((item, index) =>
|
||||
React.createElement(menuContent.content, {
|
||||
key: item.id || `menu-item-${index}`,
|
||||
item,
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
...props,
|
||||
})
|
||||
)}
|
||||
{items?.map((item, index) => (
|
||||
<MenuItemComponent
|
||||
key={item.id || `menu-item-${index}`}
|
||||
item={item}
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
);
|
||||
};
|
||||
|
||||
44
extensions/default/src/Toolbar/ToolBoxWrapper.tsx
Normal file
44
extensions/default/src/Toolbar/ToolBoxWrapper.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ToolButton } from '@ohif/ui-next';
|
||||
|
||||
/**
|
||||
* Wraps the ToolButtonList component to handle the OHIF toolbar button structure
|
||||
* @param props - Component props
|
||||
* @returns Component
|
||||
*/
|
||||
export function ToolBoxButtonGroupWrapper({ groupId, items, onInteraction, ...props }) {
|
||||
if (!items || !groupId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-popover flex flex-row space-x-1 rounded-md px-0 py-0">
|
||||
{items.map(item => (
|
||||
<ToolButton
|
||||
{...item}
|
||||
key={item.id}
|
||||
size="small"
|
||||
className={props.disabled && 'text-primary'}
|
||||
onInteraction={() =>
|
||||
onInteraction?.({ groupId, itemId: item.id, commands: item.commands })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolBoxButtonWrapper({ onInteraction, ...props }) {
|
||||
return (
|
||||
<div className="bg-popover flex flex-row rounded-md px-0 py-0">
|
||||
<ToolButton
|
||||
{...props}
|
||||
id={props.id}
|
||||
size="small"
|
||||
className={classNames(props.disabled && 'text-primary')}
|
||||
onInteraction={() => onInteraction?.({ itemId: props.id, commands: props.commands })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
84
extensions/default/src/Toolbar/ToolButtonListWrapper.tsx
Normal file
84
extensions/default/src/Toolbar/ToolButtonListWrapper.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
ToolButtonList,
|
||||
ToolButton,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
} from '@ohif/ui-next';
|
||||
|
||||
interface ButtonItem {
|
||||
id: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
isActive?: boolean;
|
||||
disabledText?: string;
|
||||
commands?: Record<string, unknown>;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface ToolButtonListWrapperProps {
|
||||
groupId: string;
|
||||
primary: ButtonItem;
|
||||
items: ButtonItem[];
|
||||
onInteraction?: (details: {
|
||||
groupId: string;
|
||||
itemId: string;
|
||||
commands?: Record<string, unknown>;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the ToolButtonList component to handle the OHIF toolbar button structure
|
||||
* @param props - Component props
|
||||
* @returns Component
|
||||
* // test
|
||||
*/
|
||||
export default function ToolButtonListWrapper({
|
||||
groupId,
|
||||
primary,
|
||||
items,
|
||||
onInteraction,
|
||||
}: ToolButtonListWrapperProps) {
|
||||
return (
|
||||
<ToolButtonList>
|
||||
<ToolButtonListDefault>
|
||||
<div
|
||||
data-cy={`${groupId}-split-button-primary`}
|
||||
data-tool={primary.id}
|
||||
data-active={primary.isActive}
|
||||
>
|
||||
<ToolButton
|
||||
{...primary}
|
||||
onInteraction={({ itemId }) =>
|
||||
onInteraction?.({ groupId, itemId, commands: primary.commands })
|
||||
}
|
||||
className={primary.className}
|
||||
/>
|
||||
</div>
|
||||
</ToolButtonListDefault>
|
||||
<ToolButtonListDivider className={primary.isActive ? 'opacity-0' : 'opacity-100'} />
|
||||
<div data-cy={`${groupId}-split-button-secondary`}>
|
||||
<ToolButtonListDropDown>
|
||||
{items.map(item => (
|
||||
<ToolButtonListItem
|
||||
key={item.id}
|
||||
{...item}
|
||||
data-cy={item.id}
|
||||
data-tool={item.id}
|
||||
data-active={item.isActive}
|
||||
onSelect={() =>
|
||||
onInteraction?.({ groupId, itemId: item.id, commands: item.commands })
|
||||
}
|
||||
>
|
||||
<span className="pl-1">{item.label || item.tooltip || item.id}</span>
|
||||
</ToolButtonListItem>
|
||||
))}
|
||||
</ToolButtonListDropDown>
|
||||
</div>
|
||||
</ToolButtonList>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,4 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from '@ohif/ui';
|
||||
import classnames from 'classnames';
|
||||
import { useToolbar } from '@ohif/core';
|
||||
|
||||
export function Toolbar({ servicesManager, buttonSection = 'primary' }) {
|
||||
@ -15,7 +13,7 @@ export function Toolbar({ servicesManager, buttonSection = 'primary' }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{toolbarButtons.map(toolDef => {
|
||||
{toolbarButtons?.map(toolDef => {
|
||||
if (!toolDef) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ToolbarButton, ButtonGroup } from '@ohif/ui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { ToolbarButton, ButtonGroup } from '@ohif/ui';
|
||||
|
||||
function ToolbarButtonGroupWithServices({ groupId, items, onInteraction, size }) {
|
||||
const getSplitButtonItems = useCallback(
|
||||
|
||||
@ -107,18 +107,18 @@ const useResizablePanels = (
|
||||
// And by virtue of the dependency on the minimum size state variables, this code
|
||||
// is executed on the render following an update of the minimum percentage sizes
|
||||
// for a panel.
|
||||
if (!resizableLeftPanelAPIRef.current.isCollapsed()) {
|
||||
if (!resizableLeftPanelAPIRef.current?.isCollapsed()) {
|
||||
const leftSize = getPercentageSize(
|
||||
leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize
|
||||
);
|
||||
resizableLeftPanelAPIRef.current.resize(leftSize);
|
||||
resizableLeftPanelAPIRef.current?.resize(leftSize);
|
||||
}
|
||||
|
||||
if (!resizableRightPanelAPIRef.current.isCollapsed()) {
|
||||
if (!resizableRightPanelAPIRef?.current?.isCollapsed()) {
|
||||
const rightSize = getPercentageSize(
|
||||
rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize
|
||||
);
|
||||
resizableRightPanelAPIRef.current.resize(rightSize);
|
||||
resizableRightPanelAPIRef?.current?.resize(rightSize);
|
||||
}
|
||||
|
||||
// This observer kicks in when the ViewportLayout resizable panel group
|
||||
@ -199,7 +199,7 @@ const useResizablePanels = (
|
||||
}, [setLeftPanelClosed]);
|
||||
|
||||
const onLeftPanelResize = useCallback(size => {
|
||||
if (!resizablePanelGroupElemRef?.current || resizableLeftPanelAPIRef.current.isCollapsed()) {
|
||||
if (!resizablePanelGroupElemRef?.current || resizableLeftPanelAPIRef.current?.isCollapsed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -228,7 +228,7 @@ const useResizablePanels = (
|
||||
}, [setRightPanelClosed]);
|
||||
|
||||
const onRightPanelResize = useCallback(size => {
|
||||
if (!resizablePanelGroupElemRef?.current || resizableRightPanelAPIRef.current.isCollapsed()) {
|
||||
if (!resizablePanelGroupElemRef?.current || resizableRightPanelAPIRef?.current?.isCollapsed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -248,7 +248,7 @@ const useResizablePanels = (
|
||||
* Note that the width attributed to the handles must be taken into account.
|
||||
*/
|
||||
const getPercentageSize = pixelSize => {
|
||||
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
|
||||
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current?.getBoundingClientRect();
|
||||
return (pixelSize / (panelGroupWidth - resizableHandlesWidth.current)) * 100;
|
||||
};
|
||||
|
||||
@ -257,7 +257,7 @@ const useResizablePanels = (
|
||||
* Note that the width attributed to the handles must be taken into account.
|
||||
*/
|
||||
const getExpandedPixelWidth = percentageSize => {
|
||||
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect();
|
||||
const { width: panelGroupWidth } = resizablePanelGroupElemRef.current?.getBoundingClientRect();
|
||||
const expandedWidth =
|
||||
(percentageSize / 100) * (panelGroupWidth - resizableHandlesWidth.current) -
|
||||
panelGroupDefinition.shared.expandedInsideBorderSize;
|
||||
|
||||
@ -1,42 +1,61 @@
|
||||
import ToolbarDivider from './Toolbar/ToolbarDivider';
|
||||
import { ToolbarButton as ToolbarButtonLegacy } from '@ohif/ui';
|
||||
import { ToolButton, utils } from '@ohif/ui-next';
|
||||
|
||||
import ToolbarLayoutSelectorWithServices from './Toolbar/ToolbarLayoutSelector';
|
||||
import ToolbarSplitButtonWithServices from './Toolbar/ToolbarSplitButtonWithServices';
|
||||
import ToolbarButtonGroupWithServices from './Toolbar/ToolbarButtonGroupWithServices';
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
|
||||
// legacy
|
||||
import ToolbarDividerLegacy from './Toolbar/ToolbarDivider';
|
||||
import ToolbarSplitButtonWithServicesLegacy from './Toolbar/ToolbarSplitButtonWithServices';
|
||||
import ToolbarButtonGroupWithServicesLegacy from './Toolbar/ToolbarButtonGroupWithServices';
|
||||
import { ProgressDropdownWithService } from './Components/ProgressDropdownWithService';
|
||||
|
||||
const getClassName = isToggled => {
|
||||
return {
|
||||
className: isToggled
|
||||
? '!text-primary-active'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
};
|
||||
};
|
||||
// new
|
||||
import ToolButtonListWrapper from './Toolbar/ToolButtonListWrapper';
|
||||
import { ToolBoxButtonGroupWrapper, ToolBoxButtonWrapper } from './Toolbar/ToolBoxWrapper';
|
||||
|
||||
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
|
||||
const { cineService } = servicesManager.services;
|
||||
return [
|
||||
// new
|
||||
{
|
||||
name: 'ohif.toolButton',
|
||||
defaultComponent: ToolButton,
|
||||
},
|
||||
{
|
||||
name: 'ohif.toolButtonList',
|
||||
defaultComponent: ToolButtonListWrapper,
|
||||
},
|
||||
{
|
||||
name: 'ohif.toolBoxButtonGroup',
|
||||
defaultComponent: ToolBoxButtonGroupWrapper,
|
||||
},
|
||||
{
|
||||
name: 'ohif.toolBoxButton',
|
||||
defaultComponent: ToolBoxButtonWrapper,
|
||||
},
|
||||
// legacy
|
||||
{
|
||||
name: 'ohif.radioGroup',
|
||||
defaultComponent: ToolbarButton,
|
||||
defaultComponent: ToolbarButtonLegacy,
|
||||
},
|
||||
{
|
||||
name: 'ohif.buttonGroup',
|
||||
defaultComponent: ToolbarButtonGroupWithServicesLegacy,
|
||||
},
|
||||
{
|
||||
name: 'ohif.divider',
|
||||
defaultComponent: ToolbarDivider,
|
||||
defaultComponent: ToolbarDividerLegacy,
|
||||
},
|
||||
{
|
||||
name: 'ohif.splitButton',
|
||||
defaultComponent: ToolbarSplitButtonWithServices,
|
||||
defaultComponent: ToolbarSplitButtonWithServicesLegacy,
|
||||
},
|
||||
// others
|
||||
{
|
||||
name: 'ohif.layoutSelector',
|
||||
defaultComponent: props =>
|
||||
ToolbarLayoutSelectorWithServices({ ...props, commandsManager, servicesManager }),
|
||||
},
|
||||
{
|
||||
name: 'ohif.buttonGroup',
|
||||
defaultComponent: ToolbarButtonGroupWithServices,
|
||||
},
|
||||
{
|
||||
name: 'ohif.progressDropdown',
|
||||
defaultComponent: ProgressDropdownWithService,
|
||||
@ -66,7 +85,9 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
|
||||
name: 'evaluate.cine',
|
||||
evaluate: () => {
|
||||
const isToggled = cineService.getState().isCineEnabled;
|
||||
return getClassName(isToggled);
|
||||
return {
|
||||
className: utils.getToggledClassName(isToggled),
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -13,9 +13,8 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
|
||||
PatientDOB: '',
|
||||
});
|
||||
const [isMixedPatients, setIsMixedPatients] = useState(false);
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
const checkMixedPatients = PatientID => {
|
||||
const checkMixedPatients = (PatientID: string) => {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
let isMixedPatients = false;
|
||||
displaySets.forEach(displaySet => {
|
||||
@ -30,8 +29,11 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
|
||||
setIsMixedPatients(isMixedPatients);
|
||||
};
|
||||
|
||||
const updatePatientInfo = () => {
|
||||
const displaySet = displaySets[0];
|
||||
const updatePatientInfo = ({ displaySetsAdded }) => {
|
||||
if (!displaySetsAdded.length) {
|
||||
return;
|
||||
}
|
||||
const displaySet = displaySetsAdded[0];
|
||||
const instance = displaySet?.instances?.[0] || displaySet?.instance;
|
||||
if (!instance) {
|
||||
return;
|
||||
@ -39,7 +41,7 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
|
||||
|
||||
setPatientInfo({
|
||||
PatientID: instance.PatientID || null,
|
||||
PatientName: instance.PatientName ? formatPN(instance.PatientName ) : null,
|
||||
PatientName: instance.PatientName ? formatPN(instance.PatientName) : null,
|
||||
PatientSex: instance.PatientSex || null,
|
||||
PatientDOB: formatDate(instance.PatientBirthDate) || null,
|
||||
});
|
||||
@ -49,15 +51,11 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
|
||||
useEffect(() => {
|
||||
const subscription = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
() => updatePatientInfo()
|
||||
props => updatePatientInfo(props)
|
||||
);
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
updatePatientInfo();
|
||||
}, [displaySets]);
|
||||
|
||||
return { patientInfo, isMixedPatients };
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,6 @@ import { PanelStudyBrowserHeader } from './Panels/StudyBrowser/PanelStudyBrowser
|
||||
import * as utils from './utils';
|
||||
import MoreDropdownMenu from './Components/MoreDropdownMenu';
|
||||
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
|
||||
|
||||
const defaultExtension: Types.Extensions.Extension = {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
|
||||
@ -323,7 +323,7 @@ function _getStatusComponent(isTracked, t) {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Icons.StatusTracking className="text-aqua-pale" />
|
||||
<Icons.StatusTracking className="text-muted-foreground" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
|
||||
@ -35,7 +35,7 @@ function _createSetToolActiveCommands(toolName, toolGroupIds = ['default', 'mpr'
|
||||
const toolbarButtons: Button[] = [
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
uiType: 'ohif.splitButton',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
groupId: 'MeasurementTools',
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
@ -81,7 +81,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Zoom',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
@ -91,7 +91,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
uiType: 'ohif.splitButton',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
groupId: 'WindowLevel',
|
||||
primary: ToolbarService.createButton({
|
||||
@ -118,7 +118,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Pan',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
@ -128,7 +128,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-capture',
|
||||
label: 'Capture',
|
||||
@ -163,7 +163,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'MoreTools',
|
||||
uiType: 'ohif.splitButton',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
groupId: 'MoreTools',
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
|
||||
@ -14,7 +14,7 @@ const ReferenceLinesListeners: RunCommand = [
|
||||
const moreTools = [
|
||||
{
|
||||
id: 'MoreTools',
|
||||
uiType: 'ohif.splitButton',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
groupId: 'MoreTools',
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
|
||||
@ -43,7 +43,7 @@ export const setToolActiveToolbar = {
|
||||
const toolbarButtons: Button[] = [
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
uiType: 'ohif.splitButton',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
groupId: 'MeasurementTools',
|
||||
// group evaluate to determine which item should move to the top
|
||||
@ -130,7 +130,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Zoom',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
@ -141,7 +141,7 @@ const toolbarButtons: Button[] = [
|
||||
// Window Level
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
uiType: 'ohif.splitButton',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
groupId: 'WindowLevel',
|
||||
primary: createButton({
|
||||
@ -170,7 +170,7 @@ const toolbarButtons: Button[] = [
|
||||
// Pan...
|
||||
{
|
||||
id: 'Pan',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-move',
|
||||
@ -181,7 +181,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'MPR',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'icon-mpr',
|
||||
label: 'MPR',
|
||||
@ -198,7 +198,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'TrackBallRotate',
|
||||
type: 'ohif.radioGroup',
|
||||
type: 'ohif.toolButton',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-3d-rotate',
|
||||
@ -208,7 +208,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-capture',
|
||||
label: 'Capture',
|
||||
@ -234,7 +234,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Crosshairs',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-crosshair',
|
||||
@ -250,4 +250,4 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export default toolbarButtons;
|
||||
export default toolbarButtons;
|
||||
@ -84,13 +84,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
* Lifecycle hooks
|
||||
*/
|
||||
onModeEnter: function ({ servicesManager, extensionManager, commandsManager }: withAppTypes) {
|
||||
const {
|
||||
measurementService,
|
||||
toolbarService,
|
||||
toolGroupService,
|
||||
panelService,
|
||||
segmentationService,
|
||||
} = servicesManager.services;
|
||||
const { measurementService, toolbarService, toolGroupService } = servicesManager.services;
|
||||
|
||||
measurementService.clearMeasurements();
|
||||
|
||||
@ -112,32 +106,32 @@ function modeFactory({ modeConfiguration }) {
|
||||
|
||||
// // ActivatePanel event trigger for when a segmentation or measurement is added.
|
||||
// // Do not force activation so as to respect the state the user may have left the UI in.
|
||||
_activatePanelTriggersSubscriptions = [
|
||||
...panelService.addActivatePanelTriggers(
|
||||
cornerstone.segmentation,
|
||||
[
|
||||
{
|
||||
sourcePubSubService: segmentationService,
|
||||
sourceEvents: [segmentationService.EVENTS.SEGMENTATION_ADDED],
|
||||
},
|
||||
],
|
||||
true
|
||||
),
|
||||
...panelService.addActivatePanelTriggers(
|
||||
tracked.measurements,
|
||||
[
|
||||
{
|
||||
sourcePubSubService: measurementService,
|
||||
sourceEvents: [
|
||||
measurementService.EVENTS.MEASUREMENT_ADDED,
|
||||
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
|
||||
],
|
||||
},
|
||||
],
|
||||
true
|
||||
),
|
||||
true,
|
||||
];
|
||||
// _activatePanelTriggersSubscriptions = [
|
||||
// ...panelService.addActivatePanelTriggers(
|
||||
// cornerstone.segmentation,
|
||||
// [
|
||||
// {
|
||||
// sourcePubSubService: segmentationService,
|
||||
// sourceEvents: [segmentationService.EVENTS.SEGMENTATION_ADDED],
|
||||
// },
|
||||
// ],
|
||||
// true
|
||||
// ),
|
||||
// ...panelService.addActivatePanelTriggers(
|
||||
// tracked.measurements,
|
||||
// [
|
||||
// {
|
||||
// sourcePubSubService: measurementService,
|
||||
// sourceEvents: [
|
||||
// measurementService.EVENTS.MEASUREMENT_ADDED,
|
||||
// measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// true
|
||||
// ),
|
||||
// true,
|
||||
// ];
|
||||
},
|
||||
onModeExit: ({ servicesManager }: withAppTypes) => {
|
||||
const {
|
||||
|
||||
@ -14,7 +14,7 @@ const ReferenceLinesListeners: RunCommand = [
|
||||
const moreTools = [
|
||||
{
|
||||
id: 'MoreTools',
|
||||
uiType: 'ohif.splitButton',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
groupId: 'MoreTools',
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
|
||||
@ -15,7 +15,7 @@ export const setToolActiveToolbar = {
|
||||
const toolbarButtons: Button[] = [
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
uiType: 'ohif.splitButton',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
groupId: 'MeasurementTools',
|
||||
// group evaluate to determine which item should move to the top
|
||||
@ -110,7 +110,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Zoom',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
@ -121,7 +121,7 @@ const toolbarButtons: Button[] = [
|
||||
// Window Level
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-window-level',
|
||||
label: 'Window Level',
|
||||
@ -138,7 +138,7 @@ const toolbarButtons: Button[] = [
|
||||
// Pan...
|
||||
{
|
||||
id: 'Pan',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-move',
|
||||
@ -149,7 +149,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'TrackballRotate',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-3d-rotate',
|
||||
@ -163,7 +163,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-capture',
|
||||
label: 'Capture',
|
||||
@ -188,7 +188,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Crosshairs',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-crosshair',
|
||||
|
||||
@ -3,7 +3,7 @@ import { ToolbarService } from '@ohif/core';
|
||||
const toolbarButtons = [
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
uiType: 'ohif.splitButton',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
groupId: 'MeasurementTools',
|
||||
// group evaluate to determine which item should move to the top
|
||||
@ -131,7 +131,7 @@ const toolbarButtons = [
|
||||
},
|
||||
{
|
||||
id: 'dragPan',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
@ -147,7 +147,7 @@ const toolbarButtons = [
|
||||
},
|
||||
{
|
||||
id: 'TagBrowser',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'dicom-tag-browser',
|
||||
label: 'Dicom Tag Browser',
|
||||
@ -161,4 +161,4 @@ const toolbarButtons = [
|
||||
},
|
||||
];
|
||||
|
||||
export default toolbarButtons;
|
||||
export default toolbarButtons;
|
||||
@ -3,9 +3,10 @@ import type { Button } from '@ohif/core/types';
|
||||
const toolbarButtons: Button[] = [
|
||||
{
|
||||
id: 'BrushTools',
|
||||
uiType: 'ohif.buttonGroup',
|
||||
uiType: 'ohif.toolBoxButtonGroup',
|
||||
props: {
|
||||
groupId: 'BrushTools',
|
||||
evaluate: 'evaluate.cornerstone.hasSegmentation',
|
||||
items: [
|
||||
{
|
||||
id: 'Brush',
|
||||
@ -134,7 +135,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Shapes',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
label: 'Shapes',
|
||||
evaluate: {
|
||||
|
||||
@ -13,7 +13,7 @@ const setToolActiveToolbar = {
|
||||
const toolbarButtons = [
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
uiType: 'ohif.splitButton',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
groupId: 'MeasurementTools',
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
@ -67,7 +67,7 @@ const toolbarButtons = [
|
||||
},
|
||||
{
|
||||
id: 'Zoom',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
@ -77,7 +77,7 @@ const toolbarButtons = [
|
||||
},
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-window-level',
|
||||
label: 'Window Level',
|
||||
@ -87,7 +87,7 @@ const toolbarButtons = [
|
||||
},
|
||||
{
|
||||
id: 'Pan',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-move',
|
||||
@ -98,7 +98,7 @@ const toolbarButtons = [
|
||||
},
|
||||
{
|
||||
id: 'TrackballRotate',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-3d-rotate',
|
||||
@ -134,7 +134,7 @@ const toolbarButtons = [
|
||||
},
|
||||
{
|
||||
id: 'Crosshairs',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-crosshair',
|
||||
|
||||
@ -3,9 +3,10 @@ import type { Button } from '@ohif/core/types';
|
||||
const toolbarButtons: Button[] = [
|
||||
{
|
||||
id: 'BrushTools',
|
||||
uiType: 'ohif.buttonGroup',
|
||||
uiType: 'ohif.toolBoxButtonGroup',
|
||||
props: {
|
||||
groupId: 'BrushTools',
|
||||
evaluate: 'evaluate.cornerstone.hasSegmentation',
|
||||
items: [
|
||||
{
|
||||
id: 'Brush',
|
||||
@ -172,14 +173,16 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Shapes',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
id: 'Shapes',
|
||||
icon: 'icon-tool-shape',
|
||||
label: 'Shapes',
|
||||
evaluate: {
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'],
|
||||
disabledText: 'Create new segmentation to enable shapes tool.',
|
||||
},
|
||||
icon: 'icon-tool-shape',
|
||||
options: [
|
||||
{
|
||||
name: 'Shape',
|
||||
|
||||
@ -20,7 +20,7 @@ export const setToolActiveToolbar = {
|
||||
const toolbarButtons: Button[] = [
|
||||
{
|
||||
id: 'Zoom',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
@ -30,7 +30,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-window-level',
|
||||
label: 'Window Level',
|
||||
@ -40,7 +40,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Pan',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
@ -50,7 +50,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'TrackballRotate',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-3d-rotate',
|
||||
@ -64,7 +64,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-capture',
|
||||
label: 'Capture',
|
||||
@ -90,7 +90,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'Crosshairs',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-crosshair',
|
||||
label: 'Crosshairs',
|
||||
@ -108,7 +108,7 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
{
|
||||
id: 'MoreTools',
|
||||
uiType: 'ohif.splitButton',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
groupId: 'MoreTools',
|
||||
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
|
||||
@ -192,14 +192,6 @@ const toolbarButtons: Button[] = [
|
||||
commands: 'invertViewport',
|
||||
evaluate: 'evaluate.viewportProperties.toggle',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Probe',
|
||||
icon: 'tool-probe',
|
||||
label: 'Probe',
|
||||
tooltip: 'Probe',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Cine',
|
||||
icon: 'tool-cine',
|
||||
@ -214,14 +206,6 @@ const toolbarButtons: Button[] = [
|
||||
},
|
||||
],
|
||||
}),
|
||||
createButton({
|
||||
id: 'Angle',
|
||||
icon: 'tool-angle',
|
||||
label: 'Angle',
|
||||
tooltip: 'Angle',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'Magnify',
|
||||
icon: 'tool-magnify',
|
||||
@ -230,22 +214,6 @@ const toolbarButtons: Button[] = [
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'RectangleROI',
|
||||
icon: 'tool-rectangle',
|
||||
label: 'Rectangle',
|
||||
tooltip: 'Rectangle',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'CalibrationLine',
|
||||
icon: 'tool-calibration',
|
||||
label: 'Calibration',
|
||||
tooltip: 'Calibration Line',
|
||||
commands: setToolActiveToolbar,
|
||||
evaluate: 'evaluate.cornerstoneTool',
|
||||
}),
|
||||
createButton({
|
||||
id: 'TagBrowser',
|
||||
icon: 'dicom-tag-browser',
|
||||
|
||||
@ -11,7 +11,7 @@ const setToolActiveToolbar = {
|
||||
const toolbarButtons = [
|
||||
{
|
||||
id: 'MeasurementTools',
|
||||
uiType: 'ohif.splitButton',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
groupId: 'MeasurementTools',
|
||||
primary: ToolbarService.createButton({
|
||||
@ -56,7 +56,7 @@ const toolbarButtons = [
|
||||
},
|
||||
{
|
||||
id: 'Zoom',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
@ -67,7 +67,7 @@ const toolbarButtons = [
|
||||
// Window Level + Presets
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-window-level',
|
||||
label: 'Window Level',
|
||||
@ -78,7 +78,7 @@ const toolbarButtons = [
|
||||
// Crosshairs Button
|
||||
{
|
||||
id: 'Crosshairs',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-crosshair',
|
||||
label: 'Crosshairs',
|
||||
@ -89,7 +89,7 @@ const toolbarButtons = [
|
||||
// Pan Button
|
||||
{
|
||||
id: 'Pan',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
@ -100,7 +100,7 @@ const toolbarButtons = [
|
||||
// Rectangle ROI Start End Threshold Button
|
||||
{
|
||||
id: 'RectangleROIStartEndThreshold',
|
||||
uiType: 'ohif.radioGroup',
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
props: {
|
||||
icon: 'tool-create-threshold',
|
||||
label: 'Rectangle ROI Threshold',
|
||||
@ -119,9 +119,10 @@ const toolbarButtons = [
|
||||
},
|
||||
{
|
||||
id: 'BrushTools',
|
||||
uiType: 'ohif.buttonGroup',
|
||||
uiType: 'ohif.toolBoxButtonGroup',
|
||||
props: {
|
||||
groupId: 'BrushTools',
|
||||
evaluate: 'evaluate.cornerstone.hasSegmentation',
|
||||
items: [
|
||||
{
|
||||
id: 'Brush',
|
||||
|
||||
@ -72,7 +72,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
// Assign an alias to the button element
|
||||
cy.get('@wwwcBtnPrimary').as('wwwcButton');
|
||||
cy.get('@wwwcButton').click();
|
||||
cy.get('@wwwcButton').should('have.class', 'bg-primary-light');
|
||||
cy.get('@wwwcButton').should('have.attr', 'data-active', 'true');
|
||||
|
||||
//drags the mouse inside the viewport to be able to interact with series
|
||||
cy.get('@viewport')
|
||||
@ -97,7 +97,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
cy.get('@panButton').click();
|
||||
|
||||
// Assert that the button has the 'active' class
|
||||
cy.get('@panButton').should('have.class', 'bg-primary-light');
|
||||
cy.get('@panButton').should('have.attr', 'data-active', 'true');
|
||||
|
||||
// Trigger the pan actions on the viewport
|
||||
cy.get('@viewport')
|
||||
|
||||
@ -18,7 +18,7 @@ describe('OHIF General Viewer', function () {
|
||||
cy.get('@zoomBtn')
|
||||
.click()
|
||||
.then($zoomBtn => {
|
||||
cy.wrap($zoomBtn).should('have.class', 'bg-primary-light');
|
||||
cy.wrap($zoomBtn).should('have.attr', 'data-active', 'true');
|
||||
});
|
||||
|
||||
const zoomLevelInitial = cy.get('@viewportInfoTopLeft').then($viewportInfo => {
|
||||
|
||||
@ -7,7 +7,7 @@ describe('OHIF MPR', () => {
|
||||
});
|
||||
|
||||
it('should not go MPR for non reconstructible displaySets', () => {
|
||||
cy.get('[data-cy="MPR"]').should('have.class', 'ohif-disabled');
|
||||
cy.get('[data-cy="MPR"]').should('have.class', 'cursor-not-allowed');
|
||||
});
|
||||
|
||||
it('should go MPR for reconstructible displaySets and come back', () => {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
//Creating aliases for Cornerstone tools buttons
|
||||
export function initCornerstoneToolsAliases() {
|
||||
cy.get('[data-cy="StackScroll"]').as('stackScrollBtn');
|
||||
// Note: stack scroll is not in the DOM when the study is loaded
|
||||
// cy.get('[data-cy="StackScroll"]').as('stackScrollBtn');
|
||||
|
||||
cy.get('[data-cy="Zoom"]').as('zoomBtn');
|
||||
cy.get('[data-cy="WindowLevel-split-button-primary"]').as('wwwcBtnPrimary');
|
||||
cy.get('[data-cy="WindowLevel-split-button-secondary"]').as('wwwcBtnSecondary');
|
||||
@ -34,7 +36,6 @@ export function initCommonElementsAliases(skipMarkers) {
|
||||
cy.get('[data-cy="viewport-overlay-bottom-right"]').as('viewportInfoBottomRight');
|
||||
cy.get('[data-cy="viewport-overlay-bottom-left"]').as('viewportInfoBottomLeft');
|
||||
|
||||
console.debug('🚀 ~ skipMarkers:', skipMarkers);
|
||||
if (skipMarkers) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -265,7 +265,7 @@ Cypress.Commands.add(
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('@lengthButton').should('have.class', 'bg-primary-light');
|
||||
cy.get('@lengthButton').should('have.attr', 'data-active', 'true');
|
||||
|
||||
cy.get('@viewport').then($viewport => {
|
||||
const [x1, y1] = firstClick;
|
||||
|
||||
@ -21,13 +21,13 @@ import {
|
||||
ViewportDialogProvider,
|
||||
CineProvider,
|
||||
UserAuthenticationProvider,
|
||||
ToolboxProvider,
|
||||
} from '@ohif/ui';
|
||||
import {
|
||||
ThemeWrapper as ThemeWrapperNext,
|
||||
NotificationProvider,
|
||||
ViewportGridProvider,
|
||||
TooltipProvider,
|
||||
ToolboxProvider,
|
||||
} from '@ohif/ui-next';
|
||||
// Viewer Project
|
||||
// TODO: Should this influence study list?
|
||||
|
||||
@ -5,7 +5,7 @@ export function useToolbar({ servicesManager, buttonSection = 'primary' }: withA
|
||||
const { EVENTS } = toolbarService;
|
||||
|
||||
const [toolbarButtons, setToolbarButtons] = useState(
|
||||
toolbarService.getButtonSection(buttonSection)
|
||||
toolbarService.getButtonSection(buttonSection as string)
|
||||
);
|
||||
|
||||
// Callback function for handling toolbar interactions
|
||||
@ -25,7 +25,7 @@ export function useToolbar({ servicesManager, buttonSection = 'primary' }: withA
|
||||
// Effect to handle toolbar modification events
|
||||
useEffect(() => {
|
||||
const handleToolbarModified = () => {
|
||||
setToolbarButtons(toolbarService.getButtonSection(buttonSection));
|
||||
setToolbarButtons(toolbarService.getButtonSection(buttonSection as string)?.filter(Boolean));
|
||||
};
|
||||
|
||||
const subs = [EVENTS.TOOL_BAR_MODIFIED, EVENTS.TOOL_BAR_STATE_MODIFIED].map(event => {
|
||||
|
||||
@ -236,8 +236,8 @@ export default class ToolbarService extends PubSubService {
|
||||
|
||||
const evaluateButtonProps = (button, props, refreshProps) => {
|
||||
if (evaluationResults.has(button.id)) {
|
||||
const { disabled, className, isActive } = evaluationResults.get(button.id);
|
||||
return { ...props, disabled, className, isActive };
|
||||
const { disabled, disabledText, className, isActive } = evaluationResults.get(button.id);
|
||||
return { ...props, disabled, disabledText, className, isActive };
|
||||
} else {
|
||||
const evaluated = props.evaluate?.({ ...refreshProps, button });
|
||||
const updatedProps = {
|
||||
@ -276,7 +276,9 @@ export default class ToolbarService extends PubSubService {
|
||||
// item in the group
|
||||
buttonProps = {
|
||||
...buttonProps,
|
||||
primary: groupEvaluated?.primary || buttonProps.primary,
|
||||
primary: groupEvaluated?.primary ?? buttonProps.primary,
|
||||
disabled: groupEvaluated?.disabled ?? buttonProps.disabled,
|
||||
disabledText: groupEvaluated?.disabledText ?? buttonProps.disabledText,
|
||||
};
|
||||
|
||||
const { primary, items } = buttonProps;
|
||||
|
||||
@ -3,12 +3,28 @@ sidebar_position: 11
|
||||
sidebar_label: Playwright Testing
|
||||
---
|
||||
|
||||
|
||||
|
||||
:::note
|
||||
You might need to run the `yarn playwright install ` for the first time if you have not
|
||||
:::
|
||||
|
||||
# Running the tests
|
||||
|
||||
```bash
|
||||
# run the tests
|
||||
yarn test:e2e:ui
|
||||
```
|
||||
|
||||
|
||||
# Writing PlayWright Tests
|
||||
|
||||
Our Playwright tests are written using the Playwright test framework. We use these tests to test our OHIF Viewer and ensure that it is working as expected.
|
||||
|
||||
In this guide, we will show you how to write Playwright tests for the OHIF Viewer.
|
||||
|
||||
|
||||
|
||||
## Using a specific study and mode
|
||||
|
||||
If you would like to use a specific study, you can use the `studyInstanceUID` property to reference the study you would like to visit. for example, if you would like to use the study with StudyInstanceUID `2.16.840.1.114362.1.11972228.22789312658.616067305.306.2` and the mode `Basic Viewer`, you can use the following code snippet:
|
||||
|
||||
@ -0,0 +1,199 @@
|
||||
---
|
||||
title: Input Number, Range, and Double Range
|
||||
---
|
||||
|
||||
|
||||
# Migration Guide: Moving to `Numeric` Component
|
||||
|
||||
This guide explains how to migrate from the existing `Input`, `InputRange`, and `InputDoubleRange` components to the new `Numeric` meta component.
|
||||
|
||||
|
||||
## Why Migrate?
|
||||
|
||||
The old components relied heavily on props, making them complex and difficult to maintain and apply custom styles. The new `Numeric` component provides a structured approach with a context-based API, reducing prop clutter and improving reusability.
|
||||
|
||||
|
||||
## `Input` > `Numeric.NumberInput`
|
||||
|
||||
### Basic Usage
|
||||
|
||||
**Old Usage:**
|
||||
|
||||
```tsx
|
||||
<Input
|
||||
id="example"
|
||||
label="Enter a number"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
type="number"
|
||||
/>
|
||||
```
|
||||
|
||||
**New Usage:**
|
||||
|
||||
```tsx
|
||||
<Numeric.Container mode="number" value={value} onChange={setValue}>
|
||||
<Numeric.Label>Enter a number</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</Numeric.Container>
|
||||
```
|
||||
|
||||
|
||||
|
||||
### `Input` with Custom Classes
|
||||
|
||||
#### **Old Usage (with containerClassName, labelClassName, and className)**
|
||||
|
||||
In the old implementation, we manually applied `containerClassName`, `labelClassName`, and `className` to style the `Input` component:
|
||||
|
||||
```tsx
|
||||
<Input
|
||||
id="example"
|
||||
label="Enter a number"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
type="number"
|
||||
containerClassName="flex flex-col space-y-2"
|
||||
labelClassName="text-gray-500 text-sm"
|
||||
className="border rounded p-2"
|
||||
/>
|
||||
```
|
||||
|
||||
|
||||
**New Usage (Migrating to `Numeric.NumberInput`)**
|
||||
|
||||
With `Numeric`, you should wrap everything inside `Numeric.Container`, and you can directly apply class names to its subcomponents:
|
||||
|
||||
```tsx
|
||||
<Numeric.Container mode="number" value={value} onChange={setValue} className="flex flex-col space-y-2">
|
||||
<Numeric.Label className="text-gray-500 text-sm">Enter a number</Numeric.Label>
|
||||
<Numeric.NumberInput className="border rounded p-2" />
|
||||
</Numeric.Container>
|
||||
```
|
||||
|
||||
|
||||
## `InputRange` > `Numeric.SingleRange`
|
||||
|
||||
### Basic Usage
|
||||
|
||||
**Old Usage:**
|
||||
|
||||
```tsx
|
||||
<InputRange
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
step={1}
|
||||
showLabel
|
||||
/>
|
||||
```
|
||||
|
||||
**New Usage:**
|
||||
|
||||
```tsx
|
||||
<Numeric.Container mode="singleRange" value={value} onChange={setValue} min={0} max={100} step={1}>
|
||||
<Numeric.Label showValue>Range</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
```
|
||||
|
||||
|
||||
### Custom Classes
|
||||
|
||||
**Old Usage**
|
||||
|
||||
```tsx
|
||||
<InputRange
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
step={1}
|
||||
containerClassName="flex flex-col space-y-2"
|
||||
inputClassName="w-full bg-gray-700"
|
||||
labelClassName="text-gray-500 text-sm"
|
||||
labelVariant="body1"
|
||||
showLabel={true}
|
||||
labelPosition="left"
|
||||
/>
|
||||
```
|
||||
|
||||
**New Usage**
|
||||
|
||||
```tsx
|
||||
<Numeric.Container mode="singleRange" value={value} onChange={setValue} min={0} max={100} step={1} className="flex flex-col space-y-2">
|
||||
<Numeric.Label className="text-gray-500 text-sm">Range</Numeric.Label>
|
||||
<Numeric.SingleRange sliderClassName="w-full bg-gray-700" />
|
||||
</Numeric.Container>
|
||||
```
|
||||
|
||||
:::note
|
||||
You now have more control over the position of the label and the slider. You can use pretty much any layout you want, whether that's flex, grid, or something else. Instead of relying on `labelPosition` to position the label, you're free to use the layout that works best for you.
|
||||
:::
|
||||
|
||||
|
||||
### AllowNumberEdit
|
||||
|
||||
**Old Usage:**
|
||||
|
||||
```tsx
|
||||
<InputRange
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
step={1}
|
||||
allowNumberEdit={true}
|
||||
showAdjustmentArrows={true}
|
||||
/>
|
||||
```
|
||||
|
||||
**New Usage:**
|
||||
|
||||
Using `Numeric.SingleRange` and `showNumberInput`
|
||||
|
||||
```tsx
|
||||
<Numeric.Container mode="singleRange" value={value} onChange={setValue} min={0} max={100} step={1}>
|
||||
<Numeric.Label showValue>Range</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
```
|
||||
|
||||
|
||||
## `InputDoubleRange` > `Numeric.DoubleRange`
|
||||
|
||||
|
||||
### Basic Usage
|
||||
**Old Usage:**
|
||||
|
||||
```tsx
|
||||
<InputDoubleRange
|
||||
values={rangeValues}
|
||||
onChange={setRangeValues}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
step={5}
|
||||
showLabel
|
||||
/>
|
||||
```
|
||||
|
||||
**New Usage:**
|
||||
|
||||
```tsx
|
||||
<Numeric.Container mode="doubleRange" values={rangeValues} onChange={setRangeValues} min={0} max={100} step={5}>
|
||||
<Numeric.Label showValue>Range</Numeric.Label>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Summary of Changes
|
||||
|
||||
| Old Component | New Component Equivalent |
|
||||
|--------------------|------------------------|
|
||||
| `<Input>` | `<Numeric.NumberInput>` |
|
||||
| `<InputRange>` | `<Numeric.SingleRange>` |
|
||||
| `<InputDoubleRange>` | `<Numeric.DoubleRange>` |
|
||||
@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Tests
|
||||
---
|
||||
|
||||
|
||||
## 1. ToolButton `data-active` Attribute
|
||||
|
||||
- **Previous**: Checked for a `bg-primary-light` class to determine if a tool was active.
|
||||
- **Now**: Check for the HTML attribute `data-active="true"`.
|
||||
|
||||
### Example
|
||||
|
||||
```diff
|
||||
- cy.get('@wwwcButton').should('have.class', 'bg-primary-light');
|
||||
+ cy.get('@wwwcButton').should('have.attr', 'data-active', 'true');
|
||||
```
|
||||
|
||||
## 2. Additional Data Attributes
|
||||
|
||||
- Each tool button now includes:
|
||||
- `data-tool="<toolId>"`
|
||||
- `data-active="<true|false>"`
|
||||
|
||||
This makes it easier to identify and assert on specific tools in the DOM.
|
||||
|
||||
### Example
|
||||
|
||||
```diff
|
||||
- <span data-cy={id}>
|
||||
+ <span
|
||||
+ data-cy={id}
|
||||
+ data-tool={id}
|
||||
+ data-active={isActive}
|
||||
+ >
|
||||
```
|
||||
|
||||
## 3. MPR Button Class Change
|
||||
|
||||
If you were targeting the `ohif-disabled` class, you need to update your tests to target the `cursor-not-allowed` class.
|
||||
|
||||
- **Previous**: `ohif-disabled`
|
||||
- **Now**: `cursor-not-allowed`
|
||||
|
||||
### Example
|
||||
|
||||
```diff
|
||||
- cy.get('[data-cy="MPR"]').should('have.class', 'ohif-disabled');
|
||||
+ cy.get('[data-cy="MPR"]').should('have.class', 'cursor-not-allowed');
|
||||
```
|
||||
|
||||
## 4. Removal of Stack Scroll Alias
|
||||
|
||||
- The `[data-cy="StackScroll"]` element is no longer reliably in the DOM at study load.
|
||||
- If needed, reintroduce or conditionally assert its presence when appropriate.
|
||||
|
||||
```diff
|
||||
- cy.get('[data-cy="StackScroll"]').as('stackScrollBtn');
|
||||
+ // Removed due to absence in DOM at study load
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
1. **Replace** all checks for `bg-primary-light` with `data-active="true"`.
|
||||
2. **Use** `data-tool` and `data-active` attributes for more robust DOM selection and assertions.
|
||||
3. **Update** MPR button checks to `cursor-not-allowed`.
|
||||
4. **Remove** the `[data-cy="StackScroll"]` alias (or only use it when the element is present).
|
||||
@ -0,0 +1,101 @@
|
||||
---
|
||||
title: Toolbar
|
||||
---
|
||||
|
||||
# Toolbar
|
||||
|
||||
## New Toolbar uiType
|
||||
|
||||
We have two new toolbar button types: `ohif.toolButtonList` and `ohif.toolButton`, which are intended to replace the `ohif.radioGroup` and `ohif.splitButton` types.
|
||||
|
||||
Note that these are backward compatible, so if you are not ready to pick up the new ui types (which are more flexible and powerful), you can continue using the old types.
|
||||
|
||||
|
||||
```js
|
||||
// Old type
|
||||
{
|
||||
uiType: 'ohif.radioGroup',
|
||||
}
|
||||
|
||||
// New type
|
||||
{
|
||||
uiType: 'ohif.toolButton',
|
||||
}
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```js
|
||||
// Old type
|
||||
{
|
||||
uiType: 'ohif.splitButton',
|
||||
}
|
||||
|
||||
// New type
|
||||
{
|
||||
uiType: 'ohif.toolButtonList',
|
||||
}
|
||||
```
|
||||
|
||||
The `ohif.buttonGroup` and `ohif.radioGroup` types used in the Toolbox have been replaced with `ohif.toolBoxButtonGroup` and `ohif.toolBoxButton` to reflect their usage in the Toolbox, which has distinct styling.
|
||||
|
||||
```js
|
||||
// Old type
|
||||
{
|
||||
uiType: 'ohif.buttonGroup',
|
||||
}
|
||||
|
||||
// New type
|
||||
{
|
||||
uiType: 'ohif.toolBoxButtonGroup',
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```js
|
||||
// Old type
|
||||
{
|
||||
uiType: 'ohif.radioGroup',
|
||||
}
|
||||
|
||||
// New type
|
||||
{
|
||||
uiType: 'ohif.toolBoxButton',
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## getToolbarModule
|
||||
|
||||
|
||||
The `getToolbarModule` function previously returned `disabled`, `disabledText`, and `className` as part of its evaluation process for the button state. These properties will still be returned, but common class names are now handled internally by the new UI button components, including `ToolButton`, `ToolButtonList`, `Toolbox`, and `ToolBoxGroup`. You can override the `className` if you need to.
|
||||
|
||||
|
||||
## ToolBox
|
||||
|
||||
Previously, the segmentation toolbox was not using an `evaluator` property. This is now taken into account
|
||||
|
||||
|
||||
```js
|
||||
// old
|
||||
{
|
||||
id: 'BrushTools',
|
||||
uiType: 'ohif.buttonGroup',
|
||||
props: {
|
||||
groupId: 'BrushTools',
|
||||
items: []
|
||||
}
|
||||
}
|
||||
|
||||
// now
|
||||
{
|
||||
id: 'BrushTools',
|
||||
uiType: 'ohif.buttonGroup',
|
||||
props: {
|
||||
groupId: 'BrushTools',
|
||||
evaluate: 'evaluate.cornerstone.hasSegmentation',
|
||||
items: []
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -1,651 +1,57 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import '../css/custom.css';
|
||||
|
||||
import Layout from '@theme/Layout';
|
||||
import { Label } from '../../../ui-next/src/components/Label';
|
||||
import { Input } from '../../../ui-next/src/components/Input';
|
||||
import { Separator } from '../../../ui-next/src/components/Separator';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../../../ui-next/src/components/Tabs';
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from '../../../ui-next/src/components/Select';
|
||||
import { Button } from '../../../ui-next/src/components/Button';
|
||||
import { Switch } from '../../../ui-next/src/components/Switch';
|
||||
import { Checkbox } from '../../../ui-next/src/components/Checkbox';
|
||||
import { Toggle } from '../../../ui-next/src/components/Toggle';
|
||||
import { Slider } from '../../../ui-next/src/components/Slider';
|
||||
import { ScrollArea } from '../../../ui-next/src/components/ScrollArea';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '../../../ui-next/src/components/DropdownMenu';
|
||||
import { Icons } from '../../../ui-next/src/components/Icons';
|
||||
import { Toaster, toast } from '../../../ui-next/src/components/Sonner';
|
||||
import { DataRow } from '../../../ui-next/src/components/DataRow';
|
||||
import DataRowExample from './patterns/DataRowExample';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from '../../../ui-next/src/components/Card';
|
||||
|
||||
interface ShowcaseRowProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export default function ComponentShowcase() {
|
||||
// Handlers to trigger different types of toasts
|
||||
const triggerSuccess = () => {
|
||||
toast.success('This is a success toast!');
|
||||
};
|
||||
|
||||
const triggerError = () => {
|
||||
toast.error('This is an error toast!');
|
||||
};
|
||||
|
||||
const triggerInfo = () => {
|
||||
toast.info('This is an info toast!');
|
||||
};
|
||||
|
||||
const triggerWarning = () => {
|
||||
toast.warning('This is a warning toast!');
|
||||
};
|
||||
|
||||
// Handler to trigger a toast.promise example
|
||||
const triggerPromiseToast = () => {
|
||||
const promise = () =>
|
||||
new Promise<{ name: string }>(resolve =>
|
||||
setTimeout(() => resolve({ name: 'Segmentation 1' }), 3000)
|
||||
);
|
||||
|
||||
toast.promise(promise(), {
|
||||
loading: 'Loading Segmentation...',
|
||||
success: data => `${data.name} has been added`,
|
||||
error: 'Error',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with description
|
||||
const triggerDescriptionToast = () => {
|
||||
toast.success('Completed', {
|
||||
description: 'This is a detailed description of the success message.',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with an action button
|
||||
const triggerActionButtonToast = () => {
|
||||
toast.info('No active segmentation detected', {
|
||||
description: 'Create a segmentation before using the Brush',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with a cancel button
|
||||
const triggerCancelButtonToast = () => {
|
||||
toast.error('No active segmentation detected', {
|
||||
description: 'Create a segmentation before using the Brush',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with both action and cancel buttons
|
||||
const triggerCombinedToast = () => {
|
||||
toast.warning('Warning!', {
|
||||
description: 'This is a warning with both action and cancel buttons.',
|
||||
action: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => alert('Retry action clicked')}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
),
|
||||
cancel: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => toast.dismiss()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a loading toast using Toaster's default loading icon
|
||||
const showLoadingToast = () => {
|
||||
toast.loading('Loading your data...');
|
||||
};
|
||||
import { TooltipProvider } from '../../../ui-next/src/components/Tooltip';
|
||||
|
||||
// Import all showcase components
|
||||
import ButtonShowcase from './components/ButtonShowcase';
|
||||
import CheckboxShowcase from './components/CheckboxShowcase';
|
||||
import DataRowShowcase from './components/DataRowShowcase';
|
||||
import DropdownMenuShowcase from './components/DropdownMenuShowcase';
|
||||
import InputShowcase from './components/InputShowcase';
|
||||
import ScrollAreaShowcase from './components/ScrollAreaShowcase';
|
||||
import SelectShowcase from './components/SelectShowcase';
|
||||
import SliderShowcase from './components/SliderShowcase';
|
||||
import SwitchShowcase from './components/SwitchShowcase';
|
||||
import TabsShowcase from './components/TabsShowcase';
|
||||
import ToastShowcase from './components/ToastShowcase';
|
||||
import ToolButtonShowcase from './components/ToolButtonShowcase';
|
||||
import ToolButtonListShowcase from './components/ToolButtonListShowcase';
|
||||
import NumericMetaShowcase from './components/NumericMetaShowcase';
|
||||
/**
|
||||
* Components List page that displays all available UI components
|
||||
*/
|
||||
export default function ComponentsList() {
|
||||
return (
|
||||
<Layout
|
||||
title="Components"
|
||||
description="OHIF Viewer Components"
|
||||
>
|
||||
<div className="text-foreground min-h-screen bg-black">
|
||||
<div className="mx-auto my-4 max-w-5xl pt-6 pb-3">
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||
<a
|
||||
href="/colors-and-type"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Colors & Typography
|
||||
</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
Color Palette and Typography Guidelines
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
<a
|
||||
href="/components-list"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Components
|
||||
</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
Essential UI Components with Variants
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
<a
|
||||
href="/patterns"
|
||||
className="focus:ring-primary block rounded-lg text-inherit no-underline hover:no-underline focus:outline-none focus:ring-2"
|
||||
>
|
||||
<Card className="hover:bg-primary/30 w-full transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
<Icons.ColorChange className="h-12 w-12" />
|
||||
Patterns
|
||||
</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
Component-Based Layout Examples
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</a>
|
||||
<TooltipProvider>
|
||||
<div className="text-foreground min-h-screen bg-black">
|
||||
<div className="mx-auto my-4 max-w-5xl pt-4 pb-6">
|
||||
<div className="ml-6 mb-6 text-base">
|
||||
<h1 className="text-foreground mb-3 text-5xl">Components</h1>
|
||||
</div>
|
||||
<TabsShowcase />
|
||||
|
||||
<ButtonShowcase />
|
||||
<CheckboxShowcase />
|
||||
<DataRowShowcase />
|
||||
<DropdownMenuShowcase />
|
||||
<InputShowcase />
|
||||
<ScrollAreaShowcase />
|
||||
<SelectShowcase />
|
||||
<SliderShowcase />
|
||||
<SwitchShowcase />
|
||||
<TabsShowcase />
|
||||
<ToastShowcase />
|
||||
<ToolButtonShowcase />
|
||||
<ToolButtonListShowcase />
|
||||
<NumericMetaShowcase />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto my-4 max-w-5xl pt-4 pb-6">
|
||||
<div className="ml-6 mb-6 text-base">
|
||||
<h1 className="text-foreground mb-3 text-5xl">Components</h1>
|
||||
</div>
|
||||
|
||||
{/* Alphabetically Sorted ShowcaseRows */}
|
||||
<ShowcaseRow
|
||||
title="Buttons"
|
||||
description="Button components and size variants. Use the primary and secondary buttons in dialogs or screens where one action is required. In the Viewer application, use ghost button in panels where many different actions are available."
|
||||
code={`
|
||||
<Button variant="default">Primary Button</Button>
|
||||
|
||||
<Button variant="secondary">Secondary Button</Button>
|
||||
|
||||
<Button variant="ghost">Ghost Button</Button>
|
||||
|
||||
<Button variant="ghost" size="icon">?</Button>
|
||||
|
||||
<Button variant="link">Link</Button>
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Button variant="default">Primary Button</Button>
|
||||
<Button variant="secondary">Secondary Button</Button>
|
||||
<Button variant="ghost">Ghost Button</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
?
|
||||
</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-wrap gap-4">
|
||||
<Button
|
||||
variant="default"
|
||||
size="lg"
|
||||
className="w-[107px]"
|
||||
>
|
||||
Large Button
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
Small Button
|
||||
</Button>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Checkbox"
|
||||
description="When possible use Switch in place of checkbox. If necessary, Checkbox provides a smaller component to change between two states or options."
|
||||
code={`
|
||||
<div className="items-top flex space-x-2">
|
||||
<Checkbox id="terms1" />
|
||||
<div className="grid gap-1.5 pt-0.5 leading-none">
|
||||
<Label>Display inactive segmentations</Label>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<div className="items-top flex space-x-2">
|
||||
<Checkbox id="terms1" />
|
||||
<div className="grid gap-1.5 pt-0.5 leading-none">
|
||||
<Label>Display inactive segmentations</Label>
|
||||
</div>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Data Row"
|
||||
description="A selectable row with action menu options and visibility toggle. Color, Secondary details, and Image Series are optional to display."
|
||||
code={`
|
||||
Example code coming soon.
|
||||
`}
|
||||
>
|
||||
{/* Render the DataRowExample component */}
|
||||
<DataRowExample />
|
||||
</ShowcaseRow>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Dropdown Menu"
|
||||
description="Dropdown menu provides a flexible list of options that can open from buttons or other elements"
|
||||
code={`
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button>Open Basic</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Align Start</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Align End</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Align Top</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start">
|
||||
<DropdownMenuItem onSelect={() => console.debug('Item 1')}>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => console.debug('Item 2')}>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => console.debug('Item 3')}>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button>Open Basic</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Align Start</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Align End</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Align Top</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
>
|
||||
<DropdownMenuItem onSelect={() => console.debug('Item 1')}>
|
||||
Item 1
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => console.debug('Item 2')}>
|
||||
Item 2
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => console.debug('Item 3')}>
|
||||
Long name Item 3
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Input"
|
||||
description="Input fields can be used with or without example text"
|
||||
code={`
|
||||
<div className="inline-block">
|
||||
<div className="mr-4 inline-block">
|
||||
<Label>Patient Weight</Label>
|
||||
</div>
|
||||
<div className="inline-block">
|
||||
<Input placeholder="(kg)" />
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<div className="inline-block">
|
||||
<div className="mr-4 inline-block">
|
||||
<Label>Patient Weight</Label>
|
||||
</div>
|
||||
<div className="inline-block">
|
||||
<Input placeholder="(kg)" />
|
||||
</div>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Scroll Area"
|
||||
description="Displays a scroll indicator when hovering within an element."
|
||||
code={`
|
||||
<ScrollArea className="border-input bg-background h-[150px] w-[350px] rounded-md border p-2 text-sm text-white">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
|
||||
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
|
||||
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua.
|
||||
</ScrollArea>
|
||||
`}
|
||||
>
|
||||
<ScrollArea className="border-input bg-background h-[150px] w-[350px] rounded-md border p-2 text-sm text-white">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
|
||||
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
|
||||
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
|
||||
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
||||
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
|
||||
mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
</ScrollArea>
|
||||
</ShowcaseRow>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Select"
|
||||
description="Switch between a list of options"
|
||||
code={`
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
`}
|
||||
>
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ShowcaseRow>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Slider"
|
||||
description="Used to select a value in a predefined range."
|
||||
code={`
|
||||
<div className="w-40 px-5">
|
||||
<Slider
|
||||
className="w-full"
|
||||
defaultValue={[50]}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<div className="w-40 px-5">
|
||||
<Slider
|
||||
className="w-full"
|
||||
defaultValue={[50]}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Switch"
|
||||
description="A toggle Switch is used to change between two different states. Use descriptive labels next to Switches that are understandable before interacting."
|
||||
code={`
|
||||
<Switch />
|
||||
`}
|
||||
>
|
||||
<Switch defaultChecked />
|
||||
<Label className="text-foreground mx-2 w-14 flex-none whitespace-nowrap text-sm">
|
||||
Sync changes in all viewports
|
||||
</Label>
|
||||
</ShowcaseRow>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Tabs"
|
||||
description="Tabs (or segmented controls) can be used to provide navigation options or allow users to switch between multiple options (e.g., tool settings) "
|
||||
code={`
|
||||
<Tabs className="w-[400px]">
|
||||
<TabsList>
|
||||
<TabsTrigger value="circle">Circle</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="sphere">Sphere</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="square">Square</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
`}
|
||||
>
|
||||
<Tabs className="w-[400px]">
|
||||
<TabsList>
|
||||
<TabsTrigger value="circle">Circle</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="sphere">Sphere</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="square">Square</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</ShowcaseRow>
|
||||
|
||||
<ShowcaseRow
|
||||
title="Toast"
|
||||
description="A toast notification displays temporary feedback messages to users above the current UI. Notifications stack into one unit after multiple cascading notifications."
|
||||
code={`
|
||||
Example code coming soon.
|
||||
|
||||
`}
|
||||
>
|
||||
{/* Toast Examples Section */}
|
||||
Simple message:
|
||||
<div className="mt-2 mb-7 space-x-2">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerPromiseToast}
|
||||
>
|
||||
Loading & Success Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerSuccess}
|
||||
>
|
||||
Success Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerError}
|
||||
>
|
||||
Error Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerInfo}
|
||||
>
|
||||
Info Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerWarning}
|
||||
>
|
||||
Warning Toast
|
||||
</Button>
|
||||
</div>
|
||||
Message with details:
|
||||
<div className="mt-2 space-x-2">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerDescriptionToast}
|
||||
>
|
||||
Success Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerActionButtonToast}
|
||||
>
|
||||
Info Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerCancelButtonToast}
|
||||
>
|
||||
Error Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerCombinedToast}
|
||||
>
|
||||
Toast with Buttons
|
||||
</Button>
|
||||
</div>
|
||||
{/* Render the Toaster component */}
|
||||
<Toaster />
|
||||
</ShowcaseRow>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
|
||||
const [showCode, setShowCode] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="bg-background mb-8 rounded-lg p-6">
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-highlight text-2xl">{title}</h2>
|
||||
</div>
|
||||
<Button
|
||||
className="text-primary"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowCode(!showCode)}
|
||||
>
|
||||
{showCode ? 'Hide Code' : 'Show Code'} <Icons.Code className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-3">
|
||||
<div className="text-base md:col-span-1">
|
||||
{description && <p className="text-secondary-foreground mt-2">{description}</p>}
|
||||
</div>
|
||||
<div className="flex min-h-[120px] items-center md:col-span-2">
|
||||
<div className="showcase-content">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
{showCode && (
|
||||
<pre className="border-input mt-4 overflow-x-auto rounded-md border bg-black p-4 text-sm">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
|
||||
// const [showCode, setShowCode] = useState(false);
|
||||
|
||||
// return (
|
||||
// <div className="bg-background mb-8 rounded-lg p-6">
|
||||
// <div className="mb-4 flex items-start justify-between">
|
||||
// <div>
|
||||
// <h2 className="text-2xl font-bold">{title}</h2>
|
||||
// {description && <p className="text-muted-foreground mt-1">{description}</p>}
|
||||
// </div>
|
||||
// <Button
|
||||
// className="text-primary"
|
||||
// variant="outline"
|
||||
// size="sm"
|
||||
// onClick={() => setShowCode(!showCode)}
|
||||
// >
|
||||
// {showCode ? 'Hide Code' : 'Show Code'} <Icons.Code className="ml-2 h-4 w-4" />
|
||||
// </Button>
|
||||
// </div>
|
||||
// <div className="showcase-content mb-4">{children}</div>
|
||||
// {showCode && (
|
||||
// <pre className="mt-4 overflow-x-auto rounded-md bg-black p-4 text-sm">
|
||||
// <code>{code}</code>
|
||||
// </pre>
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
54
platform/docs/src/pages/components/ButtonShowcase.tsx
Normal file
54
platform/docs/src/pages/components/ButtonShowcase.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* ButtonShowcase component displays button variants and examples
|
||||
*/
|
||||
export default function ButtonShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Buttons"
|
||||
description="Button components and size variants. Use the primary and secondary buttons in dialogs or screens where one action is required. In the Viewer application, use ghost button in panels where many different actions are available."
|
||||
code={`
|
||||
<Button variant="default">Primary Button</Button>
|
||||
|
||||
<Button variant="secondary">Secondary Button</Button>
|
||||
|
||||
<Button variant="ghost">Ghost Button</Button>
|
||||
|
||||
<Button variant="ghost" size="icon">?</Button>
|
||||
|
||||
<Button variant="link">Link</Button>
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Button variant="default">Primary Button</Button>
|
||||
<Button variant="secondary">Secondary Button</Button>
|
||||
<Button variant="ghost">Ghost Button</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
?
|
||||
</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-wrap gap-4">
|
||||
<Button
|
||||
variant="default"
|
||||
size="lg"
|
||||
className="w-[107px]"
|
||||
>
|
||||
Large Button
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
Small Button
|
||||
</Button>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
31
platform/docs/src/pages/components/CheckboxShowcase.tsx
Normal file
31
platform/docs/src/pages/components/CheckboxShowcase.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Checkbox } from '../../../../ui-next/src/components/Checkbox';
|
||||
import { Label } from '../../../../ui-next/src/components/Label';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* CheckboxShowcase component displays checkbox variants and examples
|
||||
*/
|
||||
export default function CheckboxShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Checkbox"
|
||||
description="When possible use Switch in place of checkbox. If necessary, Checkbox provides a smaller component to change between two states or options."
|
||||
code={`
|
||||
<div className="items-top flex space-x-2">
|
||||
<Checkbox id="terms1" />
|
||||
<div className="grid gap-1.5 pt-0.5 leading-none">
|
||||
<Label>Display inactive segmentations</Label>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<div className="items-top flex space-x-2">
|
||||
<Checkbox id="terms1" />
|
||||
<div className="grid gap-1.5 pt-0.5 leading-none">
|
||||
<Label>Display inactive segmentations</Label>
|
||||
</div>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
20
platform/docs/src/pages/components/DataRowShowcase.tsx
Normal file
20
platform/docs/src/pages/components/DataRowShowcase.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import DataRowExample from '../patterns/DataRowExample';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* DataRowShowcase component displays DataRow variants and examples
|
||||
*/
|
||||
export default function DataRowShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Data Row"
|
||||
description="A selectable row with action menu options and visibility toggle. Color, Secondary details, and Image Series are optional to display."
|
||||
code={`
|
||||
Example code coming soon.
|
||||
`}
|
||||
>
|
||||
<DataRowExample />
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
81
platform/docs/src/pages/components/DropdownMenuShowcase.tsx
Normal file
81
platform/docs/src/pages/components/DropdownMenuShowcase.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '../../../../ui-next/src/components/DropdownMenu';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* DropdownMenuShowcase component displays DropdownMenu variants and examples
|
||||
*/
|
||||
export default function DropdownMenuShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Dropdown Menu"
|
||||
description="Dropdown menu provides a flexible list of options that can open from buttons or other elements"
|
||||
code={`
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button>Open Basic</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button>Open Basic</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Align Start</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Align End</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Long name Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open Align Top</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
>
|
||||
<DropdownMenuItem onSelect={() => console.debug('Item 1')}>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => console.debug('Item 2')}>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => console.debug('Item 3')}>
|
||||
Long name Item 3
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
35
platform/docs/src/pages/components/InputShowcase.tsx
Normal file
35
platform/docs/src/pages/components/InputShowcase.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Input } from '../../../../ui-next/src/components/Input';
|
||||
import { Label } from '../../../../ui-next/src/components/Label';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* InputShowcase component displays Input variants and examples
|
||||
*/
|
||||
export default function InputShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Input"
|
||||
description="Input fields can be used with or without example text"
|
||||
code={`
|
||||
<div className="inline-block">
|
||||
<div className="mr-4 inline-block">
|
||||
<Label>Patient Weight</Label>
|
||||
</div>
|
||||
<div className="inline-block">
|
||||
<Input placeholder="(kg)" />
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<div className="inline-block">
|
||||
<div className="mr-4 inline-block">
|
||||
<Label>Patient Weight</Label>
|
||||
</div>
|
||||
<div className="inline-block">
|
||||
<Input placeholder="(kg)" />
|
||||
</div>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
260
platform/docs/src/pages/components/NumericMetaShowcase.tsx
Normal file
260
platform/docs/src/pages/components/NumericMetaShowcase.tsx
Normal file
@ -0,0 +1,260 @@
|
||||
import React from 'react';
|
||||
import Numeric from '../../../../ui-next/src/components/Numeric';
|
||||
import Icons from '../../../../ui-next/src/components/Icons';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* NumericShowcase component displays Numeric variants and examples
|
||||
*/
|
||||
export default function NumericShowcase() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Basic Number Input */}
|
||||
<ShowcaseRow
|
||||
title="Numeric - Number Input"
|
||||
description="Basic number input with min, max and a label"
|
||||
code={`
|
||||
<Numeric.Container mode="number" min={0} max={10} onChange={onChange}>
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Numeric.Label>Width</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="number" min={0} max={100} onChange={onChange}>
|
||||
<Numeric.Label className="text-secondary-foreground text-sm font-bold">Bolder</Numeric.Label>
|
||||
<Numeric.NumberInput className="w-12" />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="number" className="flex flex-row items-center justify-between" onChange={onChange} min={0} value={123465789} max={10000000000000}>
|
||||
<Numeric.Label className="flex flex-row items-center">
|
||||
<Icons.Add />
|
||||
With Icon
|
||||
</Numeric.Label>
|
||||
<Numeric.NumberInput className="w-32 text-center" />
|
||||
</Numeric.Container>`}
|
||||
>
|
||||
<div className="bg-popover flex w-[300px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container
|
||||
mode="number"
|
||||
min={0}
|
||||
max={10}
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Numeric.Label>Width</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="number"
|
||||
className="space-y-1"
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
>
|
||||
<Numeric.Label className="text-muted-foreground text-sm font-bold">
|
||||
Bolder
|
||||
</Numeric.Label>
|
||||
<Numeric.NumberInput className="w-12" />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="number"
|
||||
className="flex flex-row items-center justify-between"
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
min={0}
|
||||
value={123465789}
|
||||
max={10000000000000}
|
||||
>
|
||||
<Numeric.Label className="flex flex-row items-center">
|
||||
<Icons.Add />
|
||||
With Icon
|
||||
</Numeric.Label>
|
||||
<Numeric.NumberInput className="w-32 text-center" />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
{/* Single Range Slider */}
|
||||
<ShowcaseRow
|
||||
title="Numeric - Single Range"
|
||||
description="Single range slider with optional number input"
|
||||
code={`
|
||||
<Numeric.Container mode="singleRange" min={0} max={100} step={1} value={50} onChange={onChange}>
|
||||
<Numeric.Label>Brightness</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="singleRange" min={-50} max={50} step={1} value={0} className="flex flex-row items-center" onChange={onChange}>
|
||||
<Numeric.Label showValue>Contrast</Numeric.Label>
|
||||
<Numeric.SingleRange />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="singleRange" min={0} max={100} step={1} value={50} className="flex flex-row items-center space-x-2" onChange={onChange}>
|
||||
<Numeric.Label>Something Else </Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>`}
|
||||
>
|
||||
<div className="bg-popover flex w-[300px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={50}
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
>
|
||||
<Numeric.Label>Brightness</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={-50}
|
||||
max={50}
|
||||
step={1}
|
||||
value={0}
|
||||
className="flex flex-row items-center"
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
>
|
||||
<Numeric.Label showValue>Contrast</Numeric.Label>
|
||||
<Numeric.SingleRange />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={50}
|
||||
className="flex flex-row items-center space-x-2"
|
||||
onChange={val => console.debug('Value changed:', val)}
|
||||
>
|
||||
<Numeric.Label>Something Else </Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
{/* Double Range Slider */}
|
||||
<ShowcaseRow
|
||||
title="Numeric - Double Range"
|
||||
description="Double range slider for selecting a range of values"
|
||||
code={`
|
||||
<Numeric.Container mode="doubleRange" min={0} max={100} step={1} values={[30, 70]} onChange={onChange}>
|
||||
<Numeric.Label showValue>Window Width/Level</Numeric.Label>
|
||||
<Numeric.DoubleRange />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="doubleRange" min={0} max={100} step={1} values={[30, 70]}>
|
||||
<Numeric.Label>Window Width/Level</Numeric.Label>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="doubleRange" min={0} max={100} step={1} values={[30, 70]} className="flex flex-row items-center space-x-2" onChange={onChange}>
|
||||
<Numeric.Label>Inline double slider</Numeric.Label>
|
||||
<Numeric.DoubleRange />
|
||||
</Numeric.Container>`}
|
||||
>
|
||||
<div className="bg-popover flex w-[300px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
values={[30, 70]}
|
||||
className="space-y-1"
|
||||
onChange={vals => console.debug('Values changed:', vals)}
|
||||
>
|
||||
<Numeric.Label showValue>Window Width/Level</Numeric.Label>
|
||||
<Numeric.DoubleRange />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
values={[30, 70]}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Numeric.Label>Window Width/Level</Numeric.Label>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
values={[30, 70]}
|
||||
className="flex flex-row items-center space-x-2"
|
||||
onChange={vals => console.debug('Values changed:', vals)}
|
||||
>
|
||||
<Numeric.Label>Inline double slider</Numeric.Label>
|
||||
<Numeric.DoubleRange />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
|
||||
{/* Combined Examples */}
|
||||
<ShowcaseRow
|
||||
title="Numeric - Combined Examples"
|
||||
description="Different modes and configurations working together"
|
||||
code={`
|
||||
<Numeric.Container mode="number" min={0} max={10} step={0.1}>
|
||||
<Numeric.Label>Zoom Factor</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="singleRange" min={0} max={360} step={1}>
|
||||
<Numeric.Label showValue>Rotation</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container mode="doubleRange" min={-1000} max={3000} values={[1000, 2000]} step={10}>
|
||||
<Numeric.Label showValue>CT Window</Numeric.Label>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>`}
|
||||
>
|
||||
<div className="bg-popover flex w-[300px] flex-col space-y-4 rounded p-4">
|
||||
<Numeric.Container
|
||||
mode="number"
|
||||
min={0}
|
||||
max={10}
|
||||
step={0.1}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Numeric.Label>Zoom Factor</Numeric.Label>
|
||||
<Numeric.NumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
min={0}
|
||||
max={360}
|
||||
step={1}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Numeric.Label showValue>Rotation</Numeric.Label>
|
||||
<Numeric.SingleRange showNumberInput />
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={-1000}
|
||||
max={3000}
|
||||
values={[1000, 2000]}
|
||||
step={10}
|
||||
className="space-y-1"
|
||||
>
|
||||
<Numeric.Label showValue>CT Window</Numeric.Label>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
platform/docs/src/pages/components/ScrollAreaShowcase.tsx
Normal file
33
platform/docs/src/pages/components/ScrollAreaShowcase.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { ScrollArea } from '../../../../ui-next/src/components/ScrollArea';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* ScrollAreaShowcase component displays ScrollArea variants and examples
|
||||
*/
|
||||
export default function ScrollAreaShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Scroll Area"
|
||||
description="Displays a scroll indicator when hovering within an element."
|
||||
code={`
|
||||
<ScrollArea className="border-input bg-background h-[150px] w-[350px] rounded-md border p-2 text-sm text-white">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
|
||||
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
|
||||
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua.
|
||||
</ScrollArea>
|
||||
`}
|
||||
>
|
||||
<ScrollArea className="border-input bg-background h-[150px] w-[350px] rounded-md border p-2 text-sm text-white">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
|
||||
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
|
||||
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
|
||||
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
|
||||
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem
|
||||
ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
|
||||
labore et dolore magna aliqua.
|
||||
</ScrollArea>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
44
platform/docs/src/pages/components/SelectShowcase.tsx
Normal file
44
platform/docs/src/pages/components/SelectShowcase.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from '../../../../ui-next/src/components/Select';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* SelectShowcase component displays Select variants and examples
|
||||
*/
|
||||
export default function SelectShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Select"
|
||||
description="Switch between a list of options"
|
||||
code={`
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
`}
|
||||
>
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
49
platform/docs/src/pages/components/ShowcaseRow.tsx
Normal file
49
platform/docs/src/pages/components/ShowcaseRow.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import { Icons } from '../../../../ui-next/src/components/Icons';
|
||||
|
||||
interface ShowcaseRowProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ShowcaseRow component displays a UI component example with title, description,
|
||||
* and optional code snippet that can be toggled.
|
||||
*/
|
||||
export default function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) {
|
||||
const [showCode, setShowCode] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="bg-background mb-8 rounded-lg p-6">
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-highlight text-2xl">{title}</h2>
|
||||
</div>
|
||||
<Button
|
||||
className="text-primary"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowCode(!showCode)}
|
||||
>
|
||||
{showCode ? 'Hide Code' : 'Show Code'} <Icons.Code className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-3">
|
||||
<div className="text-base md:col-span-1">
|
||||
{description && <p className="text-secondary-foreground mt-2">{description}</p>}
|
||||
</div>
|
||||
<div className="flex min-h-[120px] items-center md:col-span-2">
|
||||
<div className="showcase-content">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
{showCode && (
|
||||
<pre className="border-input mt-4 overflow-x-auto rounded-md border bg-black p-4 text-sm">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
platform/docs/src/pages/components/SliderShowcase.tsx
Normal file
34
platform/docs/src/pages/components/SliderShowcase.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Slider } from '../../../../ui-next/src/components/Slider';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* SliderShowcase component displays Slider variants and examples
|
||||
*/
|
||||
export default function SliderShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Slider"
|
||||
description="Used to select a value in a predefined range."
|
||||
code={`
|
||||
<div className="w-40 px-5">
|
||||
<Slider
|
||||
className="w-full"
|
||||
defaultValue={[50]}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<div className="w-40 px-5">
|
||||
<Slider
|
||||
className="w-full"
|
||||
defaultValue={[50]}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
24
platform/docs/src/pages/components/SwitchShowcase.tsx
Normal file
24
platform/docs/src/pages/components/SwitchShowcase.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Switch } from '../../../../ui-next/src/components/Switch';
|
||||
import { Label } from '../../../../ui-next/src/components/Label';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* SwitchShowcase component displays Switch variants and examples
|
||||
*/
|
||||
export default function SwitchShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Switch"
|
||||
description="A toggle Switch is used to change between two different states. Use descriptive labels next to Switches that are understandable before interacting."
|
||||
code={`
|
||||
<Switch />
|
||||
`}
|
||||
>
|
||||
<Switch defaultChecked />
|
||||
<Label className="text-foreground mx-2 w-14 flex-none whitespace-nowrap text-sm">
|
||||
Sync changes in all viewports
|
||||
</Label>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
40
platform/docs/src/pages/components/TabsShowcase.tsx
Normal file
40
platform/docs/src/pages/components/TabsShowcase.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs';
|
||||
import { Separator } from '../../../../ui-next/src/components/Separator';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* TabsShowcase component displays Tabs variants and examples
|
||||
*/
|
||||
export default function TabsShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Tabs"
|
||||
description="Tabs (or segmented controls) can be used to provide navigation options or allow users to switch between multiple options (e.g., tool settings) "
|
||||
code={`
|
||||
<Tabs className="w-[400px]" onValueChange={newValue => console.log(newValue)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="circle">Circle</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="sphere">Sphere</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="square">Square</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
`}
|
||||
>
|
||||
<Tabs
|
||||
className="w-[400px]"
|
||||
onValueChange={newValue => console.log(newValue)}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="circle">Circle</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="sphere">Sphere</TabsTrigger>
|
||||
<Separator orientation="vertical" />
|
||||
<TabsTrigger value="square">Square</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
158
platform/docs/src/pages/components/ToastShowcase.tsx
Normal file
158
platform/docs/src/pages/components/ToastShowcase.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../../../../ui-next/src/components/Button';
|
||||
import { Toaster, toast } from '../../../../ui-next/src/components/Sonner';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* ToastShowcase component displays Toast variants and examples
|
||||
*/
|
||||
export default function ToastShowcase() {
|
||||
// Handlers to trigger different types of toasts
|
||||
const triggerSuccess = () => {
|
||||
toast.success('This is a success toast!');
|
||||
};
|
||||
|
||||
const triggerError = () => {
|
||||
toast.error('This is an error toast!');
|
||||
};
|
||||
|
||||
const triggerInfo = () => {
|
||||
toast.info('This is an info toast!');
|
||||
};
|
||||
|
||||
const triggerWarning = () => {
|
||||
toast.warning('This is a warning toast!');
|
||||
};
|
||||
|
||||
// Handler to trigger a toast.promise example
|
||||
const triggerPromiseToast = () => {
|
||||
const promise = () =>
|
||||
new Promise<{ name: string }>(resolve =>
|
||||
setTimeout(() => resolve({ name: 'Segmentation 1' }), 3000)
|
||||
);
|
||||
|
||||
toast.promise(promise(), {
|
||||
loading: 'Loading Segmentation...',
|
||||
success: data => `${data.name} has been added`,
|
||||
error: 'Error',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with description
|
||||
const triggerDescriptionToast = () => {
|
||||
toast.success('Completed', {
|
||||
description: 'This is a detailed description of the success message.',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with an action button
|
||||
const triggerActionButtonToast = () => {
|
||||
toast.info('No active segmentation detected', {
|
||||
description: 'Create a segmentation before using the Brush',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with a cancel button
|
||||
const triggerCancelButtonToast = () => {
|
||||
toast.error('No active segmentation detected', {
|
||||
description: 'Create a segmentation before using the Brush',
|
||||
});
|
||||
};
|
||||
|
||||
// Handler to trigger a toast with both action and cancel buttons
|
||||
const triggerCombinedToast = () => {
|
||||
toast.warning('Warning!', {
|
||||
description: 'This is a warning with both action and cancel buttons.',
|
||||
action: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => console.debug('Retry action clicked')}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
),
|
||||
cancel: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => toast.dismiss()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="Toast"
|
||||
description="A toast notification displays temporary feedback messages to users above the current UI. Notifications stack into one unit after multiple cascading notifications."
|
||||
code={`
|
||||
Example code coming soon.
|
||||
`}
|
||||
>
|
||||
Simple message:
|
||||
<div className="mt-2 mb-7 space-x-2">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerPromiseToast}
|
||||
>
|
||||
Loading & Success Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerSuccess}
|
||||
>
|
||||
Success Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerError}
|
||||
>
|
||||
Error Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerInfo}
|
||||
>
|
||||
Info Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerWarning}
|
||||
>
|
||||
Warning Toast
|
||||
</Button>
|
||||
</div>
|
||||
Message with details:
|
||||
<div className="mt-2 space-x-2">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerDescriptionToast}
|
||||
>
|
||||
Success Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerActionButtonToast}
|
||||
>
|
||||
Info Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerCancelButtonToast}
|
||||
>
|
||||
Error Toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={triggerCombinedToast}
|
||||
>
|
||||
Toast with Buttons
|
||||
</Button>
|
||||
</div>
|
||||
<Toaster />
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
ToolButtonList,
|
||||
ToolButton,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
} from '../../../../ui-next/src/components/ToolButton';
|
||||
import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip';
|
||||
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* ToolButtonListShowcase component displays ToolButtonList variants and examples
|
||||
*/
|
||||
export default function ToolButtonListShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="ToolButtonList"
|
||||
description="A compound component that combines a primary tool button with a dropdown menu of additional related tools"
|
||||
code={`
|
||||
// Example usage:
|
||||
<ToolButtonList>
|
||||
<ToolButtonListDefault>
|
||||
<ToolButton
|
||||
id="Length"
|
||||
icon="tool-length"
|
||||
label="Length"
|
||||
tooltip="Length Tool"
|
||||
onInteraction={({ itemId }) => console.debug(\`Clicked \${itemId}\`)}
|
||||
/>
|
||||
</ToolButtonListDefault>
|
||||
<ToolButtonListDivider />
|
||||
<ToolButtonListDropDown>
|
||||
<ToolButtonListItem
|
||||
icon="tool-length"
|
||||
onSelect={() => console.debug('Selected Length')}
|
||||
>
|
||||
<span className="pl-1">Length</span>
|
||||
</ToolButtonListItem>
|
||||
<ToolButtonListItem
|
||||
icon="tool-bidirectional"
|
||||
onSelect={() => console.debug('Selected Bidirectional')}
|
||||
>
|
||||
<span className="pl-1">Bidirectional</span>
|
||||
</ToolButtonListItem>
|
||||
</ToolButtonListDropDown>
|
||||
</ToolButtonList>
|
||||
`}
|
||||
>
|
||||
<div className="bg-popover flex h-11 w-[450px] items-center justify-start rounded p-2">
|
||||
<TooltipProvider>
|
||||
<ToolButtonList>
|
||||
<ToolButtonListDefault>
|
||||
<ToolButton
|
||||
id="Length"
|
||||
icon="ToolLength"
|
||||
label="Length"
|
||||
tooltip="Length Tool"
|
||||
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
|
||||
/>
|
||||
</ToolButtonListDefault>
|
||||
<ToolButtonListDivider />
|
||||
<ToolButtonListDropDown>
|
||||
<ToolButtonListItem
|
||||
icon="ToolLength"
|
||||
onSelect={() => console.debug('Selected Length')}
|
||||
>
|
||||
<span className="pl-1">Length</span>
|
||||
</ToolButtonListItem>
|
||||
<ToolButtonListItem
|
||||
icon="ToolBidirectional"
|
||||
onSelect={() => console.debug('Selected Bidirectional')}
|
||||
>
|
||||
<span className="pl-1">Bidirectional</span>
|
||||
</ToolButtonListItem>
|
||||
<ToolButtonListItem
|
||||
icon="ToolAnnotate"
|
||||
onSelect={() => console.debug('Selected Annotation')}
|
||||
>
|
||||
<span className="pl-1">Annotation</span>
|
||||
</ToolButtonListItem>
|
||||
</ToolButtonListDropDown>
|
||||
</ToolButtonList>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
54
platform/docs/src/pages/components/ToolButtonShowcase.tsx
Normal file
54
platform/docs/src/pages/components/ToolButtonShowcase.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip';
|
||||
import ToolButton from '../../../../ui-next/src/components/ToolButton/ToolButton';
|
||||
import ShowcaseRow from './ShowcaseRow';
|
||||
|
||||
/**
|
||||
* ToolButtonShowcase component displays ToolButton variants and examples
|
||||
*/
|
||||
export default function ToolButtonShowcase() {
|
||||
return (
|
||||
<ShowcaseRow
|
||||
title="ToolButton"
|
||||
description="Used to activate tools or perform single actions"
|
||||
code={`
|
||||
// Example usage:
|
||||
<ToolButton
|
||||
id="Zoom"
|
||||
icon="zoom" // must exist in your Icons or fallback to 'MissingIcon'
|
||||
label="Zoom"
|
||||
tooltip="Zoom Tool"
|
||||
isActive={false}
|
||||
onInteraction={({ itemId }) => console.debug(\`Clicked \${itemId}\`)}
|
||||
/>
|
||||
`}
|
||||
>
|
||||
<div className="bg-popover flex h-11 w-[450px] items-center justify-center rounded">
|
||||
<TooltipProvider>
|
||||
<ToolButton
|
||||
id="Zoom"
|
||||
icon="ToolZoom"
|
||||
isActive={true}
|
||||
label="Zoom"
|
||||
tooltip="Zoom"
|
||||
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
|
||||
/>
|
||||
<ToolButton
|
||||
id="Zoom"
|
||||
icon="ToolMove"
|
||||
label="Pan"
|
||||
tooltip="Pan"
|
||||
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
|
||||
/>
|
||||
<ToolButton
|
||||
id="Zoom"
|
||||
icon="ToolWindowLevel"
|
||||
label="Window Level"
|
||||
tooltip="Window Level"
|
||||
onInteraction={({ itemId }) => console.debug(`Clicked ${itemId}`)}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</ShowcaseRow>
|
||||
);
|
||||
}
|
||||
@ -38,12 +38,12 @@ export interface ButtonProps
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
({ className, variant, size, asChild = false, ...props }, forwardRef) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
ref={forwardRef}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -11,10 +11,22 @@ interface DoubleSliderProps {
|
||||
step?: number;
|
||||
defaultValue?: [number, number];
|
||||
onValueChange?: (value: [number, number]) => void;
|
||||
showNumberInputs?: boolean;
|
||||
}
|
||||
|
||||
const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
({ className, min, max, step = 1, defaultValue = [min, max], onValueChange }, ref) => {
|
||||
(
|
||||
{
|
||||
className,
|
||||
min,
|
||||
max,
|
||||
onValueChange,
|
||||
step = 1,
|
||||
defaultValue = [min, max],
|
||||
showNumberInputs = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [value, setValue] = React.useState<[number, number]>(defaultValue);
|
||||
|
||||
const prevDefaultValueRef = React.useRef<[number, number] | null>(null);
|
||||
@ -77,16 +89,18 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
ref={ref}
|
||||
className={cn('flex w-full items-center space-x-2', className)}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
value={formatValue(value[0])}
|
||||
onChange={e => handleInputChange(0, e.target.value)}
|
||||
onBlur={() => handleInputChange(0, value[0].toString())}
|
||||
className="w-14"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
/>
|
||||
{showNumberInputs && (
|
||||
<Input
|
||||
type="number"
|
||||
value={formatValue(value[0])}
|
||||
onChange={e => handleInputChange(0, e.target.value)}
|
||||
onBlur={() => handleInputChange(0, value[0].toString())}
|
||||
className="w-14"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
/>
|
||||
)}
|
||||
<SliderPrimitive.Root
|
||||
className="relative flex h-4 w-full touch-none select-none items-center"
|
||||
min={min}
|
||||
@ -101,16 +115,18 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
<SliderPrimitive.Thumb className="border-background bg-primary focus-visible:ring-ring block h-4 w-4 rounded-full border-2 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50" />
|
||||
<SliderPrimitive.Thumb className="border-background bg-primary focus-visible:ring-ring block h-4 w-4 rounded-full border-2 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
<Input
|
||||
type="number"
|
||||
value={formatValue(value[1])}
|
||||
onChange={e => handleInputChange(1, e.target.value)}
|
||||
onBlur={() => handleInputChange(1, value[1].toString())}
|
||||
className="w-14"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
/>
|
||||
{showNumberInputs && (
|
||||
<Input
|
||||
type="number"
|
||||
value={formatValue(value[1])}
|
||||
onChange={e => handleInputChange(1, e.target.value)}
|
||||
onBlur={() => handleInputChange(1, value[1].toString())}
|
||||
className="w-14"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -689,6 +689,9 @@ export const Icons = {
|
||||
'icon-status-alert': (props: IconProps) => Alert(props),
|
||||
'info-link': (props: IconProps) => InfoLink(props),
|
||||
'launch-info': (props: IconProps) => LaunchInfo(props),
|
||||
'old-trash': (props: IconProps) => Trash(props),
|
||||
'tool-point': (props: IconProps) => ToolCircle(props),
|
||||
'tool-freehand-line': (props: IconProps) => ToolFreehand(props),
|
||||
clipboard: (props: IconProps) => Clipboard(props),
|
||||
|
||||
/** Adds an icon to the set of icons */
|
||||
|
||||
@ -2447,8 +2447,6 @@ export const ToolSegBrush = (props: IconProps) => (
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@ -2490,8 +2488,6 @@ export const ToolSegEraser = (props: IconProps) => (
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@ -2592,8 +2588,6 @@ export const ToolSegThreshold = (props: IconProps) => (
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@ -3012,10 +3006,11 @@ export const ToolWindowLevel = (props: IconProps) => (
|
||||
|
||||
export const ToolWindowRegion = (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
@ -3029,8 +3024,8 @@ export const ToolWindowRegion = (props: IconProps) => (
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<path
|
||||
d="M10.3460449,22 L4,22 C3.44771525,22 3,21.5522847 3,21 L3,4 C3,3.44771525 3.44771525,3 4,3 L21,3 C21.5522847,3 22,3.44771525 22,4 L22,11.3197021"
|
||||
@ -3110,11 +3105,9 @@ export const ToolZoom = (props: IconProps) => (
|
||||
|
||||
export const ToolBrush = (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@ -3128,8 +3121,8 @@ export const ToolBrush = (props: IconProps) => (
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<path
|
||||
d="M3.24640621,21.8286833 C3.09173375,21.7551472 2.99513748,21.5971513 3.00018895,21.4259625 C3.00524042,21.2547737 3.11098486,21.1027485 3.26972426,21.0384606 C5.3260304,20.2059201 4.66362518,17.8620247 5.27421252,16.6088957 C6.02197747,15.1026514 7.84114758,14.4766383 9.35746132,15.2037675 C13.9485253,17.4422999 8.48346644,24.3211232 3.24640621,21.8286833 Z"
|
||||
@ -3153,11 +3146,9 @@ export const ToolBrush = (props: IconProps) => (
|
||||
|
||||
export const ToolEraser = (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@ -3171,8 +3162,8 @@ export const ToolEraser = (props: IconProps) => (
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<path
|
||||
d="M8.47826087,18.0030772 C7.48731947,17.9495317 6.54439458,17.5591084 5.80565217,16.8964685 L4.1066087,15.1958598 C3.49763625,14.5834856 3.49763625,13.5942339 4.1066087,12.9818598 L13.6317391,3.45672934 C14.2441133,2.84775689 15.233365,2.84775689 15.8457391,3.45672934 L19.8926087,7.5035989 C20.5015811,8.11597307 20.5015811,9.10522473 19.8926087,9.7175989 L12.7153043,16.8949033 C11.9769855,17.5579476 11.0343414,17.9489213 10.0434783,18.0030772 L8.47826087,18.0030772 Z"
|
||||
@ -3210,11 +3201,9 @@ export const ToolEraser = (props: IconProps) => (
|
||||
|
||||
export const ToolThreshold = (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@ -3228,8 +3217,8 @@ export const ToolThreshold = (props: IconProps) => (
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<g
|
||||
id="Group"
|
||||
@ -3295,11 +3284,9 @@ export const ToolThreshold = (props: IconProps) => (
|
||||
|
||||
export const ToolShape = (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@ -3313,8 +3300,8 @@ export const ToolShape = (props: IconProps) => (
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<circle
|
||||
id="Oval"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Icons } from './Icons';
|
||||
|
||||
export { Icons };
|
||||
export default Icons;
|
||||
|
||||
277
platform/ui-next/src/components/Numeric/Numeric.tsx
Normal file
277
platform/ui-next/src/components/Numeric/Numeric.tsx
Normal file
@ -0,0 +1,277 @@
|
||||
// Numeric.tsx
|
||||
import React, { createContext, useContext, useState, useCallback, PropsWithChildren } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Input } from '../Input/Input';
|
||||
import { Slider } from '../Slider/Slider';
|
||||
import { DoubleSlider } from '../DoubleSlider/DoubleSlider';
|
||||
|
||||
interface NumericMetaContextValue {
|
||||
mode: 'number' | 'singleRange' | 'doubleRange';
|
||||
singleValue: number;
|
||||
doubleValue: [number, number];
|
||||
setSingleValue: (val: number) => void;
|
||||
setDoubleValue: (vals: [number, number]) => void;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
}
|
||||
|
||||
const NumericMetaContext = createContext<NumericMetaContextValue | null>(null);
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
1) Container
|
||||
---------------------------------------------------------------------------*/
|
||||
interface NumericMetaContainerProps {
|
||||
mode: 'number' | 'singleRange' | 'doubleRange';
|
||||
value?: number; // for single-value usage
|
||||
values?: [number, number]; // for double-range usage
|
||||
onChange?: (val: number | [number, number]) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function NumericMetaContainer({
|
||||
mode,
|
||||
value = 0,
|
||||
values = [0, 100],
|
||||
onChange,
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<NumericMetaContainerProps>) {
|
||||
// Initialize state with props but don't update automatically
|
||||
const [internalSingleValue, setInternalSingleValue] = useState<number>(value);
|
||||
const [internalDoubleValue, setInternalDoubleValue] = useState<[number, number]>(values);
|
||||
|
||||
const handleSingleChange = useCallback(
|
||||
(newVal: number) => {
|
||||
setInternalSingleValue(newVal);
|
||||
onChange?.(newVal);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleDoubleChange = useCallback(
|
||||
(newVals: [number, number]) => {
|
||||
// Update internal state
|
||||
setInternalDoubleValue(newVals);
|
||||
// Notify parent if onChange is provided
|
||||
onChange?.(newVals);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<NumericMetaContext.Provider
|
||||
value={{
|
||||
mode,
|
||||
singleValue: internalSingleValue,
|
||||
doubleValue: internalDoubleValue,
|
||||
setSingleValue: handleSingleChange,
|
||||
setDoubleValue: handleDoubleChange,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
}}
|
||||
>
|
||||
<div className={cn('flex flex-col', className)}>{children}</div>
|
||||
</NumericMetaContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
2) Label sub-component
|
||||
---------------------------------------------------------------------------*/
|
||||
interface NumericMetaLabelProps {
|
||||
showValue?: boolean; // optionally show the current numeric value(s)
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function NumericMetaLabel({ children, showValue, className }: NumericMetaLabelProps) {
|
||||
const ctx = useContext(NumericMetaContext);
|
||||
if (!ctx) {
|
||||
throw new Error('NumericMetaLabel must be used inside <Numeric.Container>.');
|
||||
}
|
||||
|
||||
const { mode, singleValue, doubleValue } = ctx;
|
||||
|
||||
let displayedValue = '';
|
||||
let valueClasses = '';
|
||||
if (mode === 'number' || mode === 'singleRange') {
|
||||
displayedValue = singleValue.toString();
|
||||
valueClasses = 'w-10';
|
||||
} else if (mode === 'doubleRange') {
|
||||
displayedValue = `[${doubleValue[0]} - ${doubleValue[1]}]`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('text-foreground flex text-base', className)}>
|
||||
{children}
|
||||
{showValue && (
|
||||
<span className={cn('inline-block', valueClasses)}>{`: ${displayedValue}`}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
3) SingleRange sub-component
|
||||
---------------------------------------------------------------------------*/
|
||||
|
||||
interface SingleRangeProps {
|
||||
showNumberInput?: boolean;
|
||||
sliderClassName?: string;
|
||||
numberInputClassName?: string;
|
||||
}
|
||||
|
||||
function SingleRange({ showNumberInput, sliderClassName, numberInputClassName }: SingleRangeProps) {
|
||||
const ctx = useContext(NumericMetaContext);
|
||||
if (!ctx) {
|
||||
throw new Error('SingleRange must be used inside <Numeric.Container>.');
|
||||
}
|
||||
|
||||
const { mode, singleValue, setSingleValue, min, max, step } = ctx;
|
||||
|
||||
const handleSliderChange = useCallback(
|
||||
(val: number[]) => {
|
||||
setSingleValue(val[0]);
|
||||
},
|
||||
[setSingleValue]
|
||||
);
|
||||
|
||||
const handleNumberChange = useCallback(
|
||||
(evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const parsed = parseFloat(evt.target.value);
|
||||
if (!isNaN(parsed)) {
|
||||
setSingleValue(Math.max(min, Math.min(parsed, max)));
|
||||
}
|
||||
},
|
||||
[min, max, setSingleValue]
|
||||
);
|
||||
|
||||
if (mode !== 'singleRange') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
<Slider
|
||||
className={cn('flex-1', sliderClassName)}
|
||||
value={[singleValue]}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
onValueChange={handleSliderChange}
|
||||
/>
|
||||
{showNumberInput && (
|
||||
<Input
|
||||
type="number"
|
||||
className={cn('w-[50px] shrink-0', numberInputClassName)}
|
||||
value={singleValue}
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
onChange={handleNumberChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
4) DoubleRange sub-component
|
||||
---------------------------------------------------------------------------*/
|
||||
interface DoubleRangeProps {
|
||||
showNumberInputs?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) {
|
||||
const ctx = useContext(NumericMetaContext);
|
||||
if (!ctx) {
|
||||
throw new Error('DoubleRange must be used inside <Numeric.Container>.');
|
||||
}
|
||||
|
||||
const { mode, doubleValue, setDoubleValue, min, max, step } = ctx;
|
||||
|
||||
const handleSliderChange = useCallback(
|
||||
(values: [number, number]) => {
|
||||
setDoubleValue(values);
|
||||
},
|
||||
[setDoubleValue]
|
||||
);
|
||||
|
||||
if (mode !== 'doubleRange') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('min-w-0 flex-1', className)}>
|
||||
<DoubleSlider
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
defaultValue={doubleValue}
|
||||
onValueChange={handleSliderChange}
|
||||
showNumberInputs={showNumberInputs}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
5) Basic NumberInput sub-component
|
||||
---------------------------------------------------------------------------*/
|
||||
interface NumberInputProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function NumberInput({ className }: NumberInputProps) {
|
||||
const ctx = useContext(NumericMetaContext);
|
||||
if (!ctx) {
|
||||
throw new Error('NumberInput must be used inside <Numeric.Container>.');
|
||||
}
|
||||
|
||||
const { mode, singleValue, setSingleValue, min, max, step } = ctx;
|
||||
if (mode !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = parseFloat(evt.target.value);
|
||||
if (!isNaN(val)) {
|
||||
setSingleValue(Math.max(min, Math.min(val, max)));
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate width based on max value's length, with a minimum of 3 characters
|
||||
const maxLength = Math.max(3, max?.toString().length ?? 3);
|
||||
const calculatedWidth = `${maxLength + 1.5}ch`;
|
||||
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
value={singleValue}
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
onChange={handleChange}
|
||||
className={cn('min-w-[60px]', `w-[${calculatedWidth}]`, className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const Numeric = {
|
||||
Container: NumericMetaContainer,
|
||||
Label: NumericMetaLabel,
|
||||
SingleRange,
|
||||
DoubleRange,
|
||||
NumberInput,
|
||||
};
|
||||
|
||||
export default Numeric;
|
||||
3
platform/ui-next/src/components/Numeric/index.ts
Normal file
3
platform/ui-next/src/components/Numeric/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import Numeric from './Numeric';
|
||||
|
||||
export default Numeric;
|
||||
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import Numeric from '../Numeric';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface RowDoubleRangeProps {
|
||||
values: [number, number];
|
||||
onChange: (values: [number, number]) => void;
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
step: number;
|
||||
showLabel?: boolean;
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const RowDoubleRange: React.FC<RowDoubleRangeProps> = ({
|
||||
values,
|
||||
onChange,
|
||||
minValue,
|
||||
maxValue,
|
||||
step,
|
||||
showLabel = false,
|
||||
label = '',
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
min={minValue}
|
||||
max={maxValue}
|
||||
step={step}
|
||||
className={cn('flex flex-col space-y-2', className)}
|
||||
>
|
||||
{showLabel && <Numeric.Label showValue>{label}</Numeric.Label>}
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default RowDoubleRange;
|
||||
@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import Numeric from '../Numeric';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface RowInputRangeProps {
|
||||
value: number;
|
||||
onChange: (newValue: number) => void;
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
step?: number;
|
||||
label?: string;
|
||||
showLabel?: boolean;
|
||||
labelPosition?: 'left' | 'right';
|
||||
allowNumberEdit?: boolean;
|
||||
showNumberInput?: boolean;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
const RowInputRange: React.FC<RowInputRangeProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
minValue = 0,
|
||||
maxValue = 100,
|
||||
step = 1,
|
||||
label = '',
|
||||
showLabel = false,
|
||||
labelPosition = 'right',
|
||||
allowNumberEdit = false,
|
||||
showNumberInput = true,
|
||||
className,
|
||||
containerClassName,
|
||||
}) => {
|
||||
const handleChange = (newValue: number | [number, number]) => {
|
||||
if (typeof newValue === 'number') {
|
||||
onChange(newValue);
|
||||
} else {
|
||||
onChange(newValue[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const content = (
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
min={minValue}
|
||||
max={maxValue}
|
||||
step={step}
|
||||
className={cn('flex flex-row items-center space-x-2', className)}
|
||||
>
|
||||
{showLabel && label && labelPosition === 'left' && (
|
||||
<Numeric.Label showValue={showNumberInput}>{label}</Numeric.Label>
|
||||
)}
|
||||
<Numeric.SingleRange showNumberInput={allowNumberEdit && showNumberInput} />
|
||||
{showLabel && label && labelPosition === 'right' && (
|
||||
<Numeric.Label showValue={showNumberInput}>{label}</Numeric.Label>
|
||||
)}
|
||||
</Numeric.Container>
|
||||
);
|
||||
|
||||
return containerClassName ? <div className={containerClassName}>{content}</div> : content;
|
||||
};
|
||||
|
||||
export default RowInputRange;
|
||||
@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { Label } from '../Label';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../Tabs';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface RadioValue {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface RadioOption {
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
values: RadioValue[];
|
||||
commands?: (val: string) => void;
|
||||
}
|
||||
|
||||
interface RowSegmentedControlProps {
|
||||
option: RadioOption;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const RowSegmentedControl: React.FC<RowSegmentedControlProps> = ({ option, className }) => {
|
||||
const handleValueChange = (newVal: string) => {
|
||||
if (option.commands) {
|
||||
option.commands(newVal);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('flex items-center justify-between text-[13px]', className)}
|
||||
key={option.id}
|
||||
>
|
||||
<Label className="mr-2">{option.name}</Label>
|
||||
<div className="max-w-1/2">
|
||||
<Tabs
|
||||
value={option.value}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<TabsList className="inline-flex space-x-1">
|
||||
{option.values.map(({ label, value: itemValue }, index) => (
|
||||
<TabsTrigger
|
||||
value={itemValue}
|
||||
key={`button-${option.id}-${index}`}
|
||||
>
|
||||
{label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RowSegmentedControl;
|
||||
@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import RowInputRange from './RowInputRange';
|
||||
import RowSegmentedControl from './RowSegmentedControl';
|
||||
import RowDoubleRange from './RowDoubleRange';
|
||||
|
||||
const SETTING_TYPES = {
|
||||
RANGE: 'range',
|
||||
RADIO: 'radio',
|
||||
CUSTOM: 'custom',
|
||||
DOUBLE_RANGE: 'double-range',
|
||||
};
|
||||
|
||||
function ToolSettings({ options }) {
|
||||
if (!options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof options === 'function') {
|
||||
return options();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-foreground space-y-2 pb-4">
|
||||
{options?.map(option => {
|
||||
if (option.condition && option.condition?.({ options }) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (option.type) {
|
||||
case SETTING_TYPES.RANGE:
|
||||
return renderRangeSetting(option);
|
||||
case SETTING_TYPES.RADIO:
|
||||
return renderRadioSetting(option);
|
||||
case SETTING_TYPES.DOUBLE_RANGE:
|
||||
return renderDoubleRangeSetting(option);
|
||||
case SETTING_TYPES.CUSTOM:
|
||||
return renderCustomSetting(option);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderRangeSetting = option => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center"
|
||||
key={option.id}
|
||||
>
|
||||
<div className="w-1/3 text-[13px]">{option.name}</div>
|
||||
<div className="w-2/3">
|
||||
<RowInputRange
|
||||
minValue={option.min}
|
||||
maxValue={option.max}
|
||||
step={option.step}
|
||||
value={option.value}
|
||||
onChange={value => option.commands?.(value)}
|
||||
allowNumberEdit={true}
|
||||
inputClassName="ml-1 w-4/5 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRadioSetting = option => {
|
||||
return (
|
||||
<RowSegmentedControl
|
||||
key={option.id}
|
||||
option={option}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function renderDoubleRangeSetting(option) {
|
||||
return (
|
||||
<RowDoubleRange
|
||||
key={option.id}
|
||||
values={option.value}
|
||||
onChange={option.commands}
|
||||
minValue={option.min}
|
||||
maxValue={option.max}
|
||||
step={option.step}
|
||||
showLabel={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const renderCustomSetting = option => {
|
||||
return (
|
||||
<div key={option.id}>
|
||||
{typeof option.children === 'function' ? option.children() : option.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolSettings;
|
||||
@ -0,0 +1 @@
|
||||
export { default as ToolSettings } from './ToolSettings';
|
||||
@ -1,8 +1,6 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { ToolboxUI, useToolbox } from '../../';
|
||||
import { useToolbar } from '@ohif/core';
|
||||
import { ToolboxUI } from './';
|
||||
// Migrate this file to the new UI eventually
|
||||
import { useToolbox } from '@ohif/ui';
|
||||
|
||||
/**
|
||||
* A toolbox is a collection of buttons and commands that they invoke, used to provide
|
||||
@ -21,6 +19,8 @@ function Toolbox({
|
||||
title,
|
||||
...props
|
||||
}: withAppTypes) {
|
||||
// We should move these outside of the platform/ui-next, no file here
|
||||
// should rely on the managers and services
|
||||
const { state: toolboxState, api } = useToolbox(buttonSectionId);
|
||||
const { onInteraction, toolbarButtons } = useToolbar({
|
||||
servicesManager,
|
||||
@ -117,6 +117,9 @@ function Toolbox({
|
||||
|
||||
if (items?.length) {
|
||||
items.forEach(({ options, id }) => {
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
accumulator[id] = createEnhancedOptions(options, id);
|
||||
});
|
||||
} else if (options?.length) {
|
||||
@ -144,17 +147,15 @@ function Toolbox({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolboxUI
|
||||
{...props}
|
||||
title={title}
|
||||
toolbarButtons={toolbarButtons}
|
||||
toolboxState={toolboxState}
|
||||
handleToolSelect={id => api.handleToolSelect(id)}
|
||||
handleToolOptionChange={handleToolOptionChange}
|
||||
onInteraction={onInteraction}
|
||||
/>
|
||||
</>
|
||||
<ToolboxUI
|
||||
{...props}
|
||||
title={title}
|
||||
toolbarButtons={toolbarButtons}
|
||||
toolboxState={toolboxState}
|
||||
handleToolSelect={id => api.handleToolSelect(id)}
|
||||
handleToolOptionChange={handleToolOptionChange}
|
||||
onInteraction={onInteraction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { PanelSection } from '../../components';
|
||||
// Migrate this file to the new UI eventually
|
||||
import { ToolSettings } from '@ohif/ui';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { PanelSection } from '../../components';
|
||||
import { ToolSettings } from '../OHIFToolSettings';
|
||||
|
||||
const ItemsPerRow = 4;
|
||||
|
||||
function usePrevious(value) {
|
||||
@ -55,8 +55,8 @@ function ToolboxUI(props: withAppTypes) {
|
||||
const render = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col bg-black">
|
||||
<div className="bg-primary-dark mt-0.5 flex flex-wrap py-2">
|
||||
<div className="flex flex-col">
|
||||
<div className="bg-muted mt-0.5 flex flex-wrap space-x-2 py-2 px-1">
|
||||
{toolbarButtons.map((toolDef, index) => {
|
||||
if (!toolDef) {
|
||||
return null;
|
||||
@ -81,20 +81,16 @@ function ToolboxUI(props: withAppTypes) {
|
||||
key={id}
|
||||
className={classnames({
|
||||
[toolClasses]: true,
|
||||
'border-secondary-light flex flex-col items-center justify-center rounded-md border':
|
||||
true,
|
||||
})}
|
||||
>
|
||||
<div className="flex rounded-md bg-black">
|
||||
<Component
|
||||
{...componentProps}
|
||||
{...props}
|
||||
id={id}
|
||||
servicesManager={servicesManager}
|
||||
onInteraction={onInteraction}
|
||||
size="toolbox"
|
||||
/>
|
||||
</div>
|
||||
<Component
|
||||
{...componentProps}
|
||||
{...props}
|
||||
id={id}
|
||||
servicesManager={servicesManager}
|
||||
onInteraction={onInteraction}
|
||||
size="toolbox"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -101,7 +101,7 @@ const Thumbnail = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="group">
|
||||
<Icons.StatusTracking className="text-primary-light h-[20px] w-[20px] group-hover:hidden" />
|
||||
<Icons.StatusTracking className="text-primary-light h-[15px] w-[15px] group-hover:hidden" />
|
||||
<Icons.Cancel
|
||||
className="text-primary-light hidden h-[15px] w-[15px] group-hover:block"
|
||||
onClick={onClickUntrack}
|
||||
@ -216,7 +216,7 @@ const Thumbnail = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="group">
|
||||
<Icons.StatusTracking className="text-primary-light h-[20px] w-[20px] group-hover:hidden" />
|
||||
<Icons.StatusTracking className="text-primary-light h-[20px] w-[15px] group-hover:hidden" />
|
||||
<Icons.Cancel
|
||||
className="text-primary-light hidden h-[15px] w-[15px] group-hover:block"
|
||||
onClick={onClickUntrack}
|
||||
|
||||
110
platform/ui-next/src/components/ToolButton/ToolButton.tsx
Normal file
110
platform/ui-next/src/components/ToolButton/ToolButton.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip';
|
||||
import { Icons } from '../Icons';
|
||||
import { Button } from '../Button';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
const baseClasses = '!rounded-lg inline-flex items-center justify-center';
|
||||
const defaultClasses = 'bg-transparent text-foreground/80 hover:bg-background hover:text-highlight';
|
||||
const activeClasses = 'bg-highlight text-background hover:!bg-highlight/80';
|
||||
const disabledClasses =
|
||||
'text-common-bright hover:bg-primary-dark hover:text-primary-light opacity-40 cursor-not-allowed';
|
||||
|
||||
const sizeClasses = {
|
||||
default: {
|
||||
buttonSizeClass: 'w-10 h-10',
|
||||
iconSizeClass: 'h-7 w-7',
|
||||
},
|
||||
small: {
|
||||
buttonSizeClass: 'w-8 h-8',
|
||||
iconSizeClass: 'h-6 w-6',
|
||||
},
|
||||
};
|
||||
|
||||
interface ToolButtonProps {
|
||||
id: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
size?: 'default' | 'small';
|
||||
isActive?: boolean;
|
||||
disabled?: boolean;
|
||||
disabledText?: string;
|
||||
commands?: Record<string, unknown>;
|
||||
onInteraction?: (details: { itemId: string; commands?: Record<string, unknown> }) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function ToolButton(props: ToolButtonProps) {
|
||||
const {
|
||||
id,
|
||||
icon = 'MissingIcon',
|
||||
label,
|
||||
tooltip,
|
||||
size = 'default',
|
||||
disabled = false,
|
||||
isActive = false,
|
||||
disabledText,
|
||||
commands,
|
||||
onInteraction,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const { buttonSizeClass, iconSizeClass } = sizeClasses[size];
|
||||
|
||||
const buttonClasses = cn(
|
||||
baseClasses,
|
||||
buttonSizeClass,
|
||||
disabled ? disabledClasses : isActive ? activeClasses : defaultClasses,
|
||||
className
|
||||
);
|
||||
|
||||
const defaultTooltip = tooltip || label;
|
||||
const disabledTooltip = disabled && disabledText ? disabledText : null;
|
||||
const hasTooltip = defaultTooltip || disabledTooltip;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
className={cn(disabled && 'cursor-not-allowed')}
|
||||
>
|
||||
{/* TooltipTrigger is a span since a disabled button does not fire events and the tooltip
|
||||
will not show. */}
|
||||
<span
|
||||
data-cy={id}
|
||||
data-tool={id}
|
||||
data-active={isActive}
|
||||
>
|
||||
<Button
|
||||
className={buttonClasses}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
onInteraction?.({ itemId: id, commands });
|
||||
}
|
||||
}}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={hasTooltip ? defaultTooltip : undefined}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icons.ByName
|
||||
name={icon}
|
||||
className={iconSizeClass}
|
||||
/>
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{hasTooltip && (
|
||||
<>
|
||||
<div>{defaultTooltip}</div>
|
||||
{disabledTooltip && <div className="text-muted-foreground">{disabledTooltip}</div>}
|
||||
</>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToolButton;
|
||||
207
platform/ui-next/src/components/ToolButton/ToolButtonList.tsx
Normal file
207
platform/ui-next/src/components/ToolButton/ToolButtonList.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../Button';
|
||||
import { Icons } from '../Icons';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '../DropdownMenu';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip';
|
||||
|
||||
/**
|
||||
* ToolButtonList Component
|
||||
* Root component that wraps the default and dropdown sections
|
||||
* -----------------------------------------------
|
||||
*/
|
||||
interface ToolButtonListProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ToolButtonList = React.forwardRef<HTMLDivElement, ToolButtonListProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
ToolButtonList.displayName = 'ToolButtonList';
|
||||
|
||||
/**
|
||||
* ToolButtonListDefault Component
|
||||
* Container for the default/primary tool button
|
||||
* -----------------------------------------------
|
||||
*/
|
||||
interface ToolButtonListDefaultProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
tooltip?: string;
|
||||
disabledText?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ToolButtonListDefault = React.forwardRef<HTMLDivElement, ToolButtonListDefaultProps>(
|
||||
({ className, children, tooltip, disabledText, disabled, ...props }, ref) => {
|
||||
const hasTooltip = tooltip || disabledText;
|
||||
|
||||
const defaultContent = (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!hasTooltip) {
|
||||
return defaultContent;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>{defaultContent}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{tooltip && <div>{tooltip}</div>}
|
||||
{disabledText && disabled && <div className="text-muted-foreground">{disabledText}</div>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
);
|
||||
ToolButtonListDefault.displayName = 'ToolButtonListDefault';
|
||||
|
||||
/**
|
||||
* ToolButtonListDropDown Component
|
||||
* Container for the dropdown section with trigger and content
|
||||
* -----------------------------------------------
|
||||
*/
|
||||
interface ToolButtonListDropDownProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ToolButtonListDropDown = React.forwardRef<HTMLDivElement, ToolButtonListDropDownProps>(
|
||||
({ children, className, ...props }, ref) => (
|
||||
<DropdownMenu {...props}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'text-foreground/80 hover:bg-background hover:text-highlight border-primary',
|
||||
'inline-flex h-10 w-5 items-center justify-center',
|
||||
'!rounded-tr-lg !rounded-br-lg !rounded-tl-none !rounded-bl-none',
|
||||
'bg-transparent',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Icons.ByName
|
||||
name="chevron-down"
|
||||
className="text-primary h-5 w-5"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
ref={ref}
|
||||
side="bottom"
|
||||
align="start"
|
||||
alignOffset={-40}
|
||||
>
|
||||
{children}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
);
|
||||
ToolButtonListDropDown.displayName = 'ToolButtonListDropDown';
|
||||
|
||||
/**
|
||||
* ToolButtonListItem Component
|
||||
* Individual item in the dropdown menu
|
||||
* -----------------------------------------------
|
||||
*/
|
||||
interface ToolButtonListItemProps extends React.ComponentProps<typeof DropdownMenuItem> {
|
||||
icon?: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
disabledText?: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
const ToolButtonListItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuItem>,
|
||||
ToolButtonListItemProps
|
||||
>(({ className, children, icon, disabledText, tooltip, disabled, ...props }, ref) => {
|
||||
const defaultTooltip = tooltip || (typeof children === 'string' ? children : undefined);
|
||||
const hasTooltip = defaultTooltip || disabledText;
|
||||
|
||||
const menuItem = (
|
||||
<DropdownMenuItem
|
||||
ref={ref}
|
||||
className={cn('flex items-center space-x-2', className)}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{icon && (
|
||||
<Icons.ByName
|
||||
name={icon || 'MissingIcon'}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
|
||||
// Todo: there is a weird issue where i can't control the duration of the delay
|
||||
// for the items in this list, causing the tooltip to show up too early in the
|
||||
// dropdown menu. So i'm just removing the tooltip for list items unless the disabledText is set.
|
||||
if (!disabled) {
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>{menuItem}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{defaultTooltip && <div>{defaultTooltip}</div>}
|
||||
{disabledText && disabled && <div className="text-muted-foreground">{disabledText}</div>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
ToolButtonListItem.displayName = 'ToolButtonListItem';
|
||||
|
||||
/**
|
||||
* ToolButtonListDivider Component
|
||||
* Divider between items in the dropdown menu
|
||||
* -----------------------------------------------
|
||||
*/
|
||||
const ToolButtonListDivider = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('bg-primary h-5 w-px self-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToolButtonListDivider.displayName = 'ToolButtonListDivider';
|
||||
|
||||
export {
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
};
|
||||
8
platform/ui-next/src/components/ToolButton/index.ts
Normal file
8
platform/ui-next/src/components/ToolButton/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export { default as ToolButton } from './ToolButton';
|
||||
export {
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
} from './ToolButtonList';
|
||||
@ -52,7 +52,9 @@ import { ThumbnailList } from './ThumbnailList';
|
||||
import { PanelSection } from './PanelSection';
|
||||
import { DisplaySetMessageListTooltip } from './DisplaySetMessageListTooltip';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip';
|
||||
import { Toolbox, ToolboxUI } from './Toolbox';
|
||||
import { ToolboxUI, Toolbox } from './OHIFToolbox';
|
||||
import Numeric from './Numeric';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@ -91,8 +93,17 @@ import {
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
} from './Viewport';
|
||||
import {
|
||||
ToolButton,
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
} from './ToolButton';
|
||||
|
||||
export {
|
||||
Numeric,
|
||||
ErrorBoundary,
|
||||
Button,
|
||||
buttonVariants,
|
||||
@ -157,8 +168,8 @@ export {
|
||||
ThumbnailList,
|
||||
PanelSection,
|
||||
DisplaySetMessageListTooltip,
|
||||
Toolbox,
|
||||
ToolboxUI,
|
||||
Toolbox,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
@ -208,4 +219,10 @@ export {
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
Clipboard,
|
||||
ToolButton,
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
};
|
||||
|
||||
109
platform/ui-next/src/contextProviders/ToolboxContext.tsx
Normal file
109
platform/ui-next/src/contextProviders/ToolboxContext.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import React, { createContext, useContext, useReducer } from 'react';
|
||||
|
||||
export const initialState = {};
|
||||
|
||||
export const toolboxReducer = (state, action) => {
|
||||
const { toolbarSectionId } = action.payload;
|
||||
|
||||
if (!state[toolbarSectionId]) {
|
||||
state[toolbarSectionId] = { activeTool: null, toolOptions: {}, selectedEvent: false };
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case 'SET_ACTIVE_TOOL':
|
||||
return {
|
||||
...state,
|
||||
[toolbarSectionId]: {
|
||||
...state[toolbarSectionId],
|
||||
activeTool: action.payload.activeTool,
|
||||
selectedEvent: true,
|
||||
},
|
||||
};
|
||||
case 'UPDATE_TOOL_OPTION':
|
||||
const { toolName, optionName, value } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[toolbarSectionId]: {
|
||||
...state[toolbarSectionId],
|
||||
selectedEvent: false,
|
||||
toolOptions: {
|
||||
...state[toolbarSectionId].toolOptions,
|
||||
[toolName]: state[toolbarSectionId].toolOptions[toolName].map(option =>
|
||||
option.id === optionName ? { ...option, value } : option
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
case 'INITIALIZE_TOOL_OPTIONS':
|
||||
// Initialize tool options for each toolbarSectionId
|
||||
return {
|
||||
...state,
|
||||
selectedEvent: false,
|
||||
[action.toolbarSectionId]: {
|
||||
...state[action.toolbarSectionId],
|
||||
toolOptions: action.payload,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const ToolboxContext = createContext();
|
||||
|
||||
export const ToolboxProvider = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(toolboxReducer, initialState);
|
||||
|
||||
const handleToolSelect = (toolbarSectionId, toolName) => {
|
||||
dispatch({
|
||||
type: 'SET_ACTIVE_TOOL',
|
||||
payload: { toolbarSectionId, activeTool: toolName },
|
||||
});
|
||||
};
|
||||
|
||||
const handleToolOptionChange = (toolbarSectionId, toolName, optionName, newValue) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_TOOL_OPTION',
|
||||
payload: { toolbarSectionId, toolName, optionName, value: newValue },
|
||||
});
|
||||
};
|
||||
|
||||
const initializeToolOptions = (toolbarSectionId, toolOptions) => {
|
||||
dispatch({
|
||||
type: 'INITIALIZE_TOOL_OPTIONS',
|
||||
toolbarSectionId,
|
||||
payload: toolOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const api = { handleToolSelect, handleToolOptionChange, initializeToolOptions };
|
||||
|
||||
const value = { state, api };
|
||||
|
||||
return <ToolboxContext.Provider value={value}>{children}</ToolboxContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook for accessing toolbox state and actions for a specific toolbar section.
|
||||
* You can use this hook to access the state and actions for a specific toolbar section (
|
||||
* defined by the toolbarSectionId) in your custom toolbar components. This hook
|
||||
* helps to manage the state and actions for the tools and their options in the toolbar.
|
||||
*/
|
||||
export const useToolbox = toolbarSectionId => {
|
||||
const context = useContext(ToolboxContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useToolbox must be used within a ToolboxProvider');
|
||||
}
|
||||
const { state, api } = context;
|
||||
|
||||
return {
|
||||
state: state[toolbarSectionId] || { activeTool: null, toolOptions: {} },
|
||||
api: {
|
||||
handleToolSelect: toolName => api.handleToolSelect(toolbarSectionId, toolName),
|
||||
handleToolOptionChange: (toolName, optionName, value) =>
|
||||
api.handleToolOptionChange(toolbarSectionId, toolName, optionName, value),
|
||||
initializeToolOptions: toolOptions =>
|
||||
api.initializeToolOptions(toolbarSectionId, toolOptions),
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -1,5 +1,7 @@
|
||||
import NotificationProvider, { useNotification } from './NotificationProvider';
|
||||
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './ViewportGridProvider';
|
||||
import { ToolboxProvider, useToolbox } from './ToolboxContext';
|
||||
|
||||
export { useNotification, NotificationProvider };
|
||||
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
|
||||
export { ToolboxProvider, useToolbox };
|
||||
|
||||
@ -32,7 +32,6 @@ import {
|
||||
ThumbnailList,
|
||||
PanelSection,
|
||||
DisplaySetMessageListTooltip,
|
||||
Toolbox,
|
||||
ToolboxUI,
|
||||
DoubleSlider,
|
||||
Label,
|
||||
@ -93,11 +92,24 @@ import {
|
||||
ViewportActionCornersLocations,
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
ToolButton,
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
Toolbox,
|
||||
} from './components';
|
||||
import { DataRow } from './components/DataRow';
|
||||
|
||||
import { useNotification, NotificationProvider } from './contextProviders';
|
||||
import {
|
||||
useNotification,
|
||||
NotificationProvider,
|
||||
useToolbox,
|
||||
ToolboxProvider,
|
||||
} from './contextProviders';
|
||||
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './contextProviders';
|
||||
import * as utils from './utils';
|
||||
|
||||
export {
|
||||
ErrorBoundary,
|
||||
@ -143,7 +155,6 @@ export {
|
||||
ThumbnailList,
|
||||
PanelSection,
|
||||
DisplaySetMessageListTooltip,
|
||||
Toolbox,
|
||||
ToolboxUI,
|
||||
Label,
|
||||
Slider,
|
||||
@ -202,4 +213,14 @@ export {
|
||||
ViewportActionCornersLocations,
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
ToolButton,
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
ToolboxProvider,
|
||||
Toolbox,
|
||||
useToolbox,
|
||||
utils,
|
||||
};
|
||||
|
||||
7
platform/ui-next/src/utils/getToggledClassName.tsx
Normal file
7
platform/ui-next/src/utils/getToggledClassName.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
const getToggledClassName = isToggled => {
|
||||
return isToggled
|
||||
? '!text-primary-active'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
|
||||
};
|
||||
|
||||
export { getToggledClassName };
|
||||
3
platform/ui-next/src/utils/index.ts
Normal file
3
platform/ui-next/src/utils/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { getToggledClassName } from './getToggledClassName';
|
||||
|
||||
export { getToggledClassName };
|
||||
@ -31,7 +31,7 @@ const classes = {
|
||||
isActive
|
||||
? isExpanded
|
||||
? 'border-primary-dark !bg-primary-dark hover:border-primary-dark !text-primary-light'
|
||||
: 'border-primary-light bg-primary-light rounded-md'
|
||||
: 'border-primary-light bg-primary-light !text-black rounded-md'
|
||||
: `focus:!text-black focus:!rounded-md focus:!border-primary-light focus:!bg-primary-light ${isExpanded ? 'border-primary-dark bg-primary-dark !text-primary-light' : 'border-secondary-dark bg-secondary-dark group-hover/button:border-primary-dark group-hover/button:text-primary-light hover:!bg-primary-dark hover:border-primary-dark focus:!text-black'}`
|
||||
),
|
||||
Secondary: ({ isExpanded, primary }) =>
|
||||
|
||||
@ -76,6 +76,7 @@ ToolbarButton.propTypes = {
|
||||
commands: PropTypes.oneOfType([PropTypes.array, PropTypes.object, PropTypes.string]),
|
||||
onInteraction: PropTypes.func,
|
||||
icon: PropTypes.string.isRequired,
|
||||
disabledText: PropTypes.string,
|
||||
label: PropTypes.string.isRequired,
|
||||
/** Tooltip content can be replaced for a customized content by passing a node to this value. */
|
||||
dropdownContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||
|
||||
@ -31,7 +31,7 @@ function Toolbox({
|
||||
|
||||
useEffect(() => {
|
||||
const currentButtonIdsStr = JSON.stringify(
|
||||
toolbarButtons.map(button => {
|
||||
toolbarButtons?.map(button => {
|
||||
const { id, componentProps } = button;
|
||||
if (componentProps.items?.length) {
|
||||
return componentProps.items.map(item => `${item.id}-${item.disabled}`);
|
||||
|
||||
@ -55,7 +55,7 @@ function ToolboxUI(props: withAppTypes) {
|
||||
<>
|
||||
<div className="flex flex-col bg-black">
|
||||
<div className="bg-primary-dark mt-0.5 flex flex-wrap py-2">
|
||||
{toolbarButtons.map((toolDef, index) => {
|
||||
{toolbarButtons?.map((toolDef, index) => {
|
||||
if (!toolDef) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
export function toolboxReducer(state, action) {
|
||||
let newState = { ...state };
|
||||
|
||||
const { payload } = action;
|
||||
|
||||
switch (action.type) {
|
||||
case 'SET_ACTIVE_TOOL':
|
||||
newState = {
|
||||
...state,
|
||||
activeTool: payload.activeTool,
|
||||
};
|
||||
break;
|
||||
case 'UPDATE_TOOL_OPTION':
|
||||
newState = {
|
||||
...state,
|
||||
toolOptions: {
|
||||
...state.toolOptions,
|
||||
[payload.toolName]: state.toolOptions[payload.toolName].map(option =>
|
||||
option.id === payload.optionName ? { ...option, value: payload.value } : option
|
||||
),
|
||||
},
|
||||
};
|
||||
break;
|
||||
case 'INITIALIZE_TOOL_OPTIONS':
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const newToolOptions = Object.keys(payload?.toolOptions || {}).reduce((acc, toolId) => {
|
||||
const tool = payload.toolOptions[toolId];
|
||||
if (state.toolOptions[toolId]) {
|
||||
// Preserve existing options, potentially merging with new ones if necessary
|
||||
acc[toolId] = state.toolOptions[toolId].map(existingOption => {
|
||||
const initialOption = tool.find(option => option.id === existingOption.id);
|
||||
return initialOption
|
||||
? { ...initialOption, value: existingOption.value }
|
||||
: existingOption;
|
||||
});
|
||||
} else {
|
||||
acc[toolId] = tool;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
newState = {
|
||||
...state,
|
||||
toolOptions: newToolOptions,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
@ -38,7 +38,7 @@ export default defineConfig({
|
||||
//},
|
||||
],
|
||||
webServer: {
|
||||
command: 'yarn test:e2e:serve',
|
||||
command: 'cross-env APP_CONFIG=config/e2e.js yarn start',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 360_000,
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ const increaseSlabThickness = async (page: Page, id: string, lineNumber: number,
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
await initilizeMousePositionTracker(page);
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElemen
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '2.16.840.1.114362.1.11972228.22789312658.616067305.306.2';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
|
||||
const mode = 'Basic Viewer';
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user