mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
219 lines
5.1 KiB
TypeScript
219 lines
5.1 KiB
TypeScript
import {createContext, useCallback, useMemo} from 'react';
|
|
import styled from '@emotion/styled';
|
|
|
|
import {IconGrabbable} from 'sentry/icons';
|
|
import {space} from 'sentry/styles/space';
|
|
import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
|
|
|
|
export type DividerProps = {
|
|
'data-is-held': boolean;
|
|
'data-slide-direction': 'leftright' | 'updown';
|
|
onDoubleClick: React.MouseEventHandler<HTMLElement>;
|
|
onMouseDown: React.MouseEventHandler<HTMLElement>;
|
|
icon?: React.ReactNode;
|
|
} & React.DOMAttributes<HTMLDivElement>;
|
|
|
|
export const BaseSplitDivider = styled(({icon, ...props}: DividerProps) => (
|
|
<div {...props}>{icon || <IconGrabbable size="sm" />}</div>
|
|
))<DividerProps>`
|
|
display: grid;
|
|
place-items: center;
|
|
height: 100%;
|
|
width: 100%;
|
|
|
|
user-select: inherit;
|
|
background: inherit;
|
|
|
|
&:hover,
|
|
&[data-is-held='true'] {
|
|
background: ${p => p.theme.hover};
|
|
}
|
|
&[data-is-held='true'] {
|
|
user-select: none;
|
|
}
|
|
|
|
&[data-slide-direction='leftright'] {
|
|
cursor: ew-resize;
|
|
height: 100%;
|
|
width: ${space(2)};
|
|
}
|
|
&[data-slide-direction='updown'] {
|
|
cursor: ns-resize;
|
|
width: 100%;
|
|
height: ${space(2)};
|
|
|
|
& > svg {
|
|
transform: rotate(90deg);
|
|
}
|
|
}
|
|
`;
|
|
|
|
export const SplitPanelContext = createContext({
|
|
isMaximized: false,
|
|
isMinimized: false,
|
|
maximiseSize: () => {},
|
|
minimiseSize: () => {},
|
|
resetSize: () => {},
|
|
});
|
|
|
|
type Side = {
|
|
content: React.ReactNode;
|
|
default: number;
|
|
max: number;
|
|
min: number;
|
|
};
|
|
|
|
type CommonProps = {
|
|
availableSize: number;
|
|
SplitDivider?: React.ComponentType<DividerProps>;
|
|
onMouseDown?: (sizePct: `${number}%`) => void;
|
|
onResize?: (newSize: number) => void;
|
|
sizeStorageKey?: string;
|
|
};
|
|
|
|
export type SplitPanelProps = CommonProps &
|
|
(
|
|
| {
|
|
availableSize: number;
|
|
/**
|
|
* Content on the left side of the split
|
|
*/
|
|
left: Side;
|
|
/**
|
|
* Content on the right side of the split
|
|
*/
|
|
right: React.ReactNode;
|
|
}
|
|
| {
|
|
availableSize: number;
|
|
/**
|
|
* Content below the split
|
|
*/
|
|
bottom: React.ReactNode;
|
|
/**
|
|
* Content above of the split
|
|
*/
|
|
top: Side;
|
|
}
|
|
);
|
|
|
|
function SplitPanel(props: SplitPanelProps) {
|
|
const {
|
|
availableSize,
|
|
SplitDivider = BaseSplitDivider,
|
|
onMouseDown,
|
|
onResize,
|
|
sizeStorageKey,
|
|
} = props;
|
|
const isLeftRight = 'left' in props;
|
|
const initialSize = isLeftRight ? props.left.default : props.top.default;
|
|
const min = isLeftRight ? props.left.min : props.top.min;
|
|
const max = isLeftRight ? props.left.max : props.top.max;
|
|
|
|
const {
|
|
isHeld,
|
|
onDoubleClick,
|
|
onMouseDown: onDragStart,
|
|
size: containerSize,
|
|
setSize,
|
|
} = useResizableDrawer({
|
|
direction: isLeftRight ? 'left' : 'down',
|
|
initialSize,
|
|
min,
|
|
onResize: onResize ?? (() => {}),
|
|
sizeStorageKey,
|
|
});
|
|
|
|
const sizePct = `${
|
|
(Math.min(containerSize, max) / availableSize) * 100
|
|
}%` as `${number}%`;
|
|
|
|
const handleMouseDown = useCallback(
|
|
event => {
|
|
onMouseDown?.(sizePct);
|
|
onDragStart(event);
|
|
},
|
|
[onDragStart, sizePct, onMouseDown]
|
|
);
|
|
|
|
const isMaximized = containerSize >= max;
|
|
const isMinimized = containerSize <= min;
|
|
|
|
const contextValue = useMemo(
|
|
() => ({
|
|
isMaximized,
|
|
isMinimized,
|
|
maximiseSize: () => setSize(max),
|
|
minimiseSize: () => setSize(min),
|
|
resetSize: () => setSize(initialSize),
|
|
}),
|
|
[isMaximized, isMinimized, setSize, max, min, initialSize]
|
|
);
|
|
|
|
if (isLeftRight) {
|
|
const {left: a, right: b} = props;
|
|
|
|
return (
|
|
<SplitPanelContext.Provider value={contextValue}>
|
|
<SplitPanelContainer
|
|
className={isHeld ? 'disable-iframe-pointer' : undefined}
|
|
orientation="columns"
|
|
size={sizePct}
|
|
>
|
|
<Panel>{a.content}</Panel>
|
|
<SplitDivider
|
|
data-is-held={isHeld}
|
|
data-slide-direction="leftright"
|
|
onDoubleClick={onDoubleClick}
|
|
onMouseDown={handleMouseDown}
|
|
/>
|
|
<Panel>{b}</Panel>
|
|
</SplitPanelContainer>
|
|
</SplitPanelContext.Provider>
|
|
);
|
|
}
|
|
|
|
const {top: a, bottom: b} = props;
|
|
return (
|
|
<SplitPanelContext.Provider value={contextValue}>
|
|
<SplitPanelContainer
|
|
orientation="rows"
|
|
size={sizePct}
|
|
className={isHeld ? 'disable-iframe-pointer' : undefined}
|
|
>
|
|
<Panel>{a.content}</Panel>
|
|
<SplitDivider
|
|
data-is-held={isHeld}
|
|
data-slide-direction="updown"
|
|
onDoubleClick={onDoubleClick}
|
|
onMouseDown={handleMouseDown}
|
|
/>
|
|
<Panel>{b}</Panel>
|
|
</SplitPanelContainer>
|
|
</SplitPanelContext.Provider>
|
|
);
|
|
}
|
|
|
|
const SplitPanelContainer = styled('div')<{
|
|
orientation: 'rows' | 'columns';
|
|
size: `${number}px` | `${number}%`;
|
|
}>`
|
|
width: 100%;
|
|
height: 100%;
|
|
|
|
position: relative;
|
|
display: grid;
|
|
overflow: auto;
|
|
grid-template-${p => p.orientation}: ${p => p.size} auto 1fr;
|
|
|
|
&.disable-iframe-pointer iframe {
|
|
pointer-events: none !important;
|
|
}
|
|
`;
|
|
|
|
const Panel = styled('div')`
|
|
overflow: hidden;
|
|
`;
|
|
|
|
export default SplitPanel;
|