mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-06-12 01:14:27 +02:00
Shared sidebar layout
This commit is contained in:
@@ -3,8 +3,7 @@ import { settingsAtom, workspacesAtom } from '@yaakapp-internal/models';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import * as m from 'motion/react-m';
|
import * as m from 'motion/react-m';
|
||||||
import type { CSSProperties } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
||||||
import {
|
import {
|
||||||
useEnsureActiveCookieJar,
|
useEnsureActiveCookieJar,
|
||||||
useSubscribeActiveCookieJarId,
|
useSubscribeActiveCookieJarId,
|
||||||
@@ -40,19 +39,16 @@ import { HStack } from './core/Stacks';
|
|||||||
import { ErrorBoundary } from './ErrorBoundary';
|
import { ErrorBoundary } from './ErrorBoundary';
|
||||||
import { FolderLayout } from './FolderLayout';
|
import { FolderLayout } from './FolderLayout';
|
||||||
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
|
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
|
||||||
import { HeaderSize } from '@yaakapp-internal/ui';
|
import { HeaderSize, SidebarLayout } from '@yaakapp-internal/ui';
|
||||||
import { HttpRequestLayout } from './HttpRequestLayout';
|
import { HttpRequestLayout } from './HttpRequestLayout';
|
||||||
import { Overlay } from './Overlay';
|
import { Overlay } from './Overlay';
|
||||||
import { ResizeHandle, type ResizeHandleEvent } from '@yaakapp-internal/ui';
|
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import { SidebarActions } from './SidebarActions';
|
import { SidebarActions } from './SidebarActions';
|
||||||
import { WebsocketRequestLayout } from './WebsocketRequestLayout';
|
import { WebsocketRequestLayout } from './WebsocketRequestLayout';
|
||||||
import { WorkspaceHeader } from './WorkspaceHeader';
|
import { WorkspaceHeader } from './WorkspaceHeader';
|
||||||
|
|
||||||
const side = { gridArea: 'side' };
|
|
||||||
const head = { gridArea: 'head' };
|
const head = { gridArea: 'head' };
|
||||||
const body = { gridArea: 'body' };
|
const body = { gridArea: 'body' };
|
||||||
const drag = { gridArea: 'drag' };
|
|
||||||
|
|
||||||
export function Workspace() {
|
export function Workspace() {
|
||||||
// First, subscribe to some things applicable to workspaces
|
// First, subscribe to some things applicable to workspaces
|
||||||
@@ -66,50 +62,6 @@ export function Workspace() {
|
|||||||
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
|
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
|
||||||
const activeEnvironment = useAtomValue(activeEnvironmentAtom);
|
const activeEnvironment = useAtomValue(activeEnvironmentAtom);
|
||||||
const floating = useShouldFloatSidebar();
|
const floating = useShouldFloatSidebar();
|
||||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
|
||||||
const startWidth = useRef<number | null>(null);
|
|
||||||
|
|
||||||
const handleResizeMove = useCallback(
|
|
||||||
async ({ x, xStart }: ResizeHandleEvent) => {
|
|
||||||
if (width == null || startWidth.current == null) return;
|
|
||||||
|
|
||||||
const newWidth = startWidth.current + (x - xStart);
|
|
||||||
if (newWidth < 50) {
|
|
||||||
if (!sidebarHidden) await setSidebarHidden(true);
|
|
||||||
resetWidth();
|
|
||||||
} else {
|
|
||||||
if (sidebarHidden) await setSidebarHidden(false);
|
|
||||||
setWidth(newWidth);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[width, sidebarHidden, setSidebarHidden, resetWidth, setWidth],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleResizeStart = useCallback(() => {
|
|
||||||
startWidth.current = width ?? null;
|
|
||||||
setIsResizing(true);
|
|
||||||
}, [width]);
|
|
||||||
|
|
||||||
const handleResizeEnd = useCallback(() => {
|
|
||||||
setIsResizing(false);
|
|
||||||
startWidth.current = null;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const sideWidth = sidebarHidden ? 0 : width;
|
|
||||||
const styles = useMemo<CSSProperties>(
|
|
||||||
() => ({
|
|
||||||
gridTemplate: floating
|
|
||||||
? `
|
|
||||||
' ${head.gridArea}' auto
|
|
||||||
' ${body.gridArea}' minmax(0,1fr)
|
|
||||||
/ 1fr`
|
|
||||||
: `
|
|
||||||
' ${head.gridArea} ${head.gridArea} ${head.gridArea}' auto
|
|
||||||
' ${side.gridArea} ${drag.gridArea} ${body.gridArea}' minmax(0,1fr)
|
|
||||||
/ ${sideWidth}px 0 1fr`,
|
|
||||||
}),
|
|
||||||
[sideWidth, floating],
|
|
||||||
);
|
|
||||||
|
|
||||||
const environmentBgStyle = useMemo(() => {
|
const environmentBgStyle = useMemo(() => {
|
||||||
if (activeEnvironment?.color == null) return undefined;
|
if (activeEnvironment?.color == null) return undefined;
|
||||||
@@ -122,86 +74,87 @@ export function Workspace() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const header = (
|
||||||
<div
|
<HeaderSize
|
||||||
style={styles}
|
data-tauri-drag-region
|
||||||
className={classNames(
|
size="lg"
|
||||||
'grid w-full h-full',
|
className="relative x-theme-appHeader bg-surface"
|
||||||
// Animate sidebar width changes but only when not resizing
|
osType={osType}
|
||||||
// because it's too slow to animate on mouse move
|
hideWindowControls={settings.hideWindowControls}
|
||||||
!isResizing && 'transition-grid',
|
useNativeTitlebar={settings.useNativeTitlebar}
|
||||||
)}
|
interfaceScale={settings.interfaceScale}
|
||||||
>
|
>
|
||||||
{floating ? (
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
<Overlay
|
<div
|
||||||
open={!floatingSidebarHidden}
|
style={environmentBgStyle}
|
||||||
portalName="sidebar"
|
className="absolute inset-0 opacity-[0.07]"
|
||||||
onClose={() => setFloatingSidebarHidden(true)}
|
/>
|
||||||
zIndex={20}
|
<div
|
||||||
>
|
style={environmentBgStyle}
|
||||||
<m.div
|
className="absolute left-0 right-0 -bottom-[1px] h-[1px] opacity-20"
|
||||||
initial={{ opacity: 0, x: -20 }}
|
/>
|
||||||
animate={{ opacity: 1, x: 0 }}
|
</div>
|
||||||
className={classNames(
|
<WorkspaceHeader className="pointer-events-none" />
|
||||||
'x-theme-sidebar',
|
</HeaderSize>
|
||||||
'absolute top-0 left-0 bottom-0 bg-surface border-r border-border-subtle w-[20rem]',
|
);
|
||||||
'grid grid-rows-[auto_1fr]',
|
|
||||||
)}
|
const workspaceBody = (
|
||||||
>
|
<ErrorBoundary name="Workspace Body">
|
||||||
<HeaderSize hideControls size="lg" className="border-transparent flex items-center" osType={osType} hideWindowControls={settings.hideWindowControls} useNativeTitlebar={settings.useNativeTitlebar} interfaceScale={settings.interfaceScale}>
|
<WorkspaceBody />
|
||||||
<SidebarActions />
|
</ErrorBoundary>
|
||||||
</HeaderSize>
|
);
|
||||||
<ErrorBoundary name="Sidebar (Floating)">
|
|
||||||
<Sidebar />
|
const sidebarContent = (
|
||||||
</ErrorBoundary>
|
<div className="x-theme-sidebar overflow-hidden bg-surface h-full">
|
||||||
</m.div>
|
<ErrorBoundary name="Sidebar">
|
||||||
</Overlay>
|
<Sidebar className="border-r border-border-subtle" />
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div style={side} className={classNames('x-theme-sidebar', 'overflow-hidden bg-surface')}>
|
|
||||||
<ErrorBoundary name="Sidebar">
|
|
||||||
<Sidebar className="border-r border-border-subtle" />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
<ResizeHandle
|
|
||||||
style={drag}
|
|
||||||
className="-translate-x-[1px]"
|
|
||||||
justify="end"
|
|
||||||
side="right"
|
|
||||||
onResizeStart={handleResizeStart}
|
|
||||||
onResizeEnd={handleResizeEnd}
|
|
||||||
onResizeMove={handleResizeMove}
|
|
||||||
onReset={resetWidth}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<HeaderSize
|
|
||||||
data-tauri-drag-region
|
|
||||||
size="lg"
|
|
||||||
className="relative x-theme-appHeader bg-surface"
|
|
||||||
style={head}
|
|
||||||
osType={osType}
|
|
||||||
hideWindowControls={settings.hideWindowControls}
|
|
||||||
useNativeTitlebar={settings.useNativeTitlebar}
|
|
||||||
interfaceScale={settings.interfaceScale}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 pointer-events-none">
|
|
||||||
<div // Add subtle background
|
|
||||||
style={environmentBgStyle}
|
|
||||||
className="absolute inset-0 opacity-[0.07]"
|
|
||||||
/>
|
|
||||||
<div // Add a subtle border bottom
|
|
||||||
style={environmentBgStyle}
|
|
||||||
className="absolute left-0 right-0 -bottom-[1px] h-[1px] opacity-20"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<WorkspaceHeader className="pointer-events-none" />
|
|
||||||
</HeaderSize>
|
|
||||||
<ErrorBoundary name="Workspace Body">
|
|
||||||
<WorkspaceBody />
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid w-full h-full grid-rows-[auto_1fr]">
|
||||||
|
{header}
|
||||||
|
{floating ? (
|
||||||
|
<>
|
||||||
|
<Overlay
|
||||||
|
open={!floatingSidebarHidden}
|
||||||
|
portalName="sidebar"
|
||||||
|
onClose={() => setFloatingSidebarHidden(true)}
|
||||||
|
zIndex={20}
|
||||||
|
>
|
||||||
|
<m.div
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
className={classNames(
|
||||||
|
'x-theme-sidebar',
|
||||||
|
'absolute top-0 left-0 bottom-0 bg-surface border-r border-border-subtle w-[20rem]',
|
||||||
|
'grid grid-rows-[auto_1fr]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<HeaderSize hideControls size="lg" className="border-transparent flex items-center" osType={osType} hideWindowControls={settings.hideWindowControls} useNativeTitlebar={settings.useNativeTitlebar} interfaceScale={settings.interfaceScale}>
|
||||||
|
<SidebarActions />
|
||||||
|
</HeaderSize>
|
||||||
|
<ErrorBoundary name="Sidebar (Floating)">
|
||||||
|
<Sidebar />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</m.div>
|
||||||
|
</Overlay>
|
||||||
|
{workspaceBody}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<SidebarLayout
|
||||||
|
width={width ?? 250}
|
||||||
|
onWidthChange={setWidth}
|
||||||
|
hidden={sidebarHidden ?? false}
|
||||||
|
onHiddenChange={(hidden) => setSidebarHidden(hidden)}
|
||||||
|
sidebar={sidebarContent}
|
||||||
|
>
|
||||||
|
{workspaceBody}
|
||||||
|
</SidebarLayout>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkspaceBody() {
|
function WorkspaceBody() {
|
||||||
|
|||||||
@@ -13,15 +13,16 @@ import classNames from 'classnames';
|
|||||||
interface Props {
|
interface Props {
|
||||||
exchanges: HttpExchange[];
|
exchanges: HttpExchange[];
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExchangesTable({ exchanges, style }: Props) {
|
export function ExchangesTable({ exchanges, style, className }: Props) {
|
||||||
if (exchanges.length === 0) {
|
if (exchanges.length === 0) {
|
||||||
return <p className="text-text-subtlest text-sm">No traffic yet</p>;
|
return <p className="text-text-subtlest text-sm">No traffic yet</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table scrollable className="px-2" style={style}>
|
<Table scrollable className={classNames('px-2', className)} style={style}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>Method</TableHeaderCell>
|
<TableHeaderCell>Method</TableHeaderCell>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { HeaderSize, SplitLayout } from '@yaakapp-internal/ui';
|
import { HeaderSize, SidebarLayout } from '@yaakapp-internal/ui';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useLocalStorage } from 'react-use';
|
||||||
import { useRpcQueryWithEvent } from '../hooks/useRpcQueryWithEvent';
|
import { useRpcQueryWithEvent } from '../hooks/useRpcQueryWithEvent';
|
||||||
import { getOsType } from '../lib/tauri';
|
import { getOsType } from '../lib/tauri';
|
||||||
import { ActionIconButton } from './ActionIconButton';
|
import { ActionIconButton } from './ActionIconButton';
|
||||||
@@ -10,6 +11,7 @@ import { filteredExchangesAtom, Sidebar } from './Sidebar';
|
|||||||
export function ProxyLayout() {
|
export function ProxyLayout() {
|
||||||
const os = getOsType();
|
const os = getOsType();
|
||||||
const exchanges = useAtomValue(filteredExchangesAtom);
|
const exchanges = useAtomValue(filteredExchangesAtom);
|
||||||
|
const [sidebarWidth, setSidebarWidth] = useLocalStorage('sidebar_width', 250);
|
||||||
const { data: proxyState } = useRpcQueryWithEvent('get_proxy_state', {}, 'proxy_state_changed');
|
const { data: proxyState } = useRpcQueryWithEvent('get_proxy_state', {}, 'proxy_state_changed');
|
||||||
const isRunning = proxyState?.state === 'running';
|
const isRunning = proxyState?.state === 'running';
|
||||||
|
|
||||||
@@ -56,13 +58,13 @@ export function ProxyLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HeaderSize>
|
</HeaderSize>
|
||||||
<SplitLayout
|
<SidebarLayout
|
||||||
storageKey="proxy_sidebar"
|
width={sidebarWidth ?? 250}
|
||||||
layout="horizontal"
|
onWidthChange={setSidebarWidth}
|
||||||
defaultRatio={0.8}
|
sidebar={<Sidebar />}
|
||||||
firstSlot={({ style }) => <Sidebar style={style} />}
|
>
|
||||||
secondSlot={({ style }) => <ExchangesTable style={style} exchanges={exchanges} />}
|
<ExchangesTable exchanges={exchanges} className="overflow-auto h-full" />
|
||||||
/>
|
</SidebarLayout>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,17 +190,14 @@ function ItemInner({ item }: { item: SidebarItem }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ style }: { style?: React.CSSProperties }) {
|
export function Sidebar() {
|
||||||
const tree = useAtomValue(sidebarTreeAtom);
|
const tree = useAtomValue(sidebarTreeAtom);
|
||||||
const treeId = SIDEBAR_TREE_ID;
|
const treeId = SIDEBAR_TREE_ID;
|
||||||
|
|
||||||
const getItemKey = useCallback((item: SidebarItem) => item.id, []);
|
const getItemKey = useCallback((item: SidebarItem) => item.id, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside className="x-theme-sidebar bg-surface h-full w-full min-w-0 overflow-y-auto border-r border-border-subtle">
|
||||||
style={style}
|
|
||||||
className="x-theme-sidebar bg-surface h-full w-full min-w-0 overflow-y-auto border-r border-border-subtle"
|
|
||||||
>
|
|
||||||
<div className="pt-2 text-xs">
|
<div className="pt-2 text-xs">
|
||||||
<Tree
|
<Tree
|
||||||
treeId={treeId}
|
treeId={treeId}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import type { ResizeHandleEvent } from './ResizeHandle';
|
||||||
|
import { ResizeHandle } from './ResizeHandle';
|
||||||
|
|
||||||
|
const side = { gridArea: 'side', minWidth: 0 };
|
||||||
|
const drag = { gridArea: 'drag' };
|
||||||
|
const body = { gridArea: 'body', minWidth: 0 };
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width: number;
|
||||||
|
onWidthChange: (width: number) => void;
|
||||||
|
hidden?: boolean;
|
||||||
|
onHiddenChange?: (hidden: boolean) => void;
|
||||||
|
defaultWidth?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
className?: string;
|
||||||
|
sidebar: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SidebarLayout({
|
||||||
|
width,
|
||||||
|
onWidthChange,
|
||||||
|
hidden = false,
|
||||||
|
onHiddenChange,
|
||||||
|
defaultWidth = 250,
|
||||||
|
minWidth = 50,
|
||||||
|
className,
|
||||||
|
sidebar,
|
||||||
|
children,
|
||||||
|
}: Props) {
|
||||||
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
|
const startWidth = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const sideWidth = hidden ? 0 : width;
|
||||||
|
|
||||||
|
const styles = useMemo<CSSProperties>(
|
||||||
|
() => ({
|
||||||
|
gridTemplate: `
|
||||||
|
' ${side.gridArea} ${drag.gridArea} ${body.gridArea}' minmax(0,1fr)
|
||||||
|
/ ${sideWidth}px 0 1fr`,
|
||||||
|
}),
|
||||||
|
[sideWidth],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleResizeStart = useCallback(() => {
|
||||||
|
startWidth.current = width;
|
||||||
|
setIsResizing(true);
|
||||||
|
}, [width]);
|
||||||
|
|
||||||
|
const handleResizeEnd = useCallback(() => {
|
||||||
|
setIsResizing(false);
|
||||||
|
startWidth.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleResizeMove = useCallback(
|
||||||
|
({ x, xStart }: ResizeHandleEvent) => {
|
||||||
|
if (startWidth.current == null) return;
|
||||||
|
|
||||||
|
const newWidth = startWidth.current + (x - xStart);
|
||||||
|
if (newWidth < minWidth) {
|
||||||
|
onHiddenChange?.(true);
|
||||||
|
onWidthChange(defaultWidth);
|
||||||
|
} else {
|
||||||
|
if (hidden) onHiddenChange?.(false);
|
||||||
|
onWidthChange(newWidth);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[minWidth, hidden, onHiddenChange, onWidthChange, defaultWidth],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleReset = useCallback(() => {
|
||||||
|
onWidthChange(defaultWidth);
|
||||||
|
}, [onWidthChange, defaultWidth]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={styles}
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
'grid w-full h-full',
|
||||||
|
!isResizing && 'transition-grid',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div style={side} className="overflow-hidden">
|
||||||
|
{sidebar}
|
||||||
|
</div>
|
||||||
|
<ResizeHandle
|
||||||
|
style={drag}
|
||||||
|
className="-translate-x-[1px]"
|
||||||
|
justify="end"
|
||||||
|
side="right"
|
||||||
|
onResizeStart={handleResizeStart}
|
||||||
|
onResizeEnd={handleResizeEnd}
|
||||||
|
onResizeMove={handleResizeMove}
|
||||||
|
onReset={handleReset}
|
||||||
|
/>
|
||||||
|
<div style={body} className="min-w-0">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ export type { TreeItemProps } from "./components/tree/TreeItem";
|
|||||||
export { isSelectedFamily, selectedIdsFamily } from "./components/tree/atoms";
|
export { isSelectedFamily, selectedIdsFamily } from "./components/tree/atoms";
|
||||||
export { minPromiseMillis } from "./lib/minPromiseMillis";
|
export { minPromiseMillis } from "./lib/minPromiseMillis";
|
||||||
export { ResizeHandle } from "./components/ResizeHandle";
|
export { ResizeHandle } from "./components/ResizeHandle";
|
||||||
|
export { SidebarLayout } from "./components/SidebarLayout";
|
||||||
export type { ResizeHandleEvent } from "./components/ResizeHandle";
|
export type { ResizeHandleEvent } from "./components/ResizeHandle";
|
||||||
export { SplitLayout } from "./components/SplitLayout";
|
export { SplitLayout } from "./components/SplitLayout";
|
||||||
export type { SplitLayoutLayout, SlotProps } from "./components/SplitLayout";
|
export type { SplitLayoutLayout, SlotProps } from "./components/SplitLayout";
|
||||||
|
|||||||
Reference in New Issue
Block a user