Fix Layout
This commit is contained in:
@@ -1,15 +1,14 @@
|
|||||||
// File: src/app/component/layout/AppShell.tsx
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {ReactNode, useMemo, useState} from "react";
|
import { ReactNode, useMemo, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {usePathname} from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import type {UserSession} from "@/types/auth";
|
import type { UserSession } from "@/types/auth";
|
||||||
|
|
||||||
type NavItem = { label: string; href: string; icon?: React.ReactNode; match?: RegExp };
|
type NavItem = { label: string; href: string; icon?: React.ReactNode; match?: RegExp };
|
||||||
type Props = { user: UserSession; nav: NavItem[]; children: ReactNode };
|
type Props = { user: UserSession; nav: NavItem[]; children: ReactNode };
|
||||||
|
|
||||||
export default function AppShell({user, nav, children}: Props) {
|
export default function AppShell({ user, nav, children }: Props) {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
@@ -24,11 +23,11 @@ export default function AppShell({user, nav, children}: Props) {
|
|||||||
Boolean((item.match && item.match.test(pathname)) || pathname === item.href);
|
Boolean((item.match && item.match.test(pathname)) || pathname === item.href);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-gradient-to-b from-neutral-50 to-white text-neutral-900">
|
<div className="grid min-h-dvh grid-cols-[auto_minmax(0,1fr)] bg-gradient-to-b from-neutral-50 to-white text-neutral-900">
|
||||||
{/* Sidebar (desktop) */}
|
{/* Sidebar (desktop) */}
|
||||||
<aside
|
<aside
|
||||||
className={[
|
className={[
|
||||||
"hidden md:flex sticky top-0 h-screen flex-col min-h-0 border-r border-neutral-200/70 bg-white/70 backdrop-blur",
|
"hidden md:flex sticky top-0 h-dvh flex-col min-h-0 border-r border-neutral-200/70 bg-white/70 backdrop-blur",
|
||||||
"transition-[width] duration-300 ease-out",
|
"transition-[width] duration-300 ease-out",
|
||||||
sidebarOpen ? "w-72" : "w-20",
|
sidebarOpen ? "w-72" : "w-20",
|
||||||
"shadow-[inset_-1px_0_0_rgba(0,0,0,0.03)]",
|
"shadow-[inset_-1px_0_0_rgba(0,0,0,0.03)]",
|
||||||
@@ -37,8 +36,7 @@ export default function AppShell({user, nav, children}: Props) {
|
|||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<div className="h-16 flex items-center px-4">
|
<div className="h-16 flex items-center px-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<div className="h-9 w-9 rounded-2xl bg-neutral-900 text-white grid place-items-center font-semibold shadow-sm">
|
||||||
className="h-9 w-9 rounded-2xl bg-neutral-900 text-white grid place-items-center font-semibold shadow-sm">
|
|
||||||
E
|
E
|
||||||
</div>
|
</div>
|
||||||
{sidebarOpen && (
|
{sidebarOpen && (
|
||||||
@@ -51,7 +49,7 @@ export default function AppShell({user, nav, children}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="mx-4 mb-2 h-px bg-gradient-to-r from-transparent via-neutral-200/70 to-transparent"/>
|
<div className="mx-4 mb-2 h-px bg-gradient-to-r from-transparent via-neutral-200/70 to-transparent" />
|
||||||
|
|
||||||
{/* Tenant pill */}
|
{/* Tenant pill */}
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
@@ -62,8 +60,7 @@ export default function AppShell({user, nav, children}: Props) {
|
|||||||
].join(" ")}
|
].join(" ")}
|
||||||
title="Current tenant"
|
title="Current tenant"
|
||||||
>
|
>
|
||||||
<span
|
<span className="inline-block h-2.5 w-2.5 rounded-full bg-emerald-500/90 shadow-[0_0_0_3px_rgba(16,185,129,0.12)]" />
|
||||||
className="inline-block h-2.5 w-2.5 rounded-full bg-emerald-500/90 shadow-[0_0_0_3px_rgba(16,185,129,0.12)]"/>
|
|
||||||
{sidebarOpen ? (
|
{sidebarOpen ? (
|
||||||
<span className="truncate text-xs text-neutral-700">{user?.tenantKey ?? "—"}</span>
|
<span className="truncate text-xs text-neutral-700">{user?.tenantKey ?? "—"}</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -92,7 +89,7 @@ export default function AppShell({user, nav, children}: Props) {
|
|||||||
"h-8 w-8",
|
"h-8 w-8",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{item.icon ?? <span className="h-1.5 w-1.5 rounded-full bg-current"/>}
|
{item.icon ?? <span className="h-1.5 w-1.5 rounded-full bg-current" />}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{sidebarOpen && <span className="truncate">{item.label}</span>}
|
{sidebarOpen && <span className="truncate">{item.label}</span>}
|
||||||
@@ -108,10 +105,10 @@ export default function AppShell({user, nav, children}: Props) {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main */}
|
{/* Main */}
|
||||||
<div className="flex min-h-screen flex-1 flex-col">
|
<div className="flex min-h-dvh flex-1 flex-col min-w-0">
|
||||||
{/* Topbar */}
|
{/* Topbar */}
|
||||||
<header className="sticky top-0 z-40 border-b border-neutral-200/70 bg-white/70 backdrop-blur">
|
<header className="sticky top-0 z-40 border-b border-neutral-200/70 bg-white/70 backdrop-blur">
|
||||||
<div className="mx-auto flex h-16 max-w-screen-2xl items-center justify-between px-4">
|
<div className="mx-auto flex h-16 w-full max-w-[1280px] items-center justify-between px-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSidebarOpen((v) => !v)}
|
onClick={() => setSidebarOpen((v) => !v)}
|
||||||
@@ -129,10 +126,8 @@ export default function AppShell({user, nav, children}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
<div className="flex items-center gap-2 rounded-2xl border border-neutral-200/70 bg-white/70 px-2.5 py-1.5 shadow-sm">
|
||||||
className="flex items-center gap-2 rounded-2xl border border-neutral-200/70 bg-white/70 px-2.5 py-1.5 shadow-sm">
|
<div className="grid h-7 w-7 place-items-center rounded-xl bg-neutral-900 text-[11px] font-semibold text-white shadow-sm">
|
||||||
<div
|
|
||||||
className="grid h-7 w-7 place-items-center rounded-xl bg-neutral-900 text-[11px] font-semibold text-white shadow-sm">
|
|
||||||
{initials}
|
{initials}
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden leading-tight sm:block">
|
<div className="hidden leading-tight sm:block">
|
||||||
@@ -145,17 +140,15 @@ export default function AppShell({user, nav, children}: Props) {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<main id="main" className="mx-auto w-full max-w-screen-2xl flex-1 p-4 sm:p-6">
|
<main id="main" className="mx-auto w-full max-w-[1280px] flex-1 p-4 sm:p-6 min-w-0">
|
||||||
<div
|
<div className="rounded-3xl border border-neutral-200/70 bg-white/80 p-4 shadow-[0_10px_30px_-12px_rgba(0,0,0,0.08)] sm:p-6">
|
||||||
className="rounded-3xl border border-neutral-200/70 bg-white/80 p-4 shadow-[0_10px_30px_-12px_rgba(0,0,0,0.08)] sm:p-6">
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="mt-auto border-t border-neutral-200/70 bg-white/60 backdrop-blur">
|
<footer className="mt-auto border-t border-neutral-200/70 bg-white/60 backdrop-blur">
|
||||||
<div
|
<div className="mx-auto flex h-12 w-full max-w-[1280px] items-center justify-between px-4 text-xs text-neutral-500">
|
||||||
className="mx-auto flex h-12 max-w-screen-2xl items-center justify-between px-4 text-xs text-neutral-500">
|
|
||||||
<span>© {year} EOP</span>
|
<span>© {year} EOP</span>
|
||||||
<span className="hidden sm:inline">Built for enterprise operations</span>
|
<span className="hidden sm:inline">Built for enterprise operations</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,32 +1,28 @@
|
|||||||
import type { Metadata } from "next";
|
import "./globals.css";
|
||||||
|
import type { Metadata, Viewport } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({ subsets: ["latin"], variable: "--font-geist-sans" });
|
||||||
variable: "--font-geist-sans",
|
const geistMono = Geist_Mono({ subsets: ["latin"], variable: "--font-geist-mono" });
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Enterprise Orchestration Platform",
|
||||||
description: "Generated by create next app",
|
description: "Enterprise Orchestration Platform",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export const viewport: Viewport = {
|
||||||
children,
|
width: "device-width",
|
||||||
}: Readonly<{
|
initialScale: 1,
|
||||||
children: React.ReactNode;
|
viewportFit: "cover",
|
||||||
}>) {
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" className="h-full">
|
||||||
<body
|
{/* ใช้ className ของฟอนต์จริง ไม่ใช่แค่วางเป็น variable */}
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen overflow-x-clip`}
|
<body className={`${geistSans.className} ${geistMono.variable} antialiased min-h-dvh overflow-x-hidden`}>
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user