ui(components): Viewport components added to ui-next (#4649)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
parent
47de38f3ff
commit
230ae9758b
@ -462,7 +462,7 @@ workflows:
|
||||
- BUILD
|
||||
- DOCKER_BETA_PUBLISH:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
- DOCKER_BETA_PUBLISH_ARM
|
||||
- DOCKER_BETA_PUBLISH_ARM:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
@ -487,7 +487,7 @@ workflows:
|
||||
- HOLD_FOR_APPROVAL
|
||||
- DOCKER_RELEASE_PUBLISH:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
- DOCKER_RELEASE_PUBLISH_ARM
|
||||
- DOCKER_RELEASE_PUBLISH_ARM:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
|
||||
import { LoadingIndicatorTotalPercent } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
|
||||
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
const {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
|
||||
import { LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
|
||||
import promptHydrateRT from '../utils/promptHydrateRT';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||
import { LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
|
||||
@ -5,9 +5,10 @@ import { ExtensionManager, useToolbar } from '@ohif/core';
|
||||
|
||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||
|
||||
import { ViewportActionArrows } from '@ohif/ui';
|
||||
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
|
||||
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
|
||||
import { useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
|
||||
|
||||
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useCine, useViewportGrid } from '@ohif/ui';
|
||||
import { useCine } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { utilities as csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core';
|
||||
import { utilities as cstUtils } from '@cornerstonejs/tools';
|
||||
import DynamicVolumeControls from './DynamicVolumeControls';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import ViewportWindowLevel from '../ViewportWindowLevel/ViewportWindowLevel';
|
||||
|
||||
const ActiveViewportWindowLevel = ({ servicesManager }: withAppTypes): ReactElement => {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
import { AllInOneMenu, useViewportGrid } from '@ohif/ui';
|
||||
import { AllInOneMenu } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { Colormap } from './Colormap';
|
||||
import { Colorbar } from './Colorbar';
|
||||
import { setViewportColorbar } from './Colorbar';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { MeasurementTable } from '@ohif/ui-next';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { useMeasurements } from '../hooks/useMeasurements';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useImageViewer, useViewportGrid } from '@ohif/ui';
|
||||
import { useImageViewer } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { StudyBrowser } from '@ohif/ui-next';
|
||||
import { utils } from '@ohif/core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { Types } from '@ohif/core';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import MicroscopyPanel from './components/MicroscopyPanel/MicroscopyPanel';
|
||||
|
||||
// TODO:
|
||||
|
||||
@ -4,7 +4,7 @@ import getPanelModule from './getPanelModule';
|
||||
import getCommandsModule from './getCommandsModule';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import getDicomMicroscopySRSopClassHandler from './DicomMicroscopySRSopClassHandler';
|
||||
import MicroscopyService from './services/MicroscopyService';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
@ -43,7 +43,6 @@ const extension: Types.Extensions.Extension = {
|
||||
* that is provided by the Cornerstone extension in OHIF.
|
||||
*/
|
||||
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param props {*}
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useContext, useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Machine } from 'xstate';
|
||||
import { useMachine } from '@xstate/react';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { promptLabelAnnotation, promptSaveReport } from '@ohif/extension-default';
|
||||
import { machineConfiguration, defaultOptions, RESPONSE } from './measurementTrackingMachine';
|
||||
import promptBeginTracking from './promptBeginTracking';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { PanelMeasurement } from '@ohif/extension-cornerstone';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { StudySummary } from '@ohif/ui-next';
|
||||
import { Button, Icons } from '@ohif/ui-next';
|
||||
import { DicomMetadataStore, utils } from '@ohif/core';
|
||||
|
||||
@ -3,7 +3,8 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
import { utils } from '@ohif/core';
|
||||
import { useImageViewer, useViewportGrid, Dialog, ButtonEnums } from '@ohif/ui';
|
||||
import { useImageViewer, Dialog, ButtonEnums } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { StudyBrowser } from '@ohif/ui-next';
|
||||
|
||||
import { useTrackedMeasurements } from '../../getContextModule';
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ViewportActionArrows, useViewportGrid } from '@ohif/ui';
|
||||
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
|
||||
import { ViewportActionArrows } from '@ohif/ui';
|
||||
import { useViewportGrid, Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
|
||||
|
||||
import { annotation } from '@cornerstonejs/tools';
|
||||
import { useTrackedMeasurements } from './../getContextModule';
|
||||
|
||||
@ -19,7 +19,6 @@ import {
|
||||
ModalProvider,
|
||||
ThemeWrapper,
|
||||
ViewportDialogProvider,
|
||||
ViewportGridProvider,
|
||||
CineProvider,
|
||||
UserAuthenticationProvider,
|
||||
ToolboxProvider,
|
||||
@ -27,6 +26,7 @@ import {
|
||||
import {
|
||||
ThemeWrapper as ThemeWrapperNext,
|
||||
NotificationProvider,
|
||||
ViewportGridProvider,
|
||||
TooltipProvider,
|
||||
} from '@ohif/ui-next';
|
||||
// Viewer Project
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useCallback, useRef } from 'react';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
import { Types, MeasurementService } from '@ohif/core';
|
||||
import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui';
|
||||
import { ViewportGrid, ViewportPane } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import EmptyViewport from './EmptyViewport';
|
||||
import classNames from 'classnames';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-context-menu": "^2.2.4",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
|
||||
187
platform/ui-next/src/components/ContextMenu/ContextMenu.tsx
Normal file
187
platform/ui-next/src/components/ContextMenu/ContextMenu.tsx
Normal file
@ -0,0 +1,187 @@
|
||||
import * as React from 'react';
|
||||
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from '@radix-ui/react-icons';
|
||||
|
||||
const ContextMenu = ContextMenuPrimitive.Root;
|
||||
|
||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
||||
|
||||
const ContextMenuGroup = ContextMenuPrimitive.Group;
|
||||
|
||||
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
||||
|
||||
const ContextMenuSub = ContextMenuPrimitive.Sub;
|
||||
|
||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||
|
||||
const ContextMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
));
|
||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const ContextMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const ContextMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
));
|
||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
|
||||
|
||||
const ContextMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
|
||||
|
||||
const ContextMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const ContextMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<DotFilledIcon className="h-4 w-4 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
));
|
||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const ContextMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('text-foreground px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
|
||||
|
||||
const ContextMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
|
||||
|
||||
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
ContextMenuShortcut.displayName = 'ContextMenuShortcut';
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
};
|
||||
35
platform/ui-next/src/components/ContextMenu/index.ts
Normal file
35
platform/ui-next/src/components/ContextMenu/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
} from './ContextMenu';
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
};
|
||||
138
platform/ui-next/src/components/Viewport/PatientInfo.tsx
Normal file
138
platform/ui-next/src/components/Viewport/PatientInfo.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '../../components';
|
||||
|
||||
const classes = {
|
||||
infoHeader: 'text-base text-primary-light',
|
||||
infoText: 'text-base text-white max-w-24 truncate',
|
||||
firstRow: 'flex flex-col',
|
||||
row: 'flex flex-col ml-4',
|
||||
};
|
||||
|
||||
/**
|
||||
* A small info icon that, when clicked, can reveal additional patient details in a tooltip overlay.
|
||||
*/
|
||||
function PatientInfo({
|
||||
patientName,
|
||||
patientSex,
|
||||
patientAge,
|
||||
MRN,
|
||||
thickness,
|
||||
thicknessUnits,
|
||||
spacing,
|
||||
scanner,
|
||||
isOpen,
|
||||
showPatientInfoRef,
|
||||
}) {
|
||||
const { t } = useTranslation('PatientInfo');
|
||||
|
||||
// strip leading '0' from age if present
|
||||
while (patientAge.charAt(0) === '0') {
|
||||
patientAge = patientAge.substr(1);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={showPatientInfoRef}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Icons.Info className="hover:text-primary-light cursor-pointer text-white" />
|
||||
</TooltipTrigger>
|
||||
{isOpen && (
|
||||
<TooltipContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<div className="flex py-2">
|
||||
<div className="flex pt-1">
|
||||
<Icons.InfoLink className="text-primary-main w-4" />
|
||||
</div>
|
||||
<div className="ml-2 flex flex-col">
|
||||
<span
|
||||
className="text-base font-bold text-white"
|
||||
title={patientName}
|
||||
>
|
||||
{patientName}
|
||||
</span>
|
||||
<div className="border-secondary-main mt-4 mb-4 flex border-b pb-4">
|
||||
<div className={classnames(classes.firstRow)}>
|
||||
<span className={classnames(classes.infoHeader)}>{t('Sex')}</span>
|
||||
<span
|
||||
className={classnames(classes.infoText)}
|
||||
title={patientSex}
|
||||
>
|
||||
{patientSex}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>{t('Age')}</span>
|
||||
<span
|
||||
className={classnames(classes.infoText)}
|
||||
title={patientAge}
|
||||
>
|
||||
{patientAge}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>{t('MRN')}</span>
|
||||
<span
|
||||
className={classnames(classes.infoText)}
|
||||
title={MRN}
|
||||
>
|
||||
{MRN}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className={classnames(classes.firstRow)}>
|
||||
<span className={classnames(classes.infoHeader)}>{t('Thickness')}</span>
|
||||
<span
|
||||
className={classnames(classes.infoText)}
|
||||
title={thickness}
|
||||
>
|
||||
{thicknessUnits ? `${thickness}${thicknessUnits}` : `${thickness}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>{t('Spacing')}</span>
|
||||
<span
|
||||
className={classnames(classes.infoText)}
|
||||
title={spacing}
|
||||
>
|
||||
{spacing}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>{t('Scanner')}</span>
|
||||
<span
|
||||
className={classnames(classes.infoText)}
|
||||
title={scanner}
|
||||
>
|
||||
{scanner}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PatientInfo.propTypes = {
|
||||
patientName: PropTypes.string,
|
||||
patientSex: PropTypes.string,
|
||||
patientAge: PropTypes.string,
|
||||
MRN: PropTypes.string,
|
||||
thickness: PropTypes.string,
|
||||
thicknessUnits: PropTypes.string,
|
||||
spacing: PropTypes.string,
|
||||
scanner: PropTypes.string,
|
||||
isOpen: PropTypes.bool,
|
||||
showPatientInfoRef: PropTypes.object,
|
||||
};
|
||||
|
||||
export { PatientInfo };
|
||||
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
|
||||
const arrowClasses =
|
||||
'cursor-pointer flex items-center justify-center shrink-0 text-primary-light active:text-white hover:bg-secondary-light/60 rounded';
|
||||
|
||||
/**
|
||||
* A small set of left/right arrow icons for stepping through slices or series.
|
||||
*/
|
||||
function ViewportActionArrows({ onArrowsClick, className }) {
|
||||
return (
|
||||
<div className={classNames(className, 'flex')}>
|
||||
<div className={arrowClasses}>
|
||||
<Icons.ArrowLeftBold onClick={() => onArrowsClick(-1)} />
|
||||
</div>
|
||||
<div className={arrowClasses}>
|
||||
<Icons.ArrowRightBold onClick={() => onArrowsClick(1)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ViewportActionArrows.propTypes = {
|
||||
onArrowsClick: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export { ViewportActionArrows };
|
||||
128
platform/ui-next/src/components/Viewport/ViewportActionBar.tsx
Normal file
128
platform/ui-next/src/components/Viewport/ViewportActionBar.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import React, {
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import PatientInfo from './PatientInfo';
|
||||
|
||||
/**
|
||||
* This is the modern Viewport Action Bar, showing patient info, series date,
|
||||
* series description, and optional next/prev arrows if there's enough screen width.
|
||||
*/
|
||||
type ViewportActionBarProps = {
|
||||
studyData: any;
|
||||
onArrowsClick: (arrow: string) => void;
|
||||
onDoubleClick: MouseEventHandler;
|
||||
getStatusComponent: () => ReactElement;
|
||||
};
|
||||
|
||||
function ViewportActionBar({
|
||||
studyData,
|
||||
onArrowsClick,
|
||||
onDoubleClick,
|
||||
getStatusComponent,
|
||||
}: ViewportActionBarProps): JSX.Element {
|
||||
const { label, studyDate, seriesDescription, patientInformation } = studyData;
|
||||
const { patientName, patientSex, patientAge, MRN, thickness, thicknessUnits, spacing, scanner } =
|
||||
patientInformation;
|
||||
|
||||
const [showPatientInfo, setShowPatientInfo] = useState(false);
|
||||
const showPatientInfoElemRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// handle click outside to close patient info
|
||||
const handleClickOutside = useCallback(
|
||||
(evt: MouseEvent) => {
|
||||
if (
|
||||
showPatientInfo &&
|
||||
showPatientInfoElemRef.current &&
|
||||
!showPatientInfoElemRef.current.contains(evt.target as Node)
|
||||
) {
|
||||
setShowPatientInfo(false);
|
||||
}
|
||||
},
|
||||
[showPatientInfoElemRef, showPatientInfo]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [handleClickOutside]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-auto flex h-8 shrink-0 select-none items-center overflow-visible whitespace-nowrap px-2 text-base"
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
{getStatusComponent()}
|
||||
{!!label?.length && <span className="text-aqua-pale text-large ml-1">{label}</span>}
|
||||
<div className="border-secondary-light mx-2 border-l py-2"></div>
|
||||
<span
|
||||
data-cy="studyDate"
|
||||
className="text-white"
|
||||
>
|
||||
{studyDate}
|
||||
</span>
|
||||
<div className="border-secondary-light mx-2 border-l py-2"></div>
|
||||
<span className="text-aqua-pale mr-1 overflow-hidden text-ellipsis">{seriesDescription}</span>
|
||||
{/* Prev/Next icons */}
|
||||
<Icons.ByName
|
||||
className="hover:text-primary-light ml-auto mr-2 cursor-pointer text-white"
|
||||
name="chevron-prev"
|
||||
onClick={() => onArrowsClick('left')}
|
||||
/>
|
||||
<Icons.ByName
|
||||
className="hover:text-primary-light mr-2 cursor-pointer text-white"
|
||||
name="chevron-next"
|
||||
onClick={() => onArrowsClick('right')}
|
||||
/>
|
||||
{/* Patient Info */}
|
||||
<div onClick={() => setShowPatientInfo(!showPatientInfo)}>
|
||||
<PatientInfo
|
||||
showPatientInfoRef={showPatientInfoElemRef}
|
||||
isOpen={showPatientInfo}
|
||||
patientName={patientName}
|
||||
patientSex={patientSex}
|
||||
patientAge={patientAge}
|
||||
MRN={MRN}
|
||||
thickness={thickness}
|
||||
thicknessUnits={thicknessUnits}
|
||||
spacing={spacing}
|
||||
scanner={scanner}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ViewportActionBar.propTypes = {
|
||||
onArrowsClick: PropTypes.func.isRequired,
|
||||
onDoubleClick: PropTypes.func,
|
||||
studyData: PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
studyDate: PropTypes.string.isRequired,
|
||||
seriesDescription: PropTypes.string.isRequired,
|
||||
patientInformation: PropTypes.shape({
|
||||
patientName: PropTypes.string,
|
||||
patientSex: PropTypes.string,
|
||||
patientAge: PropTypes.string,
|
||||
MRN: PropTypes.string,
|
||||
thickness: PropTypes.string,
|
||||
thicknessUnits: PropTypes.string,
|
||||
spacing: PropTypes.string,
|
||||
scanner: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
getStatusComponent: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { ViewportActionBar };
|
||||
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* A button that can trigger commands when clicked.
|
||||
*/
|
||||
function ViewportActionButton({ onInteraction, commands, id, children }) {
|
||||
return (
|
||||
<div
|
||||
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
|
||||
// Using onMouseUp because onClick wasn't firing if pointer-events are none.
|
||||
onMouseUp={() => {
|
||||
onInteraction({
|
||||
itemId: id,
|
||||
commands,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ViewportActionButton.propTypes = {
|
||||
id: PropTypes.string,
|
||||
onInteraction: PropTypes.func.isRequired,
|
||||
commands: PropTypes.array,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export { ViewportActionButton };
|
||||
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* A small container that can render multiple "corner" items (like icons, status)
|
||||
* in each corner of the viewport: top-left, top-right, bottom-left, bottom-right.
|
||||
*/
|
||||
export enum ViewportActionCornersLocations {
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
}
|
||||
|
||||
const commonClasses = 'pointer-events-auto flex items-center gap-1';
|
||||
const locationClasses = {
|
||||
[ViewportActionCornersLocations.topLeft]: classNames(
|
||||
commonClasses,
|
||||
'absolute top-[4px] left-[0px] pl-[4px]'
|
||||
),
|
||||
[ViewportActionCornersLocations.topRight]: classNames(
|
||||
commonClasses,
|
||||
'absolute top-[4px] right-[4px] right-viewport-scrollbar'
|
||||
),
|
||||
[ViewportActionCornersLocations.bottomLeft]: classNames(
|
||||
commonClasses,
|
||||
'absolute bottom-[4px] left-[0px] pl-[4px]'
|
||||
),
|
||||
[ViewportActionCornersLocations.bottomRight]: classNames(
|
||||
commonClasses,
|
||||
'absolute bottom-[4px] right-[0px] right-viewport-scrollbar'
|
||||
),
|
||||
};
|
||||
|
||||
function ViewportActionCorners({ cornerComponents }) {
|
||||
if (!cornerComponents) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute h-full w-full select-none"
|
||||
onDoubleClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{Object.entries(cornerComponents).map(([location, locationArray]) => (
|
||||
<div
|
||||
key={location}
|
||||
className={locationClasses[location]}
|
||||
>
|
||||
{locationArray.map(componentInfo => (
|
||||
<div key={componentInfo.id}>{componentInfo.component}</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ViewportActionCorners.propTypes = {
|
||||
cornerComponents: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export { ViewportActionCorners };
|
||||
30
platform/ui-next/src/components/Viewport/ViewportGrid.tsx
Normal file
30
platform/ui-next/src/components/Viewport/ViewportGrid.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* A minimal top-level container that organizes multiple <ViewportPane>
|
||||
* children in a grid. Typically driven by a layout config.
|
||||
*/
|
||||
function ViewportGrid({ numRows, numCols, layoutType, children }) {
|
||||
return (
|
||||
<div
|
||||
data-cy="viewport-grid"
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ViewportGrid.propTypes = {
|
||||
numRows: PropTypes.number.isRequired,
|
||||
numCols: PropTypes.number.isRequired,
|
||||
layoutType: PropTypes.string,
|
||||
children: PropTypes.arrayOf(PropTypes.node).isRequired,
|
||||
};
|
||||
|
||||
export { ViewportGrid };
|
||||
22
platform/ui-next/src/components/Viewport/ViewportOverlay.css
Normal file
22
platform/ui-next/src/components/Viewport/ViewportOverlay.css
Normal file
@ -0,0 +1,22 @@
|
||||
.imageViewerViewport.empty ~ .ViewportOverlay {
|
||||
display: none;
|
||||
}
|
||||
.ViewportOverlay {
|
||||
color: #9ccef9;
|
||||
}
|
||||
.ViewportOverlay .overlay-element {
|
||||
position: absolute;
|
||||
font-weight: 400;
|
||||
text-shadow: 1px 1px #000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.overlay-top {
|
||||
top: 28px;
|
||||
}
|
||||
.overlay-bottom {
|
||||
bottom: 3px;
|
||||
}
|
||||
|
||||
.overlay-text {
|
||||
text-shadow: 0.8px 0.8px 0.5px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
69
platform/ui-next/src/components/Viewport/ViewportOverlay.tsx
Normal file
69
platform/ui-next/src/components/Viewport/ViewportOverlay.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './ViewportOverlay.css';
|
||||
|
||||
/**
|
||||
* Renders text overlays (top-left, top-right, bottom-left, bottom-right)
|
||||
* around the active viewport for metadata or status messages.
|
||||
*
|
||||
* The parent is responsible for styling offsets.
|
||||
*/
|
||||
const classes = {
|
||||
topLeft: 'overlay-top left-viewport',
|
||||
topRight: 'overlay-top right-viewport-scrollbar',
|
||||
bottomRight: 'overlay-bottom right-viewport-scrollbar',
|
||||
bottomLeft: 'overlay-bottom left-viewport',
|
||||
};
|
||||
|
||||
function ViewportOverlay({
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomRight,
|
||||
bottomLeft,
|
||||
color = 'text-primary-light',
|
||||
}) {
|
||||
const overlay = 'absolute pointer-events-none viewport-overlay';
|
||||
|
||||
return (
|
||||
<div className={classNames(color, 'overlay-text text-[13px] leading-5')}>
|
||||
<div
|
||||
data-cy="viewport-overlay-top-left"
|
||||
className={classNames(overlay, classes.topLeft)}
|
||||
>
|
||||
{topLeft}
|
||||
</div>
|
||||
<div
|
||||
data-cy="viewport-overlay-top-right"
|
||||
className={classNames(overlay, classes.topRight)}
|
||||
style={{ transform: 'translateX(-8px)' }}
|
||||
>
|
||||
{topRight}
|
||||
</div>
|
||||
<div
|
||||
data-cy="viewport-overlay-bottom-right"
|
||||
className={classNames(overlay, classes.bottomRight)}
|
||||
style={{ transform: 'translateX(-8px)' }}
|
||||
>
|
||||
{bottomRight}
|
||||
</div>
|
||||
<div
|
||||
data-cy="viewport-overlay-bottom-left"
|
||||
className={classNames(overlay, classes.bottomLeft)}
|
||||
>
|
||||
{bottomLeft}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ViewportOverlay.propTypes = {
|
||||
topLeft: PropTypes.node,
|
||||
topRight: PropTypes.node,
|
||||
bottomRight: PropTypes.node,
|
||||
bottomLeft: PropTypes.node,
|
||||
color: PropTypes.string,
|
||||
};
|
||||
|
||||
export { ViewportOverlay };
|
||||
97
platform/ui-next/src/components/Viewport/ViewportPane.tsx
Normal file
97
platform/ui-next/src/components/Viewport/ViewportPane.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useDrop } from 'react-dnd';
|
||||
|
||||
/**
|
||||
* The "pane" that encloses a Cornerstone or other type of Viewport. This handles
|
||||
* drag-and-drop for display sets, activation on click, etc.
|
||||
*/
|
||||
function ViewportPane({
|
||||
children,
|
||||
className,
|
||||
customStyle,
|
||||
isActive,
|
||||
onDrop,
|
||||
onDoubleClick,
|
||||
onInteraction = () => {},
|
||||
acceptDropsFor,
|
||||
}) {
|
||||
let dropElement = null;
|
||||
|
||||
const [{ isHovered, isHighlighted }, drop] = useDrop({
|
||||
accept: acceptDropsFor,
|
||||
drop: (droppedItem, monitor) => {
|
||||
if (monitor.canDrop() && monitor.isOver() && onDrop) {
|
||||
onInteraction();
|
||||
onDrop(droppedItem);
|
||||
}
|
||||
},
|
||||
collect: monitor => ({
|
||||
isHighlighted: monitor.canDrop(),
|
||||
isHovered: monitor.isOver(),
|
||||
}),
|
||||
});
|
||||
|
||||
const focus = () => {
|
||||
if (dropElement) {
|
||||
dropElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const onInteractionHandler = event => {
|
||||
focus();
|
||||
onInteraction(event);
|
||||
};
|
||||
|
||||
const refHandler = element => {
|
||||
drop(element);
|
||||
dropElement = element;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={refHandler}
|
||||
onPointerDown={onInteractionHandler}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onClick={onInteractionHandler}
|
||||
onScroll={onInteractionHandler}
|
||||
onWheel={onInteractionHandler}
|
||||
className={classNames(
|
||||
'group/pane h-full w-full overflow-hidden rounded-md transition duration-300',
|
||||
{
|
||||
'border-primary-light border-2': isActive,
|
||||
'border-2 border-transparent': !isActive,
|
||||
},
|
||||
className
|
||||
)}
|
||||
style={customStyle}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'h-full w-full overflow-hidden rounded-md',
|
||||
{
|
||||
'border border-transparent': isActive,
|
||||
'border-secondary-light group-hover/pane:border-primary-light/70 border': !isActive,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ViewportPane.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
acceptDropsFor: PropTypes.string.isRequired,
|
||||
onDrop: PropTypes.func.isRequired,
|
||||
onInteraction: PropTypes.func.isRequired,
|
||||
onDoubleClick: PropTypes.func,
|
||||
customStyle: PropTypes.object,
|
||||
};
|
||||
|
||||
export { ViewportPane };
|
||||
8
platform/ui-next/src/components/Viewport/index.ts
Normal file
8
platform/ui-next/src/components/Viewport/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export { ViewportActionButton } from './ViewportActionButton';
|
||||
export { PatientInfo } from './PatientInfo';
|
||||
export { ViewportActionBar } from './ViewportActionBar';
|
||||
export { ViewportActionArrows } from './ViewportActionArrows';
|
||||
export { ViewportPane } from './ViewportPane';
|
||||
export { ViewportActionCorners, ViewportActionCornersLocations } from './ViewportActionCorners';
|
||||
export { ViewportOverlay } from './ViewportOverlay';
|
||||
export { ViewportGrid } from './ViewportGrid';
|
||||
@ -79,6 +79,17 @@ import { StudySummary } from './StudySummary';
|
||||
import { ErrorBoundary } from './Errorboundary';
|
||||
import { Header } from './Header';
|
||||
import { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card';
|
||||
import {
|
||||
ViewportActionButton,
|
||||
PatientInfo,
|
||||
ViewportActionBar,
|
||||
ViewportActionArrows,
|
||||
ViewportPane,
|
||||
ViewportActionCorners,
|
||||
ViewportActionCornersLocations,
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
} from './Viewport';
|
||||
|
||||
export {
|
||||
ErrorBoundary,
|
||||
@ -183,5 +194,14 @@ export {
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
ViewportActionButton,
|
||||
PatientInfo,
|
||||
ViewportActionBar,
|
||||
ViewportActionArrows,
|
||||
ViewportPane,
|
||||
ViewportActionCorners,
|
||||
ViewportActionCornersLocations,
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
Clipboard,
|
||||
};
|
||||
|
||||
522
platform/ui-next/src/contextProviders/ViewportGridProvider.tsx
Normal file
522
platform/ui-next/src/contextProviders/ViewportGridProvider.tsx
Normal file
@ -0,0 +1,522 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useReducer,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { ViewportGridService, utils } from '@ohif/core';
|
||||
|
||||
const DEFAULT_STATE: AppTypes.ViewportGrid.State = {
|
||||
activeViewportId: null,
|
||||
layout: {
|
||||
numRows: 0,
|
||||
numCols: 0,
|
||||
layoutType: 'grid',
|
||||
},
|
||||
// this flag is used to determine if the hanging protocol layout is active
|
||||
// so that we can inherit the viewport options from the previous state
|
||||
// otherwise we will not allow that. Basically the issue is that we need
|
||||
// to be able to come out of the hanging protocol layout and go back to the
|
||||
// regular layout e.g., if we are in the MPR hanging protocol, and someone use
|
||||
// 1x1 layout by custom layout selector, there is no way to drag and drop
|
||||
// a non-reconstructible series to the viewport since it will always
|
||||
// inherit the hanging protocol layout options (volume viewport),
|
||||
// so we need to be able to switch back to the regular layout.
|
||||
isHangingProtocolLayout: false,
|
||||
// Viewports structure has been changed to Map (previously it was
|
||||
// tied to the viewportIndex which caused multiple issues. Now we have
|
||||
// moved completely to viewportId which is unique for each viewport.
|
||||
viewports: new Map(
|
||||
Object.entries({
|
||||
default: {
|
||||
viewportId: 'default',
|
||||
displaySetInstanceUIDs: [],
|
||||
isReady: false,
|
||||
viewportOptions: {
|
||||
viewportId: 'default',
|
||||
},
|
||||
displaySetSelectors: [],
|
||||
displaySetOptions: [{}],
|
||||
x: 0, // left
|
||||
y: 0, // top
|
||||
width: 100,
|
||||
height: 100,
|
||||
viewportLabel: null,
|
||||
},
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
const determineActiveViewportId = (
|
||||
state: AppTypes.ViewportGrid.State,
|
||||
newViewports: Map<string, AppTypes.ViewportGrid.Viewport>
|
||||
) => {
|
||||
const { activeViewportId } = state;
|
||||
const currentActiveViewport = state.viewports.get(activeViewportId);
|
||||
|
||||
if (!currentActiveViewport) {
|
||||
// if there is no active viewport, we should just return the first viewport
|
||||
const firstViewport = newViewports.values().next().value;
|
||||
return firstViewport.viewportOptions.viewportId;
|
||||
}
|
||||
|
||||
// for the new viewports, we should rank them by the displaySetInstanceUIDs
|
||||
// they are displaying and the orientation then we can find the active viewport
|
||||
const currentActiveDisplaySetInstanceUIDs = currentActiveViewport.displaySetInstanceUIDs;
|
||||
|
||||
// This doesn't take into account where stack viewport is converting to volumeViewport
|
||||
// since in stack viewport we don't have a concept of "orientation" as a string
|
||||
// maybe we should calculate the orientation based on the active imageId
|
||||
// so that we can compare it with the new viewports (which might be volume viewports)
|
||||
// and find the best match
|
||||
const currentOrientation = currentActiveViewport.viewportOptions.orientation;
|
||||
|
||||
const filteredNewViewports = Array.from(newViewports.values()).filter(
|
||||
viewport => viewport.displaySetInstanceUIDs?.length > 0
|
||||
);
|
||||
|
||||
const sortedViewports = Array.from(filteredNewViewports.values()).sort((a, b) => {
|
||||
// Compare orientations
|
||||
const aOrientationMatch = a.viewportOptions.orientation === currentOrientation;
|
||||
const bOrientationMatch = b.viewportOptions.orientation === currentOrientation;
|
||||
if (aOrientationMatch !== bOrientationMatch) {
|
||||
return bOrientationMatch - aOrientationMatch;
|
||||
}
|
||||
|
||||
// Compare displaySetInstanceUIDs
|
||||
const aMatch = a.displaySetInstanceUIDs.some(uid =>
|
||||
currentActiveDisplaySetInstanceUIDs.includes(uid)
|
||||
);
|
||||
const bMatch = b.displaySetInstanceUIDs.some(uid =>
|
||||
currentActiveDisplaySetInstanceUIDs.includes(uid)
|
||||
);
|
||||
if (aMatch !== bMatch) {
|
||||
return bMatch - aMatch;
|
||||
}
|
||||
|
||||
return 0; // Return 0 if no differences found
|
||||
});
|
||||
|
||||
if (!sortedViewports?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sortedViewports[0].viewportId;
|
||||
};
|
||||
|
||||
// Define the API interface
|
||||
interface ViewportGridApi {
|
||||
getState: () => AppTypes.ViewportGrid.State;
|
||||
setActiveViewportId: (index: string) => void;
|
||||
setDisplaySetsForViewport: (props: any) => void;
|
||||
setDisplaySetsForViewports: (props: any[]) => void;
|
||||
setLayout: (layout: AppTypes.ViewportGrid.Layout) => void;
|
||||
reset: () => void;
|
||||
set: (gridLayoutState: Partial<AppTypes.ViewportGrid.State>) => void;
|
||||
getNumViewportPanes: () => number;
|
||||
setViewportIsReady: (viewportId: string, isReady: boolean) => void;
|
||||
getGridViewportsReady: () => boolean;
|
||||
getActiveViewportOptionByKey: (key: string) => any;
|
||||
setViewportGridSizeChanged: (props: any) => void;
|
||||
publishViewportsReady: () => void;
|
||||
}
|
||||
|
||||
// Update the context type
|
||||
export const ViewportGridContext = createContext<[AppTypes.ViewportGrid.State, ViewportGridApi]>([
|
||||
DEFAULT_STATE,
|
||||
{} as ViewportGridApi,
|
||||
]);
|
||||
|
||||
// Update the provider props type
|
||||
interface ViewportGridProviderProps {
|
||||
children: ReactNode;
|
||||
service: ViewportGridService;
|
||||
}
|
||||
|
||||
export function ViewportGridProvider({ children, service }: ViewportGridProviderProps) {
|
||||
const viewportGridReducer = (state: AppTypes.ViewportGrid.State, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_ACTIVE_VIEWPORT_ID': {
|
||||
return { ...state, ...{ activeViewportId: action.payload } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display sets for multiple viewports.
|
||||
* This is a replacement for the older set display set for viewport (single)
|
||||
* because the old one had race conditions wherein the viewports could
|
||||
* render partially in various ways causing exceptions.
|
||||
*/
|
||||
case 'SET_DISPLAYSETS_FOR_VIEWPORTS': {
|
||||
const { payload } = action;
|
||||
const viewports = new Map(state.viewports);
|
||||
|
||||
payload.forEach(updatedViewport => {
|
||||
const { viewportId, displaySetInstanceUIDs } = updatedViewport;
|
||||
|
||||
if (!viewportId) {
|
||||
throw new Error('ViewportId is required to set display sets for viewport');
|
||||
}
|
||||
|
||||
const previousViewport = viewports.get(viewportId);
|
||||
|
||||
// remove options that were meant for one time usage
|
||||
if (previousViewport?.viewportOptions?.initialImageOptions) {
|
||||
const { useOnce } = previousViewport.viewportOptions.initialImageOptions;
|
||||
if (useOnce) {
|
||||
previousViewport.viewportOptions.initialImageOptions = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the newly provide viewportOptions and display set options
|
||||
// when provided, and otherwise fall back to the previous ones.
|
||||
// That allows for easy updates of just the display set.
|
||||
let viewportOptions = merge(
|
||||
{},
|
||||
previousViewport?.viewportOptions,
|
||||
updatedViewport?.viewportOptions
|
||||
);
|
||||
|
||||
const displaySetOptions = updatedViewport?.displaySetOptions || [];
|
||||
if (!displaySetOptions.length) {
|
||||
// Copy all the display set options, assuming a full set of displaySet UID's is provided.
|
||||
if (state.isHangingProtocolLayout) {
|
||||
displaySetOptions.push(...(previousViewport.displaySetOptions || []));
|
||||
}
|
||||
if (!displaySetOptions.length) {
|
||||
displaySetOptions.push({});
|
||||
}
|
||||
}
|
||||
|
||||
// if it is not part of the hanging protocol layout, we should remove the toolGroupId
|
||||
// and viewportType from the viewportOptions so that it doesn't
|
||||
// inherit the hanging protocol layout options, only when
|
||||
// the viewport options is not provided (e.g., when drag and drop)
|
||||
// otherwise, programmatically set options should be preserved
|
||||
if (!updatedViewport.viewportOptions && !state.isHangingProtocolLayout) {
|
||||
viewportOptions = {
|
||||
viewportId: viewportOptions.viewportId,
|
||||
};
|
||||
}
|
||||
|
||||
const newViewport = {
|
||||
...previousViewport,
|
||||
displaySetInstanceUIDs,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
// viewportLabel: getViewportLabel(viewports, viewportId),
|
||||
};
|
||||
|
||||
viewportOptions.presentationIds = service.getPresentationIds({
|
||||
viewport: newViewport,
|
||||
viewports,
|
||||
});
|
||||
|
||||
viewports.set(viewportId, {
|
||||
...viewports.get(viewportId),
|
||||
...newViewport,
|
||||
});
|
||||
});
|
||||
|
||||
return { ...state, viewports };
|
||||
}
|
||||
case 'SET_LAYOUT': {
|
||||
const {
|
||||
numCols,
|
||||
numRows,
|
||||
layoutOptions,
|
||||
layoutType = 'grid',
|
||||
activeViewportId,
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout,
|
||||
} = action.payload;
|
||||
|
||||
// If empty viewportOptions, we use numRow and numCols to calculate number of viewports
|
||||
const hasOptions = layoutOptions?.length;
|
||||
const viewports = new Map<string, AppTypes.ViewportGrid.Viewport>();
|
||||
// Options is a temporary state store which can be used by the
|
||||
// findOrCreate to store state about already found viewports. Typically,
|
||||
// it will be used to store the display set UID's which are already
|
||||
// in view so that the find or create can decide which display sets
|
||||
// haven't been viewed yet, and add them in the appropriate order.
|
||||
const options = {};
|
||||
|
||||
let activeViewportIdToSet = activeViewportId;
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
const position = col + row * numCols;
|
||||
const layoutOption = layoutOptions[position];
|
||||
|
||||
let xPos, yPos, w, h;
|
||||
if (layoutOptions && layoutOptions[position]) {
|
||||
({ x: xPos, y: yPos, width: w, height: h } = layoutOptions[position]);
|
||||
} else {
|
||||
w = 1 / numCols;
|
||||
h = 1 / numRows;
|
||||
xPos = col * w;
|
||||
yPos = row * h;
|
||||
}
|
||||
|
||||
const colIndex = Math.round(xPos * numCols);
|
||||
const rowIndex = Math.round(yPos * numRows);
|
||||
|
||||
const positionId = layoutOption?.positionId || `${colIndex}-${rowIndex}`;
|
||||
|
||||
if (hasOptions && position >= layoutOptions.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const viewport = findOrCreateViewport(position, positionId, options);
|
||||
|
||||
if (!viewport) {
|
||||
continue;
|
||||
}
|
||||
|
||||
viewport.positionId = positionId;
|
||||
|
||||
// If the viewport doesn't have a viewportId, we create one
|
||||
if (!viewport.viewportOptions?.viewportId) {
|
||||
const randomUID = utils.uuidv4().substring(0, 8);
|
||||
viewport.viewportOptions = viewport.viewportOptions || {};
|
||||
viewport.viewportOptions.viewportId = `viewport-${randomUID}`;
|
||||
}
|
||||
|
||||
viewport.viewportId = viewport.viewportOptions.viewportId;
|
||||
|
||||
// Create a new viewport object as it is getting updated here
|
||||
// and it is part of the read only state
|
||||
viewports.set(viewport.viewportId, viewport);
|
||||
|
||||
Object.assign(viewport, {
|
||||
width: w,
|
||||
height: h,
|
||||
x: xPos,
|
||||
y: yPos,
|
||||
});
|
||||
|
||||
viewport.isReady = false;
|
||||
|
||||
if (!viewport.viewportOptions.presentationIds) {
|
||||
const presentationIds = service.getPresentationIds({
|
||||
viewport,
|
||||
viewports,
|
||||
});
|
||||
viewport.viewportOptions.presentationIds = presentationIds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeViewportIdToSet =
|
||||
activeViewportIdToSet ?? determineActiveViewportId(state, viewports);
|
||||
|
||||
const ret = {
|
||||
...state,
|
||||
activeViewportId: activeViewportIdToSet,
|
||||
layout: {
|
||||
...state.layout,
|
||||
numCols,
|
||||
numRows,
|
||||
layoutType,
|
||||
},
|
||||
viewports,
|
||||
isHangingProtocolLayout,
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
case 'RESET': {
|
||||
return DEFAULT_STATE;
|
||||
}
|
||||
|
||||
case 'SET': {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
case 'VIEWPORT_IS_READY': {
|
||||
const { viewportId, isReady } = action.payload;
|
||||
const viewports = new Map(state.viewports);
|
||||
const viewport = viewports.get(viewportId);
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewports.set(viewportId, {
|
||||
...viewport,
|
||||
isReady,
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
viewports,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return action.payload;
|
||||
}
|
||||
};
|
||||
|
||||
const [viewportGridState, dispatch] = useReducer(viewportGridReducer, DEFAULT_STATE);
|
||||
|
||||
const getState = useCallback(() => {
|
||||
return viewportGridState;
|
||||
}, [viewportGridState]);
|
||||
|
||||
const getActiveViewportOptionByKey = (key: string) => {
|
||||
const { viewports, activeViewportId } = viewportGridState;
|
||||
return viewports.get(activeViewportId)?.viewportOptions?.[key];
|
||||
};
|
||||
|
||||
const setActiveViewportId = useCallback(
|
||||
index => dispatch({ type: 'SET_ACTIVE_VIEWPORT_ID', payload: index }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setDisplaySetsForViewports = useCallback(
|
||||
viewports =>
|
||||
dispatch({
|
||||
type: 'SET_DISPLAYSETS_FOR_VIEWPORTS',
|
||||
payload: viewports,
|
||||
}),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setViewportIsReady = useCallback(
|
||||
(viewportId, isReady) => {
|
||||
dispatch({
|
||||
type: 'VIEWPORT_IS_READY',
|
||||
payload: {
|
||||
viewportId,
|
||||
isReady,
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatch, viewportGridState]
|
||||
);
|
||||
|
||||
const getGridViewportsReady = useCallback(() => {
|
||||
const { viewports } = viewportGridState;
|
||||
const readyViewports = Array.from(viewports.values()).filter(viewport => viewport.isReady);
|
||||
return readyViewports.length === viewports.size;
|
||||
}, [viewportGridState]);
|
||||
|
||||
const setLayout = useCallback(
|
||||
({
|
||||
layoutType,
|
||||
numRows,
|
||||
numCols,
|
||||
layoutOptions = [],
|
||||
activeViewportId,
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout,
|
||||
}) =>
|
||||
dispatch({
|
||||
type: 'SET_LAYOUT',
|
||||
payload: {
|
||||
layoutType,
|
||||
numRows,
|
||||
numCols,
|
||||
layoutOptions,
|
||||
activeViewportId,
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout,
|
||||
},
|
||||
}),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const reset = useCallback(
|
||||
() =>
|
||||
dispatch({
|
||||
type: 'RESET',
|
||||
payload: {},
|
||||
}),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const set = useCallback(
|
||||
payload =>
|
||||
dispatch({
|
||||
type: 'SET',
|
||||
payload,
|
||||
}),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const getNumViewportPanes = useCallback(() => {
|
||||
const { layout, viewports } = viewportGridState;
|
||||
const { numRows, numCols } = layout;
|
||||
return Math.min(viewports.size, numCols * numRows);
|
||||
}, [viewportGridState]);
|
||||
|
||||
/**
|
||||
* Sets the implementation of ViewportGridService that can be used by extensions.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (service) {
|
||||
service.setServiceImplementation({
|
||||
getState,
|
||||
setActiveViewportId,
|
||||
setDisplaySetsForViewports,
|
||||
setLayout,
|
||||
reset,
|
||||
onModeExit: reset,
|
||||
set,
|
||||
getNumViewportPanes,
|
||||
setViewportIsReady,
|
||||
getGridViewportsReady,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
getState,
|
||||
service,
|
||||
setActiveViewportId,
|
||||
setDisplaySetsForViewports,
|
||||
setLayout,
|
||||
reset,
|
||||
set,
|
||||
getNumViewportPanes,
|
||||
setViewportIsReady,
|
||||
getGridViewportsReady,
|
||||
]);
|
||||
|
||||
// run many of the calls through the service itself since we want to publish events
|
||||
const api = {
|
||||
getState,
|
||||
setActiveViewportId: index => service.setActiveViewportId(index),
|
||||
setDisplaySetsForViewport: props => service.setDisplaySetsForViewports([props]),
|
||||
setDisplaySetsForViewports: props => service.setDisplaySetsForViewports(props),
|
||||
setLayout: layout => service.setLayout(layout),
|
||||
reset: () => service.reset(),
|
||||
set: gridLayoutState => service.setState(gridLayoutState), // run it through the service itself since we want to publish events
|
||||
getNumViewportPanes,
|
||||
setViewportIsReady,
|
||||
getGridViewportsReady,
|
||||
getActiveViewportOptionByKey,
|
||||
setViewportGridSizeChanged: props => service.setViewportGridSizeChanged(props),
|
||||
publishViewportsReady: () => service.publishViewportsReady(),
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewportGridContext.Provider value={[viewportGridState, api]}>
|
||||
{children}
|
||||
</ViewportGridContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
ViewportGridProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
service: PropTypes.instanceOf(ViewportGridService).isRequired,
|
||||
};
|
||||
|
||||
// Update the useViewportGrid hook
|
||||
export const useViewportGrid = (): [AppTypes.ViewportGrid.State, ViewportGridApi] =>
|
||||
useContext(ViewportGridContext);
|
||||
@ -1,3 +1,5 @@
|
||||
import NotificationProvider, { useNotification } from './NotificationProvider';
|
||||
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './ViewportGridProvider';
|
||||
|
||||
export { useNotification, NotificationProvider };
|
||||
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
|
||||
|
||||
@ -81,10 +81,20 @@ import {
|
||||
StudySummary,
|
||||
ErrorBoundary,
|
||||
Header,
|
||||
ViewportActionButton,
|
||||
PatientInfo,
|
||||
ViewportActionBar,
|
||||
ViewportActionArrows,
|
||||
ViewportPane,
|
||||
ViewportActionCorners,
|
||||
ViewportActionCornersLocations,
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
} from './components';
|
||||
import { DataRow } from './components/DataRow';
|
||||
|
||||
import { useNotification, NotificationProvider } from './contextProviders';
|
||||
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './contextProviders';
|
||||
|
||||
export {
|
||||
ErrorBoundary,
|
||||
@ -104,6 +114,9 @@ export {
|
||||
// contextProviders
|
||||
NotificationProvider,
|
||||
useNotification,
|
||||
ViewportGridContext,
|
||||
ViewportGridProvider,
|
||||
useViewportGrid,
|
||||
Separator,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
@ -174,4 +187,13 @@ export {
|
||||
TooltipContent,
|
||||
StudySummary,
|
||||
Header,
|
||||
ViewportActionButton,
|
||||
PatientInfo,
|
||||
ViewportActionBar,
|
||||
ViewportActionArrows,
|
||||
ViewportPane,
|
||||
ViewportActionCorners,
|
||||
ViewportActionCornersLocations,
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
};
|
||||
|
||||
178
yarn.lock
178
yarn.lock
@ -4670,6 +4670,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
|
||||
integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==
|
||||
|
||||
"@radix-ui/primitive@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3"
|
||||
integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==
|
||||
|
||||
"@radix-ui/react-accordion@^1.2.0":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.2.1.tgz#5c942c42c24267376b26204ec6847b17d15659b3"
|
||||
@ -4700,6 +4705,13 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
|
||||
"@radix-ui/react-arrow@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz#2103721933a8bfc6e53bbfbdc1aaad5fc8ba0dd7"
|
||||
integrity sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.0.1"
|
||||
|
||||
"@radix-ui/react-checkbox@^1.1.1":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz#6465b800420923ecc39cbeaa8f357b5f09dbfd52"
|
||||
@ -4749,6 +4761,16 @@
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-slot" "1.1.0"
|
||||
|
||||
"@radix-ui/react-collection@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.1.tgz#be2c7e01d3508e6d4b6d838f492e7d182f17d3b0"
|
||||
integrity sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs" "1.1.1"
|
||||
"@radix-ui/react-context" "1.1.1"
|
||||
"@radix-ui/react-primitive" "2.0.1"
|
||||
"@radix-ui/react-slot" "1.1.1"
|
||||
|
||||
"@radix-ui/react-compose-refs@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989"
|
||||
@ -4761,6 +4783,23 @@
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74"
|
||||
integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==
|
||||
|
||||
"@radix-ui/react-compose-refs@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec"
|
||||
integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==
|
||||
|
||||
"@radix-ui/react-context-menu@^2.2.4":
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.4.tgz#8fe70ffdfec25ada35a60d100496cace1b75a977"
|
||||
integrity sha512-ap4wdGwK52rJxGkwukU1NrnEodsUFQIooANKu+ey7d6raQ2biTcEf8za1zr0mgFHieevRTB2nK4dJeN8pTAZGQ==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.1"
|
||||
"@radix-ui/react-context" "1.1.1"
|
||||
"@radix-ui/react-menu" "2.1.4"
|
||||
"@radix-ui/react-primitive" "2.0.1"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
|
||||
"@radix-ui/react-context@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c"
|
||||
@ -4866,6 +4905,17 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-escape-keydown" "1.1.0"
|
||||
|
||||
"@radix-ui/react-dismissable-layer@1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz#4ee0f0f82d53bf5bd9db21665799bb0d1bad5ed8"
|
||||
integrity sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.1"
|
||||
"@radix-ui/react-compose-refs" "1.1.1"
|
||||
"@radix-ui/react-primitive" "2.0.1"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-escape-keydown" "1.1.0"
|
||||
|
||||
"@radix-ui/react-dropdown-menu@^2.1.1":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz#acc49577130e3c875ef0133bd1e271ea3392d924"
|
||||
@ -4920,6 +4970,15 @@
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
|
||||
"@radix-ui/react-focus-scope@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz#5c602115d1db1c4fcfa0fae4c3b09bb8919853cb"
|
||||
integrity sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs" "1.1.1"
|
||||
"@radix-ui/react-primitive" "2.0.1"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
|
||||
"@radix-ui/react-icons@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69"
|
||||
@ -4971,6 +5030,30 @@
|
||||
aria-hidden "^1.1.1"
|
||||
react-remove-scroll "2.6.0"
|
||||
|
||||
"@radix-ui/react-menu@2.1.4":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.4.tgz#ac7aec296f29608206a7c6ef6335d8f102edaa95"
|
||||
integrity sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.1"
|
||||
"@radix-ui/react-collection" "1.1.1"
|
||||
"@radix-ui/react-compose-refs" "1.1.1"
|
||||
"@radix-ui/react-context" "1.1.1"
|
||||
"@radix-ui/react-direction" "1.1.0"
|
||||
"@radix-ui/react-dismissable-layer" "1.1.3"
|
||||
"@radix-ui/react-focus-guards" "1.1.1"
|
||||
"@radix-ui/react-focus-scope" "1.1.1"
|
||||
"@radix-ui/react-id" "1.1.0"
|
||||
"@radix-ui/react-popper" "1.2.1"
|
||||
"@radix-ui/react-portal" "1.1.3"
|
||||
"@radix-ui/react-presence" "1.1.2"
|
||||
"@radix-ui/react-primitive" "2.0.1"
|
||||
"@radix-ui/react-roving-focus" "1.1.1"
|
||||
"@radix-ui/react-slot" "1.1.1"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
aria-hidden "^1.1.1"
|
||||
react-remove-scroll "^2.6.1"
|
||||
|
||||
"@radix-ui/react-popover@^1.0.7":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.2.tgz#a0cab25f69aa49ad0077d91e9e9dcd323758020c"
|
||||
@ -5025,6 +5108,22 @@
|
||||
"@radix-ui/react-use-size" "1.1.0"
|
||||
"@radix-ui/rect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-popper@1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.1.tgz#2fc66cfc34f95f00d858924e3bee54beae2dff0a"
|
||||
integrity sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==
|
||||
dependencies:
|
||||
"@floating-ui/react-dom" "^2.0.0"
|
||||
"@radix-ui/react-arrow" "1.1.1"
|
||||
"@radix-ui/react-compose-refs" "1.1.1"
|
||||
"@radix-ui/react-context" "1.1.1"
|
||||
"@radix-ui/react-primitive" "2.0.1"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
"@radix-ui/react-use-rect" "1.1.0"
|
||||
"@radix-ui/react-use-size" "1.1.0"
|
||||
"@radix-ui/rect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-portal@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1"
|
||||
@ -5049,6 +5148,14 @@
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-portal@1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440"
|
||||
integrity sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.0.1"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-presence@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"
|
||||
@ -5066,6 +5173,14 @@
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-presence@1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc"
|
||||
integrity sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs" "1.1.1"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-primitive@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0"
|
||||
@ -5081,6 +5196,13 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-slot" "1.1.0"
|
||||
|
||||
"@radix-ui/react-primitive@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e"
|
||||
integrity sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==
|
||||
dependencies:
|
||||
"@radix-ui/react-slot" "1.1.1"
|
||||
|
||||
"@radix-ui/react-roving-focus@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e"
|
||||
@ -5096,6 +5218,21 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
|
||||
"@radix-ui/react-roving-focus@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz#3b3abb1e03646937f28d9ab25e96343667ca6520"
|
||||
integrity sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.1"
|
||||
"@radix-ui/react-collection" "1.1.1"
|
||||
"@radix-ui/react-compose-refs" "1.1.1"
|
||||
"@radix-ui/react-context" "1.1.1"
|
||||
"@radix-ui/react-direction" "1.1.0"
|
||||
"@radix-ui/react-id" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.1"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
|
||||
"@radix-ui/react-scroll-area@^1.1.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.0.tgz#d09fd693728b09c50145935bec6f91efc2661729"
|
||||
@ -5205,6 +5342,13 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
|
||||
"@radix-ui/react-slot@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz#ab9a0ffae4027db7dc2af503c223c978706affc3"
|
||||
integrity sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs" "1.1.1"
|
||||
|
||||
"@radix-ui/react-switch@^1.1.0":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.1.tgz#1401658c24d66a18610f18793afbaa7fedf5429a"
|
||||
@ -20916,6 +21060,14 @@ react-remove-scroll-bar@^2.3.3, react-remove-scroll-bar@^2.3.6:
|
||||
react-style-singleton "^2.2.1"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react-remove-scroll-bar@^2.3.7:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223"
|
||||
integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==
|
||||
dependencies:
|
||||
react-style-singleton "^2.2.2"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react-remove-scroll@2.5.5:
|
||||
version "2.5.5"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77"
|
||||
@ -20938,6 +21090,17 @@ react-remove-scroll@2.6.0:
|
||||
use-callback-ref "^1.3.0"
|
||||
use-sidecar "^1.1.2"
|
||||
|
||||
react-remove-scroll@^2.6.1:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz#2518d2c5112e71ea8928f1082a58459b5c7a2a97"
|
||||
integrity sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==
|
||||
dependencies:
|
||||
react-remove-scroll-bar "^2.3.7"
|
||||
react-style-singleton "^2.2.1"
|
||||
tslib "^2.1.0"
|
||||
use-callback-ref "^1.3.3"
|
||||
use-sidecar "^1.1.2"
|
||||
|
||||
react-resize-detector@^10.0.1:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-10.0.1.tgz#ae9a8c5b6b93c4c11e03b3eb87e57fd7b62f1020"
|
||||
@ -21034,6 +21197,14 @@ react-style-singleton@^2.2.1:
|
||||
invariant "^2.2.4"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react-style-singleton@^2.2.2:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
|
||||
integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==
|
||||
dependencies:
|
||||
get-nonce "^1.0.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react-test-renderer@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.3.1.tgz#e693608a1f96283400d4a3afead6893f958b80b4"
|
||||
@ -24206,6 +24377,13 @@ use-callback-ref@^1.3.0:
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-callback-ref@^1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf"
|
||||
integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-editable@^2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/use-editable/-/use-editable-2.3.3.tgz#a292fe9ba4c291cd28d1cc2728c75a5fc8d9a33f"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user