Use Mono.png as profile picture and favicon; sync proof statuses
- Dossier proof statuses now come from projects.ts - Mono.png replaces the placeholder silhouette and the SVG favicon - Header, hero and bust tweaks Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -17,7 +17,7 @@ npm run lint
|
|||||||
- `profile.ts`: name, title, stance, dossier fields, links, map lock.
|
- `profile.ts`: name, title, stance, dossier fields, links, map lock.
|
||||||
- `dossier.ts`: the five About files.
|
- `dossier.ts`: the five About files.
|
||||||
- `projects.ts`: projects, statuses, node-map positions, featured nodes.
|
- `projects.ts`: projects, statuses, node-map positions, featured nodes.
|
||||||
- `public/profile/pfp.svg`: placeholder profile picture. Replace this file (or point `profile.pfp` at a new one).
|
- `public/profile/Mono.png`: profile picture (the favicon is a copy at `src/app/icon.png`). Replace the file, or point `profile.pfp` at a new one.
|
||||||
- `public/terrain.svg`: hero terrain contours (Dajt elevation data).
|
- `public/terrain.svg`: hero terrain contours (Dajt elevation data).
|
||||||
- `src/app/globals.css`: colour tokens, bracket/grid utilities, and all motion.
|
- `src/app/globals.css`: colour tokens, bracket/grid utilities, and all motion.
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 200">
|
|
||||||
<!-- PLACEHOLDER profile picture: replace this file with the real one. -->
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0" stop-color="#3a4247"/>
|
|
||||||
<stop offset="1" stop-color="#101315"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<path fill="url(#g)" d="M84,28 C108,28 122,46 122,72 C122,96 112,114 100,122 L102,136 C130,142 156,156 164,182 L166,200 L2,200 L4,182 C12,156 38,142 66,136 L68,122 C56,114 46,96 46,72 C46,46 60,28 84,28Z"/>
|
|
||||||
<path fill="none" stroke="#f2f4f5" stroke-opacity=".3" d="M84,28 C108,28 122,46 122,72 C122,96 112,114 100,122 L102,136 C130,142 156,156 164,182 L166,200 L2,200 L4,182 C12,156 38,142 66,136 L68,122 C56,114 46,96 46,72 C46,46 60,28 84,28Z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 788 B |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
|
||||||
<rect width="24" height="24" fill="#050607"/>
|
|
||||||
<path d="M3,8.5 V3 H8.5 M15.5,3 H21 V8.5 M21,15.5 V21 H15.5 M8.5,21 H3 V15.5" stroke="#f2f4f5" stroke-width="1.8"/>
|
|
||||||
<rect x="9.25" y="9.25" width="5.5" height="5.5" fill="#4dff95"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 314 B |
+36
-10
@@ -8,17 +8,31 @@ import { useRef } from "react";
|
|||||||
* "wireframe", a scan sweeps across and resolves it to solid, and on desktop
|
* "wireframe", a scan sweeps across and resolves it to solid, and on desktop
|
||||||
* the layers tilt apart slightly with the pointer. No 3D library needed.
|
* the layers tilt apart slightly with the pointer. No 3D library needed.
|
||||||
*/
|
*/
|
||||||
export function Bust({ src, delay = "0s", className = "" }: { src: string; delay?: string; className?: string }) {
|
export function Bust({
|
||||||
|
src,
|
||||||
|
delay = "0s",
|
||||||
|
className = "",
|
||||||
|
}: {
|
||||||
|
src: string;
|
||||||
|
delay?: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
const tiltRef = useRef<HTMLDivElement>(null);
|
const tiltRef = useRef<HTMLDivElement>(null);
|
||||||
const meshRef = useRef<HTMLDivElement>(null);
|
const meshRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
function onMove(e: React.PointerEvent<HTMLDivElement>) {
|
function onMove(e: React.PointerEvent<HTMLDivElement>) {
|
||||||
if (e.pointerType !== "mouse" || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
if (
|
||||||
|
e.pointerType !== "mouse" ||
|
||||||
|
matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||||
|
)
|
||||||
|
return;
|
||||||
const r = e.currentTarget.getBoundingClientRect();
|
const r = e.currentTarget.getBoundingClientRect();
|
||||||
const x = (e.clientX - r.left) / r.width - 0.5;
|
const x = (e.clientX - r.left) / r.width - 0.5;
|
||||||
const y = (e.clientY - r.top) / r.height - 0.5;
|
const y = (e.clientY - r.top) / r.height - 0.5;
|
||||||
if (tiltRef.current) tiltRef.current.style.transform = `rotateX(${(-y * 8).toFixed(2)}deg) rotateY(${(x * 10).toFixed(2)}deg)`;
|
if (tiltRef.current)
|
||||||
if (meshRef.current) meshRef.current.style.transform = `translate(${(x * -6).toFixed(1)}px, ${(y * -6).toFixed(1)}px)`;
|
tiltRef.current.style.transform = `rotateX(${(-y * 8).toFixed(2)}deg) rotateY(${(x * 10).toFixed(2)}deg)`;
|
||||||
|
if (meshRef.current)
|
||||||
|
meshRef.current.style.transform = `translate(${(x * -6).toFixed(1)}px, ${(y * -6).toFixed(1)}px)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onLeave() {
|
function onLeave() {
|
||||||
@@ -28,11 +42,14 @@ export function Bust({ src, delay = "0s", className = "" }: { src: string; delay
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative aspect-[168/200] [perspective:600px] ${className}`}
|
className={`relative aspect-square [perspective:600px] ${className}`}
|
||||||
onPointerMove={onMove}
|
onPointerMove={onMove}
|
||||||
onPointerLeave={onLeave}
|
onPointerLeave={onLeave}
|
||||||
>
|
>
|
||||||
<div ref={tiltRef} className="absolute inset-0 transition-transform duration-200 ease-out [transform-style:preserve-3d]">
|
<div
|
||||||
|
ref={tiltRef}
|
||||||
|
className="absolute inset-0 transition-transform duration-200 ease-out [transform-style:preserve-3d]"
|
||||||
|
>
|
||||||
{/* wireframe pass */}
|
{/* wireframe pass */}
|
||||||
<div className="absolute inset-0 [mask-image:repeating-linear-gradient(0deg,#000_0_1px,transparent_1px_5px)]">
|
<div className="absolute inset-0 [mask-image:repeating-linear-gradient(0deg,#000_0_1px,transparent_1px_5px)]">
|
||||||
<Image
|
<Image
|
||||||
@@ -44,15 +61,24 @@ export function Bust({ src, delay = "0s", className = "" }: { src: string; delay
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* solid pass */}
|
{/* solid pass */}
|
||||||
<div className="bust-solid seq absolute inset-0" style={{ "--d": delay } as React.CSSProperties}>
|
<div
|
||||||
<Image src={src} alt="Profile picture" fill sizes="168px" className="object-cover" />
|
className="bust-solid seq absolute inset-0"
|
||||||
|
style={{ "--d": delay } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt="Profile picture"
|
||||||
|
fill
|
||||||
|
sizes="168px"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* residual mesh that floats a few px above the photo */}
|
{/* residual mesh that floats a few px above the photo */}
|
||||||
<div
|
{/*<div
|
||||||
ref={meshRef}
|
ref={meshRef}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="absolute inset-0 opacity-[0.12] transition-transform duration-200 ease-out [background-image:repeating-linear-gradient(0deg,#f2f4f5_0_1px,transparent_1px_8px)] [mask-image:linear-gradient(90deg,#000,transparent_70%)]"
|
className="absolute inset-0 opacity-[0.12] transition-transform duration-200 ease-out [background-image:repeating-linear-gradient(0deg,#f2f4f5_0_1px,transparent_1px_8px)] [mask-image:linear-gradient(90deg,#000,transparent_70%)]"
|
||||||
/>
|
/>*/}
|
||||||
{/* scan line */}
|
{/* scan line */}
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|||||||
+96
-27
@@ -1,5 +1,10 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { dossierFields, lock, profile, type DossierField } from "@/content/profile";
|
import {
|
||||||
|
dossierFields,
|
||||||
|
lock,
|
||||||
|
profile,
|
||||||
|
type DossierField,
|
||||||
|
} from "@/content/profile";
|
||||||
import { Bust } from "./Bust";
|
import { Bust } from "./Bust";
|
||||||
import { Scramble } from "./Scramble";
|
import { Scramble } from "./Scramble";
|
||||||
import { LocalTime, SessionStamp } from "./Telemetry";
|
import { LocalTime, SessionStamp } from "./Telemetry";
|
||||||
@@ -12,22 +17,42 @@ const d = (s: number) => ({ "--d": `${s}s` }) as React.CSSProperties;
|
|||||||
const DOSSIER_START = 1.35;
|
const DOSSIER_START = 1.35;
|
||||||
const DOSSIER_STAGGER = 0.14;
|
const DOSSIER_STAGGER = 0.14;
|
||||||
|
|
||||||
function DossierList({ fields, className = "" }: { fields: DossierField[]; className?: string }) {
|
function DossierList({
|
||||||
|
fields,
|
||||||
|
className = "",
|
||||||
|
}: {
|
||||||
|
fields: DossierField[];
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<dl className={`grid grid-cols-[124px_minmax(0,1fr)] gap-y-2.5 border-t border-white/8 pt-[18px] text-xs leading-[1.45] xl:grid-cols-[116px_minmax(0,1fr)] ${className}`}>
|
<dl
|
||||||
|
className={`grid grid-cols-[124px_minmax(0,1fr)] gap-y-2.5 border-t border-white/8 pt-[18px] text-xs leading-[1.45] xl:grid-cols-[116px_minmax(0,1fr)] ${className}`}
|
||||||
|
>
|
||||||
{fields.map((f, i) => {
|
{fields.map((f, i) => {
|
||||||
const delay = DOSSIER_START + i * DOSSIER_STAGGER;
|
const delay = DOSSIER_START + i * DOSSIER_STAGGER;
|
||||||
return (
|
return (
|
||||||
<div key={f.label} className="contents">
|
<div key={f.label} className="contents">
|
||||||
<dt className="pt-[3px] text-[9px] uppercase tracking-[0.22em] text-mute">{f.label}</dt>
|
<dt className="pt-[3px] text-[9px] uppercase tracking-[0.22em] text-mute">
|
||||||
|
{f.label}
|
||||||
|
</dt>
|
||||||
<dd className="flex flex-wrap gap-x-3.5">
|
<dd className="flex flex-wrap gap-x-3.5">
|
||||||
{f.links
|
{f.links ? (
|
||||||
? f.links.map((l, j) => (
|
f.links.map((l, j) => (
|
||||||
<a key={l.href} href={l.href} className="border-b border-white/25 hover:border-accent">
|
<a
|
||||||
<Scramble text={l.label} delay={delay + j * 0.2} step={30} />
|
key={l.href}
|
||||||
|
href={l.href}
|
||||||
|
className="border-b border-white/25 hover:border-accent"
|
||||||
|
>
|
||||||
|
<Scramble
|
||||||
|
text={l.label}
|
||||||
|
delay={delay + j * 0.2}
|
||||||
|
step={30}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
))
|
))
|
||||||
: <Scramble text={f.value} delay={delay} step={30} />}
|
) : (
|
||||||
|
<Scramble text={f.value} delay={delay} step={30} />
|
||||||
|
)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -38,7 +63,10 @@ function DossierList({ fields, className = "" }: { fields: DossierField[]; class
|
|||||||
|
|
||||||
export function Hero() {
|
export function Hero() {
|
||||||
return (
|
return (
|
||||||
<section aria-labelledby="hero-title" className="relative overflow-hidden border-b border-line xl:flex xl:min-h-dvh xl:items-center">
|
<section
|
||||||
|
aria-labelledby="hero-title"
|
||||||
|
className="relative overflow-hidden border-b border-line xl:flex xl:min-h-dvh xl:items-center"
|
||||||
|
>
|
||||||
<TerrainLayer />
|
<TerrainLayer />
|
||||||
|
|
||||||
<div className="relative z-10 mx-auto w-full max-w-[1440px] pt-[68px] md:pt-[84px] xl:h-[864px]">
|
<div className="relative z-10 mx-auto w-full max-w-[1440px] pt-[68px] md:pt-[84px] xl:h-[864px]">
|
||||||
@@ -57,7 +85,12 @@ export function Hero() {
|
|||||||
<Scramble text="LOCK ACQUIRED" delay={0.85} step={35} nowrap />
|
<Scramble text="LOCK ACQUIRED" delay={0.85} step={35} nowrap />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Scramble text={`${lock.lat} · ${lock.lon}`} delay={0.95} step={30} nowrap />
|
<Scramble
|
||||||
|
text={`${lock.lat} · ${lock.lon}`}
|
||||||
|
delay={0.95}
|
||||||
|
step={30}
|
||||||
|
nowrap
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="uppercase">
|
<div className="uppercase">
|
||||||
<Scramble text={lock.place} delay={1.05} step={30} nowrap />
|
<Scramble text={lock.place} delay={1.05} step={30} nowrap />
|
||||||
@@ -67,7 +100,9 @@ export function Hero() {
|
|||||||
className="trace seq absolute top-[233px] right-[510px] left-[calc(var(--lx)+66px)] hidden h-px bg-accent/55 xl:block"
|
className="trace seq absolute top-[233px] right-[510px] left-[calc(var(--lx)+66px)] hidden h-px bg-accent/55 xl:block"
|
||||||
style={d(0.9)}
|
style={d(0.9)}
|
||||||
>
|
>
|
||||||
<span className="absolute -top-4 left-0 text-[9px] whitespace-nowrap tracking-[0.2em] text-accent">TRACE ▸ 0001</span>
|
<span className="absolute -top-4 left-0 text-[9px] whitespace-nowrap tracking-[0.2em] text-accent">
|
||||||
|
TRACE ▸ 0001
|
||||||
|
</span>
|
||||||
<span className="absolute -top-[3px] -right-[3px] size-[7px] bg-accent" />
|
<span className="absolute -top-[3px] -right-[3px] size-[7px] bg-accent" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,7 +111,11 @@ export function Hero() {
|
|||||||
{/* Headline */}
|
{/* Headline */}
|
||||||
<div className="xl:absolute xl:top-[364px] xl:left-16 xl:w-[620px]">
|
<div className="xl:absolute xl:top-[364px] xl:left-16 xl:w-[620px]">
|
||||||
<p className="mb-3 text-[9px] tracking-[0.28em] text-mute xl:mb-5 xl:text-[10px] xl:tracking-[0.3em]">
|
<p className="mb-3 text-[9px] tracking-[0.28em] text-mute xl:mb-5 xl:text-[10px] xl:tracking-[0.3em]">
|
||||||
<Scramble text="TARGET RESOLVED · SUBJECT 0001" delay={0.2} step={15} />
|
<Scramble
|
||||||
|
text="TARGET RESOLVED · SUBJECT 0001"
|
||||||
|
delay={0.2}
|
||||||
|
step={15}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
<h1
|
<h1
|
||||||
id="hero-title"
|
id="hero-title"
|
||||||
@@ -86,10 +125,13 @@ export function Hero() {
|
|||||||
<br />
|
<br />
|
||||||
<Scramble text="& BUILDER" delay={0.5} step={35} nowrap />
|
<Scramble text="& BUILDER" delay={0.5} step={35} nowrap />
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-4 text-[15px] leading-normal text-ink-2 xl:mt-6 xl:text-[17px]">
|
{/*<p className="mt-4 text-[15px] leading-normal text-ink-2 xl:mt-6 xl:text-[17px]">
|
||||||
<Scramble text={profile.stance} delay={0.7} step={15} />
|
<Scramble text={profile.stance} delay={0.7} step={15} />
|
||||||
</p>
|
</p>*/}
|
||||||
<div className="resolve seq mt-7 flex flex-col gap-2.5 sm:flex-row sm:flex-wrap sm:gap-3 xl:mt-10" style={d(1.4)}>
|
<div
|
||||||
|
className="resolve seq mt-7 flex flex-col gap-2.5 sm:flex-row sm:flex-wrap sm:gap-3 xl:mt-10"
|
||||||
|
style={d(1.4)}
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
href="/projects/"
|
href="/projects/"
|
||||||
className="inline-flex min-h-12 items-center justify-between gap-3 bg-ink px-[22px] text-xs font-medium tracking-[0.2em] text-night transition-colors hover:bg-accent hover:text-night"
|
className="inline-flex min-h-12 items-center justify-between gap-3 bg-ink px-[22px] text-xs font-medium tracking-[0.2em] text-night transition-colors hover:bg-accent hover:text-night"
|
||||||
@@ -100,7 +142,7 @@ export function Hero() {
|
|||||||
href="/about/"
|
href="/about/"
|
||||||
className="bracket inline-flex min-h-12 items-center justify-between gap-3 border border-white/12 px-[22px] text-xs tracking-[0.2em] transition-colors [--bc:rgb(242_244_245/0.6)] hover:border-transparent hover:text-accent hover:[--bc:var(--color-accent)]"
|
className="bracket inline-flex min-h-12 items-center justify-between gap-3 border border-white/12 px-[22px] text-xs tracking-[0.2em] transition-colors [--bc:rgb(242_244_245/0.6)] hover:border-transparent hover:text-accent hover:[--bc:var(--color-accent)]"
|
||||||
>
|
>
|
||||||
OPEN DOSSIER <span aria-hidden="true">↗</span>
|
ABOUT ME <span aria-hidden="true">↗</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<DossierList fields={dossierFields} className="mt-9 xl:hidden" />
|
<DossierList fields={dossierFields} className="mt-9 xl:hidden" />
|
||||||
@@ -120,37 +162,64 @@ export function Hero() {
|
|||||||
<figure className="bracket w-[112px] flex-none [--bc:rgb(242_244_245/0.4)] xl:w-[168px]">
|
<figure className="bracket w-[112px] flex-none [--bc:rgb(242_244_245/0.4)] xl:w-[168px]">
|
||||||
<Bust src={profile.pfp} delay="1.2s" />
|
<Bust src={profile.pfp} delay="1.2s" />
|
||||||
{profile.pfpIsPlaceholder && (
|
{profile.pfpIsPlaceholder && (
|
||||||
<figcaption className="pt-2 pb-0.5 text-[9px] tracking-[0.2em] text-mute max-xl:hidden">PFP · PLACEHOLDER</figcaption>
|
<figcaption className="pt-2 pb-0.5 text-[9px] tracking-[0.2em] text-mute max-xl:hidden">
|
||||||
|
PFP · PLACEHOLDER
|
||||||
|
</figcaption>
|
||||||
)}
|
)}
|
||||||
</figure>
|
</figure>
|
||||||
<div className="min-w-0 xl:pt-1">
|
<div className="min-w-0 xl:pt-1">
|
||||||
<div className="text-[9px] tracking-[0.24em] text-mute">ALIAS</div>
|
<div className="text-[9px] tracking-[0.24em] text-mute">
|
||||||
<h2 id="profile-name" className="mt-1.5 font-display text-[28px] leading-none font-semibold tracking-[0.06em] uppercase xl:text-4xl">
|
ALIAS
|
||||||
<Scramble text={profile.displayName.toUpperCase()} delay={1.2} step={90} nowrap />
|
</div>
|
||||||
|
<h2
|
||||||
|
id="profile-name"
|
||||||
|
className="mt-1.5 font-display text-[28px] leading-none font-semibold tracking-[0.06em] uppercase xl:text-4xl"
|
||||||
|
>
|
||||||
|
<Scramble
|
||||||
|
text={profile.displayName.toUpperCase()}
|
||||||
|
delay={1.2}
|
||||||
|
step={90}
|
||||||
|
nowrap
|
||||||
|
/>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-2.5 text-[9px] leading-[1.9] tracking-[0.16em] text-mute xl:mt-[22px]">
|
<div className="mt-2.5 text-[9px] leading-[1.9] tracking-[0.16em] text-mute xl:mt-[22px]">
|
||||||
<div className="max-xl:hidden">MESH 2.5D · RESOLVED</div>
|
<div className="max-xl:hidden">MESH 2.5D · RESOLVED</div>
|
||||||
<div className="max-xl:hidden">CLEARANCE · PUBLIC</div>
|
<div className="max-xl:hidden">CLEARANCE · PUBLIC</div>
|
||||||
{profile.pfpIsPlaceholder && <div className="xl:hidden">PFP · PLACEHOLDER</div>}
|
{profile.pfpIsPlaceholder && (
|
||||||
|
<div className="xl:hidden">PFP · PLACEHOLDER</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DossierList fields={dossierFields} className="mt-[22px] hidden xl:grid" />
|
<DossierList
|
||||||
|
fields={dossierFields}
|
||||||
|
className="mt-[22px] hidden xl:grid"
|
||||||
|
/>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 hidden xl:block">
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 hidden xl:block">
|
||||||
<div className="relative mx-auto h-20 max-w-[1440px]">
|
<div className="relative mx-auto h-20 max-w-[1440px]">
|
||||||
{/* Corner telemetry, pinned to the bottom of the screen-tall hero */}
|
{/* Corner telemetry, pinned to the bottom of the screen-tall hero */}
|
||||||
<div aria-hidden="true" className="appear seq absolute bottom-[22px] left-16 hidden text-[9px] leading-[1.8] tracking-[0.16em] text-mute/70 xl:block" style={d(0.2)}>
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="appear seq absolute bottom-[22px] left-16 hidden text-[9px] leading-[1.8] tracking-[0.16em] text-mute/70 xl:block"
|
||||||
|
style={d(0.2)}
|
||||||
|
>
|
||||||
<SessionStamp />
|
<SessionStamp />
|
||||||
</div>
|
</div>
|
||||||
<div aria-hidden="true" className="absolute bottom-[22px] left-1/2 hidden -translate-x-1/2 text-[9px] tracking-[0.3em] text-mute xl:block">
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute bottom-[22px] left-1/2 hidden -translate-x-1/2 text-[9px] tracking-[0.3em] text-mute xl:block"
|
||||||
|
>
|
||||||
SCROLL ▾ NODES
|
SCROLL ▾ NODES
|
||||||
</div>
|
</div>
|
||||||
<div aria-hidden="true" className="appear seq absolute right-16 bottom-[22px] hidden text-right text-[9px] leading-[1.8] tracking-[0.16em] xl:block" style={d(0.2)}>
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="appear seq absolute right-16 bottom-[22px] hidden text-right text-[9px] leading-[1.8] tracking-[0.16em] xl:block"
|
||||||
|
style={d(0.2)}
|
||||||
|
>
|
||||||
<div className="text-accent">
|
<div className="text-accent">
|
||||||
<LocalTime />
|
<LocalTime />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
/** selimaj.dev mark: a lock bracket around a point. */
|
|
||||||
export function Mark({ size = 24, className }: { size?: number; className?: string }) {
|
|
||||||
return (
|
|
||||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
|
|
||||||
<path d="M2.5,8 V2.5 H8 M16,2.5 H21.5 V8 M21.5,16 V21.5 H16 M8,21.5 H2.5 V16" stroke="currentColor" strokeWidth="1.3" />
|
|
||||||
<rect x="9.5" y="9.5" width="5" height="5" fill="currentColor" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Mark } from "./Mark";
|
|
||||||
import { SessionClock } from "./Telemetry";
|
import { SessionClock } from "./Telemetry";
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
@@ -13,7 +12,9 @@ const links = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const isCurrent = (pathname: string, href: string) =>
|
const isCurrent = (pathname: string, href: string) =>
|
||||||
href === "/" ? pathname === "/" : pathname.startsWith(href.replace(/\/$/, ""));
|
href === "/"
|
||||||
|
? pathname === "/"
|
||||||
|
: pathname.startsWith(href.replace(/\/$/, ""));
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -25,8 +26,11 @@ export function SiteHeader() {
|
|||||||
className={`absolute inset-x-0 top-0 z-30 ${isHome ? "" : "border-b border-line"}`}
|
className={`absolute inset-x-0 top-0 z-30 ${isHome ? "" : "border-b border-line"}`}
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex max-w-[1440px] items-center justify-between gap-6 px-4 py-3 md:px-16 md:py-[22px]">
|
<div className="mx-auto flex max-w-[1440px] items-center justify-between gap-6 px-4 py-3 md:px-16 md:py-[22px]">
|
||||||
<Link href="/" className="flex min-h-11 items-center gap-3" aria-label="selimaj.dev home">
|
<Link
|
||||||
<Mark />
|
href="/"
|
||||||
|
className="flex min-h-11 items-center gap-3"
|
||||||
|
aria-label="selimaj.dev home"
|
||||||
|
>
|
||||||
<span className="font-display text-sm font-semibold tracking-[0.24em]">
|
<span className="font-display text-sm font-semibold tracking-[0.24em]">
|
||||||
SELIMAJ<span className="text-mute">.DEV</span>
|
SELIMAJ<span className="text-mute">.DEV</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -40,7 +44,14 @@ export function SiteHeader() {
|
|||||||
onClick={() => setOpen((o) => !o)}
|
onClick={() => setOpen((o) => !o)}
|
||||||
>
|
>
|
||||||
{open ? "CLOSE" : "MENU"}
|
{open ? "CLOSE" : "MENU"}
|
||||||
<svg width="18" height="12" viewBox="0 0 18 12" fill="none" stroke="currentColor" aria-hidden="true">
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 18 12"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<path d="M0,1 H18 M6,6 H18 M0,11 H18" />
|
<path d="M0,1 H18 M6,6 H18 M0,11 H18" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -61,7 +72,12 @@ export function SiteHeader() {
|
|||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className={`flex min-h-11 items-center gap-2 uppercase md:py-3.5 ${current ? "text-ink" : "text-mute hover:text-accent"}`}
|
className={`flex min-h-11 items-center gap-2 uppercase md:py-3.5 ${current ? "text-ink" : "text-mute hover:text-accent"}`}
|
||||||
>
|
>
|
||||||
{current && <span aria-hidden="true" className="size-1.5 bg-accent shadow-[0_0_8px_var(--color-accent)]" />}
|
{current && (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-1.5 bg-accent shadow-[0_0_8px_var(--color-accent)]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{l.label}
|
{l.label}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@@ -70,7 +86,10 @@ export function SiteHeader() {
|
|||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div aria-hidden="true" className="hidden text-right text-[10px] leading-relaxed tracking-[0.14em] text-mute lg:block">
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="hidden text-right text-[10px] leading-relaxed tracking-[0.14em] text-mute lg:block"
|
||||||
|
>
|
||||||
<SessionClock />
|
<SessionClock />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+13
-10
@@ -1,4 +1,7 @@
|
|||||||
import type { Status } from "./projects";
|
import { projectById, type Status } from "./projects";
|
||||||
|
|
||||||
|
/** Proof statuses always come from the project list, so they can't drift. */
|
||||||
|
const statusOf = (id: string): Status => projectById(id)!.status;
|
||||||
|
|
||||||
export type Proof = {
|
export type Proof = {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -36,7 +39,7 @@ export const dossier: DossierFile[] = [
|
|||||||
label: "Orus",
|
label: "Orus",
|
||||||
note: "My open-source brand for privacy and opsec tools",
|
note: "My open-source brand for privacy and opsec tools",
|
||||||
href: "/projects/#orus",
|
href: "/projects/#orus",
|
||||||
status: "LIVE",
|
status: statusOf("orus"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -52,9 +55,9 @@ export const dossier: DossierFile[] = [
|
|||||||
"Osias is a blockchain-powered AI agent for trading emotions and behavior. Alongside it sits a Minecraft economy concept with jurisdictions, taxes, and IPOs.",
|
"Osias is a blockchain-powered AI agent for trading emotions and behavior. Alongside it sits a Minecraft economy concept with jurisdictions, taxes, and IPOs.",
|
||||||
],
|
],
|
||||||
proof: [
|
proof: [
|
||||||
{ label: "Osias", note: "osias.trade", href: "https://osias.trade", status: "BUILDING" },
|
{ label: "Osias", note: "osias.trade", href: "https://osias.trade", status: statusOf("osias") },
|
||||||
{ label: "Orus", note: "Open-source privacy and opsec brand", href: "/projects/#orus", status: "LIVE" },
|
{ label: "Orus", note: "Open-source privacy and opsec brand", href: "/projects/#orus", status: statusOf("orus") },
|
||||||
{ label: "EquitySMP", note: "Real-world-style server economy", href: "/projects/#equitysmp", status: "BUILDING" },
|
{ label: "EquitySMP", note: "Real-world-style server economy", href: "/projects/#equitysmp", status: statusOf("equitysmp") },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -69,9 +72,9 @@ export const dossier: DossierFile[] = [
|
|||||||
"The interesting problems are usually one layer lower than where most people stop.",
|
"The interesting problems are usually one layer lower than where most people stop.",
|
||||||
],
|
],
|
||||||
proof: [
|
proof: [
|
||||||
{ label: "Saturn", note: "Minecraft client, written from scratch", href: "/projects/#saturn", status: "ARCHIVED" },
|
{ label: "Saturn", note: "Minecraft client, written from scratch", href: "/projects/#saturn", status: statusOf("saturn") },
|
||||||
{ label: "Enclave + transport protocol", note: "Chat platform, and the protocol it runs on", href: "/projects/#enclave", status: "BUILDING" },
|
{ label: "Enclave + transport protocol", note: "Chat platform, and the protocol it runs on", href: "/projects/#enclave", status: statusOf("enclave") },
|
||||||
{ label: "git.selimaj.dev", note: "Gitea on a Docker and Coolify homelab", href: "https://git.selimaj.dev", status: "LIVE" },
|
{ label: "git.selimaj.dev", note: "Gitea on a Docker and Coolify homelab", href: "https://git.selimaj.dev", status: statusOf("git") },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -102,8 +105,8 @@ export const dossier: DossierFile[] = [
|
|||||||
"Long-range PC and 4G-controlled RC planes built around a custom watercooled powertrain. Algorithmic trading. Minecraft with friends.",
|
"Long-range PC and 4G-controlled RC planes built around a custom watercooled powertrain. Algorithmic trading. Minecraft with friends.",
|
||||||
],
|
],
|
||||||
proof: [
|
proof: [
|
||||||
{ label: "Cyberdecks", note: "Build photos and logs to come", href: "/projects/#briefcase-deck", status: "BUILDING" },
|
{ label: "Cyberdecks", note: "Build photos and logs to come", href: "/projects/#briefcase-deck", status: statusOf("briefcase-deck") },
|
||||||
{ label: "RC planes", note: "Build photos and logs to come", href: "/projects/#rc-planes", status: "BUILDING" },
|
{ label: "RC planes", note: "Build photos and logs to come", href: "/projects/#rc-planes", status: statusOf("rc-planes") },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ export const profile = {
|
|||||||
displayName: "Selimaj",
|
displayName: "Selimaj",
|
||||||
title: "Entrepreneur & Builder",
|
title: "Entrepreneur & Builder",
|
||||||
stance: "I build the layer underneath.",
|
stance: "I build the layer underneath.",
|
||||||
// PLACEHOLDER: replace this one file to swap the profile picture.
|
// Replace this one file to swap the profile picture (the favicon is src/app/icon.png).
|
||||||
pfp: "/profile/pfp.svg",
|
pfp: "/profile/Mono.png",
|
||||||
pfpIsPlaceholder: true,
|
pfpIsPlaceholder: false,
|
||||||
github: "https://github.com/selimaj-dev",
|
github: "https://github.com/selimaj-dev",
|
||||||
git: "https://git.selimaj.dev",
|
git: "https://git.selimaj.dev",
|
||||||
} as const;
|
} as const;
|
||||||
@@ -35,7 +35,7 @@ export const dossierFields: DossierField[] = [
|
|||||||
{ label: "Based", value: "Tirana, Albania" },
|
{ label: "Based", value: "Tirana, Albania" },
|
||||||
{ label: "Brands", value: "Orus" },
|
{ label: "Brands", value: "Orus" },
|
||||||
// PLACEHOLDER decision: remove this entry to stop stating it publicly.
|
// PLACEHOLDER decision: remove this entry to stop stating it publicly.
|
||||||
{ label: "Building since", value: "12" },
|
{ label: "Age", value: "16" },
|
||||||
{ label: "Focus", value: "Privacy, independence, opsec, dev tooling" },
|
{ label: "Focus", value: "Privacy, independence, opsec, dev tooling" },
|
||||||
{ label: "Known for", value: "Saturn, Enclave" },
|
{ label: "Known for", value: "Saturn, Enclave" },
|
||||||
{ label: "Record", value: "Formerly #1 on Discipline, Albania" },
|
{ label: "Record", value: "Formerly #1 on Discipline, Albania" },
|
||||||
|
|||||||
Reference in New Issue
Block a user