import { useEffect, useRef, useState } from 'react'; import { Link, useLocation } from 'react-router'; import type { LucideIcon } from 'lucide-react'; export type DockItem = { label: string; to: string; color: string; // Either a lucide glyph (rendered white on the coloured tile) or an image asset (e.g. an app favicon). icon?: LucideIcon; image?: string; }; type DockProps = { items: DockItem[]; className?: string; // The content region the dock lives in. Reveal/hide is measured from ITS bottom edge, which the // in-flow music dock shrinks when present — so the dock clears the music dock with no magic offset. boundaryRef: React.RefObject; }; const ICON_SIZE = 48; const ICON_GAP = 24; const DOCK_PADDING = 12; const MAX_SCALE = 1.5; const MAX_DISTANCE = 150; const SHOW_THRESHOLD = 24; const HIDE_THRESHOLD = 100; const getScale = (mouseX: number | null, iconCenterX: number) => { if (mouseX === null) return 1; const distance = Math.abs(mouseX - iconCenterX); if (distance > MAX_DISTANCE) return 1; return 1 + (MAX_SCALE - 1) * Math.cos((distance / MAX_DISTANCE) * (Math.PI / 2)); }; export const Dock = ({ items, className, boundaryRef }: DockProps) => { const [mouseX, setMouseX] = useState(null); const [visible, setVisible] = useState(false); const dockRef = useRef(null); const location = useLocation(); const isActive = (to: string) => (to === '/' ? location.pathname === '/' : location.pathname.startsWith(to)); useEffect(() => { const handleMouseMove = (ev: MouseEvent) => { // Distance from the bottom of the content region (which the in-flow music dock shrinks when // present) — so reveal/hide triggers just above wherever the region ends, above the music dock. const bottom = boundaryRef.current?.getBoundingClientRect().bottom ?? window.innerHeight; const distFromBottom = bottom - ev.clientY; setVisible((prev) => (prev ? distFromBottom <= HIDE_THRESHOLD : distFromBottom <= SHOW_THRESHOLD)); if (distFromBottom <= HIDE_THRESHOLD) { const rect = dockRef.current?.getBoundingClientRect(); if (rect) setMouseX(ev.clientX - rect.left); } else { setMouseX(null); } }; document.addEventListener('mousemove', handleMouseMove); return () => document.removeEventListener('mousemove', handleMouseMove); }, [boundaryRef]); return (
{items.map((item, index) => { const iconCenter = DOCK_PADDING + index * (ICON_SIZE + ICON_GAP) + ICON_SIZE / 2; const scale = getScale(mouseX, iconCenter); const active = isActive(item.to); return ( {item.label}
{item.image ? ( ) : ( item.icon && )}
{active && (
)} ); })}
); }; import { Home, MessageCircle, FileText, FolderOpen, Code, LayoutGrid, ScrollText, FolderKanban, Monitor, Mail, Globe, MonitorSmartphone, Workflow, Music, Activity, Radio, Network, ArrowDownUp, Bitcoin, Receipt, Images, CalendarDays, Contact, Clapperboard, } from 'lucide-react'; export const ALL_DOCK_ITEMS: DockItem[] = [ { label: 'Home', to: '/', icon: Home, color: '#f59e0b' }, { label: 'Files', to: '/files', icon: FolderOpen, color: '#fbbf24' }, { label: 'Email', to: '/email', icon: Mail, color: '#ef4444' }, { label: 'Calendar', to: '/calendar', icon: CalendarDays, color: '#3b82f6' }, { label: 'Contacts', to: '/contacts', icon: Contact, color: '#0ea5e9' }, { label: 'Chat', to: '/chat', icon: MessageCircle, color: '#60a5fa' }, { label: 'Music', to: '/music', icon: Music, color: '#22c55e' }, { label: 'Photos', to: '/photos', icon: Images, color: '#10b981' }, { label: 'Video', to: '/jellyfin', icon: Clapperboard, color: '#a855f7' }, { label: 'Soulseek', to: '/soulseek', image: '/slskd.png', color: '#ffffff' }, { label: 'Headscale', to: '/headscale', icon: Network, color: '#818cf8' }, { label: 'Transmission', to: '/transmission', icon: ArrowDownUp, color: '#e11d48' }, { label: 'Wallet', to: '/wallet', icon: Bitcoin, color: '#f7931a' }, { label: 'Invoices', to: '/invoices', icon: Receipt, color: '#0891b2' }, { label: 'Editor', to: '/code-editor', icon: Code, color: '#a78bfa' }, { label: 'Plans', to: '/plans', icon: FileText, color: '#f472b6' }, { label: 'Jobs', to: '/jobs', icon: Workflow, color: '#14b8a6' }, { label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' }, { label: 'Terminal', to: '/terminal', icon: Monitor, color: '#f97316' }, { label: 'Browser', to: '/browser', icon: Globe, color: '#06b6d4' }, { label: 'Desktop', to: '/desktop', icon: MonitorSmartphone, color: '#ec4899' }, { label: 'Monitor', to: '/system-monitor', icon: Activity, color: '#0ea5e9' }, { label: 'Activity', to: '/activity', icon: Radio, color: '#f59e0b' }, { label: 'Dashboards', to: '/dashboards', icon: LayoutGrid, color: '#8b5cf6' }, ]; export const DEFAULT_DOCK_PATHS = ['/', '/files', '/music', '/dashboards', '/chat'];