feat(portfolio): add status bar and floating nav
This commit is contained in:
93
components/portfolio/FeaturedDossier.tsx
Normal file
93
components/portfolio/FeaturedDossier.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { m, useReducedMotion } from "motion/react";
|
||||
import { featuredDossier } from "@/lib/portfolio";
|
||||
|
||||
const fieldRows = [
|
||||
{ label: "MISSION", value: featuredDossier.mission },
|
||||
{ label: "CONSTRAINT", value: featuredDossier.constraint ?? "—" },
|
||||
{ label: "SYSTEM", value: featuredDossier.system },
|
||||
{ label: "RESULT", value: featuredDossier.result },
|
||||
];
|
||||
|
||||
export function FeaturedDossier() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<m.section
|
||||
aria-labelledby="featured-dossier-title"
|
||||
initial={shouldReduceMotion ? false : { opacity: 0, y: 18 }}
|
||||
whileInView={shouldReduceMotion ? undefined : { opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
||||
viewport={{ once: true, amount: 0.18 }}
|
||||
className="mx-auto max-w-7xl px-4 py-20 text-blacksite-text sm:px-6 lg:px-8"
|
||||
>
|
||||
<article className="overflow-hidden border border-blacksite-line bg-blacksite-surface shadow-[0_32px_120px_rgba(0,0,0,0.42)]">
|
||||
<div className="flex items-center justify-between gap-4 border-b border-blacksite-line px-5 py-4 sm:px-7">
|
||||
<p className="section-label">FEATURED DOSSIER /.00</p>
|
||||
<span aria-hidden="true" className="size-2 rounded-full bg-signal-cyan shadow-[0_0_22px_rgba(0,240,255,0.62)]" />
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-hidden border-b border-blacksite-line px-5 pt-8 sm:px-7 lg:px-10">
|
||||
<div aria-hidden="true" className="absolute inset-x-0 top-0 h-px bg-signal-cyan/50" />
|
||||
<h2
|
||||
id="featured-dossier-title"
|
||||
className="font-display translate-y-4 text-[clamp(4.5rem,18vw,15rem)] font-semibold uppercase leading-[0.72] tracking-[-0.08em] text-blacksite-text"
|
||||
>
|
||||
{featuredDossier.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-0 lg:grid-cols-[minmax(0,1fr)_22rem]">
|
||||
<div className="divide-y divide-blacksite-line border-b border-blacksite-line lg:border-b-0 lg:border-r">
|
||||
{fieldRows.map((row) => (
|
||||
<div key={row.label} className="grid gap-3 px-5 py-6 sm:px-7 md:grid-cols-[9rem_minmax(0,1fr)] lg:px-10">
|
||||
<p className="section-label text-signal-cyan">{row.label}</p>
|
||||
<p className="text-sm leading-7 text-blacksite-muted sm:text-base">{row.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<aside className="bg-blacksite-surface2/45 px-5 py-6 sm:px-7 lg:px-8" aria-label="Featured dossier metadata and proof links">
|
||||
<div className="border-b border-blacksite-line pb-6">
|
||||
<p className="section-label mb-3">METADATA</p>
|
||||
<p className="font-mono text-xs uppercase tracking-[0.14em] text-blacksite-text">
|
||||
{featuredDossier.role} <span className="text-signal-cyan">·</span> {featuredDossier.year}
|
||||
</p>
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{featuredDossier.stack.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
className="border border-blacksite-line bg-blacksite-surface px-3 py-1 font-mono text-[11px] uppercase tracking-[0.14em] text-blacksite-muted"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6">
|
||||
<p className="section-label mb-3">PROOF LINKS</p>
|
||||
<div className="grid gap-3">
|
||||
{featuredDossier.links.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="group flex items-center justify-between gap-4 border border-blacksite-line px-4 py-3 font-mono text-xs uppercase tracking-[0.14em] text-blacksite-text transition-colors hover:border-signal-cyan/70 hover:text-signal-cyan focus-visible:outline-signal-cyan"
|
||||
>
|
||||
<span>{link.label}</span>
|
||||
<span aria-hidden="true" className="text-signal-cyan transition-transform group-hover:translate-x-0.5">
|
||||
↗
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</article>
|
||||
</m.section>
|
||||
);
|
||||
}
|
||||
78
components/portfolio/PortfolioNav.tsx
Normal file
78
components/portfolio/PortfolioNav.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const navLinks = [
|
||||
{ label: "WORK", href: "#work" },
|
||||
{ label: "CAPABILITIES", href: "#capabilities" },
|
||||
{ label: "FIELD NOTES", href: "/blog/" },
|
||||
{ label: "CONTACT", href: "#contact" },
|
||||
];
|
||||
|
||||
function openCommandPalette() {
|
||||
window.dispatchEvent(new CustomEvent("blacksite:palette"));
|
||||
}
|
||||
|
||||
export function PortfolioNav() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label="Portfolio navigation"
|
||||
className="sticky top-3 z-50 mx-auto mt-3 w-[calc(100%-2rem)] max-w-5xl rounded-xl border border-blacksite-line bg-blacksite-surface/82 px-3 py-2 text-blacksite-muted shadow-[0_18px_70px_rgba(0,0,0,0.38)] backdrop-blur-md sm:px-4"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
aria-current={pathname === "/" ? "page" : undefined}
|
||||
className="section-label whitespace-nowrap !text-blacksite-text transition-colors hover:!text-signal-cyan focus-visible:outline-signal-cyan aria-[current=page]:!text-signal-cyan"
|
||||
>
|
||||
KRISHNA / SIGNAL
|
||||
</Link>
|
||||
|
||||
<div className="flex min-w-0 flex-1 items-center justify-end gap-1 sm:gap-2">
|
||||
<div className="flex min-w-0 items-center gap-1 overflow-x-auto">
|
||||
{navLinks.map((link) => {
|
||||
const isBlogActive =
|
||||
link.href === "/blog/" &&
|
||||
(pathname === "/blog" || pathname.startsWith("/blog/"));
|
||||
|
||||
if (link.href.startsWith("/")) {
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
aria-current={isBlogActive ? "page" : undefined}
|
||||
className="section-label rounded-md px-2 py-2 transition-colors hover:!text-signal-cyan focus-visible:outline-signal-cyan aria-[current=page]:!text-signal-cyan sm:px-3"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="section-label rounded-md px-2 py-2 transition-colors hover:!text-signal-cyan focus-visible:outline-signal-cyan active:!text-signal-cyan sm:px-3"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={openCommandPalette}
|
||||
className="section-label rounded-lg border border-blacksite-line bg-blacksite-surface2 px-3 py-2 !text-blacksite-text transition-colors hover:border-signal-cyan/70 hover:!text-signal-cyan focus-visible:outline-signal-cyan active:!text-signal-cyan"
|
||||
aria-label="Open command palette"
|
||||
>
|
||||
⌘K
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
23
components/portfolio/StatusBar.tsx
Normal file
23
components/portfolio/StatusBar.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { statusLine } from "@/lib/portfolio";
|
||||
|
||||
export function StatusBar() {
|
||||
return (
|
||||
<div className="border-b border-blacksite-line bg-blacksite-bg text-blacksite-muted">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-2 sm:px-6 lg:px-8">
|
||||
<p className="section-label flex min-w-0 items-center gap-2">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="size-1.5 shrink-0 rounded-full bg-signal-green shadow-[0_0_14px_rgba(182,255,0,0.45)]"
|
||||
/>
|
||||
<span className="truncate">{statusLine}</span>
|
||||
</p>
|
||||
<a
|
||||
href="#contact"
|
||||
className="section-label shrink-0 !text-blacksite-text transition-colors hover:!text-signal-cyan focus-visible:outline-signal-cyan"
|
||||
>
|
||||
Contact ↗
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user