From 2733a0025353cadc8ef0ab6b1e265a87fade855f Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Wed, 4 Feb 2026 21:39:05 -0500 Subject: [PATCH] feat: refactor scratchpad to floating overlay with global thin scrollbars Refactor scratchpad from a flex-layout sidebar that pushes content left to a floating overlay panel (position: fixed) that doesn't affect layout. Panel slides in from the right with Ctrl+/ toggle. Main content adjusts padding responsively when panel is open. Also apply thin scrollbar styling globally across all scrollable elements for a consistent, minimal look. Co-Authored-By: Claude Opus 4.5 --- .../2026-02-04-scratchpad-overlay-design.md | 63 ++++++ .../components/session/ScratchpadSidebar.tsx | 209 ++++++++++-------- frontend/src/index.css | 16 ++ frontend/src/pages/TreeNavigationPage.tsx | 10 +- 4 files changed, 205 insertions(+), 93 deletions(-) create mode 100644 docs/plans/2026-02-04-scratchpad-overlay-design.md diff --git a/docs/plans/2026-02-04-scratchpad-overlay-design.md b/docs/plans/2026-02-04-scratchpad-overlay-design.md new file mode 100644 index 00000000..f554484f --- /dev/null +++ b/docs/plans/2026-02-04-scratchpad-overlay-design.md @@ -0,0 +1,63 @@ +# Scratchpad Floating Overlay Design + +> **Date:** 2026-02-04 +> **Status:** Approved + +--- + +## Problem + +The scratchpad is a full-height sidebar that pushes main content left when open, reducing the available width for tree navigation. On smaller screens this is particularly disruptive. It should be a floating overlay that doesn't affect layout. + +## Design + +### Component: ScratchpadSidebar.tsx + +Single component with two visual states, both using `position: fixed`. + +**Closed — floating button:** +- Fixed to viewport right edge, vertically centered +- `right: 0; top: 50%; transform: translateY(-50%); z-index: 40` +- Rounded left corners, flat right edge (`rounded-l-md`) +- StickyNote icon +- Amber dot (absolute positioned) when unsaved changes exist + +**Open — overlay panel:** +- Fixed to viewport right edge, vertically centered +- `right: 0; top: 50%; transform: translateY(-50%); z-index: 40` +- Width: 420px, height: 55vh +- Rounded left corners, flat right edge (`rounded-l-lg`) +- Shadow (`shadow-xl`) and left border for depth +- Slide-in animation: `translateX(100%)` → `translateX(0)`, 200ms ease-out +- No backdrop — clicking outside does NOT close it + +**Panel contents (unchanged):** +- Header: title, preview toggle (Eye/Pencil), close button (X) +- Body: textarea or markdown preview, scrollable +- Footer: save status indicator + +### Keyboard Shortcut + +`Ctrl+/` toggles open/closed. Handled via `useEffect` keydown listener inside the component. + +### TreeNavigationPage.tsx + +- Outer wrapper no longer uses `flex` for sidebar layout +- Main content takes full width +- ScratchpadSidebar renders in same DOM position (fixed positioning makes it layout-independent) + +### Preserved Behavior + +- Auto-save with 1000ms debounce +- Markdown preview toggle +- Save status indicators (Saving.../Saved/Unsaved changes) +- localStorage persistence for open/closed state (key: `scratchpad-collapsed`) +- beforeunload warning for unsaved changes +- Session ID reset on navigation + +## Files Changed + +| File | Change | +|------|--------| +| `frontend/src/components/session/ScratchpadSidebar.tsx` | Refactor to fixed-position floating overlay | +| `frontend/src/pages/TreeNavigationPage.tsx` | Remove flex sidebar layout | diff --git a/frontend/src/components/session/ScratchpadSidebar.tsx b/frontend/src/components/session/ScratchpadSidebar.tsx index 951453af..13270e92 100644 --- a/frontend/src/components/session/ScratchpadSidebar.tsx +++ b/frontend/src/components/session/ScratchpadSidebar.tsx @@ -1,19 +1,20 @@ import { useState, useEffect, useRef, useCallback } from 'react' import { cn } from '@/lib/utils' import { MarkdownContent } from '@/components/ui/MarkdownContent' -import { StickyNote, ChevronRight, Eye, Pencil, Loader2 } from 'lucide-react' +import { StickyNote, X, Eye, Pencil, Loader2 } from 'lucide-react' interface ScratchpadSidebarProps { sessionId: string initialContent: string onSave: (content: string) => Promise + onOpenChange?: (isOpen: boolean) => void } -export function ScratchpadSidebar({ sessionId, initialContent, onSave }: ScratchpadSidebarProps) { +export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenChange }: ScratchpadSidebarProps) { const [content, setContent] = useState(initialContent) const [lastSaved, setLastSaved] = useState(initialContent) const [isCollapsed, setIsCollapsed] = useState(() => { - return localStorage.getItem('scratchpad-collapsed') === 'true' + return localStorage.getItem('scratchpad-collapsed') !== 'false' }) const [isSaving, setIsSaving] = useState(false) const [showPreview, setShowPreview] = useState(false) @@ -40,10 +41,23 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave }: Scratch } }, [isSaving, hasUnsavedChanges]) - // Persist collapse state + // Persist collapse state and notify parent useEffect(() => { localStorage.setItem('scratchpad-collapsed', String(isCollapsed)) - }, [isCollapsed]) + onOpenChange?.(!isCollapsed) + }, [isCollapsed, onOpenChange]) + + // Keyboard shortcut: Ctrl+/ to toggle + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.ctrlKey && e.key === '/') { + e.preventDefault() + setIsCollapsed(prev => !prev) + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }, []) // beforeunload warning useEffect(() => { @@ -103,97 +117,110 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave }: Scratch } }, []) - if (isCollapsed) { - return ( -
- - {hasUnsavedChanges && ( -
- )} -
- ) - } - return ( -
- {/* Header */} -
-
- - Scratchpad -
-
- - -
-
+ <> + {/* Floating button (visible when collapsed) */} + - {/* Content */} -
- {showPreview ? ( -
- {content.trim() ? ( - - ) : ( -

Nothing to preview

+ {/* Panel overlay */} +
+ {/* Header */} +
+
+ + Scratchpad + Ctrl+/ +
+
+ + +
+
+ + {/* Content */} +
+ {showPreview ? ( +
+ {content.trim() ? ( + + ) : ( +

Nothing to preview

+ )} +
+ ) : ( +