mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-22 20:52:23 +02:00
[eric] desktop: dock fisheye magnification (mac-style wheel) + minimized traffic lights fan out on a circular arc
This commit is contained in:
@@ -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',
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user