Static-export Next.js 16 app with the tactical-map hero, Dajt terrain background, dossier pages and the projects node index. Co-Authored-By: Claude Opus 5.5 <[email protected]>
94 lines
3.8 KiB
TypeScript
94 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { featured, projectById, projects } from "@/content/projects";
|
|
import { Segment } from "./Segment";
|
|
|
|
const ROOT = featured[0];
|
|
|
|
export function FeaturedNodes() {
|
|
const [active, setActive] = useState<string | null>(null);
|
|
const nodes = featured.map((f) => ({ ...f, project: projectById(f.id)! }));
|
|
const stageRef = useRef<HTMLDivElement>(null);
|
|
const [size, setSize] = useState<{ w: number; h: number } | null>(null);
|
|
|
|
// Lines are drawn in px so they stay 1px crisp at any stage size.
|
|
useEffect(() => {
|
|
const el = stageRef.current;
|
|
if (!el) return;
|
|
const ro = new ResizeObserver(([e]) => setSize({ w: e.contentRect.width, h: e.contentRect.height }));
|
|
ro.observe(el);
|
|
return () => ro.disconnect();
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
ref={stageRef}
|
|
className="bracket grid-bg relative mt-8 flex flex-col gap-1 border border-white/6 bg-surface p-6 [--bc:rgb(242_244_245/0.35)] md:block md:h-[460px] md:p-0"
|
|
onMouseLeave={() => setActive(null)}
|
|
>
|
|
{size && (
|
|
<div aria-hidden="true" className="hidden md:block">
|
|
{nodes.slice(1).map((n) => (
|
|
<Segment
|
|
key={`${n.id}-${active === n.id}`}
|
|
x1={(ROOT.x / 100) * size.w}
|
|
y1={(ROOT.y / 100) * size.h}
|
|
x2={(n.x / 100) * size.w}
|
|
y2={(n.y / 100) * size.h}
|
|
on={active === n.id}
|
|
dashed
|
|
strong
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{nodes.map((n) => {
|
|
const on = active === n.id;
|
|
const root = n.project.root;
|
|
return (
|
|
<Link
|
|
key={n.id}
|
|
href={`/projects/#${n.id}`}
|
|
onMouseEnter={() => setActive(n.id)}
|
|
onFocus={() => setActive(n.id)}
|
|
onBlur={() => setActive(null)}
|
|
className="group relative block min-h-12 outline-none md:absolute md:top-(--y) md:left-(--x) md:min-h-0"
|
|
style={{ "--x": `${n.x}%`, "--y": `${n.y}%` } as React.CSSProperties}
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
className={`absolute box-border border ${
|
|
root
|
|
? "top-2.5 left-0 size-7 rounded-full md:-top-3.5 md:-left-3.5"
|
|
: "top-4 left-2 size-3 md:-top-1.5 md:-left-1.5"
|
|
} ${on ? "border-accent shadow-[0_0_14px_var(--color-accent)]" : "border-ink/60"} ${on && !root ? "bg-accent" : "bg-night"}`}
|
|
>
|
|
{root && <span className={`absolute top-[9px] left-[9px] size-2 rounded-full ${on ? "bg-accent" : "bg-ink"}`} />}
|
|
</span>
|
|
<span
|
|
className={`bracket ml-10 block min-h-11 px-3 py-2.5 group-focus-visible:outline group-focus-visible:outline-accent md:absolute md:-top-[22px] md:ml-0 ${
|
|
root ? "md:right-6 md:w-max md:text-right" : "md:left-6 md:w-[280px]"
|
|
} ${
|
|
on ? "[--bc:var(--color-accent)]" : "[--bc:transparent]"
|
|
}`}
|
|
>
|
|
<span className={`flex items-baseline gap-3 ${root ? "md:flex-row-reverse" : ""}`}>
|
|
<span className="font-display text-lg font-semibold tracking-[0.08em] uppercase">{n.project.name}</span>
|
|
<span className="text-[9px] tracking-[0.22em] text-mute">{root ? "ROOT" : n.project.status}</span>
|
|
</span>
|
|
<span className={`mt-1.5 block text-[11px] leading-[1.55] text-ink-2 ${on && !root ? "" : "md:sr-only"}`}>{n.project.desc}</span>
|
|
</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
|
|
<div aria-hidden="true" className="absolute right-4 bottom-3 hidden text-[9px] tracking-[0.2em] text-mute/70 md:block">
|
|
ROOT ORUS · {featured.length} OF {projects.length} NODES
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|