IMPROVED - Use swappable image icons and move controls to a corner dock
- Dock icons are now real SVG files under public/dock/ (macOS squircle look, active variants), so they can be replaced with custom art later. - Full-bleed icons (DockIcon padding 0), fixed logo src to stop the refresh flash. - Remove the top menu bar entirely; relocate theme/language/sound (+admin) into a corner control dock that expands on click.
@@ -8,7 +8,7 @@ import { SiteDock } from "@/components/layout/site-dock";
|
|||||||
import { SiteFooter } from "@/components/layout/site-footer";
|
import { SiteFooter } from "@/components/layout/site-footer";
|
||||||
import { ScrollSmootherProvider } from "@/components/scroll-smoother-provider";
|
import { ScrollSmootherProvider } from "@/components/scroll-smoother-provider";
|
||||||
import { isSuperAdmin } from "@/lib/admin-auth";
|
import { isSuperAdmin } from "@/lib/admin-auth";
|
||||||
import { getMaintenanceMode, getSiteSettings, getSiteSettingsMediaBindings } from "@/lib/app-config";
|
import { getMaintenanceMode, getSiteSettings } from "@/lib/app-config";
|
||||||
import { getLocalizedPath, resolveLocale } from "@/lib/locale";
|
import { getLocalizedPath, resolveLocale } from "@/lib/locale";
|
||||||
|
|
||||||
type SiteLayoutProps = {
|
type SiteLayoutProps = {
|
||||||
@@ -24,9 +24,8 @@ export const revalidate = 0;
|
|||||||
export default async function SiteLayout({ children, params }: SiteLayoutProps) {
|
export default async function SiteLayout({ children, params }: SiteLayoutProps) {
|
||||||
noStore();
|
noStore();
|
||||||
await params;
|
await params;
|
||||||
const [maintenanceEnabled, mediaBindings, siteSettings] = await Promise.all([
|
const [maintenanceEnabled, siteSettings] = await Promise.all([
|
||||||
getMaintenanceMode(),
|
getMaintenanceMode(),
|
||||||
getSiteSettingsMediaBindings(),
|
|
||||||
getSiteSettings(),
|
getSiteSettings(),
|
||||||
]);
|
]);
|
||||||
const localeKey = resolveLocale(await getLocale().catch(() => siteSettings.defaultLocale), siteSettings.defaultLocale);
|
const localeKey = resolveLocale(await getLocale().catch(() => siteSettings.defaultLocale), siteSettings.defaultLocale);
|
||||||
@@ -44,12 +43,7 @@ export default async function SiteLayout({ children, params }: SiteLayoutProps)
|
|||||||
<>
|
<>
|
||||||
<ScrollSmootherProvider />
|
<ScrollSmootherProvider />
|
||||||
<SiteAmbientBackdrop />
|
<SiteAmbientBackdrop />
|
||||||
<SiteDock
|
<SiteDock defaultLocale={siteSettings.defaultLocale} isSuperAdmin={authenticated} />
|
||||||
lightLogoUrl={mediaBindings.siteLogoLight?.url}
|
|
||||||
darkLogoUrl={mediaBindings.siteLogoDark?.url}
|
|
||||||
defaultLocale={siteSettings.defaultLocale}
|
|
||||||
isSuperAdmin={authenticated}
|
|
||||||
/>
|
|
||||||
<div id="smooth-wrapper">
|
<div id="smooth-wrapper">
|
||||||
<div id="smooth-content">
|
<div id="smooth-content">
|
||||||
<div className="site-content-frame flex min-h-screen flex-col">
|
<div className="site-content-frame flex min-h-screen flex-col">
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useId } from "react";
|
|
||||||
|
|
||||||
export type DockSymbol = "home" | "about" | "portfolio" | "products" | "contact";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* macOS Big Sur–style squircle app icon, drawn entirely in SVG so it stays
|
|
||||||
* crisp at every magnification step. Inactive icons use a neutral graphite
|
|
||||||
* gradient; the active icon lights up in the brand coral.
|
|
||||||
*/
|
|
||||||
export function DockAppIcon({
|
|
||||||
symbol,
|
|
||||||
active = false,
|
|
||||||
disabled = false,
|
|
||||||
}: {
|
|
||||||
symbol: DockSymbol;
|
|
||||||
active?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
}) {
|
|
||||||
const uid = useId().replace(/[:]/g, "");
|
|
||||||
const bg = `bg-${uid}`;
|
|
||||||
const gloss = `gloss-${uid}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
className="h-full w-full"
|
|
||||||
role="presentation"
|
|
||||||
style={disabled ? { opacity: 0.55 } : undefined}
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id={bg} x1="0" y1="0" x2="0" y2="1">
|
|
||||||
{active ? (
|
|
||||||
<>
|
|
||||||
<stop offset="0" stopColor="#ff8a5f" />
|
|
||||||
<stop offset="1" stopColor="#e5431f" />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<stop offset="0" stopColor="#454b57" />
|
|
||||||
<stop offset="1" stopColor="#1c2027" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id={gloss} x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stopColor="#ffffff" stopOpacity="0.28" />
|
|
||||||
<stop offset="0.5" stopColor="#ffffff" stopOpacity="0" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
{/* tile */}
|
|
||||||
<rect x="4" y="4" width="92" height="92" rx="26" fill={`url(#${bg})`} />
|
|
||||||
<rect x="4" y="4" width="92" height="92" rx="26" fill={`url(#${gloss})`} />
|
|
||||||
<rect
|
|
||||||
x="4.75"
|
|
||||||
y="4.75"
|
|
||||||
width="90.5"
|
|
||||||
height="90.5"
|
|
||||||
rx="25.25"
|
|
||||||
fill="none"
|
|
||||||
stroke="#ffffff"
|
|
||||||
strokeOpacity="0.14"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* symbol */}
|
|
||||||
<g
|
|
||||||
fill="none"
|
|
||||||
stroke="#ffffff"
|
|
||||||
strokeWidth="6"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
{symbol === "home" && (
|
|
||||||
<>
|
|
||||||
<path d="M29 49 L50 30 L71 49" />
|
|
||||||
<path d="M34 46 V70 H66 V46" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{symbol === "about" && (
|
|
||||||
<>
|
|
||||||
<circle cx="50" cy="41" r="11" />
|
|
||||||
<path d="M30 72 C30 57, 70 57, 70 72" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{symbol === "contact" && (
|
|
||||||
<>
|
|
||||||
<rect x="27" y="35" width="46" height="30" rx="6" />
|
|
||||||
<path d="M30 40 L50 54 L70 40" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</g>
|
|
||||||
|
|
||||||
{/* filled symbols */}
|
|
||||||
{symbol === "portfolio" && (
|
|
||||||
<g fill="#ffffff">
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
{symbol === "products" && (
|
|
||||||
<g
|
|
||||||
fill="none"
|
|
||||||
stroke="#ffffff"
|
|
||||||
strokeWidth="6"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||||
|
import { ShieldCheck, SlidersHorizontal, X } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useState, type ReactNode } from "react";
|
||||||
|
|
||||||
|
import { LocaleToggle } from "@/components/layout/locale-toggle";
|
||||||
|
import { SoundToggle } from "@/components/sound-toggle";
|
||||||
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
|
import { buildAdminUrl } from "@/lib/admin-routing";
|
||||||
|
import type { AppLocale } from "@/lib/locale";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type FloatingControlsProps = {
|
||||||
|
locale: string;
|
||||||
|
defaultLocale: AppLocale;
|
||||||
|
isSuperAdmin?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pill =
|
||||||
|
"flex h-11 w-11 items-center justify-center rounded-full border border-border/60 bg-background/80 text-foreground/80 shadow-panel backdrop-blur-chrome transition-colors hover:bg-accent hover:text-foreground";
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 12, scale: 0.8 },
|
||||||
|
show: { opacity: 1, y: 0, scale: 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FloatingControls({
|
||||||
|
locale,
|
||||||
|
defaultLocale,
|
||||||
|
isSuperAdmin = false,
|
||||||
|
}: FloatingControlsProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const t = useTranslations("navigation");
|
||||||
|
const reducedMotion = useReducedMotion();
|
||||||
|
|
||||||
|
const controls: ReactNode[] = [
|
||||||
|
<SoundToggle
|
||||||
|
key="sound"
|
||||||
|
ariaLabel={t("soundMute")}
|
||||||
|
mutedAriaLabel={t("soundUnmute")}
|
||||||
|
variant="ghost"
|
||||||
|
className={cn(pill, "border-transparent bg-transparent")}
|
||||||
|
/>,
|
||||||
|
<ThemeToggle
|
||||||
|
key="theme"
|
||||||
|
ariaLabel={t("themeToggle")}
|
||||||
|
variant="ghost"
|
||||||
|
className={cn(pill, "border-transparent bg-transparent")}
|
||||||
|
/>,
|
||||||
|
<LocaleToggle
|
||||||
|
key="locale"
|
||||||
|
locale={locale}
|
||||||
|
defaultLocale={defaultLocale}
|
||||||
|
className={cn(pill, "border-transparent bg-transparent p-0")}
|
||||||
|
/>,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isSuperAdmin) {
|
||||||
|
controls.push(
|
||||||
|
<a key="admin" href={buildAdminUrl("/")} aria-label="Open admin dashboard" className={pill}>
|
||||||
|
<ShieldCheck className="h-[1.125rem] w-[1.125rem]" />
|
||||||
|
</a>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none fixed bottom-0 end-0 z-50 flex flex-col items-center gap-2 p-4"
|
||||||
|
style={{ paddingBottom: "calc(1rem + env(safe-area-inset-bottom, 0px))" }}>
|
||||||
|
<AnimatePresence>
|
||||||
|
{open ? (
|
||||||
|
<motion.div
|
||||||
|
key="tray"
|
||||||
|
className="pointer-events-auto flex flex-col items-center gap-2 rounded-full border border-border/50 bg-background/60 p-1.5 shadow-panel backdrop-blur-chrome"
|
||||||
|
initial={reducedMotion ? { opacity: 0 } : "hidden"}
|
||||||
|
animate={reducedMotion ? { opacity: 1 } : "show"}
|
||||||
|
exit={reducedMotion ? { opacity: 0 } : "hidden"}
|
||||||
|
variants={{
|
||||||
|
hidden: { transition: { staggerChildren: 0.04, staggerDirection: -1 } },
|
||||||
|
show: { transition: { staggerChildren: 0.05, delayChildren: 0.02 } },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{controls.map((control, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
variants={reducedMotion ? undefined : itemVariants}
|
||||||
|
transition={{ type: "spring", stiffness: 460, damping: 28 }}
|
||||||
|
>
|
||||||
|
{control}
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
) : null}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((value) => !value)}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-label={open ? t("closeMenu") : t("openMenu")}
|
||||||
|
className={cn(pill, "pointer-events-auto")}
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
animate={{ rotate: open ? 90 : 0 }}
|
||||||
|
transition={reducedMotion ? { duration: 0 } : { type: "spring", stiffness: 400, damping: 24 }}
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{open ? <X className="h-[1.125rem] w-[1.125rem]" /> : <SlidersHorizontal className="h-[1.125rem] w-[1.125rem]" />}
|
||||||
|
</motion.span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,25 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ShieldCheck } from "lucide-react";
|
|
||||||
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 { useTheme } from "next-themes";
|
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { DockAppIcon, type DockSymbol } from "@/components/layout/dock-app-icon";
|
import { FloatingControls } from "@/components/layout/floating-controls";
|
||||||
import { LocaleToggle } from "@/components/layout/locale-toggle";
|
|
||||||
import { SoundToggle } from "@/components/sound-toggle";
|
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
|
||||||
import { Dock, DockIcon } from "@/components/ui/dock";
|
import { Dock, DockIcon } from "@/components/ui/dock";
|
||||||
import { buildAdminUrl } from "@/lib/admin-routing";
|
|
||||||
import { getLocalizedPath, stripLocalePrefix, type AppLocale } from "@/lib/locale";
|
import { getLocalizedPath, stripLocalePrefix, type AppLocale } from "@/lib/locale";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type SiteDockProps = {
|
type SiteDockProps = {
|
||||||
lightLogoUrl?: string | null;
|
|
||||||
darkLogoUrl?: string | null;
|
|
||||||
defaultLocale: AppLocale;
|
defaultLocale: AppLocale;
|
||||||
/**
|
/**
|
||||||
* Server-computed via `isSuperAdmin()` in the parent layout. This client
|
* Server-computed via `isSuperAdmin()` in the parent layout. This client
|
||||||
@@ -30,8 +21,10 @@ type SiteDockProps = {
|
|||||||
isSuperAdmin?: boolean;
|
isSuperAdmin?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type NavKey = "home" | "about" | "portfolio" | "products" | "contact";
|
||||||
|
|
||||||
type NavItem = {
|
type NavItem = {
|
||||||
key: DockSymbol;
|
key: NavKey;
|
||||||
path: string;
|
path: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
@@ -51,134 +44,16 @@ function isNavItemActive(currentPath: string, itemPath: string) {
|
|||||||
return currentPath === itemPath || currentPath.startsWith(`${itemPath}/`);
|
return currentPath === itemPath || currentPath.startsWith(`${itemPath}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuClock({ locale }: { locale: string }) {
|
export function SiteDock({ defaultLocale, isSuperAdmin = false }: SiteDockProps) {
|
||||||
const [time, setTime] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const format = () =>
|
|
||||||
setTime(
|
|
||||||
new Date().toLocaleTimeString(locale === "ar" ? "ar" : locale, {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
format();
|
|
||||||
const id = setInterval(format, 20_000);
|
|
||||||
return () => clearInterval(id);
|
|
||||||
}, [locale]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
suppressHydrationWarning
|
|
||||||
className="hidden select-none px-1 text-xs font-semibold tabular-nums text-foreground/65 sm:inline"
|
|
||||||
>
|
|
||||||
{time || "--:--"}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SiteDock({
|
|
||||||
lightLogoUrl,
|
|
||||||
darkLogoUrl,
|
|
||||||
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 { theme, resolvedTheme } = useTheme();
|
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isArabic = locale === "ar";
|
|
||||||
const currentPath = stripLocalePrefix(pathname);
|
const currentPath = stripLocalePrefix(pathname);
|
||||||
const homeHref = getLocalizedPath(locale, "/", defaultLocale);
|
const homeHref = getLocalizedPath(locale, "/", defaultLocale);
|
||||||
const adminHref = buildAdminUrl("/");
|
|
||||||
|
|
||||||
const activeTheme = theme === "system" ? resolvedTheme : theme;
|
|
||||||
const isDark = mounted && activeTheme === "dark";
|
|
||||||
const logoSrc = isDark
|
|
||||||
? darkLogoUrl || lightLogoUrl || "/logos/dark-primary.svg"
|
|
||||||
: lightLogoUrl || darkLogoUrl || "/logos/light-primary.svg";
|
|
||||||
|
|
||||||
const brandSubtitle = isArabic
|
|
||||||
? "مطوّر ويب · فريلانسر"
|
|
||||||
: "Webdesigner Bremen · Freelancer";
|
|
||||||
|
|
||||||
const controlButtonClassName =
|
|
||||||
"h-8 w-8 rounded-full border border-transparent text-foreground/70 hover:bg-accent hover:text-foreground";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ===== Top menu bar (macOS-style utility strip) ===== */}
|
|
||||||
<header
|
|
||||||
className="fixed inset-x-0 top-0 z-40 border-b border-border/60 bg-background/70 backdrop-blur-chrome"
|
|
||||||
style={{ paddingTop: "env(safe-area-inset-top, 0px)" }}
|
|
||||||
>
|
|
||||||
<div className="mx-auto flex h-12 max-w-[100rem] items-center gap-3 px-4 sm:px-6">
|
|
||||||
<Link
|
|
||||||
href={homeHref}
|
|
||||||
className="flex items-center gap-2 rounded-full px-1 py-1 font-black tracking-tight"
|
|
||||||
aria-label="mohfarawati"
|
|
||||||
>
|
|
||||||
<span className="h-2 w-2 rounded-full bg-primary shadow-[0_0_10px_hsl(var(--primary))]" />
|
|
||||||
<span className="text-sm" dir={isArabic ? "rtl" : undefined}>
|
|
||||||
{isArabic ? (
|
|
||||||
<>
|
|
||||||
<span className="text-[hsl(var(--hero-ink))] dark:text-foreground">محمد</span>{" "}
|
|
||||||
<span className="text-primary">فرواتي</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="text-[hsl(var(--hero-ink))] dark:text-foreground">MOH</span>
|
|
||||||
<span className="text-primary">FARAWATI</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className="hidden border-s border-border/60 ps-3 text-xs font-medium text-foreground/60 md:inline"
|
|
||||||
dir={isArabic ? "rtl" : undefined}
|
|
||||||
>
|
|
||||||
{brandSubtitle}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className="ms-auto flex items-center gap-1">
|
|
||||||
<MenuClock locale={locale} />
|
|
||||||
<LocaleToggle
|
|
||||||
locale={locale}
|
|
||||||
defaultLocale={defaultLocale}
|
|
||||||
className={controlButtonClassName}
|
|
||||||
/>
|
|
||||||
<SoundToggle
|
|
||||||
ariaLabel={t("soundMute")}
|
|
||||||
mutedAriaLabel={t("soundUnmute")}
|
|
||||||
variant="ghost"
|
|
||||||
className={controlButtonClassName}
|
|
||||||
/>
|
|
||||||
<ThemeToggle
|
|
||||||
ariaLabel={t("themeToggle")}
|
|
||||||
variant="ghost"
|
|
||||||
className={controlButtonClassName}
|
|
||||||
/>
|
|
||||||
{isSuperAdmin ? (
|
|
||||||
<a
|
|
||||||
href={adminHref}
|
|
||||||
aria-label="Open admin dashboard"
|
|
||||||
className="ms-1 inline-flex h-8 items-center gap-1.5 rounded-full border border-border/60 bg-background/60 px-3 text-xs font-semibold text-foreground/80 transition-colors hover:bg-accent hover:text-foreground"
|
|
||||||
>
|
|
||||||
<ShieldCheck className="h-4 w-4" />
|
|
||||||
<span className="hidden lg:inline">Admin</span>
|
|
||||||
</a>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* ===== Bottom dock (Magic UI) — primary navigation ===== */}
|
{/* ===== Bottom dock (Magic UI) — 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"
|
||||||
@@ -186,12 +61,12 @@ export function SiteDock({
|
|||||||
>
|
>
|
||||||
<Dock
|
<Dock
|
||||||
direction="bottom"
|
direction="bottom"
|
||||||
iconSize={50}
|
iconSize={52}
|
||||||
iconMagnification={86}
|
iconMagnification={90}
|
||||||
iconDistance={160}
|
iconDistance={160}
|
||||||
className="pointer-events-auto h-[68px] gap-2.5 border-border/60 bg-background/70 shadow-panel"
|
className="pointer-events-auto h-[68px] gap-2.5 border-border/60 bg-background/70 shadow-panel"
|
||||||
>
|
>
|
||||||
{/* Logo — first icon in the dock */}
|
{/* Logo — first icon in the dock (fixed src, no theme swap → no 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 rounded-[26%] bg-white shadow-[0_6px_14px_-6px_rgba(0,0,0,0.5)] ring-1 ring-black/10">
|
||||||
<Link
|
<Link
|
||||||
href={homeHref}
|
href={homeHref}
|
||||||
@@ -199,16 +74,13 @@ export function SiteDock({
|
|||||||
className="flex h-full w-full items-center justify-center rounded-[inherit]"
|
className="flex h-full w-full items-center justify-center rounded-[inherit]"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={logoSrc}
|
src="/logos/light-primary.svg"
|
||||||
alt="mohfarawati"
|
alt="mohfarawati"
|
||||||
width={96}
|
width={104}
|
||||||
height={96}
|
height={104}
|
||||||
sizes="96px"
|
sizes="104px"
|
||||||
priority
|
priority
|
||||||
className={cn(
|
className="h-full w-full rounded-[inherit] object-contain"
|
||||||
"h-full w-full rounded-[inherit] object-contain transition-opacity duration-300",
|
|
||||||
mounted ? "opacity-100" : "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<DockTip label="mohfarawati" />
|
<DockTip label="mohfarawati" />
|
||||||
@@ -220,8 +92,18 @@ export function SiteDock({
|
|||||||
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 = <DockAppIcon symbol={key} active={isActive} disabled={disabled} />;
|
const icon = (
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={label}
|
||||||
|
width={104}
|
||||||
|
height={104}
|
||||||
|
sizes="104px"
|
||||||
|
className={cn("h-full w-full rounded-[inherit] object-contain", disabled && "opacity-55")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DockIcon
|
<DockIcon
|
||||||
@@ -257,6 +139,9 @@ export function SiteDock({
|
|||||||
})}
|
})}
|
||||||
</Dock>
|
</Dock>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ===== Corner controls — theme / language / sound / admin ===== */}
|
||||||
|
<FloatingControls locale={locale} defaultLocale={defaultLocale} isSuperAdmin={isSuperAdmin} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ const DockIcon = ({
|
|||||||
...props
|
...props
|
||||||
}: DockIconProps) => {
|
}: DockIconProps) => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const padding = Math.max(2, size * 0.05);
|
const padding = 0;
|
||||||
const defaultMouseX = useMotionValue(Infinity);
|
const defaultMouseX = useMotionValue(Infinity);
|
||||||
|
|
||||||
const distanceCalc = useTransform(mouseX ?? defaultMouseX, (val: number) => {
|
const distanceCalc = useTransform(mouseX ?? defaultMouseX, (val: number) => {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<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.28"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#bg)"/>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#gl)"/>
|
||||||
|
<rect x="4.75" y="4.75" width="90.5" height="90.5" rx="25.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>
|
||||||
|
After Width: | Height: | Size: 849 B |
@@ -0,0 +1,12 @@
|
|||||||
|
<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="#454b57"/><stop offset="1" stop-color="#1c2027"/></linearGradient>
|
||||||
|
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#fff" stop-opacity="0.28"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#bg)"/>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#gl)"/>
|
||||||
|
<rect x="4.75" y="4.75" width="90.5" height="90.5" rx="25.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>
|
||||||
|
After Width: | Height: | Size: 849 B |
@@ -0,0 +1,12 @@
|
|||||||
|
<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.28"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#bg)"/>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#gl)"/>
|
||||||
|
<rect x="4.75" y="4.75" width="90.5" height="90.5" rx="25.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>
|
||||||
|
After Width: | Height: | Size: 861 B |
@@ -0,0 +1,12 @@
|
|||||||
|
<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="#454b57"/><stop offset="1" stop-color="#1c2027"/></linearGradient>
|
||||||
|
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#fff" stop-opacity="0.28"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#bg)"/>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#gl)"/>
|
||||||
|
<rect x="4.75" y="4.75" width="90.5" height="90.5" rx="25.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>
|
||||||
|
After Width: | Height: | Size: 861 B |
@@ -0,0 +1,12 @@
|
|||||||
|
<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.28"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#bg)"/>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#gl)"/>
|
||||||
|
<rect x="4.75" y="4.75" width="90.5" height="90.5" rx="25.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="M29 49 L50 30 L71 49"/><path d="M34 46 V70 H66 V46"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 840 B |
@@ -0,0 +1,12 @@
|
|||||||
|
<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="#454b57"/><stop offset="1" stop-color="#1c2027"/></linearGradient>
|
||||||
|
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#fff" stop-opacity="0.28"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#bg)"/>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#gl)"/>
|
||||||
|
<rect x="4.75" y="4.75" width="90.5" height="90.5" rx="25.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="M29 49 L50 30 L71 49"/><path d="M34 46 V70 H66 V46"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 840 B |
@@ -0,0 +1,13 @@
|
|||||||
|
<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.28"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#bg)"/>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#gl)"/>
|
||||||
|
<rect x="4.75" y="4.75" width="90.5" height="90.5" rx="25.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>
|
||||||
|
After Width: | Height: | Size: 905 B |
@@ -0,0 +1,13 @@
|
|||||||
|
<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="#454b57"/><stop offset="1" stop-color="#1c2027"/></linearGradient>
|
||||||
|
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#fff" stop-opacity="0.28"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#bg)"/>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#gl)"/>
|
||||||
|
<rect x="4.75" y="4.75" width="90.5" height="90.5" rx="25.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>
|
||||||
|
After Width: | Height: | Size: 905 B |
@@ -0,0 +1,12 @@
|
|||||||
|
<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.28"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#bg)"/>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#gl)"/>
|
||||||
|
<rect x="4.75" y="4.75" width="90.5" height="90.5" rx="25.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>
|
||||||
|
After Width: | Height: | Size: 890 B |
@@ -0,0 +1,12 @@
|
|||||||
|
<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="#454b57"/><stop offset="1" stop-color="#1c2027"/></linearGradient>
|
||||||
|
<linearGradient id="gl" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#fff" stop-opacity="0.28"/><stop offset="0.5" stop-color="#fff" stop-opacity="0"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#bg)"/>
|
||||||
|
<rect x="4" y="4" width="92" height="92" rx="26" fill="url(#gl)"/>
|
||||||
|
<rect x="4.75" y="4.75" width="90.5" height="90.5" rx="25.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>
|
||||||
|
After Width: | Height: | Size: 890 B |