mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-18 17:47:37 +01:00
Better grid layouts
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use tauri::{Runtime, Window};
|
||||
|
||||
const TRAFFIC_LIGHT_OFFSET_X: f64 = 10.0;
|
||||
const TRAFFIC_LIGHT_OFFSET_Y: f64 = 16.0;
|
||||
const TRAFFIC_LIGHT_OFFSET_Y: f64 = 18.0;
|
||||
|
||||
pub trait WindowExt {
|
||||
fn position_traffic_lights(&self);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Suspense } from 'react';
|
||||
import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom';
|
||||
import { routePaths } from '../hooks/useRoutes';
|
||||
import { WorkspaceLayout } from './layouts/WorkspaceLayout';
|
||||
|
||||
const Workspaces = lazy(() => import('./Workspaces'));
|
||||
const Workspace = lazy(() => import('./Workspace'));
|
||||
const RouteError = lazy(() => import('./RouteError'));
|
||||
import Workspace from './Workspace';
|
||||
import RouteError from './RouteError';
|
||||
import Workspaces from './Workspaces';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -22,22 +20,14 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: routePaths.workspace({ workspaceId: ':workspaceId' }),
|
||||
element: (
|
||||
<WorkspaceLayout>
|
||||
<Workspace />
|
||||
</WorkspaceLayout>
|
||||
),
|
||||
element: <Workspace />,
|
||||
},
|
||||
{
|
||||
path: routePaths.request({
|
||||
workspaceId: ':workspaceId',
|
||||
requestId: ':requestId',
|
||||
}),
|
||||
element: (
|
||||
<WorkspaceLayout>
|
||||
<Workspace />
|
||||
</WorkspaceLayout>
|
||||
),
|
||||
element: <Workspace />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -78,25 +78,16 @@ export function RequestPane({ fullHeight, className }: Props) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
className,
|
||||
'h-full py-3 grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1',
|
||||
)}
|
||||
className={classnames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}
|
||||
>
|
||||
{activeRequest && (
|
||||
<>
|
||||
<UrlBar
|
||||
className="pl-3"
|
||||
id={activeRequest.id}
|
||||
url={activeRequest.url}
|
||||
method={activeRequest.method}
|
||||
/>
|
||||
<UrlBar id={activeRequest.id} url={activeRequest.url} method={activeRequest.method} />
|
||||
<Tabs
|
||||
value={activeTab.value}
|
||||
onChangeValue={activeTab.set}
|
||||
tabs={tabs}
|
||||
className="mt-2"
|
||||
tabListClassName="pl-3"
|
||||
label="Request body"
|
||||
>
|
||||
<TabContent value="auth">
|
||||
|
||||
112
src-web/components/RequestResponse.tsx
Normal file
112
src-web/components/RequestResponse.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import classnames from 'classnames';
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { RequestPane } from './RequestPane';
|
||||
import { ResponsePane } from './ResponsePane';
|
||||
import { ResizeBar } from './Workspace';
|
||||
|
||||
const rqst = { gridArea: 'rqst' };
|
||||
const resp = { gridArea: 'resp' };
|
||||
const drag = { gridArea: 'drag' };
|
||||
|
||||
export default function RequestResponse() {
|
||||
const DEFAULT = 0.5;
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const widthKv = useKeyValue<number>({ key: 'body_width', defaultValue: DEFAULT });
|
||||
const heightKv = useKeyValue<number>({ key: 'body_height', defaultValue: DEFAULT });
|
||||
const width = widthKv.value ?? DEFAULT;
|
||||
const height = heightKv.value ?? DEFAULT;
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const vertical = false;
|
||||
const styles = useMemo<CSSProperties>(
|
||||
() => ({
|
||||
gridTemplate: vertical
|
||||
? `
|
||||
' ${rqst.gridArea}' ${1 - height}fr
|
||||
' ${drag.gridArea}' auto
|
||||
' ${resp.gridArea}' ${height}fr
|
||||
/ 1fr
|
||||
`
|
||||
: `
|
||||
' ${rqst.gridArea} ${drag.gridArea} ${resp.gridArea}' minmax(0,1fr)
|
||||
/ ${1 - width}fr auto ${width}fr
|
||||
`,
|
||||
}),
|
||||
[vertical, width, height],
|
||||
);
|
||||
|
||||
const unsub = () => {
|
||||
if (moveState.current !== null) {
|
||||
document.documentElement.removeEventListener('mousemove', moveState.current.move);
|
||||
document.documentElement.removeEventListener('mouseup', moveState.current.up);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = useCallback(
|
||||
() => (vertical ? heightKv.set(DEFAULT) : widthKv.set(DEFAULT)),
|
||||
[vertical],
|
||||
);
|
||||
|
||||
const handleResizeStart = useCallback(
|
||||
(e: ReactMouseEvent<HTMLDivElement>) => {
|
||||
if (containerRef.current === null) return;
|
||||
unsub();
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
|
||||
const mouseStartX = e.clientX;
|
||||
const mouseStartY = e.clientY;
|
||||
const startWidth = containerRect.width * width;
|
||||
const startHeight = containerRect.height * height;
|
||||
|
||||
moveState.current = {
|
||||
move: (e: MouseEvent) => {
|
||||
e.preventDefault(); // Prevent text selection and things
|
||||
if (vertical) {
|
||||
const newHeightPx = startHeight - (e.clientY - mouseStartY);
|
||||
const newHeight = newHeightPx / containerRect.height;
|
||||
heightKv.set(newHeight);
|
||||
} else {
|
||||
const newWidthPx = startWidth - (e.clientX - mouseStartX);
|
||||
const newWidth = newWidthPx / containerRect.width;
|
||||
widthKv.set(newWidth);
|
||||
}
|
||||
},
|
||||
up: (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
unsub();
|
||||
setIsResizing(false);
|
||||
},
|
||||
};
|
||||
document.documentElement.addEventListener('mousemove', moveState.current.move);
|
||||
document.documentElement.addEventListener('mouseup', moveState.current.up);
|
||||
setIsResizing(true);
|
||||
},
|
||||
[widthKv.value, heightKv.value, vertical],
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="grid w-full h-full p-3" style={styles}>
|
||||
<div style={rqst}>
|
||||
<RequestPane fullHeight />
|
||||
</div>
|
||||
<div style={drag} className={classnames('relative', vertical ? 'h-3' : 'w-3')}>
|
||||
<ResizeBar
|
||||
isResizing={isResizing}
|
||||
onResizeStart={handleResizeStart}
|
||||
onReset={handleReset}
|
||||
side={vertical ? 'top' : 'left'}
|
||||
justify="center"
|
||||
/>
|
||||
</div>
|
||||
<div style={resp}>
|
||||
<ResponsePane />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,7 +16,11 @@ import { HStack } from './core/Stacks';
|
||||
import { StatusColor } from './core/StatusColor';
|
||||
import { Webview } from './core/Webview';
|
||||
|
||||
export const ResponsePane = memo(function ResponsePane() {
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ResponsePane = memo(function ResponsePane({ className }: Props) {
|
||||
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(null);
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const responses = useResponses(activeRequestId);
|
||||
@@ -39,10 +43,10 @@ export const ResponsePane = memo(function ResponsePane() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classnames('h-full w-full p-3')}>
|
||||
<div className={classnames(className, 'h-full w-full')}>
|
||||
<div
|
||||
className={classnames(
|
||||
'max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 ',
|
||||
'bg-gray-50 max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 ',
|
||||
'dark:bg-gray-100 rounded-md overflow-hidden border border-highlight',
|
||||
'shadow shadow-gray-100 dark:shadow-gray-0',
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,11 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
||||
ref={sidebarRef}
|
||||
className={classnames(className, 'h-full relative grid grid-rows-[minmax(0,1fr)_auto]')}
|
||||
>
|
||||
<VStack as="ul" className="relative py-3 overflow-auto" draggable={false}>
|
||||
<VStack
|
||||
as="ul"
|
||||
className="relative py-3 overflow-y-auto overflow-x-visible"
|
||||
draggable={false}
|
||||
>
|
||||
<SidebarItems activeRequestId={activeRequest?.id} requests={requests} />
|
||||
</VStack>
|
||||
<HStack className="mx-1 pb-1" alignItems="center" justifyContent="end">
|
||||
|
||||
196
src-web/components/Workspace.tsx
Normal file
196
src-web/components/Workspace.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import classnames from 'classnames';
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useWindowSize } from 'react-use';
|
||||
import { useSidebarDisplay } from '../hooks/useSidebarDisplay';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { WorkspaceHeader } from './WorkspaceHeader';
|
||||
import RequestResponse from './RequestResponse';
|
||||
|
||||
const side = { gridArea: 'side' };
|
||||
const head = { gridArea: 'head' };
|
||||
const body = { gridArea: 'body' };
|
||||
|
||||
export default function Workspace() {
|
||||
const windowSize = useWindowSize();
|
||||
const styles = useMemo<CSSProperties>(
|
||||
() => ({
|
||||
gridTemplate: `
|
||||
' ${head.gridArea} ${head.gridArea}' auto
|
||||
' ${side.gridArea} ${body.gridArea}' minmax(0,1fr)
|
||||
/ auto 1fr
|
||||
`,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid w-full h-full" style={styles}>
|
||||
<SidebarContainer style={side} floating={windowSize.width < 800}>
|
||||
<Sidebar />
|
||||
</SidebarContainer>
|
||||
<HeaderContainer>
|
||||
<WorkspaceHeader className="pointer-events-none" />
|
||||
</HeaderContainer>
|
||||
<BodyContainer>
|
||||
<RequestResponse />
|
||||
</BodyContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BodyContainer = memo(function BodyContainer({ children }: { children: ReactNode }) {
|
||||
return <div style={body}>{children}</div>;
|
||||
});
|
||||
|
||||
const HeaderContainer = memo(function HeaderContainer({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="h-md px-3 w-full pl-20 bg-gray-50 border-b border-b-highlight text-gray-900 pt-[1px]"
|
||||
style={head}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface SidebarContainerProps {
|
||||
children: ReactNode;
|
||||
style: CSSProperties;
|
||||
floating?: boolean;
|
||||
}
|
||||
|
||||
const SidebarContainer = memo(function SidebarContainer({
|
||||
children,
|
||||
style,
|
||||
floating,
|
||||
}: SidebarContainerProps) {
|
||||
const sidebar = useSidebarDisplay();
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const unsub = () => {
|
||||
if (moveState.current !== null) {
|
||||
document.documentElement.removeEventListener('mousemove', moveState.current.move);
|
||||
document.documentElement.removeEventListener('mouseup', moveState.current.up);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResizeStart = useCallback(
|
||||
(e: ReactMouseEvent<HTMLDivElement>) => {
|
||||
if (sidebar.width === undefined) return;
|
||||
|
||||
unsub();
|
||||
const mouseStartX = e.clientX;
|
||||
const startWidth = sidebar.width;
|
||||
moveState.current = {
|
||||
move: (e: MouseEvent) => {
|
||||
e.preventDefault(); // Prevent text selection and things
|
||||
sidebar.set(startWidth + (e.clientX - mouseStartX));
|
||||
},
|
||||
up: (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
unsub();
|
||||
setIsResizing(false);
|
||||
},
|
||||
};
|
||||
document.documentElement.addEventListener('mousemove', moveState.current.move);
|
||||
document.documentElement.addEventListener('mouseup', moveState.current.up);
|
||||
setIsResizing(true);
|
||||
},
|
||||
[sidebar.width],
|
||||
);
|
||||
|
||||
const sidebarStyles = useMemo(
|
||||
() => ({
|
||||
width: sidebar.hidden ? 0 : sidebar.width, // No width when hidden
|
||||
borderWidth: sidebar.hidden ? 0 : undefined, // No border when hidden
|
||||
}),
|
||||
[sidebar.width, sidebar.hidden, style],
|
||||
);
|
||||
|
||||
const commonClassname = classnames('overflow-hidden bg-gray-100 border-highlight');
|
||||
|
||||
if (floating) {
|
||||
return (
|
||||
<div style={style}>
|
||||
<div
|
||||
style={sidebarStyles}
|
||||
className={classnames(
|
||||
commonClassname,
|
||||
'fixed top-11 z-20 left-1 bottom-1 border rounded-md shadow-lg',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classnames(commonClassname, 'relative h-full border-r')} style={sidebarStyles}>
|
||||
<ResizeBar
|
||||
justify="end"
|
||||
side="right"
|
||||
isResizing={isResizing}
|
||||
onResizeStart={handleResizeStart}
|
||||
onReset={sidebar.reset}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface ResizeBarProps {
|
||||
className?: string;
|
||||
barClassName?: string;
|
||||
isResizing: boolean;
|
||||
onResizeStart: (e: ReactMouseEvent<HTMLDivElement>) => void;
|
||||
onReset?: () => void;
|
||||
side: 'left' | 'right' | 'top';
|
||||
justify: 'center' | 'end' | 'start';
|
||||
}
|
||||
|
||||
export function ResizeBar({
|
||||
justify,
|
||||
className,
|
||||
onResizeStart,
|
||||
onReset,
|
||||
isResizing,
|
||||
side,
|
||||
}: ResizeBarProps) {
|
||||
const vertical = side === 'top';
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
draggable
|
||||
className={classnames(
|
||||
className,
|
||||
'group absolute z-10 flex cursor-ew-resize',
|
||||
vertical ? 'w-full h-3 cursor-ns-resize' : 'h-full w-3 cursor-ew-resize',
|
||||
justify === 'center' && 'justify-center',
|
||||
justify === 'end' && 'justify-end',
|
||||
justify === 'start' && 'justify-start',
|
||||
side === 'right' && 'right-0',
|
||||
side === 'left' && 'left-0',
|
||||
side === 'top' && 'top-0',
|
||||
)}
|
||||
onDragStart={onResizeStart}
|
||||
onDoubleClick={onReset}
|
||||
>
|
||||
{/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */}
|
||||
{isResizing && (
|
||||
<div
|
||||
className={classnames(
|
||||
'fixed inset-0 cursor-ew-resize',
|
||||
vertical && 'cursor-ns-resize',
|
||||
!vertical && 'cursor-ew-resize',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
import classnames from 'classnames';
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useWindowSize } from 'react-use';
|
||||
import { useKeyValue } from '../../hooks/useKeyValue';
|
||||
import { useSidebarDisplay } from '../../hooks/useSidebarDisplay';
|
||||
import { RequestPane } from '../RequestPane';
|
||||
import { ResponsePane } from '../ResponsePane';
|
||||
import { Sidebar } from '../Sidebar';
|
||||
import { WorkspaceHeader } from '../WorkspaceHeader';
|
||||
|
||||
const side = { gridArea: 'side' };
|
||||
const head = { gridArea: 'head' };
|
||||
const rqst = { gridArea: 'rqst' };
|
||||
const resp = { gridArea: 'resp' };
|
||||
|
||||
export function WorkspaceLayout() {
|
||||
const windowSize = useWindowSize();
|
||||
const vertical = windowSize.width < 800;
|
||||
const styles = useMemo<CSSProperties>(
|
||||
() =>
|
||||
vertical
|
||||
? {
|
||||
gridTemplate: `
|
||||
' ${head.gridArea} ${head.gridArea}' auto
|
||||
' ${side.gridArea} ${rqst.gridArea}' 1fr
|
||||
' ${side.gridArea} ${resp.gridArea}' 1fr
|
||||
/ auto 1fr
|
||||
`,
|
||||
}
|
||||
: {
|
||||
gridTemplate: `
|
||||
' ${head.gridArea} ${head.gridArea} ${head.gridArea}' auto
|
||||
' ${side.gridArea} ${rqst.gridArea} ${resp.gridArea}' 1fr
|
||||
/ auto 1fr auto
|
||||
`,
|
||||
},
|
||||
[vertical],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid w-full h-full" style={styles}>
|
||||
<SidebarContainer style={side} floating={windowSize.width < 800}>
|
||||
<Sidebar />
|
||||
</SidebarContainer>
|
||||
<HeaderContainer>
|
||||
<WorkspaceHeader className="pointer-events-none" />
|
||||
</HeaderContainer>
|
||||
<RequestContainer>
|
||||
<RequestPane fullHeight />
|
||||
</RequestContainer>
|
||||
<ResponseContainer>
|
||||
<ResponsePane />
|
||||
</ResponseContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const HeaderContainer = memo(function HeaderContainer({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="h-9 px-3 w-full pl-20 bg-gray-50 border-b border-b-highlight text-gray-900 pt-[1px]"
|
||||
style={head}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const RequestContainer = memo(function RequestContainer({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="h-full w-full" style={rqst}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const ResponseContainer = memo(function ResponseContainer({ children }: { children: ReactNode }) {
|
||||
const displayKv = useKeyValue<number>({ key: 'response_width', defaultValue: 400 });
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
|
||||
null,
|
||||
);
|
||||
const unsub = () => {
|
||||
if (moveState.current !== null) {
|
||||
document.documentElement.removeEventListener('mousemove', moveState.current.move);
|
||||
document.documentElement.removeEventListener('mouseup', moveState.current.up);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = useCallback(() => displayKv.set(500), []);
|
||||
|
||||
const handleResizeStart = useCallback(
|
||||
(e: ReactMouseEvent<HTMLDivElement>) => {
|
||||
if (displayKv.value === undefined) return;
|
||||
|
||||
unsub();
|
||||
const mouseStartX = e.clientX;
|
||||
const startWidth = displayKv.value;
|
||||
moveState.current = {
|
||||
move: (e: MouseEvent) => {
|
||||
e.preventDefault(); // Prevent text selection and things
|
||||
displayKv.set(startWidth - (e.clientX - mouseStartX));
|
||||
},
|
||||
up: (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
unsub();
|
||||
setIsResizing(false);
|
||||
},
|
||||
};
|
||||
document.documentElement.addEventListener('mousemove', moveState.current.move);
|
||||
document.documentElement.addEventListener('mouseup', moveState.current.up);
|
||||
setIsResizing(true);
|
||||
},
|
||||
[displayKv.value],
|
||||
);
|
||||
|
||||
const sidebarStyles = useMemo(
|
||||
() => ({
|
||||
width: displayKv.value, // No width when hidden
|
||||
}),
|
||||
[displayKv.value],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative" style={sidebarStyles}>
|
||||
<ResizeBar
|
||||
isResizing={isResizing}
|
||||
onResizeStart={handleResizeStart}
|
||||
side="left"
|
||||
onReset={handleReset}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface SidebarContainerProps {
|
||||
children: ReactNode;
|
||||
style: CSSProperties;
|
||||
floating?: boolean;
|
||||
}
|
||||
|
||||
const SidebarContainer = memo(function SidebarContainer({
|
||||
children,
|
||||
style,
|
||||
floating,
|
||||
}: SidebarContainerProps) {
|
||||
const sidebar = useSidebarDisplay();
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
|
||||
null,
|
||||
);
|
||||
const unsub = () => {
|
||||
if (moveState.current !== null) {
|
||||
document.documentElement.removeEventListener('mousemove', moveState.current.move);
|
||||
document.documentElement.removeEventListener('mouseup', moveState.current.up);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResizeStart = useCallback(
|
||||
(e: ReactMouseEvent<HTMLDivElement>) => {
|
||||
if (sidebar.width === undefined) return;
|
||||
|
||||
unsub();
|
||||
const mouseStartX = e.clientX;
|
||||
const startWidth = sidebar.width;
|
||||
moveState.current = {
|
||||
move: (e: MouseEvent) => {
|
||||
e.preventDefault(); // Prevent text selection and things
|
||||
sidebar.set(startWidth + (e.clientX - mouseStartX));
|
||||
},
|
||||
up: (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
unsub();
|
||||
setIsResizing(false);
|
||||
},
|
||||
};
|
||||
document.documentElement.addEventListener('mousemove', moveState.current.move);
|
||||
document.documentElement.addEventListener('mouseup', moveState.current.up);
|
||||
setIsResizing(true);
|
||||
},
|
||||
[sidebar.width],
|
||||
);
|
||||
|
||||
const sidebarStyles = useMemo(
|
||||
() => ({
|
||||
...style,
|
||||
width: sidebar.hidden ? 0 : sidebar.width, // No width when hidden
|
||||
borderWidth: sidebar.hidden ? 0 : undefined, // No border when hidden
|
||||
}),
|
||||
[sidebar.width, sidebar.hidden, style],
|
||||
);
|
||||
|
||||
const commonClassname = classnames('overflow-hidden bg-gray-100 border-highlight');
|
||||
|
||||
if (floating) {
|
||||
return (
|
||||
<div
|
||||
style={sidebarStyles}
|
||||
className={classnames(
|
||||
commonClassname,
|
||||
'fixed top-10 z-10 left-1 bottom-1 border rounded-md',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classnames(commonClassname, 'relative h-full border-r')} style={sidebarStyles}>
|
||||
<ResizeBar
|
||||
side="right"
|
||||
isResizing={isResizing}
|
||||
onResizeStart={handleResizeStart}
|
||||
onReset={sidebar.reset}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface ResizeBarProps {
|
||||
isResizing: boolean;
|
||||
onResizeStart: (e: ReactMouseEvent<HTMLDivElement>) => void;
|
||||
onReset?: () => void;
|
||||
side: 'left' | 'right';
|
||||
}
|
||||
|
||||
function ResizeBar({ onResizeStart, onReset, isResizing, side }: ResizeBarProps) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
draggable
|
||||
className={classnames(
|
||||
'group absolute z-10 w-3 top-0 bottom-0 flex justify-end cursor-ew-resize',
|
||||
side === 'right' ? '-right-0.5' : '-left-0.5',
|
||||
)}
|
||||
onDragStart={onResizeStart}
|
||||
onDoubleClick={onReset}
|
||||
>
|
||||
{/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */}
|
||||
{isResizing && <div className="fixed inset-0 cursor-ew-resize" />}
|
||||
<div // drag-divider
|
||||
className={classnames(
|
||||
'transition-colors w-1 mr-0.5 group-hover:bg-highlight h-full pointer-events-none',
|
||||
isResizing && '!bg-blue-500/70',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user