From f7ff964fe5d16277f5ebecfb748e9c1de3fef07c Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 12 Mar 2026 15:12:49 -0700 Subject: [PATCH] Floating sidebar refactor --- .../yaak-client/components/SidebarActions.tsx | 18 ++-- apps/yaak-client/components/Workspace.tsx | 94 ++++++++----------- .../components/WorkspaceHeader.tsx | 5 +- .../hooks/useShouldFloatSidebar.ts | 8 -- apps/yaak-proxy/components/ProxyLayout.tsx | 65 ++++++++++++- apps/yaak-proxy/main.tsx | 9 +- apps/yaak-proxy/package.json | 1 + package-lock.json | 1 + packages/ui/src/components/SidebarLayout.tsx | 61 +++++++----- 9 files changed, 163 insertions(+), 99 deletions(-) delete mode 100644 apps/yaak-client/hooks/useShouldFloatSidebar.ts diff --git a/apps/yaak-client/components/SidebarActions.tsx b/apps/yaak-client/components/SidebarActions.tsx index 914996c4..58fec491 100644 --- a/apps/yaak-client/components/SidebarActions.tsx +++ b/apps/yaak-client/components/SidebarActions.tsx @@ -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 ( { - // 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" diff --git a/apps/yaak-client/components/Workspace.tsx b/apps/yaak-client/components/Workspace.tsx index ce0f62f1..a2e7dfa9 100644 --- a/apps/yaak-client/components/Workspace.tsx +++ b/apps/yaak-client/components/Workspace.tsx @@ -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" /> - + ); @@ -99,7 +98,30 @@ export function Workspace() { ); - const sidebarContent = ( + const sidebarContent = floating ? ( +
+ + + + + + +
+ ) : (
@@ -110,52 +132,18 @@ export function Workspace() { return (
{header} - {floating ? ( - <> - setFloatingSidebarHidden(true)} - zIndex={20} - > - - - - - - - - - - {workspaceBody} - - ) : ( - - )} +
); } diff --git a/apps/yaak-client/components/WorkspaceHeader.tsx b/apps/yaak-client/components/WorkspaceHeader.tsx index 79d98d72..8ef8e30a 100644 --- a/apps/yaak-client/components/WorkspaceHeader.tsx +++ b/apps/yaak-client/components/WorkspaceHeader.tsx @@ -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 )} > - + diff --git a/apps/yaak-client/hooks/useShouldFloatSidebar.ts b/apps/yaak-client/hooks/useShouldFloatSidebar.ts deleted file mode 100644 index 8e8159d6..00000000 --- a/apps/yaak-client/hooks/useShouldFloatSidebar.ts +++ /dev/null @@ -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; -} diff --git a/apps/yaak-proxy/components/ProxyLayout.tsx b/apps/yaak-proxy/components/ProxyLayout.tsx index ac0612a1..96b6c90d 100644 --- a/apps/yaak-proxy/components/ProxyLayout.tsx +++ b/apps/yaak-proxy/components/ProxyLayout.tsx @@ -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 (
-
+
+
+ { + if (floating) { + setFloatingSidebarHidden(!floatingSidebarHidden); + } else { + setSidebarHidden(!sidebarHidden); + } + }} + /> +
Yaak Proxy
@@ -61,7 +84,43 @@ export function ProxyLayout() { } + hidden={sidebarHidden ?? false} + onHiddenChange={setSidebarHidden} + floatingHidden={floatingSidebarHidden ?? true} + onFloatingHiddenChange={setFloatingSidebarHidden} + onFloatingChange={setFloating} + sidebar={ + floating ? ( +
+ + setFloatingSidebarHidden(true)} + /> + + +
+ ) : ( + + ) + } >
diff --git a/apps/yaak-proxy/main.tsx b/apps/yaak-proxy/main.tsx index a5f2992d..60dd42e1 100644 --- a/apps/yaak-proxy/main.tsx +++ b/apps/yaak-proxy/main.tsx @@ -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( - + + + + + , diff --git a/apps/yaak-proxy/package.json b/apps/yaak-proxy/package.json index 2daad042..bc6268bf 100644 --- a/apps/yaak-proxy/package.json +++ b/apps/yaak-proxy/package.json @@ -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" }, diff --git a/package-lock.json b/package-lock.json index a3235a71..96542ad1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/packages/ui/src/components/SidebarLayout.tsx b/packages/ui/src/components/SidebarLayout.tsx index af4ad475..014e80d7 100644 --- a/packages/ui/src/components/SidebarLayout.tsx +++ b/packages/ui/src/components/SidebarLayout.tsx @@ -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(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(null); @@ -81,34 +98,32 @@ export function SidebarLayout({ if (floating) { return ( -
+
+ onFloatingHiddenChange?.(true)} + zIndex={20} + > + + {sidebar} + + {children} - {!hidden && ( - <> -
onHiddenChange?.(true)} - /> -
- {sidebar} -
- - )}
); } return (
{sidebar}