feat: complete ANL lab homepage with light/dark theme, responsive layout, and 100% SSG E2E test suite
This commit is contained in:
@@ -46,12 +46,12 @@ export default function HeroComposition({
|
||||
width="368"
|
||||
height="368"
|
||||
fill="none"
|
||||
stroke="#0A0A0A"
|
||||
stroke="var(--ink)"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
|
||||
{/* ---- QUIC: flowing multiplexed streams (cobalt) ---- */}
|
||||
<g stroke="#1B3A8A" fill="none" strokeWidth="2.5" strokeLinecap="round">
|
||||
<g stroke="var(--cobalt)" fill="none" strokeWidth="2.5" strokeLinecap="round">
|
||||
{[0, 1, 2, 3].map((i) => {
|
||||
const y = 250 + i * 30;
|
||||
return (
|
||||
@@ -67,7 +67,7 @@ export default function HeroComposition({
|
||||
</g>
|
||||
|
||||
{/* ---- A2A: interconnected node mesh (vermillion) ---- */}
|
||||
<g stroke="#D9342B" strokeWidth="1.5">
|
||||
<g stroke="var(--vermillion)" strokeWidth="1.5">
|
||||
{edges.map(([a, b], i) => (
|
||||
<line
|
||||
key={i}
|
||||
@@ -79,7 +79,7 @@ export default function HeroComposition({
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
<g fill="#D9342B">
|
||||
<g fill="var(--vermillion)">
|
||||
{nodes.map((n, i) => (
|
||||
<circle
|
||||
key={i}
|
||||
@@ -101,14 +101,14 @@ export default function HeroComposition({
|
||||
control points reaching y+40=380). See LAYOUT_AUDIT #3 and
|
||||
REVIEW §3 "P1 #3" — keeping them at y=24 (mirror corners) preserves
|
||||
a symmetric layout that the original LAYOUT_AUDIT mirror decided on. */}
|
||||
<text x="20" y="24" fill="#0A0A0A" fontSize="11" fontWeight="600" letterSpacing="2">
|
||||
<text x="20" y="24" fill="var(--ink)" fontSize="11" fontWeight="600" letterSpacing="2">
|
||||
A2A · MESH
|
||||
</text>
|
||||
<text
|
||||
x="360"
|
||||
y="24"
|
||||
textAnchor="end"
|
||||
fill="#0A0A0A"
|
||||
fill="var(--ink)"
|
||||
fontSize="11"
|
||||
fontWeight="600"
|
||||
letterSpacing="2"
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ==================================================================
|
||||
OFFLINE GEIST FONT STACK (@font-face)
|
||||
================================================================== */
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('/fonts/geist/Geist-Variable.woff2') format('woff2-variations'),
|
||||
url('/fonts/geist/Geist-Variable.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('/fonts/geist/GeistMono-Variable.woff2') format('woff2-variations'),
|
||||
url('/fonts/geist/GeistMono-Variable.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* ==================================================================
|
||||
EDITORIAL DESIGN SYSTEM — global tokens & type
|
||||
CUSTOMIZATION HOOK: design tokens are mirrored from tailwind.config.ts
|
||||
@@ -9,18 +30,62 @@
|
||||
================================================================== */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--ink: #0a0a0a;
|
||||
--ivory: #f5f1e8;
|
||||
--paper: #ece6d6;
|
||||
--line: #d8d1c0;
|
||||
--vermillion: #d9342b;
|
||||
--cobalt: #1b3a8a;
|
||||
|
||||
/* Offline System & CSS Font Variables */
|
||||
--font-geist: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-geist-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--font-display: 'Fraunces', Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-serif-ko: 'Noto Serif KR', 'Nanum Myeongjo', Batang, Georgia, serif;
|
||||
--font-sans: 'Geist', 'Pretendard', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
|
||||
--ink-rgb: 10, 10, 10;
|
||||
--ink-soft-rgb: 38, 36, 32;
|
||||
--ink-mute-rgb: 107, 101, 91;
|
||||
--ivory-rgb: 245, 241, 232;
|
||||
--paper-rgb: 236, 230, 214;
|
||||
--line-rgb: 216, 209, 192;
|
||||
--vermillion-rgb: 217, 52, 43;
|
||||
--vermillion-dark-rgb: 168, 35, 28;
|
||||
--cobalt-rgb: 27, 58, 138;
|
||||
--cobalt-dark-rgb: 18, 40, 99;
|
||||
|
||||
--ink: rgb(var(--ink-rgb));
|
||||
--ink-soft: rgb(var(--ink-soft-rgb));
|
||||
--ink-mute: rgb(var(--ink-mute-rgb));
|
||||
--ivory: rgb(var(--ivory-rgb));
|
||||
--paper: rgb(var(--paper-rgb));
|
||||
--line: rgb(var(--line-rgb));
|
||||
--vermillion: rgb(var(--vermillion-rgb));
|
||||
--vermillion-dark: rgb(var(--vermillion-dark-rgb));
|
||||
--cobalt: rgb(var(--cobalt-rgb));
|
||||
--cobalt-dark: rgb(var(--cobalt-dark-rgb));
|
||||
|
||||
--grain-color: rgba(10, 10, 10, 0.022);
|
||||
|
||||
/* CUSTOMIZATION HOOK — MOTION: global reveal timing */
|
||||
--reveal-duration: 600ms;
|
||||
--reveal-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
html.dark,
|
||||
html[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
|
||||
--ink-rgb: 245, 241, 232;
|
||||
--ink-soft-rgb: 216, 209, 192;
|
||||
--ink-mute-rgb: 160, 153, 142;
|
||||
--ivory-rgb: 18, 18, 18;
|
||||
--paper-rgb: 30, 28, 26;
|
||||
--line-rgb: 51, 48, 43;
|
||||
--vermillion-rgb: 235, 75, 66;
|
||||
--vermillion-dark-rgb: 255, 107, 97;
|
||||
--cobalt-rgb: 77, 119, 255;
|
||||
--cobalt-dark-rgb: 112, 148, 255;
|
||||
|
||||
--grain-color: rgba(245, 241, 232, 0.03);
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
@@ -29,7 +94,7 @@ body {
|
||||
@apply bg-ivory text-ink font-sans antialiased;
|
||||
/* Subtle paper grain — pure CSS, no asset. */
|
||||
background-image: radial-gradient(
|
||||
rgba(10, 10, 10, 0.022) 1px,
|
||||
var(--grain-color) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 4px 4px;
|
||||
|
||||
@@ -1,37 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Fraunces, Noto_Serif_KR, Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
/**
|
||||
* CUSTOMIZATION HOOK — FONTS
|
||||
* The editorial system pairs an English display serif (Fraunces) and a
|
||||
* Korean serif (Noto Serif KR) for headlines, with a clean sans for body.
|
||||
* Pretendard is preferred for body but is not served by Google Fonts, so
|
||||
* Inter is loaded as the web fallback (see tailwind.config.ts sans stack).
|
||||
*/
|
||||
const fraunces = Fraunces({
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600"],
|
||||
style: ["normal", "italic"],
|
||||
variable: "--font-display",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const notoSerifKr = Noto_Serif_KR({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "600", "700"],
|
||||
variable: "--font-serif-ko",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "ANL · AI 에이전트 네트워크 연구실",
|
||||
@@ -41,17 +12,31 @@ export const metadata: Metadata = {
|
||||
"경북대학교 컴퓨터학부 AI 에이전트 네트워크 연구실 (ANL). QUIC, gRPC, A2A 기반 에이전트 통신 및 멀티에이전트 오케스트레이션 연구.",
|
||||
};
|
||||
|
||||
const themeScript = `
|
||||
(function() {
|
||||
try {
|
||||
var storedTheme = localStorage.getItem('theme');
|
||||
var supportDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (storedTheme === 'dark' || (!storedTheme && supportDarkMode)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
`;
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html
|
||||
lang="ko"
|
||||
className={`${fraunces.variable} ${notoSerifKr.variable} ${inter.variable}`}
|
||||
>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||
{/* No-JS fallback for the .reveal scroll-reveal: without this, every
|
||||
page is blank if hydration fails (e.g. Google Fonts request blocked). */}
|
||||
<noscript>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="container-content py-20 sm:py-32 flex flex-col items-center justify-center text-center min-h-[60vh]">
|
||||
<div className="border border-line bg-paper p-8 sm:p-12 rounded-lg max-w-xl w-full space-y-6 shadow-sm">
|
||||
<div className="inline-block px-3 py-1 bg-vermillion/10 text-vermillion font-mono text-xs font-bold rounded tracking-widest uppercase">
|
||||
404 Error
|
||||
</div>
|
||||
<h1 className="font-serif text-3xl sm:text-4xl font-normal text-ink leading-tight">
|
||||
페이지를 찾을 수 없습니다
|
||||
</h1>
|
||||
<p className="text-ink-mute text-sm sm:text-base leading-relaxed">
|
||||
요청하신 페이지가 존재하지 않거나 이전되었을 수 있습니다.
|
||||
<br />
|
||||
입력하신 주소가 올바른지 다시 한번 확인해 주시기 바랍니다.
|
||||
</p>
|
||||
<div className="pt-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center justify-center px-6 py-3 border border-ink bg-ink text-ivory hover:bg-vermillion hover:border-vermillion transition-colors duration-200 text-sm font-medium rounded-md"
|
||||
>
|
||||
메인 페이지로 돌아가기
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user