[eric] dock: real macOS magnify (hovered tile grows, neighbors slide away, no pop-out) + round chat glyph everywhere

This commit is contained in:
ciregenz
2026-07-21 19:50:48 -07:00
parent 8d67e7c376
commit 582084996a
2 changed files with 41 additions and 10 deletions
@@ -4,7 +4,7 @@ import type { LucideIcon } from 'lucide-react';
import {
Timer, Clock, Calendar, ListChecks, Wallet, BarChart3, Megaphone, Code,
Terminal, Palette, PenLine, FileText, BookOpen, FlaskConical, Mail,
MessageSquare, Plane, Map, Dumbbell, HeartPulse, Utensils, ChefHat, Coffee,
MessageCircle, Plane, Map, Dumbbell, HeartPulse, Utensils, ChefHat, Coffee,
Music, Video, Image, Camera, ShoppingCart, Bot, Gamepad2, Home, Shield,
Users, Scale, Building2, Newspaper, Briefcase, Rocket, Globe, Database,
Wrench, Lightbulb, Target, Trophy, Bell, Folder, Package, Truck,
@@ -29,7 +29,7 @@ const KEYWORDS: Record<string, LucideIcon> = {
research: BookOpen, study: BookOpen, learning: BookOpen, course: BookOpen, education: BookOpen, school: BookOpen, exam: BookOpen, thesis: BookOpen,
science: FlaskConical, lab: FlaskConical, experiment: FlaskConical, chemistry: FlaskConical, biology: FlaskConical, physics: FlaskConical,
mail: Mail, email: Mail, inbox: Mail, outreach: Mail, newsletter: Mail,
chat: MessageSquare, message: MessageSquare, messaging: MessageSquare, support: MessageSquare, dm: MessageSquare,
chat: MessageCircle, message: MessageCircle, messaging: MessageCircle, support: MessageCircle, dm: MessageCircle,
travel: Plane, trip: Plane, flight: Plane, vacation: Plane, tour: Plane, itinerary: Plane,
map: Map, location: Map, geo: Map, route: Map, navigation: Map,
fitness: Dumbbell, workout: Dumbbell, gym: Dumbbell, exercise: Dumbbell, training: Dumbbell,
@@ -8,7 +8,7 @@ import EventRepeatIcon from '@mui/icons-material/EventRepeat';
import StickyNote2OutlinedIcon from '@mui/icons-material/StickyNote2Outlined';
import HistoryIcon from '@mui/icons-material/History';
import { pickIcon } from '../canvas/DashboardGlyph';
import { MessageSquare } from 'lucide-react';
import { MessageCircle } from 'lucide-react';
import { openWorkflowsApp } from '@/shared/state/dashboardLayoutSlice';
import SettingsIcon from '@mui/icons-material/Settings';
import AppsRoundedIcon from '@mui/icons-material/AppsRounded';
@@ -98,6 +98,37 @@ function DesktopDock({
onAddNote,
}: DesktopDockProps): React.ReactElement | null {
const dispatch = useAppDispatch();
const dockBodyRef = useRef<HTMLDivElement | null>(null);
// macOS Dock magnification: the tile under the cursor grows on a bell curve and its neighbors
// SLIDE AWAY to make room (no horizontal pop-out, the part that read as wobble). Each tile that
// grows by `extra` pushes every tile past it by extra/2, so the column opens up smoothly.
const applyDockMagnify = useCallback((clientY: number | null) => {
const root = dockBodyRef.current;
if (!root) return;
const tiles = Array.from(root.querySelectorAll<HTMLElement>('.osw-dock-tile'));
if (tiles.length === 0) return;
if (clientY == null) {
tiles.forEach((t) => { t.style.transform = ''; t.style.zIndex = ''; });
return;
}
const rootTop = root.getBoundingClientRect().top;
const cy = clientY - rootTop;
const BOOST = 0.5; // hovered tile grows to 1.5x
const FALLOFF = 44; // px spread of the bell curve (how many neighbors react)
const bases = tiles.map((t) => t.offsetTop + t.offsetHeight / 2);
const scales = bases.map((b) => 1 + BOOST * Math.exp(-(((cy - b) / FALLOFF) ** 2)));
const extra = scales.map((s) => TILE * (s - 1));
tiles.forEach((t, i) => {
let shift = 0;
for (let j = 0; j < tiles.length; j++) {
if (j === i) continue;
shift += (extra[j] / 2) * Math.sign(bases[i] - bases[j]);
}
t.style.transform = `translateY(${shift.toFixed(1)}px) scale(${scales[i].toFixed(3)})`;
t.style.transformOrigin = 'left center';
t.style.zIndex = String(10 + Math.round((scales[i] - 1) * 100));
});
}, []);
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);
@@ -108,7 +139,7 @@ function DesktopDock({
const session = sessions[card.session_id];
if (!session) continue;
const title = displayChatTitle(session);
const ChatIcon = pickIcon(title) || MessageSquare;
const ChatIcon = pickIcon(title) || MessageCircle;
list.push({
id: card.session_id,
label: title,
@@ -202,7 +233,9 @@ function DesktopDock({
return (
<Box
onMouseLeave={endHover}
ref={dockBodyRef}
onMouseMove={(e: React.MouseEvent) => applyDockMagnify(e.clientY)}
onMouseLeave={() => { endHover(); applyDockMagnify(null); }}
sx={{
position: 'absolute',
left: 12,
@@ -219,15 +252,13 @@ function DesktopDock({
backdropFilter: 'blur(20px) saturate(160%)',
WebkitBackdropFilter: 'blur(20px) saturate(160%)',
boxShadow: '0 8px 28px rgba(0,0,0,0.35)',
// One tasteful hover for every tile: a spring-y lift (replaces the fisheye that read as wobble),
// and every tile is a rounder squircle so they feel like real app icons, not lame little squares.
// Rounder squircle tiles (real app icons, not lame squares). The magnify transform is set
// imperatively per-frame by applyDockMagnify; a short ease smooths the chase + the reset.
'& .osw-dock-tile': {
borderRadius: '12px',
transition: 'transform 0.16s cubic-bezier(.34,1.56,.64,1)',
transition: 'transform 0.12s ease-out',
willChange: 'transform',
},
'& .osw-dock-tile:hover': { transform: 'scale(1.16)' },
'& .osw-dock-tile:active': { transform: 'scale(1.04)' },
}}
>
{entries.map((entry) => {