diff --git a/bun.lockb b/bun.lockb index 0ffd08fccbc..e6df1f25624 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/extensions/default/src/Components/SidePanelWithServices.tsx b/extensions/default/src/Components/SidePanelWithServices.tsx index c4fd32a1c0d..298f5ae03b9 100644 --- a/extensions/default/src/Components/SidePanelWithServices.tsx +++ b/extensions/default/src/Components/SidePanelWithServices.tsx @@ -7,47 +7,58 @@ export type SidePanelWithServicesProps = { side: 'left' | 'right'; className?: string; activeTabIndex: number; - tabs: any; + tabs?: any; expandedWidth?: number; + onClose: () => void; + onOpen: () => void; + isExpanded: boolean; + collapsedWidth?: number; + expandedInsideBorderSize?: number; + collapsedInsideBorderSize?: number; + collapsedOutsideBorderSize?: number; }; const SidePanelWithServices = ({ servicesManager, side, activeTabIndex: activeTabIndexProp, + isExpanded, tabs: tabsProp, - expandedWidth, + onOpen, + onClose, ...props }: SidePanelWithServicesProps) => { const panelService = servicesManager?.services?.panelService; // Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM. // Thus going to the Study List page and back to the viewer resets this flag for a SidePanel. - const [sidePanelOpen, setSidePanelOpen] = useState(activeTabIndexProp !== null); - const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp); + const [sidePanelExpanded, setSidePanelExpanded] = useState(isExpanded); + const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0); + const [closedManually, setClosedManually] = useState(false); const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side)); const handleActiveTabIndexChange = useCallback(({ activeTabIndex }) => { setActiveTabIndex(activeTabIndex); - setSidePanelOpen(activeTabIndex !== null); }, []); const handleOpen = useCallback(() => { - setSidePanelOpen(true); - // If panel is being opened but no tab is active, set first tab as active - if (activeTabIndex === null && tabs.length > 0) { - setActiveTabIndex(0); - } - }, [activeTabIndex, tabs]); + setSidePanelExpanded(true); + onOpen?.(); + }, [onOpen]); const handleClose = useCallback(() => { - setSidePanelOpen(false); - setActiveTabIndex(null); - }, []); + setSidePanelExpanded(false); + setClosedManually(true); + onClose?.(); + }, [onClose]); + + useEffect(() => { + setSidePanelExpanded(isExpanded); + }, [isExpanded]); /** update the active tab index from outside */ useEffect(() => { - setActiveTabIndex(activeTabIndexProp); + setActiveTabIndex(activeTabIndexProp ?? 0); }, [activeTabIndexProp]); useEffect(() => { @@ -71,9 +82,12 @@ const SidePanelWithServices = ({ const activatePanelSubscription = panelService.subscribe( panelService.EVENTS.ACTIVATE_PANEL, (activatePanelEvent: Types.ActivatePanelEvent) => { - if (sidePanelOpen || activatePanelEvent.forceActive) { + if (sidePanelExpanded || activatePanelEvent.forceActive) { const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId); if (tabIndex !== -1) { + if (!closedManually) { + setSidePanelExpanded(true); + } setActiveTabIndex(tabIndex); } } @@ -83,7 +97,7 @@ const SidePanelWithServices = ({ return () => { activatePanelSubscription.unsubscribe(); }; - }, [tabs, sidePanelOpen, panelService]); + }, [tabs, sidePanelExpanded, panelService, closedManually]); return ( ); }; diff --git a/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx b/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx new file mode 100644 index 00000000000..9b7c377962a --- /dev/null +++ b/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx @@ -0,0 +1,325 @@ +import { useState, useCallback, useLayoutEffect, useRef } from 'react'; +import { getPanelElement, getPanelGroupElement } from 'react-resizable-panels'; + +// Id needed to grab the panel group for converting pixels to percentages +const viewerLayoutResizablePanelGroupId = 'viewerLayoutResizablePanelGroup'; +const viewerLayoutResizableLeftPanelId = 'viewerLayoutResizableLeftPanel'; +const viewerLayoutResizableRightPanelId = 'viewerLayoutResizableRightPanel'; + +const sidePanelExpandedDefaultWidth = 280; +const sidePanelExpandedInsideBorderSize = 4; +const sidePanelExpandedDefaultOffsetWidth = + sidePanelExpandedDefaultWidth + sidePanelExpandedInsideBorderSize; +const sidePanelCollapsedInsideBorderSize = 4; +const sidePanelCollapsedOutsideBorderSize = 8; +const sidePanelCollapsedWidth = 25; +const sidePanelCollapsedOffsetWidth = + sidePanelCollapsedWidth + + sidePanelCollapsedInsideBorderSize + + sidePanelCollapsedOutsideBorderSize; + +/** + * Set the minimum and maximum css style width attributes for the given element. + * The two style attributes are cleared whenever the width + * arguments is undefined. + *

+ * This utility is used as part of a HACK throughout the ViewerLayout component as + * the means of restricting the side panel widths during the resizing of the + * browser window. In general, the widths are always set unless the resize + * handle for either side panel is being dragged (i.e. a side panel is being resized). + * + * @param elem the element + * @param width the max and min width to set on the element + */ +const setMinMaxWidth = (elem, width?) => { + elem.style.minWidth = width === undefined ? '' : `${width}px`; + elem.style.maxWidth = elem.style.minWidth; +}; + +const useResizablePanels = ( + leftPanelClosed, + setLeftPanelClosed, + rightPanelClosed, + setRightPanelClosed +) => { + const [leftPanelExpandedWidth, setLeftPanelExpandedWidth] = useState( + sidePanelExpandedDefaultWidth + ); + const [rightPanelExpandedWidth, setRightPanelExpandedWidth] = useState( + sidePanelExpandedDefaultWidth + ); + + // Percentage sizes. + const [resizablePanelCollapsedSize, setResizablePanelCollapsedSize] = useState(0); + const [resizablePanelDefaultSize, setResizablePanelDefaultSize] = useState(0); + + const resizablePanelGroupElemRef = useRef(null); + const resizableLeftPanelElemRef = useRef(null); + const resizableRightPanelElemRef = useRef(null); + const resizableLeftPanelAPIRef = useRef(null); + const resizableRightPanelAPIRef = useRef(null); + const isResizableHandleDraggingRef = useRef(false); + + // This useLayoutEffect is used to... + // - Grab a reference to the various resizable panel elements needed for + // converting between percentages and pixels in various callbacks. + // - Expand those panels that are initially expanded. + useLayoutEffect(() => { + const panelGroupElem = getPanelGroupElement(viewerLayoutResizablePanelGroupId); + + resizablePanelGroupElemRef.current = panelGroupElem; + const { width: panelGroupWidth } = panelGroupElem.getBoundingClientRect(); + + const leftPanelElem = getPanelElement(viewerLayoutResizableLeftPanelId); + resizableLeftPanelElemRef.current = leftPanelElem; + + const rightPanelElem = getPanelElement(viewerLayoutResizableRightPanelId); + resizableRightPanelElemRef.current = rightPanelElem; + + const resizablePanelExpandedSize = + (sidePanelExpandedDefaultOffsetWidth / panelGroupWidth) * 100; + + // Since both resizable panels are collapsed by default (i.e. their default size is zero), + // on the very first render check if either/both side panels should be expanded. + if (!leftPanelClosed) { + resizableLeftPanelAPIRef?.current?.expand(resizablePanelExpandedSize); + setMinMaxWidth(leftPanelElem, sidePanelExpandedDefaultOffsetWidth); + } + + if (!rightPanelClosed) { + resizableRightPanelAPIRef?.current?.expand(resizablePanelExpandedSize); + setMinMaxWidth(rightPanelElem, sidePanelExpandedDefaultOffsetWidth); + } + }, []); // no dependencies because this useLayoutEffect is only needed on the very first render + + // This useLayoutEffect follows the pattern prescribed by the react-resizable-panels + // readme for converting between pixel values and percentages. An example of + // the pattern can be found here: + // https://github.com/bvaughn/react-resizable-panels/issues/46#issuecomment-1368108416 + // This useLayoutEffect is used to... + // - Ensure that the percentage size is up-to-date with the pixel sizes + // - Add a resize observer to the resizable panel group to reset various state + // values whenever the resizable panel group is resized (e.g. whenever the + // browser window is resized). + useLayoutEffect(() => { + const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect(); + + // Ensure the side panels' percentage size is in synch with the pixel width of the + // expanded side panels. In general the two get out-of-sync during a browser + // window resize. Note that this code is here and NOT in the ResizeObserver + // because it has to be done AFTER the minimum percentage size for a panel is + // updated which occurs only AFTER the render following a resize. And by virtue + // of the dependency on the `resizablePanelDefaultSize` state, this code + // is executed on the render following an update of the minimum percentage size + // for a panel. + if (!resizableLeftPanelAPIRef.current.isCollapsed()) { + const leftSize = + ((leftPanelExpandedWidth + sidePanelExpandedInsideBorderSize) / panelGroupWidth) * 100; + resizableLeftPanelAPIRef.current.resize(leftSize); + } + + if (!resizableRightPanelAPIRef.current.isCollapsed()) { + const rightSize = + ((rightPanelExpandedWidth + sidePanelExpandedInsideBorderSize) / panelGroupWidth) * 100; + resizableRightPanelAPIRef.current.resize(rightSize); + } + + // This observer kicks in when the ViewportLayout resizable panel group + // component is resized. This typically occurs when the browser window resizes. + const observer = new ResizeObserver(() => { + const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect(); + const defaultSize = (sidePanelExpandedDefaultOffsetWidth / panelGroupWidth) * 100; + + // Set the new default and collapsed resizable panel sizes. + setResizablePanelDefaultSize(Math.min(50, defaultSize)); + setResizablePanelCollapsedSize((sidePanelCollapsedOffsetWidth / panelGroupWidth) * 100); + + if ( + resizableLeftPanelAPIRef.current.isCollapsed() && + resizableRightPanelAPIRef.current.isCollapsed() + ) { + return; + } + + // The code that follows is to handle cases when the group panel is resized to be + // too small to display either side panel at its current width. + + // Determine the current widths of the two side panels. + let leftPanelOffsetWidth = resizableLeftPanelAPIRef.current.isCollapsed() + ? sidePanelCollapsedOffsetWidth + : leftPanelExpandedWidth + sidePanelExpandedInsideBorderSize; + + let rightPanelOffsetWidth = resizableRightPanelAPIRef.current.isCollapsed() + ? sidePanelCollapsedOffsetWidth + : rightPanelExpandedWidth + sidePanelExpandedInsideBorderSize; + + if ( + !resizableLeftPanelAPIRef.current.isCollapsed() && + leftPanelOffsetWidth + rightPanelOffsetWidth > panelGroupWidth + ) { + // There is not enough space to show both panels at their pre-resize widths. + // Note that at this point, the viewport grid component is zero width. + // Reduce the left panel width so that both panels might fit. + leftPanelOffsetWidth = Math.max( + panelGroupWidth - rightPanelOffsetWidth, + sidePanelExpandedDefaultOffsetWidth + ); + setLeftPanelExpandedWidth(leftPanelOffsetWidth - sidePanelExpandedInsideBorderSize); + setMinMaxWidth(resizableLeftPanelElemRef.current, leftPanelOffsetWidth); + } + + if ( + !resizableRightPanelAPIRef.current.isCollapsed() && + rightPanelOffsetWidth + leftPanelOffsetWidth > panelGroupWidth + ) { + // There is not enough space to show both panels at their pre-resize widths. + // Note that at this point, the viewport grid component is zero width. + // Reduce the right panel width so that both panels might fit. + rightPanelOffsetWidth = Math.max( + panelGroupWidth - leftPanelOffsetWidth, + sidePanelExpandedDefaultOffsetWidth + ); + setRightPanelExpandedWidth(rightPanelOffsetWidth - sidePanelExpandedInsideBorderSize); + setMinMaxWidth(resizableRightPanelElemRef.current, rightPanelOffsetWidth); + } + }); + + observer.observe(resizablePanelGroupElemRef.current); + + return () => { + observer.disconnect(); + }; + }, [leftPanelExpandedWidth, resizablePanelDefaultSize, rightPanelExpandedWidth]); + + /** + * Handles dragging of either side panel resize handle. + */ + const onHandleDragging = useCallback( + isStartDrag => { + if (isStartDrag) { + isResizableHandleDraggingRef.current = true; + + setMinMaxWidth(resizableLeftPanelElemRef.current); + setMinMaxWidth(resizableRightPanelElemRef.current); + } else { + isResizableHandleDraggingRef.current = false; + + if (resizableLeftPanelAPIRef?.current?.isExpanded()) { + setMinMaxWidth( + resizableLeftPanelElemRef.current, + leftPanelExpandedWidth + sidePanelExpandedInsideBorderSize + ); + } + + if (resizableRightPanelAPIRef?.current?.isExpanded()) { + setMinMaxWidth( + resizableRightPanelElemRef.current, + rightPanelExpandedWidth + sidePanelExpandedInsideBorderSize + ); + } + } + }, + [leftPanelExpandedWidth, rightPanelExpandedWidth] + ); + + const onLeftPanelClose = useCallback(() => { + setLeftPanelClosed(true); + setMinMaxWidth(resizableLeftPanelElemRef.current); + resizableLeftPanelAPIRef?.current?.collapse(); + }, [setLeftPanelClosed]); + + const onLeftPanelOpen = useCallback(() => { + resizableLeftPanelAPIRef?.current?.expand(); + if (!isResizableHandleDraggingRef.current) { + setMinMaxWidth( + resizableLeftPanelElemRef.current, + leftPanelExpandedWidth + sidePanelExpandedInsideBorderSize + ); + } + setLeftPanelClosed(false); + }, [leftPanelExpandedWidth, setLeftPanelClosed]); + + const onLeftPanelResize = useCallback(size => { + if (resizableLeftPanelAPIRef.current.isCollapsed()) { + return; + } + + const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect(); + setLeftPanelExpandedWidth((size / 100) * panelGroupWidth - sidePanelExpandedInsideBorderSize); + }, []); + + const onRightPanelClose = useCallback(() => { + setRightPanelClosed(true); + setMinMaxWidth(resizableRightPanelElemRef.current); + resizableRightPanelAPIRef?.current?.collapse(); + }, [setRightPanelClosed]); + + const onRightPanelOpen = useCallback(() => { + resizableRightPanelAPIRef?.current?.expand(); + if (!isResizableHandleDraggingRef.current) { + setMinMaxWidth( + resizableRightPanelElemRef.current, + rightPanelExpandedWidth + sidePanelExpandedInsideBorderSize + ); + } + setRightPanelClosed(false); + }, [rightPanelExpandedWidth, setRightPanelClosed]); + + const onRightPanelResize = useCallback(size => { + if (resizableRightPanelAPIRef?.current?.isCollapsed()) { + return; + } + const { width: panelGroupWidth } = resizablePanelGroupElemRef.current.getBoundingClientRect(); + setRightPanelExpandedWidth((size / 100) * panelGroupWidth - sidePanelExpandedInsideBorderSize); + }, []); + + return [ + { + expandedWidth: leftPanelExpandedWidth, + collapsedWidth: sidePanelCollapsedWidth, + collapsedInsideBorderSize: sidePanelCollapsedInsideBorderSize, + collapsedOutsideBorderSize: sidePanelCollapsedOutsideBorderSize, + expandedInsideBorderSize: sidePanelExpandedInsideBorderSize, + onClose: onLeftPanelClose, + onOpen: onLeftPanelOpen, + }, + { + expandedWidth: rightPanelExpandedWidth, + collapsedWidth: sidePanelCollapsedWidth, + collapsedInsideBorderSize: sidePanelCollapsedInsideBorderSize, + collapsedOutsideBorderSize: sidePanelCollapsedOutsideBorderSize, + expandedInsideBorderSize: sidePanelExpandedInsideBorderSize, + onClose: onRightPanelClose, + onOpen: onRightPanelOpen, + }, + { direction: 'horizontal', id: viewerLayoutResizablePanelGroupId }, + { + defaultSize: resizablePanelDefaultSize, + minSize: resizablePanelDefaultSize, + onResize: onLeftPanelResize, + collapsible: true, + collapsedSize: resizablePanelCollapsedSize, + onCollapse: () => setLeftPanelClosed(true), + onExpand: () => setLeftPanelClosed(false), + ref: resizableLeftPanelAPIRef, + order: 0, + id: viewerLayoutResizableLeftPanelId, + }, + { order: 1, id: 'viewerLayoutResizableViewportGridPanel' }, + { + defaultSize: resizablePanelDefaultSize, + minSize: resizablePanelDefaultSize, + onResize: onRightPanelResize, + collapsible: true, + collapsedSize: resizablePanelCollapsedSize, + onCollapse: () => setRightPanelClosed(true), + onExpand: () => setRightPanelClosed(false), + ref: resizableRightPanelAPIRef, + order: 2, + id: viewerLayoutResizableRightPanelId, + }, + onHandleDragging, + ]; +}; + +export default useResizablePanels; diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx index e19a24c68ab..b11ec0da4cc 100644 --- a/extensions/default/src/ViewerLayout/index.tsx +++ b/extensions/default/src/ViewerLayout/index.tsx @@ -6,7 +6,8 @@ import { HangingProtocolService, CommandsManager } from '@ohif/core'; import { useAppConfig } from '@state'; import ViewerHeader from './ViewerHeader'; import SidePanelWithServices from '../Components/SidePanelWithServices'; -import { Onboarding } from '@ohif/ui-next'; +import { Onboarding, ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@ohif/ui-next'; +import useResizablePanels from './ResizablePanelsHook'; function ViewerLayout({ // From Extension Module Params @@ -19,6 +20,8 @@ function ViewerLayout({ ViewportGridComp, leftPanelClosed = false, rightPanelClosed = false, + leftPanelResizable = false, + rightPanelResizable = false, }: withAppTypes): React.FunctionComponent { const [appConfig] = useAppConfig(); @@ -35,6 +38,21 @@ function ViewerLayout({ const [leftPanelClosedState, setLeftPanelClosed] = useState(leftPanelClosed); const [rightPanelClosedState, setRightPanelClosed] = useState(rightPanelClosed); + const [ + leftPanelProps, + rightPanelProps, + resizablePanelGroupProps, + resizableLeftPanelProps, + resizableViewportGridPanelProps, + resizableRightPanelProps, + onHandleDragging, + ] = useResizablePanels( + leftPanelClosed, + setLeftPanelClosed, + rightPanelClosed, + setRightPanelClosed + ); + /** * Set body classes (tailwindcss) that don't allow vertical * or horizontal overflow (no scrolling). Also guarantee window @@ -123,31 +141,56 @@ function ViewerLayout({ > {showLoadingIndicator && } - {/* LEFT SIDEPANELS */} - {hasLeftPanels ? ( - - ) : null} - {/* TOOLBAR + GRID */} -

-
- -
-
- {hasRightPanels ? ( - - ) : null} + + {/* LEFT SIDEPANELS */} + + {hasLeftPanels ? ( + <> + + + + + + ) : null} + {/* TOOLBAR + GRID */} + +
+
+ +
+
+
+ {hasRightPanels ? ( + <> + + + + + + ) : null} +
diff --git a/modes/longitudinal/src/index.ts b/modes/longitudinal/src/index.ts index 68ce4ae38e6..6afe135faa9 100644 --- a/modes/longitudinal/src/index.ts +++ b/modes/longitudinal/src/index.ts @@ -117,21 +117,30 @@ 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], - }, - ]), - ...panelService.addActivatePanelTriggers(tracked.measurements, [ - { - sourcePubSubService: measurementService, - sourceEvents: [ - measurementService.EVENTS.MEASUREMENT_ADDED, - measurementService.EVENTS.RAW_MEASUREMENT_ADDED, - ], - }, - ]), + ...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) => { @@ -181,8 +190,10 @@ function modeFactory({ modeConfiguration }) { id: ohif.layout, props: { leftPanels: [tracked.thumbnailList], + leftPanelResizable: true, rightPanels: [cornerstone.segmentation, tracked.measurements], rightPanelClosed: true, + rightPanelResizable: true, viewports: [ { namespace: tracked.viewport, diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js index ad68375641d..febbaf41ac4 100644 --- a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js +++ b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js @@ -15,7 +15,7 @@ describe('OHIF Measurement Panel', function () { cy.get('@RightCollapseBtn').click(); cy.get('@measurementsPanel').should('not.exist'); - cy.get('@RightCollapseBtn').click(); + cy.get('@RightCollapseBtn').click({ force: true }); // segmentation panel should be visible cy.get('@segmentationPanel').should('be.visible'); diff --git a/platform/core/src/services/PanelService/PanelService.tsx b/platform/core/src/services/PanelService/PanelService.tsx index 4900283e99a..91a01ac9158 100644 --- a/platform/core/src/services/PanelService/PanelService.tsx +++ b/platform/core/src/services/PanelService/PanelService.tsx @@ -151,13 +151,7 @@ export default class PanelService extends PubSubService { panelsIds.forEach(panelId => this.addPanel(position, panelId, options)); } - public setPanels( - panels: { [key in PanelPosition]: string[] }, - options: { - rightPanelClosed?: boolean; - leftPanelClosed?: boolean; - } - ): void { + public setPanels(panels: { [key in PanelPosition]: string[] }, options): void { this.reset(); Object.keys(panels).forEach((position: PanelPosition) => { diff --git a/platform/ui-next/package.json b/platform/ui-next/package.json index e7f60255b37..30d7c33400d 100644 --- a/platform/ui-next/package.json +++ b/platform/ui-next/package.json @@ -53,6 +53,7 @@ "next-themes": "^0.3.0", "react": "^18.3.1", "react-day-picker": "^8.10.1", + "react-resizable-panels": "^2.1.7", "react-shepherd": "6.1.1", "shepherd.js": "13.0.3", "sonner": "^1.5.0", diff --git a/platform/ui-next/src/components/Resizable/Resizable.tsx b/platform/ui-next/src/components/Resizable/Resizable.tsx new file mode 100644 index 00000000000..967152b6b18 --- /dev/null +++ b/platform/ui-next/src/components/Resizable/Resizable.tsx @@ -0,0 +1,43 @@ +'use client'; +import React from 'react'; + +import { GripVertical } from 'lucide-react'; +import * as ResizablePrimitive from 'react-resizable-panels'; + +import cn from 'classnames'; + +const ResizablePanelGroup = ({ + className, + ...props +}: React.ComponentProps) => ( + +); + +const ResizablePanel = ResizablePrimitive.Panel; + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean; +}) => ( + div]:rotate-90', + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+); + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; diff --git a/platform/ui-next/src/components/Resizable/index.ts b/platform/ui-next/src/components/Resizable/index.ts new file mode 100644 index 00000000000..88432781a98 --- /dev/null +++ b/platform/ui-next/src/components/Resizable/index.ts @@ -0,0 +1,3 @@ +import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './Resizable'; + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; diff --git a/platform/ui-next/src/components/SidePanel/SidePanel.tsx b/platform/ui-next/src/components/SidePanel/SidePanel.tsx index f331edb6a5d..5d8be607cd9 100644 --- a/platform/ui-next/src/components/SidePanel/SidePanel.tsx +++ b/platform/ui-next/src/components/SidePanel/SidePanel.tsx @@ -1,39 +1,64 @@ import classnames from 'classnames'; -import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useState } from 'react'; import { Icons } from '../Icons'; -import { TooltipTrigger, TooltipContent, TooltipProvider, Tooltip } from '../Tooltip'; +import { TooltipTrigger, TooltipContent, Tooltip } from '../Tooltip'; import { Separator } from '../Separator'; +/** + * SidePanel component properties. + * Note that the component monitors changes to the various widths and border sizes and will resize dynamically + * @property {boolean} isExpanded - boolean indicating if the side panel is expanded/open or collapsed + * @property {number} expandedWidth - the width of this side panel when expanded not including any borders or margins + * @property {number} collapsedWidth - the width of this side panel when collapsed not including any borders or margins + * @property {number} expandedInsideBorderSize - the width of the space between the expanded side panel content and viewport grid + * @property {number} collapsedInsideBorderSize - the width of the space between the collapsed side panel content and the viewport grid + * @property {number} collapsedOutsideBorderSize - the width of the space between the collapsed side panel content and the edge of the browser window + */ +type SidePanelProps = { + side: 'left' | 'right'; + className: string; + activeTabIndex: number; + onOpen: () => void; + onClose: () => void; + onActiveTabIndexChange: () => void; + isExpanded: boolean; + expandedWidth: number; + collapsedWidth: number; + expandedInsideBorderSize: number; + collapsedInsideBorderSize: number; + collapsedOutsideBorderSize: number; + tabs: any; +}; + type StyleMap = { open: { - left: { marginLeft: string }; - right: { marginRight: string }; + left: { + marginLeft: string; // the space between the expanded/open left side panel and the browser window left edge + marginRight: string; // the space between the expanded/open left side panel and the viewport grid + }; + right: { + marginLeft: string; // the space between the expanded/open right side panel and the viewport grid + marginRight: string; // the space between the expanded/open right side panel and the browser window right edge + }; }; closed: { - left: { marginLeft: string }; - right: { marginRight: string }; + left: { + marginLeft: string; // the space between the collapsed/closed left panel and the browser window left edge + marginRight: string; // the space between the collapsed/closed left panel and the viewport grid + alignItems: 'flex-end'; // the flexbox layout align-items property + }; + right: { + marginLeft: string; // the space between the collapsed/closed right panel and the viewport grid + marginRight: string; // the space between the collapsed/closed right panel and the browser window right edge + alignItems: 'flex-start'; // the flexbox layout align-items property + }; }; }; -const borderSize = 4; -const collapsedWidth = 25; const closeIconWidth = 30; const gridHorizontalPadding = 10; const tabSpacerWidth = 2; -const baseClasses = - 'transition-all duration-300 ease-in-out bg-black border-black justify-start box-content flex flex-col'; - -const classesMap = { - open: { - left: `mr-1`, - right: `ml-1`, - }, - closed: { - left: `mr-2 items-end`, - right: `ml-2 items-start`, - }, -}; +const baseClasses = 'bg-black border-black justify-start box-content flex flex-col'; const openStateIconName = { left: 'SidePanelCloseLeft', @@ -106,19 +131,29 @@ const getTabIconClassNames = (numTabs: number, isActiveTab: boolean) => { }; const createStyleMap = ( expandedWidth: number, - borderSize: number, - collapsedWidth: number + expandedInsideBorderSize: number, + collapsedWidth: number, + collapsedInsideBorderSize: number, + collapsedOutsideBorderSize: number ): StyleMap => { - const collapsedHideWidth = expandedWidth - collapsedWidth - borderSize; + const collapsedHideWidth = expandedWidth - collapsedWidth - collapsedInsideBorderSize; return { open: { - left: { marginLeft: '0px' }, - right: { marginRight: '0px' }, + left: { marginLeft: '0px', marginRight: `${expandedInsideBorderSize}px` }, + right: { marginLeft: `${expandedInsideBorderSize}px`, marginRight: '0px' }, }, closed: { - left: { marginLeft: `-${collapsedHideWidth}px` }, - right: { marginRight: `-${collapsedHideWidth}px` }, + left: { + marginLeft: `-${collapsedHideWidth}px`, + marginRight: `${collapsedOutsideBorderSize}px`, + alignItems: `flex-end`, + }, + right: { + marginLeft: `${collapsedOutsideBorderSize}px`, + marginRight: `-${collapsedHideWidth}px`, + alignItems: `flex-start`, + }, }, }; }; @@ -143,47 +178,67 @@ const createBaseStyle = (expandedWidth: number) => { height: '99.8%', }; }; + const SidePanel = ({ side, className, - activeTabIndex: activeTabIndexProp = null, + activeTabIndex: activeTabIndexProp, + isExpanded, tabs, onOpen, onClose, - expandedWidth = 280, onActiveTabIndexChange, -}) => { - const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null); - const [activeTabIndex, setActiveTabIndex] = useState(0); - - const styleMap = createStyleMap(expandedWidth, borderSize, collapsedWidth); - const baseStyle = createBaseStyle(expandedWidth); - const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding; - const gridWidth = getGridWidth(tabs.length, gridAvailableWidth); + expandedWidth = 280, + collapsedWidth = 25, + expandedInsideBorderSize = 4, + collapsedInsideBorderSize = 8, + collapsedOutsideBorderSize = 4, +}: SidePanelProps) => { + const [panelOpen, setPanelOpen] = useState(isExpanded); + const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0); + + const [styleMap, setStyleMap] = useState( + createStyleMap( + expandedWidth, + expandedInsideBorderSize, + collapsedWidth, + collapsedInsideBorderSize, + collapsedOutsideBorderSize + ) + ); + + const [baseStyle, setBaseStyle] = useState(createBaseStyle(expandedWidth)); + + const [gridAvailableWidth, setGridAvailableWidth] = useState( + expandedWidth - closeIconWidth - gridHorizontalPadding + ); + + const [gridWidth, setGridWidth] = useState(getGridWidth(tabs.length, gridAvailableWidth)); const openStatus = panelOpen ? 'open' : 'closed'; const style = Object.assign({}, styleMap[openStatus][side], baseStyle); const updatePanelOpen = useCallback( - (panelOpen: boolean) => { - setPanelOpen(panelOpen); - if (panelOpen && onOpen) { - onOpen(); - } else if (onClose && !panelOpen) { - onClose(); + (isOpen: boolean) => { + setPanelOpen(isOpen); + if (isOpen !== panelOpen) { + // only fire events for changes + if (isOpen && onOpen) { + onOpen(); + } else if (onClose && !isOpen) { + onClose(); + } } }, - [onOpen, onClose] + [panelOpen, onOpen, onClose] ); const updateActiveTabIndex = useCallback( - (activeTabIndex: number) => { - if (activeTabIndex === null) { - updatePanelOpen(false); - return; + (activeTabIndex: number, forceOpen: boolean = false) => { + if (forceOpen) { + updatePanelOpen(true); } setActiveTabIndex(activeTabIndex); - updatePanelOpen(true); if (onActiveTabIndexChange) { onActiveTabIndexChange({ activeTabIndex }); @@ -193,7 +248,35 @@ const SidePanel = ({ ); useEffect(() => { - updateActiveTabIndex(activeTabIndexProp); + updatePanelOpen(isExpanded); + }, [isExpanded, updatePanelOpen]); + + useEffect(() => { + setStyleMap( + createStyleMap( + expandedWidth, + expandedInsideBorderSize, + collapsedWidth, + collapsedInsideBorderSize, + collapsedOutsideBorderSize + ) + ); + setBaseStyle(createBaseStyle(expandedWidth)); + + const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding; + setGridAvailableWidth(gridAvailableWidth); + setGridWidth(getGridWidth(tabs.length, gridAvailableWidth)); + }, [ + collapsedInsideBorderSize, + collapsedWidth, + expandedWidth, + expandedInsideBorderSize, + tabs.length, + collapsedOutsideBorderSize, + ]); + + useEffect(() => { + updateActiveTabIndex(activeTabIndexProp ?? 0); }, [activeTabIndexProp, updateActiveTabIndex]); const getCloseStateComponent = () => { @@ -223,7 +306,7 @@ const SidePanel = ({ data-cy={`${childComponent.name}-btn`} className="text-primary-active hover:cursor-pointer" onClick={() => { - return childComponent.disabled ? null : updateActiveTabIndex(index); + return childComponent.disabled ? null : updateActiveTabIndex(index, true); }} > {React.createElement(Icons[childComponent.iconName] || Icons.MissingIcon, { @@ -374,7 +457,7 @@ const SidePanel = ({ return (
{panelOpen ? ( @@ -394,14 +477,4 @@ const SidePanel = ({ ); }; -SidePanel.propTypes = { - side: PropTypes.oneOf(['left', 'right']).isRequired, - className: PropTypes.string, - activeTabIndex: PropTypes.number, - onOpen: PropTypes.func, - onClose: PropTypes.func, - onActiveTabIndexChange: PropTypes.func, - expandedWidth: PropTypes.number, -}; - export { SidePanel }; diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts index 20e613921f4..c2d57b74832 100644 --- a/platform/ui-next/src/components/index.ts +++ b/platform/ui-next/src/components/index.ts @@ -27,6 +27,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '. import { Clipboard } from './Clipboard'; import { Combobox } from './Combobox'; import { Popover, PopoverContent, PopoverTrigger, PopoverAnchor } from './Popover'; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from './Resizable'; import { Calendar } from './Calendar'; import { DatePickerWithRange } from './DateRange'; import { Separator } from './Separator'; @@ -121,6 +122,9 @@ export { PopoverContent, PopoverTrigger, PopoverAnchor, + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, Calendar, DatePickerWithRange, Input, diff --git a/platform/ui-next/src/index.ts b/platform/ui-next/src/index.ts index 874f222da1f..a37eb59ca01 100644 --- a/platform/ui-next/src/index.ts +++ b/platform/ui-next/src/index.ts @@ -44,6 +44,9 @@ import { PopoverAnchor, PopoverContent, PopoverTrigger, + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, Select, SelectTrigger, SelectContent, @@ -150,6 +153,9 @@ export { PopoverAnchor, PopoverContent, PopoverTrigger, + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, Select, SelectTrigger, SelectContent, diff --git a/yarn.lock b/yarn.lock index 54a49352d5d..e53f588a56c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1394,9 +1394,9 @@ integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== "@cornerstonejs/adapters@^2.15.3": - version "2.15.3" - resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-2.15.3.tgz#0b29519748fd1a51522f5a0c362317d51af98a7f" - integrity sha512-TeYSM+qUh+x1XrPR4Tqowp3+T6X+g+H9i2siF33gOw+cIDi4EQ14WNcvWSB2tHmqHvg/Rmta9Kvgha0dSoWTzA== + version "2.15.5" + resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-2.15.5.tgz#2fce5e7ad5a3e7b5f229c5a1f4e07d6c91f9836c" + integrity sha512-ohdp8waX52ZpPhnrJJnM64Dlc2+X+DCL8HvYB/SjvE/H1YsHk1Pa5trd5taEKq1GJks3LX19XIuWTpKj/wXTUA== dependencies: "@babel/runtime-corejs2" "^7.17.8" buffer "^6.0.3" @@ -1430,18 +1430,18 @@ integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg== "@cornerstonejs/core@^2.15.3": - version "2.15.3" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-2.15.3.tgz#62b82ac8ebc6a5cf30f96e358bf7654f4eee71b4" - integrity sha512-qQDvDUtONz7ydI6yf2RPI+p7/846IxnSPgCPgqLaVohjWjHeZkyhV5oY6lN50ZxgHkWmBvinw6l2wjvh4x1bKw== + version "2.15.5" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-2.15.5.tgz#2c81bf28a1e15dfbe25215a7df6ae29b70e7d621" + integrity sha512-P+Dzsdgp9uSqcIBRECHI0Kj1G2//2UacstRmsRc3uxXgJ0lJ7AaLS31F4xVNOf+5mHMsB8BUsxdDg6iujX9lQA== dependencies: "@kitware/vtk.js" "32.9.0" comlink "^4.4.1" gl-matrix "^3.4.3" "@cornerstonejs/dicom-image-loader@^2.15.3": - version "2.15.3" - resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-2.15.3.tgz#1ab90d03fab2674cbdfd6a56f4d3da585f69abf6" - integrity sha512-/njbngdKBV3DEcD5UrPD9B3QC/eXS8gx9U3hfAvABFihaSQFd7/rEgKte1EbShQ9Qdtx1FPiaX+uMmhGPxVsQA== + version "2.15.5" + resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-2.15.5.tgz#867636c95321dc03d5eb960266af300609f28d54" + integrity sha512-XbWHlfNe6/+W6xFYSSBcOQNTJfCg0FRduh+AXfMxES96xTSdK/1LObeOOtVq5QCslUU90ERHuUYSY8wDNiys7w== dependencies: "@cornerstonejs/codec-charls" "^1.2.3" "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2" @@ -1453,9 +1453,9 @@ uuid "^9.0.0" "@cornerstonejs/tools@^2.15.3": - version "2.15.3" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-2.15.3.tgz#b778f67f13faf62d625c6ce8bbe8eed656f29ba0" - integrity sha512-NzEeII/xTc4kFjvCZ0HV3LNOtlkFvPobxwHePuMq9wa8/2M89+blDrbrm1IOkO0xKrKMNnTqt1wTzBjcy+TKGA== + version "2.15.5" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-2.15.5.tgz#e38ff0e5a7158f073f361068ff0bbc2fe337fe7b" + integrity sha512-uOphCv2ulmRijy72emPUkXYw1JXYYygTPKWNmaEA6PX17fE6NlQfYdJc4GHVkXiqj2YJCDzN1PwukI1dWfL9Vg== dependencies: "@types/offscreencanvas" "2019.7.3" comlink "^4.4.1" @@ -3731,10 +3731,10 @@ resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== -"@remix-run/router@1.21.0": - version "1.21.0" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.21.0.tgz#c65ae4262bdcfe415dbd4f64ec87676e4a56e2b5" - integrity sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA== +"@remix-run/router@1.19.2": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.19.2.tgz#0c896535473291cb41f152c180bedd5680a3b273" + integrity sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA== "@rollup/plugin-babel@^5.2.0": version "5.3.1" @@ -5068,9 +5068,9 @@ "@types/node" "*" "@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.4.tgz#88c29e3052cec3536d64b6ce5015a30dfcbefca7" - integrity sha512-5kz9ScmzBdzTgB/3susoCgfqNDzBjvLL4taparufgSvlwjdLy6UyUy9T/tCpYd2GIdIilCatC4iSQS0QSYHt0w== + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz#91f06cda1049e8f17eeab364798ed79c97488a1c" + integrity sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw== dependencies: "@types/node" "*" "@types/qs" "*" @@ -5480,9 +5480,9 @@ integrity sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg== "@types/ws@^8.2.2": - version "8.5.13" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20" - integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA== + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== dependencies: "@types/node" "*" @@ -10099,44 +10099,7 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== -express@^4.17.1: - version "4.21.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" - integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.3" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.7.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~2.0.0" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.3.1" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.3" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.12" - proxy-addr "~2.0.7" - qs "6.13.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.19.0" - serve-static "1.16.2" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -express@^4.17.3: +express@^4.17.1, express@^4.17.3: version "4.21.1" resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== @@ -10531,12 +10494,7 @@ flow-parser@0.*: resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.247.1.tgz#4a512c882cd08126825fe00b1ee452637ef44371" integrity sha512-DHwcm06fWbn2Z6uFD3NaBZ5lMOoABIQ4asrVA80IWvYjjT5WdbghkUOL1wIcbLcagnFTdCZYOlSNnKNp/xnRZQ== -follow-redirects@^1.0.0: - version "1.15.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== - -follow-redirects@^1.14.9, follow-redirects@^1.15.0, follow-redirects@^1.15.6: +follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0, follow-redirects@^1.15.6: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== @@ -11468,9 +11426,9 @@ http-errors@~1.6.2: statuses ">= 1.4.0 < 2" http-parser-js@>=0.5.1: - version "0.5.9" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.9.tgz#b817b3ca0edea6236225000d795378707c169cec" - integrity sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw== + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== http-proxy-agent@^5.0.0: version "5.0.0" @@ -15701,11 +15659,6 @@ path-to-regexp@0.1.10: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== -path-to-regexp@0.1.12: - version "0.1.12" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" - integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== - path-to-regexp@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" @@ -17160,6 +17113,11 @@ react-remove-scroll@^2.6.1: use-callback-ref "^1.3.3" use-sidecar "^1.1.2" +react-resizable-panels@^2.1.7: + version "2.1.7" + resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz#afd29d8a3d708786a9f95183a38803c89f13c2e7" + integrity sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA== + 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" @@ -17168,19 +17126,19 @@ react-resize-detector@^10.0.1: lodash "^4.17.21" react-router-dom@^6.8.1: - version "6.28.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.28.1.tgz#b78fe452d2cd31919b80e57047a896bfa1509f8c" - integrity sha512-YraE27C/RdjcZwl5UCqF/ffXnZDxpJdk9Q6jw38SZHjXs7NNdpViq2l2c7fO7+4uWaEfcwfGCv3RSg4e1By/fQ== + version "6.26.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.26.2.tgz#a6e3b0cbd6bfd508e42b9342099d015a0ac59680" + integrity sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ== dependencies: - "@remix-run/router" "1.21.0" - react-router "6.28.1" + "@remix-run/router" "1.19.2" + react-router "6.26.2" -react-router@6.28.1, react-router@^6.23.1: - version "6.28.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.28.1.tgz#f82317ab24eee67d7beb7b304c0378b2b48fa178" - integrity sha512-2omQTA3rkMljmrvvo6WtewGdVh45SpL9hGiCI9uUrwGGfNFDIvGK4gYJsKlJoNVi6AQZcopSCballL+QGOm7fA== +react-router@6.26.2, react-router@^6.23.1: + version "6.26.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.26.2.tgz#2f0a68999168954431cdc29dd36cec3b6fa44a7e" + integrity sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A== dependencies: - "@remix-run/router" "1.21.0" + "@remix-run/router" "1.19.2" react-select@5.7.4: version "5.7.4" @@ -17406,9 +17364,9 @@ readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable util-deprecate "^1.0.1" readable-stream@^4.5.2: - version "4.5.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" - integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + version "4.7.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91" + integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== dependencies: abort-controller "^3.0.0" buffer "^6.0.3" @@ -18699,7 +18657,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -18717,15 +18675,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -18828,7 +18777,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -18849,13 +18798,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.0, strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -20687,7 +20629,7 @@ worker-loader@3.0.8, worker-loader@^3.0.8: loader-utils "^2.0.0" schema-utils "^3.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -20713,15 +20655,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"