From c41a2e38c9d0bb8501ae470b3b5f20c610562749 Mon Sep 17 00:00:00 2001 From: moh Date: Sun, 20 Sep 2026 20:38:35 +0200 Subject: [PATCH] IMPROVED - Make dock feel like the real macOS dock - Stronger magnification (50 to 92) with snappier spring tracking. - macOS-style launch bounce on click; running-dot indicator for the active page (works with fixed app-icon images, no recolor). - Per-item icon paths in navItems so real macOS app icons can be dropped in. - Drop the now-unused -active icon variants. --- components/layout/site-dock.tsx | 93 ++++++++++++++++++++------------ components/ui/dock.tsx | 6 ++- public/dock/about-active.svg | 12 ----- public/dock/contact-active.svg | 12 ----- public/dock/portfolio-active.svg | 13 ----- public/dock/products-active.svg | 12 ----- 6 files changed, 64 insertions(+), 84 deletions(-) delete mode 100644 public/dock/about-active.svg delete mode 100644 public/dock/contact-active.svg delete mode 100644 public/dock/portfolio-active.svg delete mode 100644 public/dock/products-active.svg diff --git a/components/layout/site-dock.tsx b/components/layout/site-dock.tsx index f256900..a695806 100644 --- a/components/layout/site-dock.tsx +++ b/components/layout/site-dock.tsx @@ -1,9 +1,11 @@ "use client"; +import { motion, useReducedMotion } from "framer-motion"; import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useLocale, useTranslations } from "next-intl"; +import { useState } from "react"; import { FloatingControls } from "@/components/layout/floating-controls"; import { Dock, DockIcon } from "@/components/ui/dock"; @@ -26,15 +28,17 @@ type NavKey = "about" | "portfolio" | "products" | "contact"; type NavItem = { key: NavKey; path: string; + /** Icon file under /public. Swap these for real macOS app icons anytime. */ + icon: string; disabled?: boolean; }; // "Home" is intentionally omitted — the logo (first dock icon) is the home link. const navItems: NavItem[] = [ - { key: "about", path: "/about" }, - { key: "portfolio", path: "/portfolio" }, - { key: "products", path: "/products", disabled: true }, - { key: "contact", path: "/contact" }, + { key: "about", path: "/about", icon: "/dock/about.svg" }, + { key: "portfolio", path: "/portfolio", icon: "/dock/portfolio.svg" }, + { key: "products", path: "/products", icon: "/dock/products.svg", disabled: true }, + { key: "contact", path: "/contact", icon: "/dock/contact.svg" }, ]; function isNavItemActive(currentPath: string, itemPath: string) { @@ -48,23 +52,36 @@ export function SiteDock({ defaultLocale, isSuperAdmin = false }: SiteDockProps) const locale = useLocale(); const pathname = usePathname(); const t = useTranslations("navigation"); + const reducedMotion = useReducedMotion(); + const [bouncing, setBouncing] = useState(null); const currentPath = stripLocalePrefix(pathname); const homeHref = getLocalizedPath(locale, "/", defaultLocale); + // macOS-style launch hop on click. + const bounceProps = (id: string) => + reducedMotion + ? {} + : { + onClick: () => setBouncing(id), + animate: bouncing === id ? { y: [0, -18, 0, -6, 0] } : { y: 0 }, + transition: { duration: 0.5, ease: "easeOut" as const }, + onAnimationComplete: () => setBouncing((cur) => (cur === id ? null : cur)), + }; + return ( <> - {/* ===== Bottom dock (Magic UI) — primary navigation ===== */} + {/* ===== Bottom dock — primary navigation ===== */}
{/* ── Logo (fixed src → no refresh flash) ── */} @@ -73,15 +90,17 @@ export function SiteDock({ defaultLocale, isSuperAdmin = false }: SiteDockProps) aria-label="mohfarawati — Home" className="flex h-full w-full items-center justify-center rounded-[inherit]" > - mohfarawati + + mohfarawati + @@ -89,32 +108,31 @@ export function SiteDock({ defaultLocale, isSuperAdmin = false }: SiteDockProps)
{/* ── Pages ── */} - {navItems.map(({ key, path, disabled }) => { + {navItems.map(({ key, path, icon: iconSrc, disabled }) => { const itemPath = path || "/"; const isActive = isNavItemActive(currentPath, itemPath); const label = t(key); - const src = `/dock/${key}${isActive ? "-active" : ""}.svg`; const icon = ( - {label} + + {label} + ); return ( {disabled ? ( )} + + {/* macOS running indicator */} + {isActive ? ( + + ) : null} + ); diff --git a/components/ui/dock.tsx b/components/ui/dock.tsx index 7dd36d6..3a4cf2f 100644 --- a/components/ui/dock.tsx +++ b/components/ui/dock.tsx @@ -123,10 +123,12 @@ const DockIcon = ({ [size, targetSize, size], ); + // Snappy, near-instant tracking with a touch of smoothing — closer to how the + // real macOS dock follows the cursor than a soft/bouncy spring. const scaleSize = useSpring(sizeTransform, { mass: 0.1, - stiffness: 150, - damping: 12, + stiffness: 280, + damping: 22, }); return ( diff --git a/public/dock/about-active.svg b/public/dock/about-active.svg deleted file mode 100644 index bdda326..0000000 --- a/public/dock/about-active.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/public/dock/contact-active.svg b/public/dock/contact-active.svg deleted file mode 100644 index 14b191e..0000000 --- a/public/dock/contact-active.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/public/dock/portfolio-active.svg b/public/dock/portfolio-active.svg deleted file mode 100644 index 20e00c9..0000000 --- a/public/dock/portfolio-active.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/dock/products-active.svg b/public/dock/products-active.svg deleted file mode 100644 index 5acf486..0000000 --- a/public/dock/products-active.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file