feat(resizable-side-panels): Make the left and right side panels (optionally) resizable. (#4672)

This commit is contained in:
Joe Boccanfuso 2025-01-21 11:20:34 -05:00 committed by GitHub
parent a518854a4c
commit d90a4cfb16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 694 additions and 244 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -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 (
<SidePanel
@ -91,10 +105,10 @@ const SidePanelWithServices = ({
side={side}
tabs={tabs}
activeTabIndex={activeTabIndex}
isExpanded={sidePanelExpanded}
onOpen={handleOpen}
onClose={handleClose}
onActiveTabIndexChange={handleActiveTabIndexChange}
expandedWidth={expandedWidth}
/>
);
};

View File

@ -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.
* <p>
* 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;

View File

@ -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({
>
<React.Fragment>
{showLoadingIndicator && <LoadingIndicatorProgress className="h-full w-full bg-black" />}
{/* LEFT SIDEPANELS */}
{hasLeftPanels ? (
<SidePanelWithServices
side="left"
activeTabIndex={leftPanelClosedState ? null : 0}
servicesManager={servicesManager}
/>
) : null}
{/* TOOLBAR + GRID */}
<div className="flex h-full flex-1 flex-col">
<div className="relative flex h-full flex-1 items-center justify-center overflow-hidden bg-black">
<ViewportGridComp
servicesManager={servicesManager}
viewportComponents={viewportComponents}
commandsManager={commandsManager}
/>
</div>
</div>
{hasRightPanels ? (
<SidePanelWithServices
side="right"
activeTabIndex={rightPanelClosedState ? null : 0}
servicesManager={servicesManager}
/>
) : null}
<ResizablePanelGroup {...resizablePanelGroupProps}>
{/* LEFT SIDEPANELS */}
{hasLeftPanels ? (
<>
<ResizablePanel {...resizableLeftPanelProps}>
<SidePanelWithServices
side="left"
isExpanded={!leftPanelClosedState}
servicesManager={servicesManager}
{...leftPanelProps}
/>
</ResizablePanel>
<ResizableHandle
onDragging={onHandleDragging}
disabled={!leftPanelResizable}
className="!w-0"
/>
</>
) : null}
{/* TOOLBAR + GRID */}
<ResizablePanel {...resizableViewportGridPanelProps}>
<div className="flex h-full flex-1 flex-col">
<div className="relative flex h-full flex-1 items-center justify-center overflow-hidden bg-black">
<ViewportGridComp
servicesManager={servicesManager}
viewportComponents={viewportComponents}
commandsManager={commandsManager}
/>
</div>
</div>
</ResizablePanel>
{hasRightPanels ? (
<>
<ResizableHandle
onDragging={onHandleDragging}
disabled={!rightPanelResizable}
className="!w-0"
/>
<ResizablePanel {...resizableRightPanelProps}>
<SidePanelWithServices
side="right"
isExpanded={!rightPanelClosedState}
servicesManager={servicesManager}
{...rightPanelProps}
/>
</ResizablePanel>
</>
) : null}
</ResizablePanelGroup>
</React.Fragment>
</div>
<Onboarding />

View File

@ -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,

View File

@ -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');

View File

@ -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) => {

View File

@ -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",

View File

@ -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<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', className)}
{...props}
/>
);
const ResizablePanel = ResizablePrimitive.Panel;
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
className
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-sm border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@ -0,0 +1,3 @@
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './Resizable';
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@ -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);
expandedWidth = 280,
collapsedWidth = 25,
expandedInsideBorderSize = 4,
collapsedInsideBorderSize = 8,
collapsedOutsideBorderSize = 4,
}: SidePanelProps) => {
const [panelOpen, setPanelOpen] = useState(isExpanded);
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0);
const styleMap = createStyleMap(expandedWidth, borderSize, collapsedWidth);
const baseStyle = createBaseStyle(expandedWidth);
const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding;
const gridWidth = getGridWidth(tabs.length, gridAvailableWidth);
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 (
<div
className={classnames(className, baseClasses, classesMap[openStatus][side])}
className={classnames(className, baseClasses)}
style={style}
>
{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 };

View File

@ -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,

View File

@ -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,

163
yarn.lock
View File

@ -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"