diff --git a/components/portfolio/WorkIndex.tsx b/components/portfolio/WorkIndex.tsx new file mode 100644 index 0000000..44610c1 --- /dev/null +++ b/components/portfolio/WorkIndex.tsx @@ -0,0 +1,244 @@ +"use client"; + +import { useState } from "react"; +import { m, useReducedMotion } from "motion/react"; + +import { dossiers, type Accent, type Dossier } from "@/lib/portfolio"; + +const accentText: Record = { + cyan: "text-signal-cyan", + orange: "text-signal-orange", + green: "text-signal-green", + violet: "text-signal-violet", +}; + +const accentBorder: Record = { + cyan: "border-l-signal-cyan", + orange: "border-l-signal-orange", + green: "border-l-signal-green", + violet: "border-l-signal-violet", +}; + +const accentGlow: Record = { + cyan: "shadow-[0_0_32px_rgba(0,240,255,0.12)]", + orange: "shadow-[0_0_32px_rgba(255,90,31,0.12)]", + green: "shadow-[0_0_32px_rgba(182,255,0,0.1)]", + violet: "shadow-[0_0_32px_rgba(128,93,255,0.12)]", +}; + +function isLiveStatus(status: string) { + return /\b(live|deployed)\b/i.test(status); +} + +function headlineProof(dossier: Dossier) { + return dossier.proof.find((proof) => proof.metric) ?? dossier.proof[0]; +} + +export function WorkIndex() { + const shouldReduceMotion = useReducedMotion(); + const [activeId, setActiveId] = useState(null); + + return ( +
+
+
+

WORK INDEX /.04

+

+ Eight field dossiers. No thumbnails. +

+
+

+ Proof-first project records: mission, system constraints, and measurable + outcomes are exposed on hover, keyboard focus, touch, and reduced motion. +

+
+ + +
+ PROJECT + TYPE + STATUS + YEAR +
+ +
+ {dossiers.map((dossier, index) => { + const isActive = activeId === dossier.id; + const shouldDim = activeId !== null && !isActive; + const proof = headlineProof(dossier); + const detailsExpanded = shouldReduceMotion || isActive; + const liveStatus = isLiveStatus(dossier.status); + + return ( +
setActiveId(dossier.id)} + onMouseLeave={() => setActiveId(null)} + onFocus={() => setActiveId(dossier.id)} + onBlur={(event) => { + if ( + !event.currentTarget.contains( + event.relatedTarget as Node | null, + ) + ) { + setActiveId(null); + } + }} + className={`group border-l-2 border-l-transparent bg-blacksite-bg2/20 px-4 py-5 outline-none transition duration-200 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-signal-cyan ${ + isActive + ? `${accentBorder[dossier.accent]} bg-blacksite-surface/70 ${accentGlow[dossier.accent]}` + : "" + } ${shouldDim ? "opacity-45" : "opacity-100"}`} + > +
+
+

+ PROJECT +

+
+ + {String(index + 1).padStart(2, "0")} + +

+ {dossier.title} +

+
+
+ +
+

+ TYPE +

+

+ {dossier.type} +

+
+ +
+

+ STATUS +

+

+ {dossier.status} +

+
+ +
+

+ YEAR +

+

+ {dossier.year} +

+
+
+ + +
+
+

MISSION

+

+ {dossier.mission} +

+ {dossier.constraint ? ( +

+ Constraint: {dossier.constraint} +

+ ) : null} +
+ +
+

STACK

+
+ {dossier.stack.map((item) => ( + + {item} + + ))} +
+
+ +
+

PROOF

+

+ {proof?.metric ?? proof?.label ?? dossier.result} +

+

+ {proof?.metric ? proof.label : dossier.result} +

+
+ +
+

LINKS

+ {dossier.links.length > 0 ? ( +
+ {dossier.links.map((link) => ( + + {link.label} ↗ + + ))} +
+ ) : ( +

+ Closed / internal artifact +

+ )} +
+
+
+
+ ); + })} +
+
+
+ ); +}