diff --git a/src-tauri/src/window_ext.rs b/src-tauri/src/window_ext.rs
index 91e05267..d3f072d3 100644
--- a/src-tauri/src/window_ext.rs
+++ b/src-tauri/src/window_ext.rs
@@ -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);
diff --git a/src-web/components/AppRouter.tsx b/src-web/components/AppRouter.tsx
index daed690f..ff28eab9 100644
--- a/src-web/components/AppRouter.tsx
+++ b/src-web/components/AppRouter.tsx
@@ -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: (
-
-
-
- ),
+ element: ,
},
{
path: routePaths.request({
workspaceId: ':workspaceId',
requestId: ':requestId',
}),
- element: (
-
-
-
- ),
+ element: ,
},
],
},
diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx
index 8f89ef20..9d79c0db 100644
--- a/src-web/components/RequestPane.tsx
+++ b/src-web/components/RequestPane.tsx
@@ -78,25 +78,16 @@ export function RequestPane({ fullHeight, className }: Props) {
return (
{activeRequest && (
<>
-
+
diff --git a/src-web/components/RequestResponse.tsx b/src-web/components/RequestResponse.tsx
new file mode 100644
index 00000000..e0c4ad9b
--- /dev/null
+++ b/src-web/components/RequestResponse.tsx
@@ -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(null);
+ const widthKv = useKeyValue({ key: 'body_width', defaultValue: DEFAULT });
+ const heightKv = useKeyValue({ key: 'body_height', defaultValue: DEFAULT });
+ const width = widthKv.value ?? DEFAULT;
+ const height = heightKv.value ?? DEFAULT;
+ const [isResizing, setIsResizing] = useState(false);
+ const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
+ null,
+ );
+
+ const vertical = false;
+ const styles = useMemo(
+ () => ({
+ 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) => {
+ 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 (
+
+ );
+}
diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx
index 5e263c7e..18680952 100644
--- a/src-web/components/ResponsePane.tsx
+++ b/src-web/components/ResponsePane.tsx
@@ -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(null);
const activeRequestId = useActiveRequestId();
const responses = useResponses(activeRequestId);
@@ -39,10 +43,10 @@ export const ResponsePane = memo(function ResponsePane() {
);
return (
-
+
-
+
diff --git a/src-web/components/Workspace.tsx b/src-web/components/Workspace.tsx
new file mode 100644
index 00000000..76a50442
--- /dev/null
+++ b/src-web/components/Workspace.tsx
@@ -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(
+ () => ({
+ gridTemplate: `
+ ' ${head.gridArea} ${head.gridArea}' auto
+ ' ${side.gridArea} ${body.gridArea}' minmax(0,1fr)
+ / auto 1fr
+ `,
+ }),
+ [],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const BodyContainer = memo(function BodyContainer({ children }: { children: ReactNode }) {
+ return {children}
;
+});
+
+const HeaderContainer = memo(function HeaderContainer({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ );
+});
+
+interface SidebarContainerProps {
+ children: ReactNode;
+ style: CSSProperties;
+ floating?: boolean;
+}
+
+const SidebarContainer = memo(function SidebarContainer({
+ children,
+ style,
+ floating,
+}: SidebarContainerProps) {
+ const sidebar = useSidebarDisplay();
+ const [isResizing, setIsResizing] = useState(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) => {
+ 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 (
+
+ );
+ }
+
+ return (
+
+
+ {children}
+
+ );
+});
+
+interface ResizeBarProps {
+ className?: string;
+ barClassName?: string;
+ isResizing: boolean;
+ onResizeStart: (e: ReactMouseEvent) => 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 (
+
+ {/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */}
+ {isResizing && (
+
+ )}
+
+ );
+}
diff --git a/src-web/components/layouts/WorkspaceLayout.tsx b/src-web/components/layouts/WorkspaceLayout.tsx
deleted file mode 100644
index c4684f41..00000000
--- a/src-web/components/layouts/WorkspaceLayout.tsx
+++ /dev/null
@@ -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(
- () =>
- 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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-const HeaderContainer = memo(function HeaderContainer({ children }: { children: ReactNode }) {
- return (
-
- {children}
-
- );
-});
-
-const RequestContainer = memo(function RequestContainer({ children }: { children: ReactNode }) {
- return (
-
- {children}
-
- );
-});
-
-const ResponseContainer = memo(function ResponseContainer({ children }: { children: ReactNode }) {
- const displayKv = useKeyValue({ key: 'response_width', defaultValue: 400 });
- const [isResizing, setIsResizing] = useState(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) => {
- 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 (
-
-
- {children}
-
- );
-});
-
-interface SidebarContainerProps {
- children: ReactNode;
- style: CSSProperties;
- floating?: boolean;
-}
-
-const SidebarContainer = memo(function SidebarContainer({
- children,
- style,
- floating,
-}: SidebarContainerProps) {
- const sidebar = useSidebarDisplay();
- const [isResizing, setIsResizing] = useState(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) => {
- 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 (
-
- {children}
-
- );
- }
-
- return (
-
-
- {children}
-
- );
-});
-
-interface ResizeBarProps {
- isResizing: boolean;
- onResizeStart: (e: ReactMouseEvent) => void;
- onReset?: () => void;
- side: 'left' | 'right';
-}
-
-function ResizeBar({ onResizeStart, onReset, isResizing, side }: ResizeBarProps) {
- return (
-
- {/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */}
- {isResizing &&
}
-
-
- );
-}