Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b513551ca | ||
|
|
57423b1baf | ||
|
|
c41a2e38c9 | ||
|
|
9fd928cce6 | ||
|
|
1e211c1201 | ||
|
|
2fc0641512 |
@@ -5,18 +5,11 @@ import { Container } from "@/components/layout/container";
|
|||||||
import { HomeHero } from "@/components/layout/home-hero";
|
import { HomeHero } from "@/components/layout/home-hero";
|
||||||
import { MotionFade } from "@/components/motion-fade";
|
import { MotionFade } from "@/components/motion-fade";
|
||||||
import { MagicBentoSection } from "@/components/home/magic-bento-section";
|
import { MagicBentoSection } from "@/components/home/magic-bento-section";
|
||||||
import { CapabilitiesSection } from "@/components/home/capabilities-section";
|
|
||||||
import { ContactCtaSection } from "@/components/home/contact-cta-section";
|
import { ContactCtaSection } from "@/components/home/contact-cta-section";
|
||||||
import { MarqueeSection } from "@/components/home/marquee-section";
|
import { MarqueeSection } from "@/components/home/marquee-section";
|
||||||
import { ProcessSection } from "@/components/home/process-section";
|
|
||||||
import { ProjectsSection } from "@/components/home/projects-section";
|
|
||||||
import type {
|
import type {
|
||||||
CapabilitiesSectionCopy,
|
|
||||||
ContactCtaSectionCopy,
|
ContactCtaSectionCopy,
|
||||||
MarqueeSectionCopy,
|
MarqueeSectionCopy,
|
||||||
ProcessSectionCopy,
|
|
||||||
ProjectCardCopy,
|
|
||||||
ProjectsSectionCopy,
|
|
||||||
} from "@/components/home/types";
|
} from "@/components/home/types";
|
||||||
import { buildLocalizedMetadata } from "@/lib/metadata";
|
import { buildLocalizedMetadata } from "@/lib/metadata";
|
||||||
import { getLocalizedPath, resolveLocale } from "@/lib/locale";
|
import { getLocalizedPath, resolveLocale } from "@/lib/locale";
|
||||||
@@ -25,10 +18,6 @@ import {
|
|||||||
getSiteSettings,
|
getSiteSettings,
|
||||||
splitMarqueeRowItems,
|
splitMarqueeRowItems,
|
||||||
} from "@/lib/app-config";
|
} from "@/lib/app-config";
|
||||||
import {
|
|
||||||
getLocalizedValue,
|
|
||||||
getPublishedPortfolioProjects,
|
|
||||||
} from "@/lib/portfolio";
|
|
||||||
import { type MarqueeRow } from "@/components/layout/stacked-marquee-section";
|
import { type MarqueeRow } from "@/components/layout/stacked-marquee-section";
|
||||||
|
|
||||||
type HomePageProps = {
|
type HomePageProps = {
|
||||||
@@ -65,10 +54,9 @@ export default async function HomePage({ params }: HomePageProps) {
|
|||||||
siteSettings.defaultLocale,
|
siteSettings.defaultLocale,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [t, marqueeSettings, publishedProjects] = await Promise.all([
|
const [t, marqueeSettings] = await Promise.all([
|
||||||
getTranslations({ locale: localeKey, namespace: "homepage" }),
|
getTranslations({ locale: localeKey, namespace: "homepage" }),
|
||||||
getMarqueeSettings(),
|
getMarqueeSettings(),
|
||||||
getPublishedPortfolioProjects(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const bentoCards = [
|
const bentoCards = [
|
||||||
@@ -112,28 +100,6 @@ export default async function HomePage({ params }: HomePageProps) {
|
|||||||
description: t("marquee.description"),
|
description: t("marquee.description"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const projectsCopy: ProjectsSectionCopy = {
|
|
||||||
eyebrow: t("projects.eyebrow"),
|
|
||||||
title: t("projects.title"),
|
|
||||||
description: t("projects.description"),
|
|
||||||
stackLabel: t("projects.stackLabel"),
|
|
||||||
fallbackItems: t.raw("projects.fallbackItems") as ProjectsSectionCopy["fallbackItems"],
|
|
||||||
};
|
|
||||||
|
|
||||||
const capabilitiesCopy: CapabilitiesSectionCopy = {
|
|
||||||
eyebrow: t("capabilities.eyebrow"),
|
|
||||||
title: t("capabilities.title"),
|
|
||||||
description: t("capabilities.description"),
|
|
||||||
items: t.raw("capabilities.items") as CapabilitiesSectionCopy["items"],
|
|
||||||
};
|
|
||||||
|
|
||||||
const processCopy: ProcessSectionCopy = {
|
|
||||||
eyebrow: t("process.eyebrow"),
|
|
||||||
title: t("process.title"),
|
|
||||||
description: t("process.description"),
|
|
||||||
steps: t.raw("process.steps") as ProcessSectionCopy["steps"],
|
|
||||||
};
|
|
||||||
|
|
||||||
const contactCtaCopy: ContactCtaSectionCopy = {
|
const contactCtaCopy: ContactCtaSectionCopy = {
|
||||||
eyebrow: t("contactCta.eyebrow"),
|
eyebrow: t("contactCta.eyebrow"),
|
||||||
title: t("contactCta.title"),
|
title: t("contactCta.title"),
|
||||||
@@ -157,45 +123,6 @@ export default async function HomePage({ params }: HomePageProps) {
|
|||||||
|
|
||||||
const portfolioHref = getLocalizedPath(localeKey, "/portfolio", siteSettings.defaultLocale);
|
const portfolioHref = getLocalizedPath(localeKey, "/portfolio", siteSettings.defaultLocale);
|
||||||
const contactHref = getLocalizedPath(localeKey, "/contact", siteSettings.defaultLocale);
|
const contactHref = getLocalizedPath(localeKey, "/contact", siteSettings.defaultLocale);
|
||||||
const stackSets = t.raw("projects.stackSets") as string[][];
|
|
||||||
|
|
||||||
const selectedProjects = [
|
|
||||||
...publishedProjects.filter((project) => project.isFeatured),
|
|
||||||
...publishedProjects.filter((project) => !project.isFeatured),
|
|
||||||
].slice(0, 3);
|
|
||||||
|
|
||||||
const projectCards: ProjectCardCopy[] = selectedProjects.map((project, index) => ({
|
|
||||||
title: getLocalizedValue(project.title, localeKey),
|
|
||||||
summary: getLocalizedValue(project.summary, localeKey),
|
|
||||||
stack: stackSets[index] ?? stackSets[stackSets.length - 1] ?? [],
|
|
||||||
href: getLocalizedPath(
|
|
||||||
localeKey,
|
|
||||||
`/portfolio/${project.slug}`,
|
|
||||||
siteSettings.defaultLocale,
|
|
||||||
),
|
|
||||||
cta: t("projects.cta"),
|
|
||||||
meta: [
|
|
||||||
getLocalizedValue(project.category.name, localeKey),
|
|
||||||
getLocalizedValue(project.serviceLabel, localeKey),
|
|
||||||
String(project.projectYear),
|
|
||||||
],
|
|
||||||
featured: index === 0,
|
|
||||||
coverImagePath: project.coverImagePath,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const fallbackProjectCards: ProjectCardCopy[] = projectsCopy.fallbackItems.map((item) => ({
|
|
||||||
title: item.title,
|
|
||||||
summary: item.summary,
|
|
||||||
stack: item.stack,
|
|
||||||
href: item.href === "/portfolio" ? portfolioHref : item.href === "/contact" ? contactHref : item.href,
|
|
||||||
cta: t("projects.cta"),
|
|
||||||
meta: [t("projects.fallbackMeta")],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mergedProjectCards = [
|
|
||||||
...projectCards,
|
|
||||||
...fallbackProjectCards.slice(0, Math.max(0, 3 - projectCards.length)),
|
|
||||||
].slice(0, 3);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -226,19 +153,7 @@ export default async function HomePage({ params }: HomePageProps) {
|
|||||||
<MarqueeSection copy={marqueeCopy} rows={marqueeRows} />
|
<MarqueeSection copy={marqueeCopy} rows={marqueeRows} />
|
||||||
</MotionFade>
|
</MotionFade>
|
||||||
|
|
||||||
<MotionFade delay={0.08}>
|
|
||||||
<ProjectsSection copy={projectsCopy} items={mergedProjectCards} />
|
|
||||||
</MotionFade>
|
|
||||||
|
|
||||||
<MotionFade delay={0.1}>
|
<MotionFade delay={0.1}>
|
||||||
<CapabilitiesSection copy={capabilitiesCopy} />
|
|
||||||
</MotionFade>
|
|
||||||
|
|
||||||
<MotionFade delay={0.12}>
|
|
||||||
<ProcessSection copy={processCopy} />
|
|
||||||
</MotionFade>
|
|
||||||
|
|
||||||
<MotionFade delay={0.14}>
|
|
||||||
<ContactCtaSection copy={contactCtaCopy} contactHref={contactHref} />
|
<ContactCtaSection copy={contactCtaCopy} contactHref={contactHref} />
|
||||||
</MotionFade>
|
</MotionFade>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { StackedMarqueeSection, type MarqueeRow } from "@/components/layout/stacked-marquee-section";
|
import { StackedMarqueeSection, type MarqueeRow } from "@/components/layout/stacked-marquee-section";
|
||||||
import { AppCard } from "@/components/ui/app-card";
|
|
||||||
import { SectionHeading } from "./section-heading";
|
import { SectionHeading } from "./section-heading";
|
||||||
import type { MarqueeSectionCopy } from "./types";
|
import type { MarqueeSectionCopy } from "./types";
|
||||||
|
|
||||||
@@ -10,18 +9,18 @@ type MarqueeSectionProps = {
|
|||||||
|
|
||||||
export function MarqueeSection({ copy, rows }: MarqueeSectionProps) {
|
export function MarqueeSection({ copy, rows }: MarqueeSectionProps) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-8 overflow-hidden">
|
<section className="space-y-8">
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
eyebrow={copy.eyebrow}
|
eyebrow={copy.eyebrow}
|
||||||
title={copy.title}
|
title={copy.title}
|
||||||
description={copy.description}
|
description={copy.description}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppCard className="overflow-hidden">
|
{/* Full-bleed band: breaks out of the centered container to span the
|
||||||
<div className="rounded-nested border border-border/70 bg-background py-6">
|
entire site width instead of sitting inside a card. */}
|
||||||
|
<div className="relative left-1/2 w-screen -translate-x-1/2 overflow-hidden border-y border-border/60 bg-background/40 py-8">
|
||||||
<StackedMarqueeSection rows={rows} className="py-0" />
|
<StackedMarqueeSection rows={rows} className="py-0" />
|
||||||
</div>
|
</div>
|
||||||
</AppCard>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type FloatingControlsProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pill =
|
const pill =
|
||||||
"flex h-11 w-11 items-center justify-center rounded-full border border-border/60 bg-background/85 text-foreground/80 shadow-panel backdrop-blur-chrome transition-colors hover:bg-accent hover:text-foreground";
|
"flex h-11 w-11 items-center justify-center rounded-full border border-border/60 bg-background/85 text-foreground/80 backdrop-blur-chrome transition-colors hover:bg-accent hover:text-foreground";
|
||||||
|
|
||||||
/** Radius (px) of the arc the items fan out along. */
|
/** Radius (px) of the arc the items fan out along. */
|
||||||
const RADIUS = 92;
|
const RADIUS = 92;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { FloatingControls } from "@/components/layout/floating-controls";
|
import { FloatingControls } from "@/components/layout/floating-controls";
|
||||||
import { Dock, DockIcon } from "@/components/ui/dock";
|
import { Dock, DockIcon } from "@/components/ui/dock";
|
||||||
@@ -26,15 +28,17 @@ type NavKey = "about" | "portfolio" | "products" | "contact";
|
|||||||
type NavItem = {
|
type NavItem = {
|
||||||
key: NavKey;
|
key: NavKey;
|
||||||
path: string;
|
path: string;
|
||||||
|
/** Icon file under /public. Swap these for real macOS app icons anytime. */
|
||||||
|
icon: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// "Home" is intentionally omitted — the logo (first dock icon) is the home link.
|
// "Home" is intentionally omitted — the logo (first dock icon) is the home link.
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
{ key: "about", path: "/about" },
|
{ key: "about", path: "/about", icon: "/dock/about.svg" },
|
||||||
{ key: "portfolio", path: "/portfolio" },
|
{ key: "portfolio", path: "/portfolio", icon: "/dock/portfolio.svg" },
|
||||||
{ key: "products", path: "/products", disabled: true },
|
{ key: "products", path: "/products", icon: "/dock/products.svg", disabled: true },
|
||||||
{ key: "contact", path: "/contact" },
|
{ key: "contact", path: "/contact", icon: "/dock/contact.svg" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function isNavItemActive(currentPath: string, itemPath: string) {
|
function isNavItemActive(currentPath: string, itemPath: string) {
|
||||||
@@ -48,30 +52,47 @@ export function SiteDock({ defaultLocale, isSuperAdmin = false }: SiteDockProps)
|
|||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const t = useTranslations("navigation");
|
const t = useTranslations("navigation");
|
||||||
|
const reducedMotion = useReducedMotion();
|
||||||
|
const [bouncing, setBouncing] = useState<string | null>(null);
|
||||||
|
|
||||||
const currentPath = stripLocalePrefix(pathname);
|
const currentPath = stripLocalePrefix(pathname);
|
||||||
const homeHref = getLocalizedPath(locale, "/", defaultLocale);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ===== Bottom dock (Magic UI) — primary navigation ===== */}
|
{/* ===== Bottom dock — primary navigation ===== */}
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none fixed inset-x-0 bottom-0 z-40 flex justify-center px-4"
|
className="pointer-events-none fixed inset-x-0 bottom-0 z-40 flex justify-center px-4"
|
||||||
style={{ paddingBottom: "calc(0.9rem + env(safe-area-inset-bottom, 0px))" }}
|
style={{ paddingBottom: "calc(0.9rem + env(safe-area-inset-bottom, 0px))" }}
|
||||||
>
|
>
|
||||||
<Dock
|
<Dock
|
||||||
direction="bottom"
|
direction="bottom"
|
||||||
iconSize={52}
|
iconSize={50}
|
||||||
iconMagnification={72}
|
iconMagnification={66}
|
||||||
iconDistance={150}
|
iconDistance={140}
|
||||||
className="pointer-events-auto h-[68px] gap-2.5 border-border/60 bg-background/70 shadow-panel"
|
className="pointer-events-auto h-[66px] gap-3 border-border/60 bg-background/70"
|
||||||
>
|
>
|
||||||
{/* ── Logo (fixed src → no refresh flash) ── */}
|
{/* ── Logo (fixed src → no refresh flash) ── */}
|
||||||
<DockIcon className="group relative overflow-visible rounded-[26%] bg-white shadow-[0_6px_14px_-6px_rgba(0,0,0,0.5)] ring-1 ring-black/10">
|
<DockIcon className="group relative overflow-visible">
|
||||||
<Link
|
<Link
|
||||||
href={homeHref}
|
href={homeHref}
|
||||||
aria-label="mohfarawati — Home"
|
aria-label="mohfarawati — Home"
|
||||||
className="flex h-full w-full items-center justify-center rounded-[inherit]"
|
className="flex h-full w-full items-center justify-center"
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
className="flex h-full w-full items-center justify-center overflow-hidden rounded-[22%] bg-white ring-1 ring-black/10"
|
||||||
|
{...bounceProps("logo")}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/logos/light-primary.svg"
|
src="/logos/light-primary.svg"
|
||||||
@@ -80,46 +101,43 @@ export function SiteDock({ defaultLocale, isSuperAdmin = false }: SiteDockProps)
|
|||||||
height={104}
|
height={104}
|
||||||
sizes="104px"
|
sizes="104px"
|
||||||
priority
|
priority
|
||||||
className="h-full w-full rounded-[inherit] object-contain"
|
className="h-full w-full object-contain"
|
||||||
/>
|
/>
|
||||||
|
</motion.span>
|
||||||
</Link>
|
</Link>
|
||||||
<DockTip label="mohfarawati" />
|
<DockTip label="mohfarawati" />
|
||||||
</DockIcon>
|
</DockIcon>
|
||||||
|
|
||||||
<div className="mx-0.5 h-9 w-px self-center bg-border/70" aria-hidden />
|
<div className="mx-0.5 h-9 w-px self-center bg-border/70" aria-hidden />
|
||||||
|
|
||||||
{/* ── Pages ── */}
|
{/* ── Pages (icon art carries its own squircle shape) ── */}
|
||||||
{navItems.map(({ key, path, disabled }) => {
|
{navItems.map(({ key, path, icon: iconSrc, disabled }) => {
|
||||||
const itemPath = path || "/";
|
const itemPath = path || "/";
|
||||||
const isActive = isNavItemActive(currentPath, itemPath);
|
const isActive = isNavItemActive(currentPath, itemPath);
|
||||||
const label = t(key);
|
const label = t(key);
|
||||||
const src = `/dock/${key}${isActive ? "-active" : ""}.svg`;
|
|
||||||
|
|
||||||
const icon = (
|
const icon = (
|
||||||
|
<motion.span
|
||||||
|
className="flex h-full w-full items-center justify-center"
|
||||||
|
{...(disabled ? {} : bounceProps(key))}
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src={src}
|
src={iconSrc}
|
||||||
alt={label}
|
alt={label}
|
||||||
width={104}
|
width={104}
|
||||||
height={104}
|
height={104}
|
||||||
sizes="104px"
|
sizes="104px"
|
||||||
className={cn("h-full w-full rounded-[inherit] object-contain", disabled && "opacity-55")}
|
className={cn("h-full w-full object-contain", disabled && "opacity-55")}
|
||||||
/>
|
/>
|
||||||
|
</motion.span>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DockIcon
|
<DockIcon key={key} className="group relative overflow-visible">
|
||||||
key={key}
|
|
||||||
className={cn(
|
|
||||||
"group relative overflow-visible rounded-[26%] transition-shadow duration-200",
|
|
||||||
isActive
|
|
||||||
? "shadow-[0_12px_26px_-8px_hsl(var(--primary)/0.65)]"
|
|
||||||
: "shadow-[0_6px_14px_-6px_rgba(0,0,0,0.5)] hover:shadow-[0_10px_22px_-8px_rgba(0,0,0,0.6)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{disabled ? (
|
{disabled ? (
|
||||||
<span
|
<span
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
className="flex h-full w-full cursor-not-allowed items-center justify-center rounded-[inherit]"
|
className="flex h-full w-full cursor-not-allowed items-center justify-center"
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
@@ -128,11 +146,20 @@ export function SiteDock({ defaultLocale, isSuperAdmin = false }: SiteDockProps)
|
|||||||
href={getLocalizedPath(locale, itemPath, defaultLocale)}
|
href={getLocalizedPath(locale, itemPath, defaultLocale)}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-current={isActive ? "page" : undefined}
|
aria-current={isActive ? "page" : undefined}
|
||||||
className="flex h-full w-full items-center justify-center rounded-[inherit]"
|
className="flex h-full w-full items-center justify-center"
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* macOS running indicator */}
|
||||||
|
{isActive ? (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute -bottom-[7px] left-1/2 h-[5px] w-[5px] -translate-x-1/2 rounded-full bg-foreground/70"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<DockTip label={label} soon={disabled ? t("soon") : undefined} />
|
<DockTip label={label} soon={disabled ? t("soon") : undefined} />
|
||||||
</DockIcon>
|
</DockIcon>
|
||||||
);
|
);
|
||||||
@@ -150,7 +177,7 @@ function DockTip({ label, soon }: { label: string; soon?: string }) {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
className="pointer-events-none absolute bottom-[calc(100%+0.85rem)] left-1/2 -translate-x-1/2 translate-y-1 whitespace-nowrap rounded-lg border border-border/60 bg-background/90 px-3 py-1.5 text-sm font-semibold text-foreground opacity-0 shadow-md backdrop-blur-chrome transition-all duration-150 group-hover:translate-y-0 group-hover:opacity-100"
|
className="pointer-events-none absolute bottom-[calc(100%+0.85rem)] left-1/2 -translate-x-1/2 translate-y-1 whitespace-nowrap rounded-lg border border-border/60 bg-background/90 px-3 py-1.5 text-sm font-semibold text-foreground opacity-0 backdrop-blur-chrome transition-all duration-150 group-hover:translate-y-0 group-hover:opacity-100"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{soon ? <span className="ms-1.5 text-xs tracking-wide text-primary">{soon}</span> : null}
|
{soon ? <span className="ms-1.5 text-xs tracking-wide text-primary">{soon}</span> : null}
|
||||||
|
|||||||
@@ -123,10 +123,12 @@ const DockIcon = ({
|
|||||||
[size, targetSize, size],
|
[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, {
|
const scaleSize = useSpring(sizeTransform, {
|
||||||
mass: 0.1,
|
mass: 0.1,
|
||||||
stiffness: 150,
|
stiffness: 280,
|
||||||
damping: 12,
|
damping: 22,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff8a5f"/><stop offset="1" stop-color="#e5431f"/></linearGradient>
|
|
||||||
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#fff" stop-opacity="0.26"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#bg)"/>
|
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#gl)"/>
|
|
||||||
<rect x="0.75" y="0.75" width="98.5" height="98.5" rx="27.25" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.5"/>
|
|
||||||
<g fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<circle cx="50" cy="41" r="11"/><path d="M30 72 C30 57, 70 57, 70 72"/></g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 853 B |
@@ -4,9 +4,9 @@
|
|||||||
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0" stop-color="#fff" stop-opacity="0.26"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
<stop offset="0" stop-color="#fff" stop-opacity="0.26"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#bg)"/>
|
<rect x="0" y="0" width="100" height="100" rx="22" fill="url(#bg)"/>
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#gl)"/>
|
<rect x="0" y="0" width="100" height="100" rx="22" fill="url(#gl)"/>
|
||||||
<rect x="0.75" y="0.75" width="98.5" height="98.5" rx="27.25" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.5"/>
|
<rect x="0.75" y="0.75" width="98.5" height="98.5" rx="21.25" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.5"/>
|
||||||
<g fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
|
<g fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<circle cx="50" cy="41" r="11"/><path d="M30 72 C30 57, 70 57, 70 72"/></g>
|
<circle cx="50" cy="41" r="11"/><path d="M30 72 C30 57, 70 57, 70 72"/></g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 853 B After Width: | Height: | Size: 853 B |
@@ -1,12 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff8a5f"/><stop offset="1" stop-color="#e5431f"/></linearGradient>
|
|
||||||
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#fff" stop-opacity="0.26"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#bg)"/>
|
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#gl)"/>
|
|
||||||
<rect x="0.75" y="0.75" width="98.5" height="98.5" rx="27.25" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.5"/>
|
|
||||||
<g fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<rect x="27" y="35" width="46" height="30" rx="6"/><path d="M30 40 L50 54 L70 40"/></g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 865 B |
@@ -4,9 +4,9 @@
|
|||||||
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0" stop-color="#fff" stop-opacity="0.26"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
<stop offset="0" stop-color="#fff" stop-opacity="0.26"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#bg)"/>
|
<rect x="0" y="0" width="100" height="100" rx="22" fill="url(#bg)"/>
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#gl)"/>
|
<rect x="0" y="0" width="100" height="100" rx="22" fill="url(#gl)"/>
|
||||||
<rect x="0.75" y="0.75" width="98.5" height="98.5" rx="27.25" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.5"/>
|
<rect x="0.75" y="0.75" width="98.5" height="98.5" rx="21.25" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.5"/>
|
||||||
<g fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
|
<g fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<rect x="27" y="35" width="46" height="30" rx="6"/><path d="M30 40 L50 54 L70 40"/></g>
|
<rect x="27" y="35" width="46" height="30" rx="6"/><path d="M30 40 L50 54 L70 40"/></g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 865 B After Width: | Height: | Size: 865 B |
@@ -1,13 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff8a5f"/><stop offset="1" stop-color="#e5431f"/></linearGradient>
|
|
||||||
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#fff" stop-opacity="0.26"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#bg)"/>
|
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#gl)"/>
|
|
||||||
<rect x="0.75" y="0.75" width="98.5" height="98.5" rx="27.25" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.5"/>
|
|
||||||
<g fill="#fff">
|
|
||||||
<rect x="32" y="32" width="15" height="15" rx="4"/><rect x="53" y="32" width="15" height="15" rx="4"/>
|
|
||||||
<rect x="32" y="53" width="15" height="15" rx="4"/><rect x="53" y="53" width="15" height="15" rx="4"/></g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 909 B |
@@ -1,13 +1,18 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#454b57"/><stop offset="1" stop-color="#1c2027"/></linearGradient>
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#4aa8ff"/><stop offset="1" stop-color="#7b5cff"/></linearGradient>
|
||||||
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0" stop-color="#fff" stop-opacity="0.26"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
<stop offset="0" stop-color="#fff" stop-opacity="0.30"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
|
<clipPath id="card"><rect x="27" y="31" width="46" height="38" rx="7"/></clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#bg)"/>
|
<rect x="0" y="0" width="100" height="100" rx="22" fill="url(#bg)"/>
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#gl)"/>
|
<rect x="0" y="0" width="100" height="100" rx="22" fill="url(#gl)"/>
|
||||||
<rect x="0.75" y="0.75" width="98.5" height="98.5" rx="27.25" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.5"/>
|
<rect x="0.75" y="0.75" width="98.5" height="98.5" rx="21.25" fill="none" stroke="#fff" stroke-opacity="0.18" stroke-width="1.5"/>
|
||||||
<g fill="#fff">
|
<rect x="27" y="31" width="46" height="38" rx="7" fill="#ffffff"/>
|
||||||
<rect x="32" y="32" width="15" height="15" rx="4"/><rect x="53" y="32" width="15" height="15" rx="4"/>
|
<g clip-path="url(#card)">
|
||||||
<rect x="32" y="53" width="15" height="15" rx="4"/><rect x="53" y="53" width="15" height="15" rx="4"/></g>
|
<circle cx="40" cy="44" r="5.5" fill="#ffc23d"/>
|
||||||
|
<path d="M27 69 L43 52 L53 61 L61 51 L73 69 Z" fill="#37b877"/>
|
||||||
|
<path d="M55 69 L66 57 L73 64 L73 69 Z" fill="#2c9c86"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 909 B After Width: | Height: | Size: 1.0 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff8a5f"/><stop offset="1" stop-color="#e5431f"/></linearGradient>
|
|
||||||
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#fff" stop-opacity="0.26"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#bg)"/>
|
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#gl)"/>
|
|
||||||
<rect x="0.75" y="0.75" width="98.5" height="98.5" rx="27.25" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.5"/>
|
|
||||||
<g fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M50 28 L71 40 L71 60 L50 72 L29 60 L29 40 Z"/><path d="M29 40 L50 52 L71 40"/><path d="M50 52 L50 72"/></g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 894 B |
@@ -4,9 +4,9 @@
|
|||||||
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0" stop-color="#fff" stop-opacity="0.26"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
<stop offset="0" stop-color="#fff" stop-opacity="0.26"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#bg)"/>
|
<rect x="0" y="0" width="100" height="100" rx="22" fill="url(#bg)"/>
|
||||||
<rect x="0" y="0" width="100" height="100" rx="28" fill="url(#gl)"/>
|
<rect x="0" y="0" width="100" height="100" rx="22" fill="url(#gl)"/>
|
||||||
<rect x="0.75" y="0.75" width="98.5" height="98.5" rx="27.25" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.5"/>
|
<rect x="0.75" y="0.75" width="98.5" height="98.5" rx="21.25" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.5"/>
|
||||||
<g fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
|
<g fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M50 28 L71 40 L71 60 L50 72 L29 60 L29 40 Z"/><path d="M29 40 L50 52 L71 40"/><path d="M50 52 L50 72"/></g>
|
<path d="M50 28 L71 40 L71 60 L50 72 L29 60 L29 40 Z"/><path d="M29 40 L50 52 L71 40"/><path d="M50 52 L50 72"/></g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 894 B After Width: | Height: | Size: 894 B |