From ea4e01480307acc8f94149a2eec5afa8321e88eb Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 20 Jul 2026 23:35:13 -0700 Subject: [PATCH] [eric] desktop: dock fisheye magnification (mac-style wheel) + minimized traffic lights fan out on a circular arc --- .../app/pages/Dashboard/cards/AgentCard.tsx | 7 +-- .../pages/Dashboard/cards/WindowControls.tsx | 19 ++++++++ .../pages/Dashboard/desktop/DesktopDock.tsx | 43 +++++++++++++++---- .../Dashboard/desktop/MinimizedStack.tsx | 8 ++-- 4 files changed, 61 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/pages/Dashboard/cards/AgentCard.tsx b/frontend/src/app/pages/Dashboard/cards/AgentCard.tsx index 959dee61..1e12171a 100644 --- a/frontend/src/app/pages/Dashboard/cards/AgentCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/AgentCard.tsx @@ -35,7 +35,7 @@ import { setTiledCard, clearTiledCard, } from '@/shared/state/dashboardLayoutSlice'; -import WindowControls from './WindowControls'; +import WindowControls, { ARC_CHIP_SX } from './WindowControls'; import { useTiledStyle } from './tileZones'; import AgentNarratorPill from '../desktop/AgentNarratorPill'; import { extractLatestTodos } from '../desktop/agentTodos'; @@ -982,14 +982,15 @@ const AgentCard: React.FC = ({ onPointerDown={handleDragPointerDown} onPointerMove={handleDragPointerMove} onPointerUp={handleDragPointerUp} + className="osw-pill-host" sx={{ position: 'relative', touchAction: 'none', userSelect: 'none', pt: '26px', mt: '-26px', '&:hover .osw-pill-lights': { opacity: 1, pointerEvents: 'auto' } }} > e.stopPropagation()} sx={{ - position: 'absolute', top: 0, left: 4, zIndex: 2, display: 'flex', alignItems: 'center', - px: 1, py: 0.5, borderRadius: 999, background: 'rgba(24,14,32,0.85)', + ...ARC_CHIP_SX, + position: 'absolute', top: -8, left: 4, zIndex: 2, background: 'rgba(24,14,32,0.85)', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)', opacity: 0, pointerEvents: 'none', transition: 'opacity 140ms ease', }} diff --git a/frontend/src/app/pages/Dashboard/cards/WindowControls.tsx b/frontend/src/app/pages/Dashboard/cards/WindowControls.tsx index dd5f9599..392c7b29 100644 --- a/frontend/src/app/pages/Dashboard/cards/WindowControls.tsx +++ b/frontend/src/app/pages/Dashboard/cards/WindowControls.tsx @@ -31,6 +31,25 @@ const dotSx = (color: string): Record => ({ '& > span': { fontSize: 9, fontWeight: 800, lineHeight: 1, color: 'rgba(0,0,0,0.5)', opacity: 0, transition: 'opacity 120ms', pointerEvents: 'none' }, }); +// Circular chip form for minimized pills/thumbnails: the three dots collapse to a point and fan out +// along an arc on hover (OptionWheel / macOS-dock energy), instead of a flat row in a lozenge. +export const ARC_CHIP_SX: Record = { + width: 40, + height: 40, + borderRadius: 999, + '& .osw-window-lights': { position: 'relative', width: '100%', height: '100%', display: 'block' }, + '& .osw-window-lights > *': { + position: 'absolute', left: '50%', top: '50%', + transform: 'translate(-50%, -50%) scale(0.35)', opacity: 0, + transition: 'transform 190ms cubic-bezier(.3,.9,.3,1), opacity 150ms ease', + }, + // red / yellow / green land at 150deg / 90deg / 30deg on a 12px arc over the chip's crown. + // Keyed off the HOST (whole pill/thumb) hover, so grazing any part of it fans the dots out. + '.osw-pill-host:hover & .osw-window-lights > :nth-of-type(1)': { transform: 'translate(calc(-50% - 11px), calc(-50% + 5px)) scale(1)', opacity: 1 }, + '.osw-pill-host:hover & .osw-window-lights > :nth-of-type(2)': { transform: 'translate(-50%, calc(-50% - 7px)) scale(1)', opacity: 1, transitionDelay: '40ms' }, + '.osw-pill-host:hover & .osw-window-lights > :nth-of-type(3)': { transform: 'translate(calc(-50% + 11px), calc(-50% + 5px)) scale(1)', opacity: 1, transitionDelay: '80ms' }, +}; + function WindowControls({ onClose, onMinimize, onTile, tiled }: WindowControlsProps): React.ReactElement { const [menuOpen, setMenuOpen] = useState(false); const closeTimer = useRef(null); diff --git a/frontend/src/app/pages/Dashboard/desktop/DesktopDock.tsx b/frontend/src/app/pages/Dashboard/desktop/DesktopDock.tsx index 1e93a9bf..fdcc6ee8 100644 --- a/frontend/src/app/pages/Dashboard/desktop/DesktopDock.tsx +++ b/frontend/src/app/pages/Dashboard/desktop/DesktopDock.tsx @@ -98,6 +98,21 @@ function DesktopDock({ onAddNote, }: DesktopDockProps): React.ReactElement | null { const dispatch = useAppDispatch(); + const dockBodyRef = useRef(null); + // macOS-dock fisheye: tiles swell as the cursor nears and neighbors fall off along the curve, + // with a slight outward bulge so the column reads as a wheel (OptionWheel-style), not a ruler. + const applyFisheye = useCallback((clientY: number | null) => { + const root = dockBodyRef.current; + if (!root) return; + const rootTop = root.getBoundingClientRect().top; + root.querySelectorAll('.osw-dock-tile').forEach((el) => { + if (clientY == null) { el.style.transform = ''; return; } + const cy = rootTop + el.offsetTop + el.offsetHeight / 2; + const t = Math.max(0, 1 - Math.abs(clientY - cy) / 90); + const emph = t * t; + el.style.transform = `translateX(${(9 * emph).toFixed(1)}px) scale(${(1 + 0.5 * emph).toFixed(3)})`; + }); + }, []); const [hovered, setHovered] = useState<{ id: string; top: number } | null>(null); const [liveShot, setLiveShot] = useState<{ id: string; dataUrl: string } | null>(null); const hoverTimer = useRef(null); @@ -202,7 +217,9 @@ function DesktopDock({ return ( applyFisheye(e.clientY)} + onMouseLeave={() => { endHover(); applyFisheye(null); }} sx={{ position: 'absolute', left: 12, @@ -231,6 +248,7 @@ function DesktopDock({ endHover(); onFocusCard(entry.id, entry.rect); }} + className="osw-dock-tile" sx={{ position: 'relative', width: TILE, @@ -243,8 +261,9 @@ function DesktopDock({ cursor: 'pointer', overflow: 'hidden', flexShrink: 0, - transition: 'transform 0.15s ease', - '&:hover': { transform: 'scale(1.12)' }, + transition: 'transform 0.12s ease-out', + willChange: 'transform', + transformOrigin: 'left center', ...(isActive && { outline: '2px solid #6aa2ff', outlineOffset: '2px' }), }} > @@ -277,6 +296,7 @@ function DesktopDock({ {a.icon} @@ -299,6 +320,7 @@ function DesktopDock({ dispatch(openSettingsModal(undefined))} onMouseEnter={endHover} + className="osw-dock-tile" sx={{ width: TILE, height: TILE, @@ -309,8 +331,9 @@ function DesktopDock({ justifyContent: 'center', cursor: 'pointer', flexShrink: 0, - transition: 'transform 0.15s ease', - '&:hover': { transform: 'scale(1.12)' }, + transition: 'transform 0.12s ease-out', + willChange: 'transform', + transformOrigin: 'left center', }} > @@ -318,6 +341,7 @@ function DesktopDock({ diff --git a/frontend/src/app/pages/Dashboard/desktop/MinimizedStack.tsx b/frontend/src/app/pages/Dashboard/desktop/MinimizedStack.tsx index 18789319..4b0a675f 100644 --- a/frontend/src/app/pages/Dashboard/desktop/MinimizedStack.tsx +++ b/frontend/src/app/pages/Dashboard/desktop/MinimizedStack.tsx @@ -5,7 +5,7 @@ import LanguageIcon from '@mui/icons-material/Language'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { toggleMinimizeCard, setTiledCard, recordClosedCard } from '@/shared/state/dashboardLayoutSlice'; import { removeBrowserCardCleanly } from '@/shared/browserTeardown'; -import WindowControls from '../cards/WindowControls'; +import WindowControls, { ARC_CHIP_SX } from '../cards/WindowControls'; import { getMinimizedShot, dropMinimizedShot } from './minimizedShots'; import type { BrowserCardPosition } from '@/shared/state/dashboardLayoutSlice'; @@ -56,7 +56,7 @@ function MinimizedStack({ browserCards, onRestore }: MinimizedStackProps): React key={bc.browser_id} onClick={restore} title={activeTab?.title || 'Browser'} - className="osw-card" + className="osw-card osw-pill-host" sx={{ position: 'relative', width: THUMB_W, @@ -73,8 +73,8 @@ function MinimizedStack({ browserCards, onRestore }: MinimizedStackProps): React className="osw-pill-lights" onClick={(e: React.MouseEvent) => e.stopPropagation()} sx={{ - position: 'absolute', top: 3, left: 3, zIndex: 2, display: 'flex', alignItems: 'center', - px: 1, py: 0.5, borderRadius: 999, background: 'rgba(24,14,32,0.85)', + ...ARC_CHIP_SX, + position: 'absolute', top: 2, left: 2, zIndex: 2, background: 'rgba(24,14,32,0.85)', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)', opacity: 0, pointerEvents: 'none', transition: 'opacity 140ms ease', }}