[eric] desktop: dock fisheye magnification (mac-style wheel) + minimized traffic lights fan out on a circular arc

This commit is contained in:
ciregenz
2026-07-20 23:35:13 -07:00
parent dd4d6f3faa
commit ea4e014803
4 changed files with 61 additions and 16 deletions
@@ -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<Props> = ({
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' } }}
>
<Box
className="osw-pill-lights osw-card"
onPointerDown={(e: React.PointerEvent) => 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',
}}
@@ -31,6 +31,25 @@ const dotSx = (color: string): Record<string, unknown> => ({
'& > 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<string, unknown> = {
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<number | null>(null);
@@ -98,6 +98,21 @@ function DesktopDock({
onAddNote,
}: DesktopDockProps): React.ReactElement | null {
const dispatch = useAppDispatch();
const dockBodyRef = useRef<HTMLDivElement | null>(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<HTMLElement>('.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<number | null>(null);
@@ -202,7 +217,9 @@ function DesktopDock({
return (
<Box
onMouseLeave={endHover}
ref={dockBodyRef}
onMouseMove={(e: React.MouseEvent) => 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({
<Box
onClick={a.act}
onMouseEnter={endHover}
className="osw-dock-tile"
sx={{
width: TILE,
height: TILE,
@@ -287,8 +307,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',
}}
>
{a.icon}
@@ -299,6 +320,7 @@ function DesktopDock({
<Box
onClick={() => 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',
}}
>
<SettingsIcon sx={{ fontSize: 18, color: '#e8e8ee' }} />
@@ -318,6 +341,7 @@ function DesktopDock({
<Box
onClick={onApplications}
onMouseEnter={endHover}
className="osw-dock-tile"
sx={{
width: TILE,
height: TILE,
@@ -328,8 +352,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',
}}
>
<AppsRoundedIcon sx={{ fontSize: 18, color: '#e8e8ee' }} />
@@ -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',
}}