Move split layout

This commit is contained in:
Gregory Schier
2026-03-12 14:00:29 -07:00
parent 0b7705d915
commit 7e7faa69df
25 changed files with 113 additions and 84 deletions

View File

@@ -1,6 +1,9 @@
import type { Environment, Workspace } from '@yaakapp-internal/models'; import type { Environment, Workspace } from '@yaakapp-internal/models';
import { duplicateModel, patchModel } from '@yaakapp-internal/models'; import { duplicateModel, patchModel } from '@yaakapp-internal/models';
import type { TreeHandle, TreeNode, TreeProps } from '@yaakapp-internal/ui';
import { Icon, SplitLayout, Tree } from '@yaakapp-internal/ui';
import { atom, useAtomValue } from 'jotai'; import { atom, useAtomValue } from 'jotai';
import { atomFamily } from 'jotai/utils';
import { useCallback, useLayoutEffect, useRef, useState } from 'react'; import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { createSubEnvironmentAndActivate } from '../commands/createEnvironment'; import { createSubEnvironmentAndActivate } from '../commands/createEnvironment';
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
@@ -9,6 +12,7 @@ import {
useEnvironmentsBreakdown, useEnvironmentsBreakdown,
} from '../hooks/useEnvironmentsBreakdown'; } from '../hooks/useEnvironmentsBreakdown';
import { useHotKey } from '../hooks/useHotKey'; import { useHotKey } from '../hooks/useHotKey';
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from '../lib/jotai';
import { isBaseEnvironment, isSubEnvironment } from '../lib/model_util'; import { isBaseEnvironment, isSubEnvironment } from '../lib/model_util';
@@ -17,15 +21,10 @@ import { showColorPicker } from '../lib/showColorPicker';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
import type { ContextMenuProps, DropdownItem } from './core/Dropdown'; import type { ContextMenuProps, DropdownItem } from './core/Dropdown';
import { ContextMenu } from './core/Dropdown'; import { ContextMenu } from './core/Dropdown';
import { Icon, Tree } from '@yaakapp-internal/ui';
import type { TreeNode, TreeHandle, TreeProps } from '@yaakapp-internal/ui';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { IconTooltip } from './core/IconTooltip'; import { IconTooltip } from './core/IconTooltip';
import { InlineCode } from './core/InlineCode'; import { InlineCode } from './core/InlineCode';
import type { PairEditorHandle } from './core/PairEditor'; import type { PairEditorHandle } from './core/PairEditor';
import { SplitLayout } from './core/SplitLayout';
import { atomFamily } from 'jotai/utils';
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator'; import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
import { EnvironmentEditor } from './EnvironmentEditor'; import { EnvironmentEditor } from './EnvironmentEditor';
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip'; import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
@@ -55,7 +54,7 @@ export function EnvironmentEditDialog({ initialEnvironmentId, setRef }: Props) {
return ( return (
<SplitLayout <SplitLayout
name="env_editor" storageKey="env_editor"
defaultRatio={0.75} defaultRatio={0.75}
layout="horizontal" layout="horizontal"
className="gap-0" className="gap-0"
@@ -155,24 +154,39 @@ function EnvironmentEditDialogSidebar({
[], [],
); );
const handleDuplicateSelected = useCallback(async (items: TreeModel[]) => { const handleDuplicateSelected = useCallback(
if (items.length === 1 && items[0]) { async (items: TreeModel[]) => {
const newId = await duplicateModel(items[0]); if (items.length === 1 && items[0]) {
setSelectedEnvironmentId(newId); const newId = await duplicateModel(items[0]);
} else { setSelectedEnvironmentId(newId);
await Promise.all(items.map(duplicateModel)); } else {
} await Promise.all(items.map(duplicateModel));
}, [setSelectedEnvironmentId]); }
},
[setSelectedEnvironmentId],
);
useHotKey('sidebar.selected.rename', handleRenameSelected, { enable: treeHasFocus, allowDefault: true, priority: 100 }); useHotKey('sidebar.selected.rename', handleRenameSelected, {
useHotKey('sidebar.selected.delete', useCallback(() => { enable: treeHasFocus,
const items = getSelectedTreeModels(); allowDefault: true,
if (items) handleDeleteSelected(items); priority: 100,
}, [getSelectedTreeModels, handleDeleteSelected]), { enable: treeHasFocus, priority: 100 }); });
useHotKey('sidebar.selected.duplicate', useCallback(async () => { useHotKey(
const items = getSelectedTreeModels(); 'sidebar.selected.delete',
if (items) await handleDuplicateSelected(items); useCallback(() => {
}, [getSelectedTreeModels, handleDuplicateSelected]), { enable: treeHasFocus, priority: 100 }); const items = getSelectedTreeModels();
if (items) handleDeleteSelected(items);
}, [getSelectedTreeModels, handleDeleteSelected]),
{ enable: treeHasFocus, priority: 100 },
);
useHotKey(
'sidebar.selected.duplicate',
useCallback(async () => {
const items = getSelectedTreeModels();
if (items) await handleDuplicateSelected(items);
}, [getSelectedTreeModels, handleDuplicateSelected]),
{ enable: treeHasFocus, priority: 100 },
);
const getContextMenu = useCallback( const getContextMenu = useCallback(
(items: TreeModel[]): ContextMenuProps['items'] => { (items: TreeModel[]): ContextMenuProps['items'] => {
@@ -249,7 +263,12 @@ function EnvironmentEditDialogSidebar({
return menuItems; return menuItems;
}, },
[baseEnvironments.length, handleDeleteEnvironment, setSelectedEnvironmentId], [
baseEnvironments.length,
handleDeleteEnvironment,
handleDuplicateSelected,
handleRenameSelected,
],
); );
const handleDragEnd = useCallback(async function handleDragEnd({ const handleDragEnd = useCallback(async function handleDragEnd({

View File

@@ -7,10 +7,11 @@ import { useActiveRequest } from '../hooks/useActiveRequest';
import { useGrpc } from '../hooks/useGrpc'; import { useGrpc } from '../hooks/useGrpc';
import { useGrpcProtoFiles } from '../hooks/useGrpcProtoFiles'; import { useGrpcProtoFiles } from '../hooks/useGrpcProtoFiles';
import { activeGrpcConnectionAtom, useGrpcEvents } from '../hooks/usePinnedGrpcConnection'; import { activeGrpcConnectionAtom, useGrpcEvents } from '../hooks/usePinnedGrpcConnection';
import { SplitLayout } from '@yaakapp-internal/ui';
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
import { workspaceLayoutAtom } from '../lib/atoms'; import { workspaceLayoutAtom } from '../lib/atoms';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
import { HotkeyList } from './core/HotkeyList'; import { HotkeyList } from './core/HotkeyList';
import { SplitLayout } from './core/SplitLayout';
import { GrpcRequestPane } from './GrpcRequestPane'; import { GrpcRequestPane } from './GrpcRequestPane';
import { GrpcResponsePane } from './GrpcResponsePane'; import { GrpcResponsePane } from './GrpcResponsePane';
@@ -22,6 +23,8 @@ const emptyArray: string[] = [];
export function GrpcConnectionLayout({ style }: Props) { export function GrpcConnectionLayout({ style }: Props) {
const workspaceLayout = useAtomValue(workspaceLayoutAtom); const workspaceLayout = useAtomValue(workspaceLayoutAtom);
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
const wsId = activeWorkspace?.id ?? 'n/a';
const activeRequest = useActiveRequest('grpc_request'); const activeRequest = useActiveRequest('grpc_request');
const activeConnection = useAtomValue(activeGrpcConnectionAtom); const activeConnection = useAtomValue(activeGrpcConnectionAtom);
const grpcEvents = useGrpcEvents(activeConnection?.id ?? null); const grpcEvents = useGrpcEvents(activeConnection?.id ?? null);
@@ -79,7 +82,7 @@ export function GrpcConnectionLayout({ style }: Props) {
return ( return (
<SplitLayout <SplitLayout
name="grpc_layout" storageKey={`grpc_layout::${wsId}`}
className="p-3 gap-1.5" className="p-3 gap-1.5"
style={style} style={style}
layout={workspaceLayout} layout={workspaceLayout}

View File

@@ -3,7 +3,7 @@ import classNames from 'classnames';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import { useAuthTab } from '../hooks/useAuthTab'; import { useAuthTab } from '../hooks/useAuthTab';
import { useContainerSize } from '../hooks/useContainerQuery'; import { useContainerSize } from '@yaakapp-internal/ui';
import type { ReflectResponseService } from '../hooks/useGrpc'; import type { ReflectResponseService } from '../hooks/useGrpc';
import { useHeadersTab } from '../hooks/useHeadersTab'; import { useHeadersTab } from '../hooks/useHeadersTab';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders'; import { useInheritedHeaders } from '../hooks/useInheritedHeaders';

View File

@@ -92,7 +92,7 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
getEventKey={(event) => event.id} getEventKey={(event) => event.id}
error={activeConnection.error} error={activeConnection.error}
header={header} header={header}
splitLayoutName="grpc_events" splitLayoutStorageKey="grpc_events"
defaultRatio={0.4} defaultRatio={0.4}
renderRow={({ event, isActive, onClick }) => ( renderRow={({ event, isActive, onClick }) => (
<GrpcEventRow event={event} isActive={isActive} onClick={onClick} /> <GrpcEventRow event={event} isActive={isActive} onClick={onClick} />

View File

@@ -1,11 +1,12 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from '@yaakapp-internal/models';
import type { SlotProps } from '@yaakapp-internal/ui';
import { SplitLayout } from '@yaakapp-internal/ui';
import classNames from 'classnames'; import classNames from 'classnames';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { useCurrentGraphQLSchema } from '../hooks/useIntrospectGraphQL'; import { useCurrentGraphQLSchema } from '../hooks/useIntrospectGraphQL';
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
import { workspaceLayoutAtom } from '../lib/atoms'; import { workspaceLayoutAtom } from '../lib/atoms';
import type { SlotProps } from './core/SplitLayout';
import { SplitLayout } from './core/SplitLayout';
import { GraphQLDocsExplorer } from './graphql/GraphQLDocsExplorer'; import { GraphQLDocsExplorer } from './graphql/GraphQLDocsExplorer';
import { showGraphQLDocExplorerAtom } from './graphql/graphqlAtoms'; import { showGraphQLDocExplorerAtom } from './graphql/graphqlAtoms';
import { HttpRequestPane } from './HttpRequestPane'; import { HttpRequestPane } from './HttpRequestPane';
@@ -20,10 +21,12 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
const showGraphQLDocExplorer = useAtomValue(showGraphQLDocExplorerAtom); const showGraphQLDocExplorer = useAtomValue(showGraphQLDocExplorerAtom);
const graphQLSchema = useCurrentGraphQLSchema(activeRequest); const graphQLSchema = useCurrentGraphQLSchema(activeRequest);
const workspaceLayout = useAtomValue(workspaceLayoutAtom); const workspaceLayout = useAtomValue(workspaceLayoutAtom);
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
const wsId = activeWorkspace?.id ?? 'n/a';
const requestResponseSplit = ({ style }: Pick<SlotProps, 'style'>) => ( const requestResponseSplit = ({ style }: Pick<SlotProps, 'style'>) => (
<SplitLayout <SplitLayout
name="http_layout" storageKey={`http_layout::${wsId}`}
className="p-3 gap-1.5" className="p-3 gap-1.5"
style={style} style={style}
layout={workspaceLayout} layout={workspaceLayout}
@@ -47,7 +50,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
) { ) {
return ( return (
<SplitLayout <SplitLayout
name="graphql_layout" storageKey={`graphql_layout::${wsId}`}
defaultRatio={1 / 3} defaultRatio={1 / 3}
firstSlot={requestResponseSplit} firstSlot={requestResponseSplit}
secondSlot={({ style, orientation }) => ( secondSlot={({ style, orientation }) => (

View File

@@ -55,7 +55,7 @@ function Inner({ response, viewMode }: Props) {
isLoading={isLoading} isLoading={isLoading}
loadingMessage="Loading events..." loadingMessage="Loading events..."
emptyMessage="No events recorded" emptyMessage="No events recorded"
splitLayoutName="http_response_events" splitLayoutStorageKey="http_response_events"
defaultRatio={0.25} defaultRatio={0.25}
renderRow={({ event, isActive, onClick }) => { renderRow={({ event, isActive, onClick }) => {
const display = getEventDisplay(event.event); const display = getEventDisplay(event.event);

View File

@@ -7,7 +7,7 @@ import { useAtomValue } from 'jotai';
import { useState } from 'react'; import { useState } from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace'; import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { clamp } from '../../lib/clamp'; import { clamp } from '@yaakapp-internal/ui';
import { showConfirm } from '../../lib/confirm'; import { showConfirm } from '../../lib/confirm';
import { invokeCmd } from '../../lib/tauri'; import { invokeCmd } from '../../lib/tauri';
import { CargoFeature } from '../CargoFeature'; import { CargoFeature } from '../CargoFeature';

View File

@@ -3,8 +3,9 @@ import classNames from 'classnames';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { SplitLayout } from '@yaakapp-internal/ui';
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
import { workspaceLayoutAtom } from '../lib/atoms'; import { workspaceLayoutAtom } from '../lib/atoms';
import { SplitLayout } from './core/SplitLayout';
import { WebsocketRequestPane } from './WebsocketRequestPane'; import { WebsocketRequestPane } from './WebsocketRequestPane';
import { WebsocketResponsePane } from './WebsocketResponsePane'; import { WebsocketResponsePane } from './WebsocketResponsePane';
@@ -15,9 +16,11 @@ interface Props {
export function WebsocketRequestLayout({ activeRequest, style }: Props) { export function WebsocketRequestLayout({ activeRequest, style }: Props) {
const workspaceLayout = useAtomValue(workspaceLayoutAtom); const workspaceLayout = useAtomValue(workspaceLayoutAtom);
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
const wsId = activeWorkspace?.id ?? 'n/a';
return ( return (
<SplitLayout <SplitLayout
name="websocket_layout" storageKey={`websocket_layout::${wsId}`}
className="p-3 gap-1.5" className="p-3 gap-1.5"
layout={workspaceLayout} layout={workspaceLayout}
style={style} style={style}

View File

@@ -69,7 +69,7 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
getEventKey={(event) => event.id} getEventKey={(event) => event.id}
error={activeConnection.error} error={activeConnection.error}
header={header} header={header}
splitLayoutName="websocket_events" splitLayoutStorageKey="websocket_events"
defaultRatio={0.4} defaultRatio={0.4}
renderRow={({ event, isActive, onClick }) => ( renderRow={({ event, isActive, onClick }) => (
<WebsocketEventRow event={event} isActive={isActive} onClick={onClick} /> <WebsocketEventRow event={event} isActive={isActive} onClick={onClick} />

View File

@@ -43,8 +43,7 @@ import { GrpcConnectionLayout } from './GrpcConnectionLayout';
import { HeaderSize } from '@yaakapp-internal/ui'; import { HeaderSize } from '@yaakapp-internal/ui';
import { HttpRequestLayout } from './HttpRequestLayout'; import { HttpRequestLayout } from './HttpRequestLayout';
import { Overlay } from './Overlay'; import { Overlay } from './Overlay';
import type { ResizeHandleEvent } from './ResizeHandle'; import { ResizeHandle, type ResizeHandleEvent } from '@yaakapp-internal/ui';
import { ResizeHandle } from './ResizeHandle';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
import { SidebarActions } from './SidebarActions'; import { SidebarActions } from './SidebarActions';
import { WebsocketRequestLayout } from './WebsocketRequestLayout'; import { WebsocketRequestLayout } from './WebsocketRequestLayout';

View File

@@ -8,7 +8,7 @@ import { AutoScroller } from './AutoScroller';
import { Banner } from './Banner'; import { Banner } from './Banner';
import { Button } from './Button'; import { Button } from './Button';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { SplitLayout } from './SplitLayout'; import { SplitLayout } from '@yaakapp-internal/ui';
import { HStack } from './Stacks'; import { HStack } from './Stacks';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -37,8 +37,8 @@ interface EventViewerProps<T> {
/** Error message to display as a banner */ /** Error message to display as a banner */
error?: string | null; error?: string | null;
/** Name for SplitLayout state persistence */ /** Key for SplitLayout state persistence */
splitLayoutName: string; splitLayoutStorageKey: string;
/** Default ratio for the split (0.0 - 1.0) */ /** Default ratio for the split (0.0 - 1.0) */
defaultRatio?: number; defaultRatio?: number;
@@ -66,7 +66,7 @@ export function EventViewer<T>({
renderDetail, renderDetail,
header, header,
error, error,
splitLayoutName, splitLayoutStorageKey,
defaultRatio = 0.4, defaultRatio = 0.4,
enableKeyboardNav = true, enableKeyboardNav = true,
isLoading = false, isLoading = false,
@@ -151,7 +151,7 @@ export function EventViewer<T>({
<div ref={containerRef} className="h-full"> <div ref={containerRef} className="h-full">
<SplitLayout <SplitLayout
layout="vertical" layout="vertical"
name={splitLayoutName} storageKey={splitLayoutStorageKey}
defaultRatio={defaultRatio} defaultRatio={defaultRatio}
minHeightPx={10} minHeightPx={10}
firstSlot={({ style }) => ( firstSlot={({ style }) => (

View File

@@ -23,7 +23,7 @@ import { Icon } from '@yaakapp-internal/ui';
import { InlineCode } from '../core/InlineCode'; import { InlineCode } from '../core/InlineCode';
import { Input } from '../core/Input'; import { Input } from '../core/Input';
import { Separator } from '../core/Separator'; import { Separator } from '../core/Separator';
import { SplitLayout } from '../core/SplitLayout'; import { SplitLayout } from '@yaakapp-internal/ui';
import { HStack } from '../core/Stacks'; import { HStack } from '../core/Stacks';
import { EmptyStateText } from '../EmptyStateText'; import { EmptyStateText } from '../EmptyStateText';
import { gitCallbacks } from './callbacks'; import { gitCallbacks } from './callbacks';
@@ -185,13 +185,13 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
return ( return (
<div className="h-full px-2 pb-4"> <div className="h-full px-2 pb-4">
<SplitLayout <SplitLayout
name="commit-horizontal" storageKey="commit-horizontal"
layout="horizontal" layout="horizontal"
defaultRatio={0.6} defaultRatio={0.6}
firstSlot={({ style }) => ( firstSlot={({ style }) => (
<div style={style} className="h-full px-4"> <div style={style} className="h-full px-4">
<SplitLayout <SplitLayout
name="commit-vertical" storageKey="commit-vertical"
layout="vertical" layout="vertical"
defaultRatio={0.35} defaultRatio={0.35}
firstSlot={({ style: innerStyle }) => ( firstSlot={({ style: innerStyle }) => (

View File

@@ -24,7 +24,7 @@ import { useAtomValue } from 'jotai';
import type { CSSProperties, HTMLAttributes, KeyboardEvent, ReactNode } from 'react'; import type { CSSProperties, HTMLAttributes, KeyboardEvent, ReactNode } from 'react';
import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useClickOutside } from '../../hooks/useClickOutside'; import { useClickOutside } from '../../hooks/useClickOutside';
import { useContainerSize } from '../../hooks/useContainerQuery'; import { useContainerSize } from '@yaakapp-internal/ui';
import { Icon, useDebouncedValue } from '@yaakapp-internal/ui'; import { Icon, useDebouncedValue } from '@yaakapp-internal/ui';
import { useStateWithDeps } from '../../hooks/useStateWithDeps'; import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import { jotaiStore } from '../../lib/jotai'; import { jotaiStore } from '../../lib/jotai';

View File

@@ -38,7 +38,7 @@ function ActualEventStreamViewer({ response }: Props) {
events={events.data ?? []} events={events.data ?? []}
getEventKey={(_, index) => String(index)} getEventKey={(_, index) => String(index)}
error={events.error ? String(events.error) : null} error={events.error ? String(events.error) : null}
splitLayoutName="sse_events" splitLayoutStorageKey="sse_events"
defaultRatio={0.4} defaultRatio={0.4}
renderRow={({ event, index, isActive, onClick }) => ( renderRow={({ event, index, isActive, onClick }) => (
<EventViewerRow <EventViewerRow

View File

@@ -5,7 +5,7 @@ import './PdfViewer.css';
import type { PDFDocumentProxy } from 'pdfjs-dist'; import type { PDFDocumentProxy } from 'pdfjs-dist';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Document, Page } from 'react-pdf'; import { Document, Page } from 'react-pdf';
import { useContainerSize } from '../../hooks/useContainerQuery'; import { useContainerSize } from '@yaakapp-internal/ui';
import('react-pdf').then(({ pdfjs }) => { import('react-pdf').then(({ pdfjs }) => {
pdfjs.GlobalWorkerOptions.workerSrc = new URL( pdfjs.GlobalWorkerOptions.workerSrc = new URL(

View File

@@ -3,7 +3,7 @@ import type { UpdateInfo } from "@yaakapp-internal/tauri-client";
import type { Atom } from "jotai"; import type { Atom } from "jotai";
import { atom } from "jotai"; import { atom } from "jotai";
import { selectAtom } from "jotai/utils"; import { selectAtom } from "jotai/utils";
import type { SplitLayoutLayout } from "../components/core/SplitLayout"; import type { SplitLayoutLayout } from "@yaakapp-internal/ui";
import { atomWithKVStorage } from "./atoms/atomWithKVStorage"; import { atomWithKVStorage } from "./atoms/atomWithKVStorage";
export function deepEqualAtom<T>(a: Atom<T>) { export function deepEqualAtom<T>(a: Atom<T>) {

View File

@@ -12,15 +12,16 @@ import classNames from 'classnames';
interface Props { interface Props {
exchanges: HttpExchange[]; exchanges: HttpExchange[];
style?: React.CSSProperties;
} }
export function ExchangesTable({ exchanges }: Props) { export function ExchangesTable({ exchanges, style }: 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"> <Table scrollable className="px-2" style={style}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableHeaderCell>Method</TableHeaderCell> <TableHeaderCell>Method</TableHeaderCell>

View File

@@ -1,4 +1,4 @@
import { HeaderSize } from '@yaakapp-internal/ui'; import { HeaderSize, SplitLayout } from '@yaakapp-internal/ui';
import classNames from 'classnames'; import classNames from 'classnames';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useRpcQueryWithEvent } from '../hooks/useRpcQueryWithEvent'; import { useRpcQueryWithEvent } from '../hooks/useRpcQueryWithEvent';
@@ -56,12 +56,13 @@ export function ProxyLayout() {
</div> </div>
</div> </div>
</HeaderSize> </HeaderSize>
<div className="grid grid-cols-[auto_1fr] min-h-0"> <SplitLayout
<Sidebar /> storageKey="proxy_sidebar"
<main className="overflow-auto"> layout="horizontal"
<ExchangesTable exchanges={exchanges} /> defaultRatio={0.8}
</main> firstSlot={({ style }) => <Sidebar style={style} />}
</div> secondSlot={({ style }) => <ExchangesTable style={style} exchanges={exchanges} />}
/>
</div> </div>
); );
} }

View File

@@ -190,14 +190,17 @@ function ItemInner({ item }: { item: SidebarItem }) {
); );
} }
export function Sidebar() { export function Sidebar({ style }: { style?: React.CSSProperties }) {
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 className="x-theme-sidebar bg-surface h-full w-[250px] min-w-0 overflow-y-auto border-r border-border-subtle"> <aside
style={style}
className="x-theme-sidebar bg-surface h-full w-[250px] 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}

View File

@@ -89,7 +89,6 @@ export function ResizeHandle({
className={classNames( className={classNames(
className, className,
'group z-10 flex select-none transition-colors hover:bg-surface-active rounded-full', 'group z-10 flex select-none transition-colors hover:bg-surface-active rounded-full',
// 'bg-info', // For debugging
vertical ? 'w-full h-1.5 cursor-row-resize' : 'h-full w-1.5 cursor-col-resize', vertical ? 'w-full h-1.5 cursor-row-resize' : 'h-full w-1.5 cursor-col-resize',
justify === 'center' && 'justify-center', justify === 'center' && 'justify-center',
justify === 'end' && 'justify-end', justify === 'end' && 'justify-end',
@@ -99,11 +98,9 @@ export function ResizeHandle({
side === 'top' && 'top-0', side === 'top' && 'top-0',
)} )}
> >
{/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */}
{isResizing && ( {isResizing && (
<div <div
className={classNames( className={classNames(
// 'bg-[rgba(255,0,0,0.1)]', // For debugging
'fixed -left-[100vw] -right-[100vw] -top-[100vh] -bottom-[100vh]', 'fixed -left-[100vw] -right-[100vw] -top-[100vh] -bottom-[100vh]',
vertical && 'cursor-row-resize', vertical && 'cursor-row-resize',
!vertical && 'cursor-col-resize', !vertical && 'cursor-col-resize',

View File

@@ -1,13 +1,11 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties, ReactNode } from 'react'; import type { CSSProperties, ReactNode } from 'react';
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import { useLocalStorage } from 'react-use'; import { useLocalStorage } from 'react-use';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace'; import { useContainerSize } from '../hooks/useContainerSize';
import { useContainerSize } from '../../hooks/useContainerQuery'; import { clamp } from '../lib/clamp';
import { clamp } from '../../lib/clamp'; import type { ResizeHandleEvent } from './ResizeHandle';
import type { ResizeHandleEvent } from '../ResizeHandle'; import { ResizeHandle } from './ResizeHandle';
import { ResizeHandle } from '../ResizeHandle';
export type SplitLayoutLayout = 'responsive' | 'horizontal' | 'vertical'; export type SplitLayoutLayout = 'responsive' | 'horizontal' | 'vertical';
@@ -17,7 +15,7 @@ export interface SlotProps {
} }
interface Props { interface Props {
name: string; storageKey: string;
firstSlot: (props: SlotProps) => ReactNode; firstSlot: (props: SlotProps) => ReactNode;
secondSlot: null | ((props: SlotProps) => ReactNode); secondSlot: null | ((props: SlotProps) => ReactNode);
style?: CSSProperties; style?: CSSProperties;
@@ -41,7 +39,7 @@ export function SplitLayout({
firstSlot, firstSlot,
secondSlot, secondSlot,
className, className,
name, storageKey,
layout = 'responsive', layout = 'responsive',
resizeHandleClassName, resizeHandleClassName,
defaultRatio = 0.5, defaultRatio = 0.5,
@@ -49,13 +47,8 @@ export function SplitLayout({
minWidthPx = 10, minWidthPx = 10,
}: Props) { }: Props) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const activeWorkspace = useAtomValue(activeWorkspaceAtom); const [widthRaw, setWidth] = useLocalStorage<number>(`${storageKey}_width`);
const [widthRaw, setWidth] = useLocalStorage<number>( const [heightRaw, setHeight] = useLocalStorage<number>(`${storageKey}_height`);
`${name}_width::${activeWorkspace?.id ?? 'n/a'}`,
);
const [heightRaw, setHeight] = useLocalStorage<number>(
`${name}_height::${activeWorkspace?.id ?? 'n/a'}`,
);
const width = widthRaw ?? defaultRatio; const width = widthRaw ?? defaultRatio;
let height = heightRaw ?? defaultRatio; let height = heightRaw ?? defaultRatio;
@@ -94,7 +87,6 @@ export function SplitLayout({
(e: ResizeHandleEvent) => { (e: ResizeHandleEvent) => {
if (containerRef.current === null) return; if (containerRef.current === null) return;
// const containerRect = containerRef.current.getBoundingClientRect();
const { paddingLeft, paddingRight, paddingTop, paddingBottom } = getComputedStyle( const { paddingLeft, paddingRight, paddingTop, paddingBottom } = getComputedStyle(
containerRef.current, containerRef.current,
); );

View File

@@ -5,13 +5,15 @@ export function Table({
children, children,
className, className,
scrollable, scrollable,
style,
}: { }: {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
scrollable?: boolean; scrollable?: boolean;
style?: React.CSSProperties;
}) { }) {
return ( return (
<div className={classNames('w-full', scrollable && 'h-full overflow-y-auto')}> <div style={style} className={classNames('w-full', scrollable && 'h-full overflow-y-auto')}>
<table <table
className={classNames( className={classNames(
className, className,

View File

@@ -20,4 +20,10 @@ export type { TreeNode } from "./components/tree/common";
export type { TreeItemProps } from "./components/tree/TreeItem"; 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 type { ResizeHandleEvent } from "./components/ResizeHandle";
export { SplitLayout } from "./components/SplitLayout";
export type { SplitLayoutLayout, SlotProps } from "./components/SplitLayout";
export { Table, TableBody, TableHead, TableRow, TableCell, TruncatedWideTableCell, TableHeaderCell } from "./components/Table"; export { Table, TableBody, TableHead, TableRow, TableCell, TruncatedWideTableCell, TableHeaderCell } from "./components/Table";
export { clamp } from "./lib/clamp";
export { useContainerSize } from "./hooks/useContainerSize";