mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-13 13:55:56 +01:00
Floating sidebar refactor
This commit is contained in:
@@ -1,28 +1,28 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
|
||||
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { CreateDropdown } from './CreateDropdown';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
|
||||
export function SidebarActions() {
|
||||
const floating = useShouldFloatSidebar();
|
||||
const [normalHidden, setNormalHidden] = useSidebarHidden();
|
||||
interface Props {
|
||||
floating?: boolean;
|
||||
}
|
||||
|
||||
export function SidebarActions({ floating = false }: Props) {
|
||||
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
|
||||
const [floatingHidden, setFloatingHidden] = useFloatingSidebarHidden();
|
||||
|
||||
const hidden = floating ? floatingHidden : normalHidden;
|
||||
const hidden = floating ? floatingHidden : sidebarHidden;
|
||||
const setHidden = useMemo(
|
||||
() => (floating ? setFloatingHidden : setNormalHidden),
|
||||
[floating, setFloatingHidden, setNormalHidden],
|
||||
() => (floating ? setFloatingHidden : setSidebarHidden),
|
||||
[floating, setFloatingHidden, setSidebarHidden],
|
||||
);
|
||||
|
||||
return (
|
||||
<HStack className="h-full">
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
// NOTE: We're not using the (h) => !h pattern here because the data
|
||||
// might be different if another window changed it (out of sync)
|
||||
await setHidden(!hidden);
|
||||
}}
|
||||
className="pointer-events-auto"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import { settingsAtom, workspacesAtom } from '@yaakapp-internal/models';
|
||||
import { HeaderSize, Overlay, SidebarLayout } from '@yaakapp-internal/ui';
|
||||
import { HeaderSize, SidebarLayout } from '@yaakapp-internal/ui';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import * as m from 'motion/react-m';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
useEnsureActiveCookieJar,
|
||||
useSubscribeActiveCookieJarId,
|
||||
@@ -24,7 +24,6 @@ import { useSubscribeRecentCookieJars } from '../hooks/useRecentCookieJars';
|
||||
import { useSubscribeRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
import { useSubscribeRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useSubscribeRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
||||
import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTitle';
|
||||
@@ -55,11 +54,11 @@ export function Workspace() {
|
||||
const workspaces = useAtomValue(workspacesAtom);
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const osType = type();
|
||||
const [width, setWidth, resetWidth] = useSidebarWidth();
|
||||
const [width, setWidth] = useSidebarWidth();
|
||||
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
|
||||
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
|
||||
const activeEnvironment = useAtomValue(activeEnvironmentAtom);
|
||||
const floating = useShouldFloatSidebar();
|
||||
const [floating, setFloating] = useState(false);
|
||||
|
||||
const environmentBgStyle = useMemo(() => {
|
||||
if (activeEnvironment?.color == null) return undefined;
|
||||
@@ -89,7 +88,7 @@ export function Workspace() {
|
||||
className="absolute left-0 right-0 -bottom-[1px] h-[1px] opacity-20"
|
||||
/>
|
||||
</div>
|
||||
<WorkspaceHeader className="pointer-events-none" />
|
||||
<WorkspaceHeader className="pointer-events-none" floatingSidebar={floating} />
|
||||
</HeaderSize>
|
||||
);
|
||||
|
||||
@@ -99,7 +98,30 @@ export function Workspace() {
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
const sidebarContent = (
|
||||
const sidebarContent = floating ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'x-theme-sidebar',
|
||||
'h-full bg-surface border-r border-border-subtle',
|
||||
'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 floating />
|
||||
</HeaderSize>
|
||||
<ErrorBoundary name="Sidebar (Floating)">
|
||||
<Sidebar />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
) : (
|
||||
<div className="x-theme-sidebar overflow-hidden bg-surface h-full">
|
||||
<ErrorBoundary name="Sidebar">
|
||||
<Sidebar className="border-r border-border-subtle" />
|
||||
@@ -110,52 +132,18 @@ export function Workspace() {
|
||||
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>
|
||||
)}
|
||||
<SidebarLayout
|
||||
width={width ?? 250}
|
||||
onWidthChange={setWidth}
|
||||
hidden={sidebarHidden ?? false}
|
||||
onHiddenChange={(hidden) => setSidebarHidden(hidden)}
|
||||
floatingHidden={floatingSidebarHidden ?? true}
|
||||
onFloatingHiddenChange={(hidden) => setFloatingSidebarHidden(hidden)}
|
||||
onFloatingChange={setFloating}
|
||||
sidebar={sidebarContent}
|
||||
>
|
||||
{workspaceBody}
|
||||
</SidebarLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
floatingSidebar?: boolean;
|
||||
}
|
||||
|
||||
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
||||
export const WorkspaceHeader = memo(function WorkspaceHeader({ className, floatingSidebar }: Props) {
|
||||
const togglePalette = useToggleCommandPalette();
|
||||
const [workspaceLayout, setWorkspaceLayout] = useAtom(workspaceLayoutAtom);
|
||||
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||
@@ -41,7 +42,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
)}
|
||||
>
|
||||
<HStack space={0.5} className={classNames('flex-1 pointer-events-none')}>
|
||||
<SidebarActions />
|
||||
<SidebarActions floating={floatingSidebar} />
|
||||
<CookieDropdown />
|
||||
<HStack className="min-w-0">
|
||||
<WorkspaceActionsDropdown />
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { useWindowSize } from 'react-use';
|
||||
|
||||
const WINDOW_FLOATING_SIDEBAR_WIDTH = 600;
|
||||
|
||||
export function useShouldFloatSidebar() {
|
||||
const windowSize = useWindowSize();
|
||||
return windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HeaderSize, SidebarLayout } from '@yaakapp-internal/ui';
|
||||
import { HeaderSize, IconButton, SidebarLayout } from '@yaakapp-internal/ui';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { useRpcQueryWithEvent } from '../hooks/useRpcQueryWithEvent';
|
||||
import { getOsType } from '../lib/tauri';
|
||||
@@ -12,8 +13,15 @@ export function ProxyLayout() {
|
||||
const os = getOsType();
|
||||
const exchanges = useAtomValue(filteredExchangesAtom);
|
||||
const [sidebarWidth, setSidebarWidth] = useLocalStorage('sidebar_width', 250);
|
||||
const [sidebarHidden, setSidebarHidden] = useLocalStorage('sidebar_hidden', false);
|
||||
const [floatingSidebarHidden, setFloatingSidebarHidden] = useLocalStorage(
|
||||
'floating_sidebar_hidden',
|
||||
true,
|
||||
);
|
||||
const [floating, setFloating] = useState(false);
|
||||
const { data: proxyState } = useRpcQueryWithEvent('get_proxy_state', {}, 'proxy_state_changed');
|
||||
const isRunning = proxyState?.state === 'running';
|
||||
const isHidden = floating ? (floatingSidebarHidden ?? true) : (sidebarHidden ?? false);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -30,7 +38,22 @@ export function ProxyLayout() {
|
||||
interfaceScale={1}
|
||||
className="x-theme-appHeader bg-surface"
|
||||
>
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto]">
|
||||
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto]">
|
||||
<div className="flex items-center pl-1">
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Toggle sidebar"
|
||||
icon={isHidden ? 'left_panel_hidden' : 'left_panel_visible'}
|
||||
iconColor="secondary"
|
||||
onClick={() => {
|
||||
if (floating) {
|
||||
setFloatingSidebarHidden(!floatingSidebarHidden);
|
||||
} else {
|
||||
setSidebarHidden(!sidebarHidden);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div data-tauri-drag-region className="flex items-center text-sm px-2">
|
||||
Yaak Proxy
|
||||
</div>
|
||||
@@ -61,7 +84,43 @@ export function ProxyLayout() {
|
||||
<SidebarLayout
|
||||
width={sidebarWidth ?? 250}
|
||||
onWidthChange={setSidebarWidth}
|
||||
sidebar={<Sidebar />}
|
||||
hidden={sidebarHidden ?? false}
|
||||
onHiddenChange={setSidebarHidden}
|
||||
floatingHidden={floatingSidebarHidden ?? true}
|
||||
onFloatingHiddenChange={setFloatingSidebarHidden}
|
||||
onFloatingChange={setFloating}
|
||||
sidebar={
|
||||
floating ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'x-theme-sidebar',
|
||||
'h-full bg-surface border-r border-border-subtle',
|
||||
'grid grid-rows-[auto_1fr]',
|
||||
)}
|
||||
>
|
||||
<HeaderSize
|
||||
hideControls
|
||||
size="lg"
|
||||
className="border-transparent flex items-center pl-1"
|
||||
osType={os}
|
||||
hideWindowControls={false}
|
||||
useNativeTitlebar={false}
|
||||
interfaceScale={1}
|
||||
>
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Toggle sidebar"
|
||||
icon="left_panel_visible"
|
||||
iconColor="secondary"
|
||||
onClick={() => setFloatingSidebarHidden(true)}
|
||||
/>
|
||||
</HeaderSize>
|
||||
<Sidebar />
|
||||
</div>
|
||||
) : (
|
||||
<Sidebar />
|
||||
)
|
||||
}
|
||||
>
|
||||
<ExchangesTable exchanges={exchanges} className="overflow-auto h-full" />
|
||||
</SidebarLayout>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { createStore, Provider } from 'jotai';
|
||||
import { LazyMotion, MotionConfig } from 'motion/react';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { ProxyLayout } from './components/ProxyLayout';
|
||||
@@ -26,11 +27,17 @@ listen('model_write', (payload) => {
|
||||
);
|
||||
});
|
||||
|
||||
const motionFeatures = () => import('framer-motion').then((mod) => mod.domAnimation);
|
||||
|
||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={jotaiStore}>
|
||||
<ProxyLayout />
|
||||
<LazyMotion strict features={motionFeatures}>
|
||||
<MotionConfig transition={{ duration: 0.1 }}>
|
||||
<ProxyLayout />
|
||||
</MotionConfig>
|
||||
</LazyMotion>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@yaakapp-internal/ui": "^1.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"jotai": "^2.18.0",
|
||||
"motion": "^12.4.7",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -236,6 +236,7 @@
|
||||
"@yaakapp-internal/ui": "^1.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"jotai": "^2.18.0",
|
||||
"motion": "^12.4.7",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import classNames from 'classnames';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useContainerSize } from '../hooks/useContainerSize';
|
||||
import { Overlay } from './Overlay';
|
||||
import type { ResizeHandleEvent } from './ResizeHandle';
|
||||
import { ResizeHandle } from './ResizeHandle';
|
||||
|
||||
const FLOATING_BREAKPOINT = 600;
|
||||
|
||||
const side = { gridArea: 'side', minWidth: 0 };
|
||||
const drag = { gridArea: 'drag' };
|
||||
const body = { gridArea: 'body', minWidth: 0 };
|
||||
@@ -13,7 +18,9 @@ interface Props {
|
||||
onWidthChange: (width: number) => void;
|
||||
hidden?: boolean;
|
||||
onHiddenChange?: (hidden: boolean) => void;
|
||||
floating?: boolean;
|
||||
floatingHidden?: boolean;
|
||||
onFloatingHiddenChange?: (hidden: boolean) => void;
|
||||
onFloatingChange?: (floating: boolean) => void;
|
||||
floatingWidth?: number;
|
||||
defaultWidth?: number;
|
||||
minWidth?: number;
|
||||
@@ -27,7 +34,9 @@ export function SidebarLayout({
|
||||
onWidthChange,
|
||||
hidden = false,
|
||||
onHiddenChange,
|
||||
floating = false,
|
||||
floatingHidden = true,
|
||||
onFloatingHiddenChange,
|
||||
onFloatingChange,
|
||||
floatingWidth = 320,
|
||||
defaultWidth = 250,
|
||||
minWidth = 50,
|
||||
@@ -35,6 +44,14 @@ export function SidebarLayout({
|
||||
sidebar,
|
||||
children,
|
||||
}: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const containerSize = useContainerSize(containerRef);
|
||||
const floating = containerSize.width > 0 && containerSize.width <= FLOATING_BREAKPOINT;
|
||||
|
||||
useEffect(() => {
|
||||
onFloatingChange?.(floating);
|
||||
}, [floating]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const startWidth = useRef<number | null>(null);
|
||||
|
||||
@@ -81,34 +98,32 @@ export function SidebarLayout({
|
||||
|
||||
if (floating) {
|
||||
return (
|
||||
<div className={classNames(className, 'relative w-full h-full overflow-hidden')}>
|
||||
<div ref={containerRef} className={classNames(className, 'w-full h-full min-h-0')}>
|
||||
<Overlay
|
||||
open={!floatingHidden}
|
||||
portalName="sidebar"
|
||||
onClose={() => onFloatingHiddenChange?.(true)}
|
||||
zIndex={20}
|
||||
>
|
||||
<m.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
style={{ width: floatingWidth }}
|
||||
className="absolute top-0 left-0 bottom-0"
|
||||
>
|
||||
{sidebar}
|
||||
</m.div>
|
||||
</Overlay>
|
||||
{children}
|
||||
{!hidden && (
|
||||
<>
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 z-20 transition-opacity"
|
||||
onClick={() => onHiddenChange?.(true)}
|
||||
/>
|
||||
<div
|
||||
style={{ width: floatingWidth }}
|
||||
className="absolute top-0 left-0 bottom-0 z-20 animate-slide-in-left"
|
||||
>
|
||||
{sidebar}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={styles}
|
||||
className={classNames(
|
||||
className,
|
||||
'grid w-full h-full',
|
||||
!isResizing && 'transition-grid',
|
||||
)}
|
||||
className={classNames(className, 'grid w-full h-full', !isResizing && 'transition-grid')}
|
||||
>
|
||||
<div style={side} className="overflow-hidden">
|
||||
{sidebar}
|
||||
|
||||
Reference in New Issue
Block a user