mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
Fix: Responsive design for mobile (iPhone SE)
- SiteNav: Add hamburger menu for mobile, hide desktop links on small screens - StatCard: Stack vertically on mobile (icon+label top, number bottom) - Icon: Add line3Horizontal and xmark custom icons for hamburger menu
This commit is contained in:
@@ -66,6 +66,41 @@ const sfIcons = {
|
||||
|
||||
/** Custom SVG icons not available in SF Symbols. */
|
||||
const customIcons = {
|
||||
/** Three horizontal lines — hamburger menu icon. */
|
||||
line3Horizontal: (size: number) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="4" y1="6" x2="20" y2="6" />
|
||||
<line x1="4" y1="12" x2="20" y2="12" />
|
||||
<line x1="4" y1="18" x2="20" y2="18" />
|
||||
</svg>
|
||||
),
|
||||
/** X mark — close icon. */
|
||||
xmark: (size: number) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
),
|
||||
/** Two overlapping rectangles — standard copy-to-clipboard metaphor. */
|
||||
copy: (size: number) => (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import Icon from "./Icon";
|
||||
@@ -25,7 +26,7 @@ interface NavLink {
|
||||
|
||||
const NAV_LINKS: NavLink[] = [
|
||||
{ href: "/dashboard", label: "Dashboard", icon: "chart", page: "dashboard" },
|
||||
{ href: "/documentation/tuikit", label: "Documentation", icon: "book" },
|
||||
{ href: "/documentation/tuikit", label: "Docs", icon: "book" },
|
||||
{ href: "https://github.com/phranck/TUIkit", label: "GitHub", icon: "code", external: true },
|
||||
];
|
||||
|
||||
@@ -34,28 +35,34 @@ const NAV_LINKS: NavLink[] = [
|
||||
*
|
||||
* Renders the TUIkit logo as a home link, navigation items with optional
|
||||
* active state, and the theme switcher. Fixed at the top with backdrop blur.
|
||||
* On mobile, shows a hamburger menu that expands to show links.
|
||||
*/
|
||||
export default function SiteNav({ activePage }: SiteNavProps) {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<nav aria-label="Main navigation" className="fixed top-0 z-50 w-full border-b border-border/50 bg-background/80 backdrop-blur-xl">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-3 sm:px-6 sm:py-4">
|
||||
{/* Logo + brand */}
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<Image
|
||||
src="/tuikit-logo.png"
|
||||
alt="TUIkit Logo"
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-lg"
|
||||
width={28}
|
||||
height={28}
|
||||
className="rounded-lg sm:h-8 sm:w-8"
|
||||
/>
|
||||
{activePage === "home" ? (
|
||||
<span className="text-2xl font-semibold text-foreground">TUIkit</span>
|
||||
<span className="text-xl font-semibold text-foreground sm:text-2xl">TUIkit</span>
|
||||
) : (
|
||||
<Link href="/" className="text-2xl font-semibold text-foreground transition-colors hover:text-accent">
|
||||
<Link href="/" className="text-xl font-semibold text-foreground transition-colors hover:text-accent sm:text-2xl">
|
||||
TUIkit
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
|
||||
{/* Desktop nav links */}
|
||||
<div className="hidden items-center gap-4 sm:flex sm:gap-6">
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = link.page === activePage;
|
||||
|
||||
@@ -63,7 +70,7 @@ export default function SiteNav({ activePage }: SiteNavProps) {
|
||||
return (
|
||||
<span
|
||||
key={link.href}
|
||||
className="flex items-center gap-1.5 text-lg text-foreground"
|
||||
className="flex items-center gap-1.5 text-base text-foreground sm:text-lg"
|
||||
aria-current="page"
|
||||
>
|
||||
{link.icon && <Icon name={link.icon} size={18} className="text-current" />}
|
||||
@@ -77,7 +84,7 @@ export default function SiteNav({ activePage }: SiteNavProps) {
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
{...(link.external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
||||
className="flex items-center gap-1.5 text-lg text-muted transition-colors hover:text-foreground"
|
||||
className="flex items-center gap-1.5 text-base text-muted transition-colors hover:text-foreground sm:text-lg"
|
||||
>
|
||||
{link.icon && <Icon name={link.icon} size={18} className="text-current" />}
|
||||
{link.label}
|
||||
@@ -88,7 +95,58 @@ export default function SiteNav({ activePage }: SiteNavProps) {
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile: theme switcher + hamburger */}
|
||||
<div className="flex items-center gap-3 sm:hidden">
|
||||
<ThemeSwitcher />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted transition-colors hover:bg-accent/10 hover:text-foreground"
|
||||
aria-label={menuOpen ? "Close menu" : "Open menu"}
|
||||
aria-expanded={menuOpen}
|
||||
>
|
||||
<Icon name={menuOpen ? "xmark" : "line3Horizontal"} size={22} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu dropdown */}
|
||||
{menuOpen && (
|
||||
<div className="border-t border-border/50 bg-background/95 px-4 py-4 backdrop-blur-xl sm:hidden">
|
||||
<div className="flex flex-col gap-3">
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = link.page === activePage;
|
||||
|
||||
if (isActive) {
|
||||
return (
|
||||
<span
|
||||
key={link.href}
|
||||
className="flex items-center gap-2 rounded-lg bg-accent/10 px-3 py-2 text-base text-foreground"
|
||||
aria-current="page"
|
||||
>
|
||||
{link.icon && <Icon name={link.icon} size={18} className="text-accent" />}
|
||||
{link.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
{...(link.external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="flex items-center gap-2 rounded-lg px-3 py-2 text-base text-muted transition-colors hover:bg-accent/5 hover:text-foreground"
|
||||
>
|
||||
{link.icon && <Icon name={link.icon} size={18} className="text-current" />}
|
||||
{link.label}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,10 @@ interface StatCardProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* A single metric card with icon + label on top and the number on the right.
|
||||
* A single metric card with icon + label on top and the number below.
|
||||
*
|
||||
* On mobile: stacked vertically (icon+label top, number bottom center).
|
||||
* On larger screens: horizontal layout (icon+label left, number right).
|
||||
*
|
||||
* When `onClick` is provided, renders as a `<button>` with native keyboard
|
||||
* and focus support. Otherwise renders as a static `<div>`.
|
||||
@@ -29,7 +32,7 @@ interface StatCardProps {
|
||||
export default function StatCard({ label, value, icon, loading = false, onClick, active = false, id }: StatCardProps) {
|
||||
const interactive = !!onClick;
|
||||
|
||||
const baseClasses = "flex w-full items-center justify-between rounded-xl border p-5 backdrop-blur-xl transition-all duration-300";
|
||||
const baseClasses = "flex w-full flex-col items-center gap-2 rounded-xl border p-4 backdrop-blur-xl transition-all duration-300 sm:flex-row sm:items-center sm:justify-between sm:gap-3 sm:p-5";
|
||||
const stateClasses = active
|
||||
? "border-accent/50 bg-accent/10"
|
||||
: "border-border bg-frosted-glass hover:border-accent/30";
|
||||
@@ -39,20 +42,20 @@ export default function StatCard({ label, value, icon, loading = false, onClick,
|
||||
const className = `${baseClasses} ${stateClasses} ${interactiveClasses}`;
|
||||
|
||||
const content = loading ? (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full flex-col items-center gap-2 sm:flex-row sm:justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-6 w-6 rounded-md bg-accent/10 animate-skeleton" />
|
||||
<div className="h-5 w-16 rounded-md bg-accent/10 animate-skeleton" />
|
||||
<div className="h-5 w-5 rounded-md bg-accent/10 animate-skeleton sm:h-6 sm:w-6" />
|
||||
<div className="h-4 w-14 rounded-md bg-accent/10 animate-skeleton sm:h-5 sm:w-16" />
|
||||
</div>
|
||||
<div className="h-8 w-14 rounded-md bg-accent/10 animate-skeleton" />
|
||||
<div className="h-7 w-12 rounded-md bg-accent/10 animate-skeleton sm:h-8 sm:w-14" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="flex items-center gap-2 text-lg text-muted">
|
||||
<Icon name={icon} size={22} className="text-accent" />
|
||||
{label}
|
||||
<p className="flex items-center gap-1.5 text-sm text-muted sm:gap-2 sm:text-lg">
|
||||
<Icon name={icon} size={18} className="text-accent sm:h-[22px] sm:w-[22px]" />
|
||||
<span className="whitespace-nowrap">{label}</span>
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-foreground text-glow tabular-nums">
|
||||
<p className="text-2xl font-bold text-foreground text-glow tabular-nums sm:text-3xl">
|
||||
{value.toLocaleString()}
|
||||
</p>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user