Hero decode effect, static terrain, and selimaj.dev rebrand
- Brute-force scramble that decodes hero values left to right - Full-screen hero on desktop with telemetry pinned to the bottom - Terrain no longer drifts; home skips the page transition - selimaj.dev is the root brand; Orus becomes one brand with its own projects, and independent projects branch from the root Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -1,7 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { dossier } from "@/content/dossier";
|
import { dossier } from "@/content/dossier";
|
||||||
import { orus, profile } from "@/content/profile";
|
import { profile } from "@/content/profile";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Dossier",
|
title: "Dossier",
|
||||||
@@ -25,8 +25,8 @@ export default function AboutPage() {
|
|||||||
<dl className="bracket grid min-w-[280px] grid-cols-[110px_minmax(0,1fr)] gap-y-2 border border-white/6 bg-surface px-[22px] py-[18px] text-[11px] [--bc:rgb(242_244_245/0.4)]">
|
<dl className="bracket grid min-w-[280px] grid-cols-[110px_minmax(0,1fr)] gap-y-2 border border-white/6 bg-surface px-[22px] py-[18px] text-[11px] [--bc:rgb(242_244_245/0.4)]">
|
||||||
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">SUBJECT</dt>
|
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">SUBJECT</dt>
|
||||||
<dd>{profile.displayName}</dd>
|
<dd>{profile.displayName}</dd>
|
||||||
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">ROOT</dt>
|
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">DOMAIN</dt>
|
||||||
<dd>{orus.name}</dd>
|
<dd>selimaj.dev</dd>
|
||||||
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">FILES</dt>
|
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">FILES</dt>
|
||||||
<dd>{String(dossier.length).padStart(2, "0")}</dd>
|
<dd>{String(dossier.length).padStart(2, "0")}</dd>
|
||||||
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">ACCESS</dt>
|
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">ACCESS</dt>
|
||||||
|
|||||||
@@ -97,10 +97,6 @@ html[data-boot] .seq {
|
|||||||
animation-fill-mode: both;
|
animation-fill-mode: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drift {
|
|
||||||
animation: drift 80s ease-in-out infinite alternate;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
.spin {
|
.spin {
|
||||||
animation: spin 16s linear infinite;
|
animation: spin 16s linear infinite;
|
||||||
transform-origin: 60px 60px;
|
transform-origin: 60px 60px;
|
||||||
@@ -185,11 +181,6 @@ html[data-boot] .seq {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes drift {
|
|
||||||
to {
|
|
||||||
transform: translate3d(-28px, 14px, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
<rect width="24" height="24" fill="#050607"/>
|
<rect width="24" height="24" fill="#050607"/>
|
||||||
<circle cx="12" cy="12" r="8.5" stroke="#f2f4f5" stroke-width="1.6" stroke-dasharray="44 9"/>
|
<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"/>
|
||||||
<circle cx="12" cy="12" r="2.6" fill="#4dff95"/>
|
<rect x="9.25" y="9.25" width="5.5" height="5.5" fill="#4dff95"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 275 B After Width: | Height: | Size: 314 B |
+1
-1
@@ -17,7 +17,7 @@ const jetbrains = JetBrains_Mono({
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: { default: "Selimaj · Entrepreneur & Builder", template: "%s · Selimaj" },
|
title: { default: "Selimaj · Entrepreneur & Builder", template: "%s · Selimaj" },
|
||||||
description: "Selimaj, entrepreneur and builder. Founder of Orus. I build the layer underneath.",
|
description: "Selimaj, entrepreneur and builder. I build the layer underneath.",
|
||||||
metadataBase: new URL("https://selimaj.dev"),
|
metadataBase: new URL("https://selimaj.dev"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import { projects } from "@/content/projects";
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
description: "Everything built and conceived under Orus, from Saturn and Enclave to cyberdecks and RC planes.",
|
description: "Everything I’ve built and conceived, from Enclave and Osias to cyberdecks and RC planes.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProjectsPage() {
|
export default function ProjectsPage() {
|
||||||
return (
|
return (
|
||||||
<main className="pt-[68px] md:pt-[85px]">
|
<main className="pt-[68px] md:pt-[85px]">
|
||||||
<div className="mx-auto max-w-[1440px] px-4 pt-10 pb-20 md:px-16 md:pt-16 md:pb-24">
|
<div className="mx-auto max-w-[1440px] px-4 pt-10 pb-20 md:px-16 md:pt-16 md:pb-24">
|
||||||
<div className="label tracking-[0.3em]">Node index · Root Orus · {projects.length} nodes</div>
|
<div className="label tracking-[0.3em]">Node index · Root selimaj.dev · {projects.length} nodes</div>
|
||||||
<h1 className="mt-3 font-display text-[52px] leading-[0.95] font-semibold uppercase md:text-7xl">Projects</h1>
|
<h1 className="mt-3 font-display text-[52px] leading-[0.95] font-semibold uppercase md:text-7xl">Projects</h1>
|
||||||
<p className="mt-[18px] max-w-[560px] text-[13px] leading-[1.7] text-ink-2">
|
<p className="mt-[18px] max-w-[560px] text-[13px] leading-[1.7] text-ink-2">
|
||||||
Most of what I've built and conceived. Concepts are labelled as concepts.
|
Most of what I've built and conceived. Concepts are labelled as concepts.
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
// Re-mounts on every navigation, so each page resolves in from wireframe.
|
// Re-mounts on every navigation, so each page resolves in from wireframe.
|
||||||
|
// Home is skipped: the hero runs its own sequence and the terrain stays still.
|
||||||
export default function Template({ children }: { children: React.ReactNode }) {
|
export default function Template({ children }: { children: React.ReactNode }) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
if (pathname === "/") return <>{children}</>;
|
||||||
return <div className="page-resolve seq">{children}</div>;
|
return <div className="page-resolve seq">{children}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ export function BootSequence() {
|
|||||||
sessionStorage.setItem("boot", "1");
|
sessionStorage.setItem("boot", "1");
|
||||||
} catch {}
|
} catch {}
|
||||||
setLeaving(true);
|
setLeaving(true);
|
||||||
outTimer = window.setTimeout(() => root.removeAttribute("data-boot"), 250);
|
outTimer = window.setTimeout(() => {
|
||||||
|
root.removeAttribute("data-boot");
|
||||||
|
window.dispatchEvent(new Event("boot:done"));
|
||||||
|
}, 250);
|
||||||
};
|
};
|
||||||
const timer = window.setTimeout(finish, DURATION);
|
const timer = window.setTimeout(finish, DURATION);
|
||||||
window.addEventListener("keydown", finish);
|
window.addEventListener("keydown", finish);
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { featured, projectById, projects } from "@/content/projects";
|
import { brandOf, featured, featuredRoot, projectById, projects, root } from "@/content/projects";
|
||||||
import { Segment } from "./Segment";
|
import { Segment } from "./Segment";
|
||||||
|
|
||||||
const ROOT = featured[0];
|
|
||||||
|
|
||||||
export function FeaturedNodes() {
|
export function FeaturedNodes() {
|
||||||
const [active, setActive] = useState<string | null>(null);
|
const [active, setActive] = useState<string | null>(null);
|
||||||
const nodes = featured.map((f) => ({ ...f, project: projectById(f.id)! }));
|
const nodes = featured.map((f) => ({ ...f, project: projectById(f.id)! }));
|
||||||
@@ -30,11 +28,11 @@ export function FeaturedNodes() {
|
|||||||
>
|
>
|
||||||
{size && (
|
{size && (
|
||||||
<div aria-hidden="true" className="hidden md:block">
|
<div aria-hidden="true" className="hidden md:block">
|
||||||
{nodes.slice(1).map((n) => (
|
{nodes.map((n) => (
|
||||||
<Segment
|
<Segment
|
||||||
key={`${n.id}-${active === n.id}`}
|
key={`${n.id}-${active === n.id}`}
|
||||||
x1={(ROOT.x / 100) * size.w}
|
x1={(featuredRoot.x / 100) * size.w}
|
||||||
y1={(ROOT.y / 100) * size.h}
|
y1={(featuredRoot.y / 100) * size.h}
|
||||||
x2={(n.x / 100) * size.w}
|
x2={(n.x / 100) * size.w}
|
||||||
y2={(n.y / 100) * size.h}
|
y2={(n.y / 100) * size.h}
|
||||||
on={active === n.id}
|
on={active === n.id}
|
||||||
@@ -45,9 +43,29 @@ export function FeaturedNodes() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Root: the site itself, labelled to the left so no line crosses it. */}
|
||||||
|
<div
|
||||||
|
className="relative min-h-12 md:absolute md:top-(--y) md:left-(--x) md:min-h-0"
|
||||||
|
style={{ "--x": `${featuredRoot.x}%`, "--y": `${featuredRoot.y}%` } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute top-2.5 left-0 box-border size-7 rounded-full border border-ink/70 bg-night md:-top-3.5 md:-left-3.5"
|
||||||
|
>
|
||||||
|
<span className="absolute top-[9px] left-[9px] size-2 rounded-full bg-ink" />
|
||||||
|
</span>
|
||||||
|
<span className="ml-10 block px-3 py-2.5 md:absolute md:-top-[22px] md:right-6 md:ml-0 md:w-max md:text-right">
|
||||||
|
<span className="flex items-baseline gap-3 md:flex-row-reverse">
|
||||||
|
<span className="font-display text-lg font-semibold tracking-[0.08em] uppercase">{root.name}</span>
|
||||||
|
<span className="text-[9px] tracking-[0.22em] text-mute uppercase">Root</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{nodes.map((n) => {
|
{nodes.map((n) => {
|
||||||
const on = active === n.id;
|
const on = active === n.id;
|
||||||
const root = n.project.root;
|
const brand = brandOf(n.project);
|
||||||
|
const meta = n.id === "orus" ? "BRAND" : brand ? `${n.project.status} · ${brand.toUpperCase()}` : n.project.status;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={n.id}
|
key={n.id}
|
||||||
@@ -60,33 +78,27 @@ export function FeaturedNodes() {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`absolute box-border border ${
|
className={`absolute top-4 left-2 box-border size-3 border md:-top-1.5 md:-left-1.5 ${n.id === "orus" ? "rotate-45" : ""} ${
|
||||||
root
|
on ? "border-accent bg-accent shadow-[0_0_14px_var(--color-accent)]" : "border-ink/60 bg-night"
|
||||||
? "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
|
<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 ${
|
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:left-6 md:ml-0 md:w-[300px] ${
|
||||||
root ? "md:right-6 md:w-max md:text-right" : "md:left-6 md:w-[280px]"
|
|
||||||
} ${
|
|
||||||
on ? "[--bc:var(--color-accent)]" : "[--bc:transparent]"
|
on ? "[--bc:var(--color-accent)]" : "[--bc:transparent]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className={`flex items-baseline gap-3 ${root ? "md:flex-row-reverse" : ""}`}>
|
<span className="flex items-baseline gap-3">
|
||||||
<span className="font-display text-lg font-semibold tracking-[0.08em] uppercase">{n.project.name}</span>
|
<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 className="text-[9px] tracking-[0.22em] text-mute">{meta}</span>
|
||||||
</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 className={`mt-1.5 block text-[11px] leading-[1.55] text-ink-2 ${on ? "" : "md:sr-only"}`}>{n.project.desc}</span>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div aria-hidden="true" className="absolute right-4 bottom-3 hidden text-[9px] tracking-[0.2em] text-mute/70 md:block">
|
<div aria-hidden="true" className="absolute right-4 bottom-3 hidden text-[9px] tracking-[0.2em] text-mute/70 uppercase md:block">
|
||||||
ROOT ORUS · {featured.length} OF {projects.length} NODES
|
Root {root.sub} · {featured.length} of {projects.length} nodes
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+57
-48
@@ -1,39 +1,47 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { dossierFields, lock, orus, 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 { LocalTime, SessionStamp } from "./Telemetry";
|
import { LocalTime, SessionStamp } from "./Telemetry";
|
||||||
import { TerrainLayer } from "./TerrainLayer";
|
import { TerrainLayer } from "./TerrainLayer";
|
||||||
import { LockReticle, TiranaMap } from "./TiranaMap";
|
import { LockReticle, TiranaMap } from "./TiranaMap";
|
||||||
|
|
||||||
const d = (s: number) => ({ "--d": `${s}s` }) as React.CSSProperties;
|
const d = (s: number) => ({ "--d": `${s}s` }) as React.CSSProperties;
|
||||||
|
|
||||||
|
/** Dossier values decode one after another, starting once the card has built. */
|
||||||
|
const DOSSIER_START = 1.35;
|
||||||
|
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) => (
|
{fields.map((f, i) => {
|
||||||
<div key={f.label} className="contents">
|
const delay = DOSSIER_START + i * DOSSIER_STAGGER;
|
||||||
<dt className="pt-[3px] text-[9px] uppercase tracking-[0.22em] text-mute">{f.label}</dt>
|
return (
|
||||||
<dd className="flex flex-wrap gap-x-3.5">
|
<div key={f.label} className="contents">
|
||||||
{f.links
|
<dt className="pt-[3px] text-[9px] uppercase tracking-[0.22em] text-mute">{f.label}</dt>
|
||||||
? f.links.map((l) => (
|
<dd className="flex flex-wrap gap-x-3.5">
|
||||||
<a key={l.href} href={l.href} className="border-b border-white/25 hover:border-accent">
|
{f.links
|
||||||
{l.label}
|
? f.links.map((l, j) => (
|
||||||
</a>
|
<a 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} />
|
||||||
: f.value}
|
</a>
|
||||||
</dd>
|
))
|
||||||
</div>
|
: <Scramble text={f.value} delay={delay} step={30} />}
|
||||||
))}
|
</dd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</dl>
|
</dl>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Hero() {
|
export function Hero() {
|
||||||
return (
|
return (
|
||||||
<section aria-labelledby="hero-title" className="relative overflow-hidden border-b border-line">
|
<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 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]">
|
||||||
{/* Lock assembly: a strip on small screens, the full stage on desktop. */}
|
{/* Lock assembly: a strip on small screens, the full stage on desktop. */}
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -44,15 +52,16 @@ export function Hero() {
|
|||||||
className="reticle seq absolute top-[calc(50%-60px)] left-[calc(var(--lx)-60px)] max-xl:scale-75 xl:top-[174px]"
|
className="reticle seq absolute top-[calc(50%-60px)] left-[calc(var(--lx)-60px)] max-xl:scale-75 xl:top-[174px]"
|
||||||
style={d(0.25)}
|
style={d(0.25)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div className="absolute bottom-3 left-4 text-[9px] leading-[1.7] tracking-[0.14em] text-mute xl:bottom-auto xl:left-[calc(var(--lx)-60px)] xl:top-[306px] xl:text-[10px]">
|
||||||
className="resolve seq absolute bottom-3 left-4 text-[9px] leading-[1.7] tracking-[0.14em] text-mute xl:bottom-auto xl:left-[calc(var(--lx)-60px)] xl:top-[306px] xl:text-[10px]"
|
<div className="text-accent">
|
||||||
style={d(0.85)}
|
<Scramble text="LOCK ACQUIRED" delay={0.85} step={35} nowrap />
|
||||||
>
|
</div>
|
||||||
<div className="text-accent">LOCK ACQUIRED</div>
|
|
||||||
<div>
|
<div>
|
||||||
{lock.lat} · {lock.lon}
|
<Scramble text={`${lock.lat} · ${lock.lon}`} delay={0.95} step={30} nowrap />
|
||||||
|
</div>
|
||||||
|
<div className="uppercase">
|
||||||
|
<Scramble text={lock.place} delay={1.05} step={30} nowrap />
|
||||||
</div>
|
</div>
|
||||||
<div className="uppercase">{lock.place}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
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"
|
||||||
@@ -66,20 +75,19 @@ export function Hero() {
|
|||||||
<div className="flex flex-col gap-9 px-4 pt-6 pb-16 md:px-16 xl:block xl:p-0">
|
<div className="flex flex-col gap-9 px-4 pt-6 pb-16 md:px-16 xl:block xl:p-0">
|
||||||
{/* 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="resolve seq mb-3 text-[9px] tracking-[0.28em] text-mute xl:mb-5 xl:text-[10px] xl:tracking-[0.3em]" style={d(1.1)}>
|
<p className="mb-3 text-[9px] tracking-[0.28em] text-mute xl:mb-5 xl:text-[10px] xl:tracking-[0.3em]">
|
||||||
TARGET RESOLVED · SUBJECT 0001
|
<Scramble text="TARGET RESOLVED · SUBJECT 0001" delay={0.2} step={15} />
|
||||||
</p>
|
</p>
|
||||||
<h1
|
<h1
|
||||||
id="hero-title"
|
id="hero-title"
|
||||||
className="resolve seq font-display text-[46px] leading-[0.96] font-semibold uppercase sm:text-[64px] xl:text-[84px] xl:leading-[0.94] xl:tracking-[0.01em]"
|
className="font-display text-[46px] leading-[0.96] font-semibold uppercase sm:text-[64px] xl:text-[84px] xl:leading-[0.94] xl:tracking-[0.01em]"
|
||||||
style={d(1.2)}
|
|
||||||
>
|
>
|
||||||
Entrepreneur
|
<Scramble text="ENTREPRENEUR" delay={0.3} step={35} nowrap />
|
||||||
<br />
|
<br />
|
||||||
& Builder
|
<Scramble text="& BUILDER" delay={0.5} step={35} nowrap />
|
||||||
</h1>
|
</h1>
|
||||||
<p className="resolve seq mt-4 text-[15px] leading-normal text-ink-2 xl:mt-6 xl:text-[17px]" style={d(1.3)}>
|
<p className="mt-4 text-[15px] leading-normal text-ink-2 xl:mt-6 xl:text-[17px]">
|
||||||
{profile.stance}
|
<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
|
||||||
@@ -95,10 +103,6 @@ export function Hero() {
|
|||||||
OPEN DOSSIER <span aria-hidden="true">↗</span>
|
OPEN DOSSIER <span aria-hidden="true">↗</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="resolve seq mt-10 hidden max-w-[580px] items-start gap-3.5 xl:flex" style={d(1.5)}>
|
|
||||||
<span className="pt-[3px] text-[9px] whitespace-nowrap tracking-[0.24em] text-mute">ROOT ─ ORUS</span>
|
|
||||||
<span className="text-xs leading-[1.6] text-mute">{orus.oneLiner}</span>
|
|
||||||
</div>
|
|
||||||
<DossierList fields={dossierFields} className="mt-9 xl:hidden" />
|
<DossierList fields={dossierFields} className="mt-9 xl:hidden" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -122,7 +126,7 @@ export function Hero() {
|
|||||||
<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">ALIAS</div>
|
||||||
<h2 id="profile-name" className="mt-1.5 font-display text-[28px] leading-none font-semibold tracking-[0.06em] uppercase xl:text-4xl">
|
<h2 id="profile-name" className="mt-1.5 font-display text-[28px] leading-none font-semibold tracking-[0.06em] uppercase xl:text-4xl">
|
||||||
{profile.displayName}
|
<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>
|
||||||
@@ -135,18 +139,23 @@ export function Hero() {
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Corner telemetry */}
|
</div>
|
||||||
<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 />
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 hidden xl:block">
|
||||||
</div>
|
<div className="relative mx-auto h-20 max-w-[1440px]">
|
||||||
<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">
|
{/* Corner telemetry, pinned to the bottom of the screen-tall hero */}
|
||||||
SCROLL ▾ NODES
|
<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>
|
<SessionStamp />
|
||||||
<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>
|
||||||
<div className="text-accent">
|
<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">
|
||||||
<LocalTime />
|
SCROLL ▾ NODES
|
||||||
|
</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 className="text-accent">
|
||||||
|
<LocalTime />
|
||||||
|
</div>
|
||||||
|
<div className="text-mute/70">VISITOR · UNRESOLVED</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-mute/70">VISITOR · UNRESOLVED</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/** 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export function OrusMark({ 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}>
|
|
||||||
<circle cx="12" cy="12" r="9.5" stroke="currentColor" strokeWidth="1.2" strokeDasharray="50 10" />
|
|
||||||
<circle cx="12" cy="12" r="2.6" fill="currentColor" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,37 +1,40 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { groups, projects, type GroupId, type Project } from "@/content/projects";
|
import { brandOf, groups, projects, root, type GroupId, type Project } from "@/content/projects";
|
||||||
import { Segment } from "./Segment";
|
import { Segment } from "./Segment";
|
||||||
import { StatusTag } from "./StatusTag";
|
import { StatusTag } from "./StatusTag";
|
||||||
|
|
||||||
const W = 1200;
|
const W = 1200;
|
||||||
const H = 720;
|
const H = 720;
|
||||||
const root = projects.find((p) => p.root)!;
|
|
||||||
const groupIds = Object.keys(groups) as GroupId[];
|
const groupIds = Object.keys(groups) as GroupId[];
|
||||||
|
const isHub = (p: Project) => groups[p.group].node === p.id;
|
||||||
|
|
||||||
function Marker({ p, on }: { p: Project; on: boolean }) {
|
function Marker({ p, on }: { p: Project; on: boolean }) {
|
||||||
const size = p.root ? 30 : 14;
|
const hub = isHub(p);
|
||||||
|
const size = hub ? 22 : 14;
|
||||||
const rest =
|
const rest =
|
||||||
p.status === "CONCEPT" ? "border border-dashed border-ink/60" : p.status === "ARCHIVED" ? "border border-ink/30" : "border border-ink/65";
|
p.status === "CONCEPT" ? "border border-dashed border-ink/60" : p.status === "ARCHIVED" ? "border border-ink/30" : "border border-ink/65";
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`relative box-border flex-none ${p.root ? "rounded-full" : ""} ${
|
className={`relative box-border flex-none ${hub ? "rotate-45" : ""} ${
|
||||||
on ? (p.root ? "border border-accent shadow-[0_0_14px_var(--color-accent)]" : "bg-accent shadow-[0_0_12px_var(--color-accent)]") : `${rest} bg-surface`
|
on ? "bg-accent shadow-[0_0_12px_var(--color-accent)]" : `${rest} bg-surface`
|
||||||
}`}
|
}`}
|
||||||
style={{ width: size, height: size }}
|
style={{ width: size, height: size }}
|
||||||
>
|
/>
|
||||||
{p.root && <span className={`absolute top-[10px] left-[10px] size-2 rounded-full ${on ? "bg-accent" : "bg-ink"}`} />}
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectCard({ p }: { p: Project }) {
|
function ProjectCard({ p }: { p: Project }) {
|
||||||
|
const brand = brandOf(p);
|
||||||
const body = (
|
const body = (
|
||||||
<>
|
<>
|
||||||
<span className="flex items-center justify-between">
|
<span className="flex items-center justify-between gap-3">
|
||||||
<StatusTag status={p.status} />
|
<span className="flex items-center gap-3">
|
||||||
|
<StatusTag status={p.status} />
|
||||||
|
{brand && <span className="text-[9px] tracking-[0.22em] text-mute uppercase">{brand}</span>}
|
||||||
|
</span>
|
||||||
<span aria-hidden="true" className="text-mute transition-colors group-hover:text-accent group-focus-visible:text-accent">
|
<span aria-hidden="true" className="text-mute transition-colors group-hover:text-accent group-focus-visible:text-accent">
|
||||||
{p.href ? "↗" : "→"}
|
{p.href ? "↗" : "→"}
|
||||||
</span>
|
</span>
|
||||||
@@ -62,7 +65,7 @@ function ProjectCard({ p }: { p: Project }) {
|
|||||||
|
|
||||||
export function ProjectIndex() {
|
export function ProjectIndex() {
|
||||||
const [view, setView] = useState<"map" | "list">("map");
|
const [view, setView] = useState<"map" | "list">("map");
|
||||||
const [active, setActive] = useState<string>("orus");
|
const [active, setActive] = useState<string | null>(null);
|
||||||
|
|
||||||
// Deep links (/projects/#enclave) select that node; small screens start on the list.
|
// Deep links (/projects/#enclave) select that node; small screens start on the list.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -75,7 +78,7 @@ export function ProjectIndex() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sel = projects.find((p) => p.id === active) ?? root;
|
const sel = projects.find((p) => p.id === active) ?? null;
|
||||||
const count = (s: Project["status"]) => String(projects.filter((p) => p.status === s).length).padStart(2, "0");
|
const count = (s: Project["status"]) => String(projects.filter((p) => p.status === s).length).padStart(2, "0");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -112,10 +115,10 @@ export function ProjectIndex() {
|
|||||||
<div className="mt-6 overflow-x-auto border border-white/6">
|
<div className="mt-6 overflow-x-auto border border-white/6">
|
||||||
<div className="grid-bg relative mx-auto bg-surface [background-size:40px_40px]" style={{ width: W, height: H }}>
|
<div className="grid-bg relative mx-auto bg-surface [background-size:40px_40px]" style={{ width: W, height: H }}>
|
||||||
{groupIds.map((k) => (
|
{groupIds.map((k) => (
|
||||||
<Segment key={k} x1={root.map.x} y1={root.map.y} x2={groups[k].x} y2={groups[k].y} on={!sel.root && sel.group === k} strong />
|
<Segment key={k} x1={root.map.x} y1={root.map.y} x2={groups[k].x} y2={groups[k].y} on={!!sel && sel.group === k} strong />
|
||||||
))}
|
))}
|
||||||
{projects
|
{projects
|
||||||
.filter((p) => !p.root)
|
.filter((p) => !isHub(p))
|
||||||
.map((p) => (
|
.map((p) => (
|
||||||
<Segment
|
<Segment
|
||||||
key={`${p.id}-${active === p.id}`}
|
key={`${p.id}-${active === p.id}`}
|
||||||
@@ -127,25 +130,41 @@ export function ProjectIndex() {
|
|||||||
dashed={p.status === "CONCEPT"}
|
dashed={p.status === "CONCEPT"}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{groupIds.map((k) => {
|
|
||||||
const g = groups[k];
|
{/* Plain hubs (groups without a project of their own). */}
|
||||||
return (
|
{groupIds
|
||||||
<div key={k} aria-hidden="true">
|
.filter((k) => !groups[k].node)
|
||||||
<div
|
.map((k) => {
|
||||||
className="absolute text-[9px] whitespace-nowrap tracking-[0.24em] text-mute uppercase"
|
const g = groups[k];
|
||||||
style={{ top: g.y - 7, ...(g.side === "l" ? { right: W - g.x + 14 } : { left: g.x + 14 }) }}
|
return (
|
||||||
>
|
<div key={k} aria-hidden="true">
|
||||||
{g.label}
|
<div
|
||||||
|
className="absolute text-[9px] whitespace-nowrap tracking-[0.24em] text-mute uppercase"
|
||||||
|
style={{ top: g.y - 7, ...(g.side === "l" ? { right: W - g.x + 14 } : { left: g.x + 14 }) }}
|
||||||
|
>
|
||||||
|
{g.label}
|
||||||
|
</div>
|
||||||
|
<div className="absolute box-border size-1.5 rotate-45 border border-ink/40" style={{ left: g.x - 3, top: g.y - 3 }} />
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute box-border size-1.5 rotate-45 border border-ink/40" style={{ left: g.x - 3, top: g.y - 3 }} />
|
);
|
||||||
</div>
|
})}
|
||||||
);
|
|
||||||
})}
|
{/* Root: the site itself. */}
|
||||||
|
<div className="absolute flex items-center gap-3 whitespace-nowrap" style={{ left: root.map.x - 15, top: root.map.y - 15 }}>
|
||||||
|
<span aria-hidden="true" className="relative box-border size-[30px] flex-none rounded-full border border-ink/70 bg-surface">
|
||||||
|
<span className="absolute top-[10px] left-[10px] size-2 rounded-full bg-ink" />
|
||||||
|
</span>
|
||||||
|
<span className="flex flex-col gap-[3px]">
|
||||||
|
<span className="font-display text-xl font-semibold tracking-[0.08em] uppercase">{root.name}</span>
|
||||||
|
<span className="text-[8px] tracking-[0.22em] text-mute uppercase">Root · {root.sub}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{projects.map((p) => {
|
{projects.map((p) => {
|
||||||
const on = active === p.id;
|
const on = active === p.id;
|
||||||
const g = groups[p.group];
|
const g = groups[p.group];
|
||||||
const labelLeft = !p.root && p.map.x < g.x;
|
const labelLeft = (p.label ?? (p.map.x < g.x ? "l" : "r")) === "l";
|
||||||
const m = p.root ? 30 : 14;
|
const m = isHub(p) ? 22 : 14;
|
||||||
const pad = 8;
|
const pad = 8;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -165,15 +184,16 @@ export function ProjectIndex() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Marker p={p} on={on} />
|
<Marker p={p} on={on} />
|
||||||
<span className="flex flex-col gap-[3px]">
|
{/* A hub's label sits a little high so its root line passes underneath. */}
|
||||||
|
<span className={`flex flex-col gap-[3px] ${isHub(p) ? "-translate-y-4" : ""}`}>
|
||||||
<span
|
<span
|
||||||
className={`font-display font-semibold tracking-[0.08em] uppercase ${p.root ? "text-xl" : "text-[13px]"} ${
|
className={`font-display font-semibold tracking-[0.08em] uppercase ${isHub(p) ? "text-base" : "text-[13px]"} ${
|
||||||
p.status === "ARCHIVED" && !on ? "text-ink/70" : "text-ink"
|
p.status === "ARCHIVED" && !on ? "text-ink/70" : "text-ink"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{p.name}
|
{p.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[8px] tracking-[0.22em] text-mute">{p.root ? "ROOT" : p.status}</span>
|
<span className="text-[8px] tracking-[0.22em] text-mute">{isHub(p) ? "BRAND" : p.status}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -181,8 +201,8 @@ export function ProjectIndex() {
|
|||||||
<div aria-hidden="true" className="absolute bottom-3 left-4 text-[9px] tracking-[0.2em] text-mute/70">
|
<div aria-hidden="true" className="absolute bottom-3 left-4 text-[9px] tracking-[0.2em] text-mute/70">
|
||||||
GRID 40 · ROOT {root.map.x},{root.map.y}
|
GRID 40 · ROOT {root.map.x},{root.map.y}
|
||||||
</div>
|
</div>
|
||||||
<div aria-hidden="true" className="absolute right-4 bottom-3 text-[9px] tracking-[0.2em] text-accent uppercase">
|
<div aria-hidden="true" className="absolute right-4 bottom-3 text-[9px] tracking-[0.2em] uppercase">
|
||||||
TRACKING ▸ {sel.name}
|
{sel ? <span className="text-accent">TRACKING ▸ {sel.name}</span> : <span className="text-mute/70">AWAITING TARGET</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,38 +210,48 @@ export function ProjectIndex() {
|
|||||||
<section
|
<section
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
aria-label="Selected node"
|
aria-label="Selected node"
|
||||||
className="bracket mt-4 grid gap-6 border border-white/6 bg-surface px-7 py-6 [--bc:var(--color-accent)] md:grid-cols-[minmax(0,1fr)_auto] md:items-center md:gap-x-12"
|
className={`bracket mt-4 grid min-h-[176px] gap-6 border border-white/6 bg-surface px-7 py-6 md:grid-cols-[minmax(0,1fr)_auto] md:items-center md:gap-x-12 ${
|
||||||
|
sel ? "[--bc:var(--color-accent)]" : "[--bc:rgb(242_244_245/0.3)]"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div>
|
{sel ? (
|
||||||
<div className="flex flex-wrap items-center gap-3.5">
|
<>
|
||||||
<StatusTag status={sel.status} />
|
<div>
|
||||||
<span className="text-[9px] tracking-[0.24em] text-mute uppercase">{sel.root ? "Root" : groups[sel.group].label}</span>
|
<div className="flex flex-wrap items-center gap-3.5">
|
||||||
</div>
|
<StatusTag status={sel.status} />
|
||||||
<h2 className="mt-3 font-display text-3xl font-semibold tracking-[0.04em] uppercase">{sel.name}</h2>
|
<span className="text-[9px] tracking-[0.24em] text-mute uppercase">
|
||||||
<p className="mt-2 max-w-[720px] text-[13px] leading-[1.7] text-ink-2">{sel.desc}</p>
|
{isHub(sel) ? "Brand" : (brandOf(sel) ?? groups[sel.group].label)}
|
||||||
<div className="mt-3.5 flex flex-wrap gap-2">
|
</span>
|
||||||
{sel.tags.map((t) => (
|
</div>
|
||||||
<span key={t} className="bg-surface-2 px-2 py-1 text-[10px] tracking-[0.1em] text-mute">
|
<h2 className="mt-3 font-display text-3xl font-semibold tracking-[0.04em] uppercase">{sel.name}</h2>
|
||||||
{t}
|
<p className="mt-2 max-w-[720px] text-[13px] leading-[1.7] text-ink-2">{sel.desc}</p>
|
||||||
</span>
|
<div className="mt-3.5 flex flex-wrap gap-2">
|
||||||
))}
|
{sel.tags.map((t) => (
|
||||||
</div>
|
<span key={t} className="bg-surface-2 px-2 py-1 text-[10px] tracking-[0.1em] text-mute">
|
||||||
</div>
|
{t}
|
||||||
{sel.href ? (
|
</span>
|
||||||
<a href={sel.href} className="inline-flex min-h-12 items-center gap-3 justify-self-start border border-white/14 px-5 text-[11px] tracking-[0.2em] hover:border-accent">
|
))}
|
||||||
VISIT <span aria-hidden="true">↗</span>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
|
{sel.href ? (
|
||||||
|
<a href={sel.href} className="inline-flex min-h-12 items-center gap-3 justify-self-start border border-white/14 px-5 text-[11px] tracking-[0.2em] hover:border-accent">
|
||||||
|
VISIT <span aria-hidden="true">↗</span>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setView("list");
|
||||||
|
requestAnimationFrame(() => document.getElementById(sel.id)?.scrollIntoView({ block: "center" }));
|
||||||
|
}}
|
||||||
|
className="inline-flex min-h-12 items-center gap-3 justify-self-start border border-white/14 px-5 text-[11px] tracking-[0.2em] hover:border-accent hover:text-accent"
|
||||||
|
>
|
||||||
|
VIEW IN LIST <span aria-hidden="true">→</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<p className="text-[11px] tracking-[0.2em] text-mute">HOVER OR SELECT A NODE TO TRACK IT.</p>
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setView("list");
|
|
||||||
requestAnimationFrame(() => document.getElementById(sel.id)?.scrollIntoView({ block: "center" }));
|
|
||||||
}}
|
|
||||||
className="inline-flex min-h-12 items-center gap-3 justify-self-start border border-white/14 px-5 text-[11px] tracking-[0.2em] hover:border-accent hover:text-accent"
|
|
||||||
>
|
|
||||||
VIEW IN LIST <span aria-hidden="true">→</span>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&@$*+=<>/";
|
||||||
|
const LOWER = "abcdefghijklmnopqrstuvwxyz0123456789#%&@$*+=<>/";
|
||||||
|
const DIGIT = "0123456789#%&@$*";
|
||||||
|
/** Characters that stay put while everything around them is brute-forced. */
|
||||||
|
const FIXED = /[\s.,·()\-/]/;
|
||||||
|
|
||||||
|
const FRAME_MS = 45;
|
||||||
|
|
||||||
|
function noise(ch: string) {
|
||||||
|
if (FIXED.test(ch)) return ch;
|
||||||
|
const pool = /[0-9]/.test(ch) ? DIGIT : ch === ch.toLowerCase() && ch !== ch.toUpperCase() ? LOWER : UPPER;
|
||||||
|
return pool[Math.floor(Math.random() * pool.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
type Frame = { done: string; head: string; rest: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text that brute-forces itself: every character cycles through random
|
||||||
|
* glyphs, then the real value locks in from left to right.
|
||||||
|
*
|
||||||
|
* Server and no-JS output is the plain text. With reduced motion it never
|
||||||
|
* animates. While the boot screen is up it waits for `boot:done`.
|
||||||
|
*/
|
||||||
|
export function Scramble({
|
||||||
|
text,
|
||||||
|
delay = 0,
|
||||||
|
step = 45,
|
||||||
|
nowrap = false,
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
/** Seconds before characters start locking in (noise runs from the start). */
|
||||||
|
delay?: number;
|
||||||
|
/** Milliseconds per character as the value locks in. */
|
||||||
|
step?: number;
|
||||||
|
nowrap?: boolean;
|
||||||
|
}) {
|
||||||
|
const [frame, setFrame] = useState<Frame | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||||
|
|
||||||
|
const chars = Array.from(text);
|
||||||
|
let raf = 0;
|
||||||
|
let start = 0;
|
||||||
|
let last = -Infinity;
|
||||||
|
|
||||||
|
const build = (locked: number): Frame => ({
|
||||||
|
done: chars.slice(0, locked).join(""),
|
||||||
|
head: locked < chars.length ? chars[locked] : "",
|
||||||
|
rest: chars
|
||||||
|
.slice(locked + 1)
|
||||||
|
.map(noise)
|
||||||
|
.join(""),
|
||||||
|
});
|
||||||
|
|
||||||
|
const loop = (t: number) => {
|
||||||
|
if (!start) start = t;
|
||||||
|
const elapsed = t - start - delay * 1000;
|
||||||
|
const locked = elapsed < 0 ? 0 : Math.floor(elapsed / step);
|
||||||
|
if (locked >= chars.length) {
|
||||||
|
setFrame(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (t - last >= FRAME_MS) {
|
||||||
|
last = t;
|
||||||
|
// Before the reveal starts, the first character is noise too.
|
||||||
|
setFrame(elapsed < 0 ? { done: "", head: noise(chars[0]), rest: build(0).rest } : build(locked));
|
||||||
|
}
|
||||||
|
raf = requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
|
||||||
|
const begin = () => {
|
||||||
|
raf = requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.documentElement.hasAttribute("data-boot")) {
|
||||||
|
// Hide the real value under the boot screen, then start when it lifts.
|
||||||
|
raf = requestAnimationFrame(() => setFrame({ done: "", head: "", rest: chars.map(noise).join("") }));
|
||||||
|
window.addEventListener("boot:done", begin, { once: true });
|
||||||
|
} else {
|
||||||
|
begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
window.removeEventListener("boot:done", begin);
|
||||||
|
};
|
||||||
|
}, [text, delay, step]);
|
||||||
|
|
||||||
|
if (!frame) return <>{text}</>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="relative inline-block max-w-full align-top">
|
||||||
|
<span className="sr-only">{text}</span>
|
||||||
|
{/* Invisible copy keeps the box exactly the size of the real text. */}
|
||||||
|
<span aria-hidden="true" className={`invisible ${nowrap ? "whitespace-nowrap" : ""}`}>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
<span aria-hidden="true" className={`absolute inset-0 ${nowrap ? "whitespace-nowrap" : ""}`}>
|
||||||
|
{frame.done}
|
||||||
|
<span className="text-accent">{frame.head}</span>
|
||||||
|
<span className="opacity-50">{frame.rest}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ export function SiteFooter() {
|
|||||||
return (
|
return (
|
||||||
<footer className="border-t border-line">
|
<footer className="border-t border-line">
|
||||||
<div className="mx-auto flex max-w-[1440px] flex-wrap justify-between gap-6 px-4 pt-7 pb-10 text-[10px] tracking-[0.18em] text-mute md:px-16">
|
<div className="mx-auto flex max-w-[1440px] flex-wrap justify-between gap-6 px-4 pt-7 pb-10 text-[10px] tracking-[0.18em] text-mute md:px-16">
|
||||||
<span>{profile.displayName.toUpperCase()} · BUILT UNDER ORUS</span>
|
<span>SELIMAJ.DEV · TIRANA, AL</span>
|
||||||
<span className="flex gap-6">
|
<span className="flex gap-6">
|
||||||
<a href={profile.github} className="hover:text-accent">GITHUB</a>
|
<a href={profile.github} className="hover:text-accent">GITHUB</a>
|
||||||
<a href={profile.git} className="hover:text-accent">GIT.SELIMAJ.DEV</a>
|
<a href={profile.git} className="hover:text-accent">GIT.SELIMAJ.DEV</a>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
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 { OrusMark } from "./OrusMark";
|
import { Mark } from "./Mark";
|
||||||
import { SessionClock } from "./Telemetry";
|
import { SessionClock } from "./Telemetry";
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
@@ -25,10 +25,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="Orus · selimaj.dev home">
|
<Link href="/" className="flex min-h-11 items-center gap-3" aria-label="selimaj.dev home">
|
||||||
<OrusMark />
|
<Mark />
|
||||||
<span className="font-display text-sm font-semibold tracking-[0.24em]">ORUS</span>
|
<span className="font-display text-sm font-semibold tracking-[0.24em]">
|
||||||
<span className="hidden text-[10px] tracking-[0.18em] text-mute sm:inline">/ SELIMAJ.DEV</span>
|
SELIMAJ<span className="text-mute">.DEV</span>
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Hero background, bottom to top: terrain contours (real Dajt elevation data),
|
* Hero background, bottom to top: terrain contours (real Dajt elevation data),
|
||||||
* grid, scanlines, vignette. Purely decorative.
|
* grid, scanlines, vignette. Purely decorative and deliberately static.
|
||||||
*/
|
*/
|
||||||
export function TerrainLayer({ className = "" }: { className?: string }) {
|
export function TerrainLayer({ className = "" }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
@@ -9,7 +9,7 @@ export function TerrainLayer({ className = "" }: { className?: string }) {
|
|||||||
<img
|
<img
|
||||||
src="/terrain.svg"
|
src="/terrain.svg"
|
||||||
alt=""
|
alt=""
|
||||||
className="drift absolute -inset-10 h-[calc(100%+80px)] w-[calc(100%+80px)] max-w-none object-cover opacity-20 max-xl:object-[60%_50%]"
|
className="absolute -inset-10 h-[calc(100%+80px)] w-[calc(100%+80px)] max-w-none object-cover opacity-20 max-xl:object-[60%_50%]"
|
||||||
/>
|
/>
|
||||||
<div className="grid-bg absolute inset-0" />
|
<div className="grid-bg absolute inset-0" />
|
||||||
<div className="scan-bg absolute inset-0" />
|
<div className="scan-bg absolute inset-0" />
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const dossier: DossierFile[] = [
|
|||||||
proof: [
|
proof: [
|
||||||
{
|
{
|
||||||
label: "Orus",
|
label: "Orus",
|
||||||
note: "A community that has grown around the work",
|
note: "My open-source brand for privacy and opsec tools",
|
||||||
href: "/projects/#orus",
|
href: "/projects/#orus",
|
||||||
status: "LIVE",
|
status: "LIVE",
|
||||||
},
|
},
|
||||||
@@ -44,16 +44,16 @@ export const dossier: DossierFile[] = [
|
|||||||
slug: "entrepreneurship",
|
slug: "entrepreneurship",
|
||||||
no: "02",
|
no: "02",
|
||||||
title: "Entrepreneurship",
|
title: "Entrepreneurship",
|
||||||
summary: "Orus as the umbrella. Systems where incentives and behavior matter.",
|
summary: "Brands and ventures. Systems where incentives and behavior matter.",
|
||||||
proofSummary: "Osias · Orus · EquitySMP",
|
proofSummary: "Osias · Orus · EquitySMP",
|
||||||
lead: "Orus is the umbrella for everything serious I make.",
|
lead: "I build brands, and ventures that stand on their own.",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Beyond it I design systems where incentives and behavior matter.",
|
"Orus is one of the brands: open-source software for privacy, independence, and opsec. Outside it I design systems where incentives and behavior matter.",
|
||||||
"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: "BUILDING" },
|
||||||
{ label: "Orus", note: "The umbrella org", href: "/projects/#orus", status: "LIVE" },
|
{ label: "Orus", note: "Open-source privacy and opsec brand", href: "/projects/#orus", status: "LIVE" },
|
||||||
{ 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: "BUILDING" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ export const profile = {
|
|||||||
git: "https://git.selimaj.dev",
|
git: "https://git.selimaj.dev",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const orus = {
|
|
||||||
name: "Orus",
|
|
||||||
oneLiner:
|
|
||||||
"A community and toolset for privacy, independence, and opsec, plus the dev tools built under it.",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map lock. Privacy rule: city centre only (Skanderbeg Square, a public
|
* Map lock. Privacy rule: city centre only (Skanderbeg Square, a public
|
||||||
* landmark). Never put anything finer than city scale here.
|
* landmark). Never put anything finer than city scale here.
|
||||||
@@ -39,7 +33,7 @@ export type DossierField = {
|
|||||||
|
|
||||||
export const dossierFields: DossierField[] = [
|
export const dossierFields: DossierField[] = [
|
||||||
{ label: "Based", value: "Tirana, Albania" },
|
{ label: "Based", value: "Tirana, Albania" },
|
||||||
{ label: "Affiliation", value: "Orus (founder)" },
|
{ 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: "Building since", value: "12" },
|
||||||
{ label: "Focus", value: "Privacy, independence, opsec, dev tooling" },
|
{ label: "Focus", value: "Privacy, independence, opsec, dev tooling" },
|
||||||
|
|||||||
+67
-46
@@ -1,6 +1,6 @@
|
|||||||
export type Status = "LIVE" | "BUILDING" | "CONCEPT" | "ARCHIVED";
|
export type Status = "LIVE" | "BUILDING" | "CONCEPT" | "ARCHIVED";
|
||||||
|
|
||||||
export type GroupId = "core" | "ventures" | "infra" | "hardware";
|
export type GroupId = "orus" | "ventures" | "infra" | "hardware";
|
||||||
|
|
||||||
export type Project = {
|
export type Project = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -10,57 +10,91 @@ export type Project = {
|
|||||||
desc: string;
|
desc: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
href?: string;
|
href?: string;
|
||||||
root?: boolean;
|
|
||||||
/** Position on the 1200×720 node map. */
|
/** Position on the 1200×720 node map. */
|
||||||
map: { x: number; y: number };
|
map: { x: number; y: number };
|
||||||
|
/** Which side of the marker the label sits on (defaults to away from its hub). */
|
||||||
|
label?: "l" | "r";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const groups: Record<GroupId, { label: string; x: number; y: number; side: "l" | "r" }> = {
|
/** The map's root: the site itself. Not a project. */
|
||||||
core: { label: "Core", x: 360, y: 220, side: "l" },
|
export const root = {
|
||||||
|
name: "Selimaj",
|
||||||
|
sub: "selimaj.dev",
|
||||||
|
map: { x: 600, y: 360 },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups branch off the root. A group with `node` uses that project as its hub
|
||||||
|
* (Orus is a brand with its own projects); the rest get a plain labelled hub.
|
||||||
|
*/
|
||||||
|
export const groups: Record<
|
||||||
|
GroupId,
|
||||||
|
{ label: string; x: number; y: number; side: "l" | "r"; node?: string }
|
||||||
|
> = {
|
||||||
|
orus: { label: "Orus", x: 340, y: 240, side: "r", node: "orus" },
|
||||||
ventures: { label: "Ventures & systems", x: 840, y: 220, side: "r" },
|
ventures: { label: "Ventures & systems", x: 840, y: 220, side: "r" },
|
||||||
infra: { label: "Infrastructure", x: 360, y: 520, side: "l" },
|
infra: { label: "Infrastructure", x: 360, y: 520, side: "l" },
|
||||||
hardware: { label: "Hardware", x: 840, y: 520, side: "r" },
|
hardware: { label: "Hardware", x: 840, y: 520, side: "r" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// PLACEHOLDER: statuses for everything except Orus and the self-hosted
|
// PLACEHOLDER: statuses other than Orus and the self-hosted infrastructure are
|
||||||
// infrastructure are best guesses. Links are filled in where known.
|
// best guesses. Links are filled in where known.
|
||||||
export const projects: Project[] = [
|
export const projects: Project[] = [
|
||||||
{
|
{
|
||||||
id: "orus",
|
id: "orus",
|
||||||
name: "Orus",
|
name: "Orus",
|
||||||
group: "core",
|
group: "orus",
|
||||||
root: true,
|
|
||||||
status: "LIVE",
|
status: "LIVE",
|
||||||
desc: "Community and org for privacy, independence, opsec, and dev tools. Root of everything below.",
|
desc: "One of my brands: open-source software for privacy, independence and opsec. Home to Enclave, Saturn and the cyberdecks.",
|
||||||
tags: ["community", "opsec", "org"],
|
tags: ["brand", "oss", "privacy"],
|
||||||
map: { x: 600, y: 360 },
|
map: { x: 340, y: 240 },
|
||||||
|
label: "r",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "saturn",
|
id: "saturn",
|
||||||
name: "Saturn",
|
name: "Saturn",
|
||||||
group: "core",
|
group: "orus",
|
||||||
status: "ARCHIVED",
|
status: "BUILDING",
|
||||||
desc: "Minecraft client. Did most of what I set out to do with it.",
|
desc: "Highly advanced and customizable Minecraft client with free emotes and cosmetics",
|
||||||
tags: ["minecraft", "client"],
|
tags: ["minecraft", "client"],
|
||||||
map: { x: 200, y: 90 },
|
map: { x: 190, y: 100 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "enclave",
|
id: "enclave",
|
||||||
name: "Enclave",
|
name: "Enclave",
|
||||||
group: "core",
|
group: "orus",
|
||||||
status: "BUILDING",
|
status: "BUILDING",
|
||||||
desc: "Discord-like chat platform, running on a custom transport protocol.",
|
desc: "Discord-like chat platform, running on a custom transport protocol.",
|
||||||
tags: ["chat", "realtime"],
|
tags: ["chat", "realtime"],
|
||||||
map: { x: 420, y: 70 },
|
map: { x: 430, y: 80 },
|
||||||
|
label: "r",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "enclave-protocol",
|
id: "enclave-protocol",
|
||||||
name: "Enclave protocol",
|
name: "Enclave protocol",
|
||||||
group: "core",
|
group: "orus",
|
||||||
status: "CONCEPT",
|
status: "CONCEPT",
|
||||||
desc: "Transport protocol replacing HTTP and WSS; Enclave’s chat protocol runs on top of it. Concept and design notes.",
|
desc: "Transport protocol replacing HTTP and WSS; Enclave’s chat protocol runs on top of it. Concept and design notes.",
|
||||||
tags: ["networking", "protocol design"],
|
tags: ["networking", "protocol design"],
|
||||||
map: { x: 190, y: 300 },
|
map: { x: 200, y: 330 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "briefcase-deck",
|
||||||
|
name: "Briefcase deck",
|
||||||
|
group: "orus",
|
||||||
|
status: "CONCEPT",
|
||||||
|
desc: "Briefcase cyberdeck: the most versatile and doable build.",
|
||||||
|
tags: ["hardware", "cyberdeck"],
|
||||||
|
map: { x: 160, y: 215 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "compact-deck",
|
||||||
|
name: "Compact deck",
|
||||||
|
group: "orus",
|
||||||
|
status: "CONCEPT",
|
||||||
|
desc: "Compact premium cyberdeck: a Flipper Zero, but better.",
|
||||||
|
tags: ["hardware", "cyberdeck"],
|
||||||
|
map: { x: 290, y: 400 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "osias",
|
id: "osias",
|
||||||
@@ -94,7 +128,7 @@ export const projects: Project[] = [
|
|||||||
id: "algo-trading",
|
id: "algo-trading",
|
||||||
name: "Algo trading",
|
name: "Algo trading",
|
||||||
group: "ventures",
|
group: "ventures",
|
||||||
status: "BUILDING",
|
status: "ARCHIVED",
|
||||||
desc: "Several repos exploring automated crypto trading strategies.",
|
desc: "Several repos exploring automated crypto trading strategies.",
|
||||||
tags: ["trading", "crypto"],
|
tags: ["trading", "crypto"],
|
||||||
map: { x: 980, y: 400 },
|
map: { x: 980, y: 400 },
|
||||||
@@ -107,7 +141,7 @@ export const projects: Project[] = [
|
|||||||
desc: "Gitea at git.selimaj.dev, on a Docker and Coolify homelab server behind Cloudflare.",
|
desc: "Gitea at git.selimaj.dev, on a Docker and Coolify homelab server behind Cloudflare.",
|
||||||
tags: ["gitea", "docker", "coolify"],
|
tags: ["gitea", "docker", "coolify"],
|
||||||
href: "https://git.selimaj.dev",
|
href: "https://git.selimaj.dev",
|
||||||
map: { x: 190, y: 450 },
|
map: { x: 190, y: 470 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "homelab",
|
id: "homelab",
|
||||||
@@ -118,41 +152,28 @@ export const projects: Project[] = [
|
|||||||
tags: ["infra", "docker"],
|
tags: ["infra", "docker"],
|
||||||
map: { x: 300, y: 660 },
|
map: { x: 300, y: 660 },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "briefcase-deck",
|
|
||||||
name: "Briefcase deck",
|
|
||||||
group: "hardware",
|
|
||||||
status: "BUILDING",
|
|
||||||
desc: "Briefcase cyberdeck: the most versatile and doable build.",
|
|
||||||
tags: ["hardware", "cyberdeck"],
|
|
||||||
map: { x: 1020, y: 470 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "compact-deck",
|
|
||||||
name: "Compact deck",
|
|
||||||
group: "hardware",
|
|
||||||
status: "CONCEPT",
|
|
||||||
desc: "Compact premium cyberdeck: a Flipper Zero, but better.",
|
|
||||||
tags: ["hardware", "cyberdeck"],
|
|
||||||
map: { x: 1000, y: 640 },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "rc-planes",
|
id: "rc-planes",
|
||||||
name: "RC planes",
|
name: "RC planes",
|
||||||
group: "hardware",
|
group: "hardware",
|
||||||
status: "BUILDING",
|
status: "CONCEPT",
|
||||||
desc: "Custom long-range, PC and 4G-controlled planes pushing speed and efficiency, built around a custom watercooled powertrain.",
|
desc: "Custom long-range, PC and 4G-controlled planes pushing speed and efficiency, built around a custom watercooled powertrain.",
|
||||||
tags: ["hardware", "rc", "4g"],
|
tags: ["hardware", "rc", "4g"],
|
||||||
map: { x: 720, y: 660 },
|
map: { x: 1000, y: 620 },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const projectById = (id: string) => projects.find((p) => p.id === id);
|
export const projectById = (id: string) => projects.find((p) => p.id === id);
|
||||||
|
|
||||||
/** Home page featured strip: positions are percentages of the stage. */
|
/** The brand a project is published under, if any. */
|
||||||
|
export const brandOf = (p: Project) =>
|
||||||
|
p.group === "orus" && p.id !== "orus" ? "Orus" : undefined;
|
||||||
|
|
||||||
|
/** Home page featured strip, branching from the root. Positions are % of the stage. */
|
||||||
|
export const featuredRoot = { x: 16, y: 50 };
|
||||||
export const featured = [
|
export const featured = [
|
||||||
{ id: "orus", x: 18, y: 50 },
|
{ id: "orus", x: 42, y: 16 },
|
||||||
{ id: "saturn", x: 46, y: 22 },
|
{ id: "enclave", x: 66, y: 36 },
|
||||||
{ id: "enclave", x: 68, y: 52 },
|
{ id: "saturn", x: 62, y: 64 },
|
||||||
{ id: "equitysmp", x: 44, y: 80 },
|
{ id: "osias", x: 40, y: 84 },
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user