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 Link from "next/link";
|
||||
import { dossier } from "@/content/dossier";
|
||||
import { orus, profile } from "@/content/profile";
|
||||
import { profile } from "@/content/profile";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
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)]">
|
||||
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">SUBJECT</dt>
|
||||
<dd>{profile.displayName}</dd>
|
||||
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">ROOT</dt>
|
||||
<dd>{orus.name}</dd>
|
||||
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">DOMAIN</dt>
|
||||
<dd>selimaj.dev</dd>
|
||||
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">FILES</dt>
|
||||
<dd>{String(dossier.length).padStart(2, "0")}</dd>
|
||||
<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;
|
||||
}
|
||||
|
||||
.drift {
|
||||
animation: drift 80s ease-in-out infinite alternate;
|
||||
will-change: transform;
|
||||
}
|
||||
.spin {
|
||||
animation: spin 16s linear infinite;
|
||||
transform-origin: 60px 60px;
|
||||
@@ -185,11 +181,6 @@ html[data-boot] .seq {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes drift {
|
||||
to {
|
||||
transform: translate3d(-28px, 14px, 0);
|
||||
}
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<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"/>
|
||||
<circle cx="12" cy="12" r="2.6" fill="#4dff95"/>
|
||||
<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: 275 B After Width: | Height: | Size: 314 B |
+1
-1
@@ -17,7 +17,7 @@ const jetbrains = JetBrains_Mono({
|
||||
|
||||
export const metadata: Metadata = {
|
||||
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"),
|
||||
};
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ import { projects } from "@/content/projects";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
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() {
|
||||
return (
|
||||
<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="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>
|
||||
<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.
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
// 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 }) {
|
||||
const pathname = usePathname();
|
||||
if (pathname === "/") return <>{children}</>;
|
||||
return <div className="page-resolve seq">{children}</div>;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,10 @@ export function BootSequence() {
|
||||
sessionStorage.setItem("boot", "1");
|
||||
} catch {}
|
||||
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);
|
||||
window.addEventListener("keydown", finish);
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
import Link from "next/link";
|
||||
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";
|
||||
|
||||
const ROOT = featured[0];
|
||||
|
||||
export function FeaturedNodes() {
|
||||
const [active, setActive] = useState<string | null>(null);
|
||||
const nodes = featured.map((f) => ({ ...f, project: projectById(f.id)! }));
|
||||
@@ -30,11 +28,11 @@ export function FeaturedNodes() {
|
||||
>
|
||||
{size && (
|
||||
<div aria-hidden="true" className="hidden md:block">
|
||||
{nodes.slice(1).map((n) => (
|
||||
{nodes.map((n) => (
|
||||
<Segment
|
||||
key={`${n.id}-${active === n.id}`}
|
||||
x1={(ROOT.x / 100) * size.w}
|
||||
y1={(ROOT.y / 100) * size.h}
|
||||
x1={(featuredRoot.x / 100) * size.w}
|
||||
y1={(featuredRoot.y / 100) * size.h}
|
||||
x2={(n.x / 100) * size.w}
|
||||
y2={(n.y / 100) * size.h}
|
||||
on={active === n.id}
|
||||
@@ -45,9 +43,29 @@ export function FeaturedNodes() {
|
||||
</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) => {
|
||||
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 (
|
||||
<Link
|
||||
key={n.id}
|
||||
@@ -60,33 +78,27 @@ export function FeaturedNodes() {
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`absolute box-border border ${
|
||||
root
|
||||
? "top-2.5 left-0 size-7 rounded-full md:-top-3.5 md:-left-3.5"
|
||||
: "top-4 left-2 size-3 md:-top-1.5 md:-left-1.5"
|
||||
} ${on ? "border-accent shadow-[0_0_14px_var(--color-accent)]" : "border-ink/60"} ${on && !root ? "bg-accent" : "bg-night"}`}
|
||||
>
|
||||
{root && <span className={`absolute top-[9px] left-[9px] size-2 rounded-full ${on ? "bg-accent" : "bg-ink"}`} />}
|
||||
</span>
|
||||
className={`absolute top-4 left-2 box-border size-3 border md:-top-1.5 md:-left-1.5 ${n.id === "orus" ? "rotate-45" : ""} ${
|
||||
on ? "border-accent bg-accent shadow-[0_0_14px_var(--color-accent)]" : "border-ink/60 bg-night"
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`bracket ml-10 block min-h-11 px-3 py-2.5 group-focus-visible:outline group-focus-visible:outline-accent md:absolute md:-top-[22px] md:ml-0 ${
|
||||
root ? "md:right-6 md:w-max md:text-right" : "md:left-6 md:w-[280px]"
|
||||
} ${
|
||||
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] ${
|
||||
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="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 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>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
<div aria-hidden="true" className="absolute right-4 bottom-3 hidden text-[9px] tracking-[0.2em] text-mute/70 md:block">
|
||||
ROOT ORUS · {featured.length} OF {projects.length} NODES
|
||||
<div aria-hidden="true" className="absolute right-4 bottom-3 hidden text-[9px] tracking-[0.2em] text-mute/70 uppercase md:block">
|
||||
Root {root.sub} · {featured.length} of {projects.length} nodes
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+38
-29
@@ -1,39 +1,47 @@
|
||||
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 { Scramble } from "./Scramble";
|
||||
import { LocalTime, SessionStamp } from "./Telemetry";
|
||||
import { TerrainLayer } from "./TerrainLayer";
|
||||
import { LockReticle, TiranaMap } from "./TiranaMap";
|
||||
|
||||
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 }) {
|
||||
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}`}>
|
||||
{fields.map((f) => (
|
||||
{fields.map((f, i) => {
|
||||
const delay = DOSSIER_START + i * DOSSIER_STAGGER;
|
||||
return (
|
||||
<div key={f.label} className="contents">
|
||||
<dt className="pt-[3px] text-[9px] uppercase tracking-[0.22em] text-mute">{f.label}</dt>
|
||||
<dd className="flex flex-wrap gap-x-3.5">
|
||||
{f.links
|
||||
? f.links.map((l) => (
|
||||
? f.links.map((l, j) => (
|
||||
<a key={l.href} href={l.href} className="border-b border-white/25 hover:border-accent">
|
||||
{l.label}
|
||||
<Scramble text={l.label} delay={delay + j * 0.2} step={30} />
|
||||
</a>
|
||||
))
|
||||
: f.value}
|
||||
: <Scramble text={f.value} delay={delay} step={30} />}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
export function Hero() {
|
||||
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 />
|
||||
|
||||
<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. */}
|
||||
<div
|
||||
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]"
|
||||
style={d(0.25)}
|
||||
/>
|
||||
<div
|
||||
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]"
|
||||
style={d(0.85)}
|
||||
>
|
||||
<div className="text-accent">LOCK ACQUIRED</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]">
|
||||
<div className="text-accent">
|
||||
<Scramble text="LOCK ACQUIRED" delay={0.85} step={35} nowrap />
|
||||
</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 className="uppercase">{lock.place}</div>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
{/* Headline */}
|
||||
<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)}>
|
||||
TARGET RESOLVED · SUBJECT 0001
|
||||
<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} />
|
||||
</p>
|
||||
<h1
|
||||
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]"
|
||||
style={d(1.2)}
|
||||
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]"
|
||||
>
|
||||
Entrepreneur
|
||||
<Scramble text="ENTREPRENEUR" delay={0.3} step={35} nowrap />
|
||||
<br />
|
||||
& Builder
|
||||
<Scramble text="& BUILDER" delay={0.5} step={35} nowrap />
|
||||
</h1>
|
||||
<p className="resolve seq mt-4 text-[15px] leading-normal text-ink-2 xl:mt-6 xl:text-[17px]" style={d(1.3)}>
|
||||
{profile.stance}
|
||||
<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} />
|
||||
</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)}>
|
||||
<Link
|
||||
@@ -95,10 +103,6 @@ export function Hero() {
|
||||
OPEN DOSSIER <span aria-hidden="true">↗</span>
|
||||
</Link>
|
||||
</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" />
|
||||
</div>
|
||||
|
||||
@@ -122,7 +126,7 @@ export function Hero() {
|
||||
<div className="min-w-0 xl:pt-1">
|
||||
<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">
|
||||
{profile.displayName}
|
||||
<Scramble text={profile.displayName.toUpperCase()} delay={1.2} step={90} nowrap />
|
||||
</h2>
|
||||
<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>
|
||||
@@ -135,7 +139,11 @@ export function Hero() {
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{/* Corner telemetry */}
|
||||
</div>
|
||||
|
||||
<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]">
|
||||
{/* 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)}>
|
||||
<SessionStamp />
|
||||
</div>
|
||||
@@ -149,6 +157,7 @@ export function Hero() {
|
||||
<div className="text-mute/70">VISITOR · UNRESOLVED</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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";
|
||||
|
||||
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 { StatusTag } from "./StatusTag";
|
||||
|
||||
const W = 1200;
|
||||
const H = 720;
|
||||
const root = projects.find((p) => p.root)!;
|
||||
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 }) {
|
||||
const size = p.root ? 30 : 14;
|
||||
const hub = isHub(p);
|
||||
const size = hub ? 22 : 14;
|
||||
const rest =
|
||||
p.status === "CONCEPT" ? "border border-dashed border-ink/60" : p.status === "ARCHIVED" ? "border border-ink/30" : "border border-ink/65";
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`relative box-border flex-none ${p.root ? "rounded-full" : ""} ${
|
||||
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`
|
||||
className={`relative box-border flex-none ${hub ? "rotate-45" : ""} ${
|
||||
on ? "bg-accent shadow-[0_0_12px_var(--color-accent)]" : `${rest} bg-surface`
|
||||
}`}
|
||||
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 }) {
|
||||
const brand = brandOf(p);
|
||||
const body = (
|
||||
<>
|
||||
<span className="flex items-center justify-between">
|
||||
<span className="flex items-center justify-between gap-3">
|
||||
<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">
|
||||
{p.href ? "↗" : "→"}
|
||||
</span>
|
||||
@@ -62,7 +65,7 @@ function ProjectCard({ p }: { p: Project }) {
|
||||
|
||||
export function ProjectIndex() {
|
||||
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.
|
||||
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");
|
||||
|
||||
return (
|
||||
@@ -112,10 +115,10 @@ export function ProjectIndex() {
|
||||
<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 }}>
|
||||
{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
|
||||
.filter((p) => !p.root)
|
||||
.filter((p) => !isHub(p))
|
||||
.map((p) => (
|
||||
<Segment
|
||||
key={`${p.id}-${active === p.id}`}
|
||||
@@ -127,7 +130,11 @@ export function ProjectIndex() {
|
||||
dashed={p.status === "CONCEPT"}
|
||||
/>
|
||||
))}
|
||||
{groupIds.map((k) => {
|
||||
|
||||
{/* Plain hubs (groups without a project of their own). */}
|
||||
{groupIds
|
||||
.filter((k) => !groups[k].node)
|
||||
.map((k) => {
|
||||
const g = groups[k];
|
||||
return (
|
||||
<div key={k} aria-hidden="true">
|
||||
@@ -141,11 +148,23 @@ export function ProjectIndex() {
|
||||
</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) => {
|
||||
const on = active === p.id;
|
||||
const g = groups[p.group];
|
||||
const labelLeft = !p.root && p.map.x < g.x;
|
||||
const m = p.root ? 30 : 14;
|
||||
const labelLeft = (p.label ?? (p.map.x < g.x ? "l" : "r")) === "l";
|
||||
const m = isHub(p) ? 22 : 14;
|
||||
const pad = 8;
|
||||
return (
|
||||
<button
|
||||
@@ -165,15 +184,16 @@ export function ProjectIndex() {
|
||||
}}
|
||||
>
|
||||
<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
|
||||
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.name}
|
||||
</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>
|
||||
</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">
|
||||
GRID 40 · ROOT {root.map.x},{root.map.y}
|
||||
</div>
|
||||
<div aria-hidden="true" className="absolute right-4 bottom-3 text-[9px] tracking-[0.2em] text-accent uppercase">
|
||||
TRACKING ▸ {sel.name}
|
||||
<div aria-hidden="true" className="absolute right-4 bottom-3 text-[9px] tracking-[0.2em] uppercase">
|
||||
{sel ? <span className="text-accent">TRACKING ▸ {sel.name}</span> : <span className="text-mute/70">AWAITING TARGET</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,12 +210,18 @@ export function ProjectIndex() {
|
||||
<section
|
||||
aria-live="polite"
|
||||
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)]"
|
||||
}`}
|
||||
>
|
||||
{sel ? (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-3.5">
|
||||
<StatusTag status={sel.status} />
|
||||
<span className="text-[9px] tracking-[0.24em] text-mute uppercase">{sel.root ? "Root" : groups[sel.group].label}</span>
|
||||
<span className="text-[9px] tracking-[0.24em] text-mute uppercase">
|
||||
{isHub(sel) ? "Brand" : (brandOf(sel) ?? groups[sel.group].label)}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="mt-3 font-display text-3xl font-semibold tracking-[0.04em] uppercase">{sel.name}</h2>
|
||||
<p className="mt-2 max-w-[720px] text-[13px] leading-[1.7] text-ink-2">{sel.desc}</p>
|
||||
@@ -223,6 +249,10 @@ export function ProjectIndex() {
|
||||
VIEW IN LIST <span aria-hidden="true">→</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-[11px] tracking-[0.2em] text-mute">HOVER OR SELECT A NODE TO TRACK IT.</p>
|
||||
)}
|
||||
</section>
|
||||
</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 (
|
||||
<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">
|
||||
<span>{profile.displayName.toUpperCase()} · BUILT UNDER ORUS</span>
|
||||
<span>SELIMAJ.DEV · TIRANA, AL</span>
|
||||
<span className="flex gap-6">
|
||||
<a href={profile.github} className="hover:text-accent">GITHUB</a>
|
||||
<a href={profile.git} className="hover:text-accent">GIT.SELIMAJ.DEV</a>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { OrusMark } from "./OrusMark";
|
||||
import { Mark } from "./Mark";
|
||||
import { SessionClock } from "./Telemetry";
|
||||
|
||||
const links = [
|
||||
@@ -25,10 +25,11 @@ export function SiteHeader() {
|
||||
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]">
|
||||
<Link href="/" className="flex min-h-11 items-center gap-3" aria-label="Orus · selimaj.dev home">
|
||||
<OrusMark />
|
||||
<span className="font-display text-sm font-semibold tracking-[0.24em]">ORUS</span>
|
||||
<span className="hidden text-[10px] tracking-[0.18em] text-mute sm:inline">/ SELIMAJ.DEV</span>
|
||||
<Link href="/" className="flex min-h-11 items-center gap-3" aria-label="selimaj.dev home">
|
||||
<Mark />
|
||||
<span className="font-display text-sm font-semibold tracking-[0.24em]">
|
||||
SELIMAJ<span className="text-mute">.DEV</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 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 }) {
|
||||
return (
|
||||
@@ -9,7 +9,7 @@ export function TerrainLayer({ className = "" }: { className?: string }) {
|
||||
<img
|
||||
src="/terrain.svg"
|
||||
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="scan-bg absolute inset-0" />
|
||||
|
||||
@@ -34,7 +34,7 @@ export const dossier: DossierFile[] = [
|
||||
proof: [
|
||||
{
|
||||
label: "Orus",
|
||||
note: "A community that has grown around the work",
|
||||
note: "My open-source brand for privacy and opsec tools",
|
||||
href: "/projects/#orus",
|
||||
status: "LIVE",
|
||||
},
|
||||
@@ -44,16 +44,16 @@ export const dossier: DossierFile[] = [
|
||||
slug: "entrepreneurship",
|
||||
no: "02",
|
||||
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",
|
||||
lead: "Orus is the umbrella for everything serious I make.",
|
||||
lead: "I build brands, and ventures that stand on their own.",
|
||||
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.",
|
||||
],
|
||||
proof: [
|
||||
{ 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" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -15,12 +15,6 @@ export const profile = {
|
||||
git: "https://git.selimaj.dev",
|
||||
} 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
|
||||
* landmark). Never put anything finer than city scale here.
|
||||
@@ -39,7 +33,7 @@ export type DossierField = {
|
||||
|
||||
export const dossierFields: DossierField[] = [
|
||||
{ label: "Based", value: "Tirana, Albania" },
|
||||
{ label: "Affiliation", value: "Orus (founder)" },
|
||||
{ label: "Brands", value: "Orus" },
|
||||
// PLACEHOLDER decision: remove this entry to stop stating it publicly.
|
||||
{ label: "Building since", value: "12" },
|
||||
{ label: "Focus", value: "Privacy, independence, opsec, dev tooling" },
|
||||
|
||||
+67
-46
@@ -1,6 +1,6 @@
|
||||
export type Status = "LIVE" | "BUILDING" | "CONCEPT" | "ARCHIVED";
|
||||
|
||||
export type GroupId = "core" | "ventures" | "infra" | "hardware";
|
||||
export type GroupId = "orus" | "ventures" | "infra" | "hardware";
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
@@ -10,57 +10,91 @@ export type Project = {
|
||||
desc: string;
|
||||
tags: string[];
|
||||
href?: string;
|
||||
root?: boolean;
|
||||
/** Position on the 1200×720 node map. */
|
||||
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" }> = {
|
||||
core: { label: "Core", x: 360, y: 220, side: "l" },
|
||||
/** The map's root: the site itself. Not a project. */
|
||||
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" },
|
||||
infra: { label: "Infrastructure", x: 360, y: 520, side: "l" },
|
||||
hardware: { label: "Hardware", x: 840, y: 520, side: "r" },
|
||||
};
|
||||
|
||||
// PLACEHOLDER: statuses for everything except Orus and the self-hosted
|
||||
// infrastructure are best guesses. Links are filled in where known.
|
||||
// PLACEHOLDER: statuses other than Orus and the self-hosted infrastructure are
|
||||
// best guesses. Links are filled in where known.
|
||||
export const projects: Project[] = [
|
||||
{
|
||||
id: "orus",
|
||||
name: "Orus",
|
||||
group: "core",
|
||||
root: true,
|
||||
group: "orus",
|
||||
status: "LIVE",
|
||||
desc: "Community and org for privacy, independence, opsec, and dev tools. Root of everything below.",
|
||||
tags: ["community", "opsec", "org"],
|
||||
map: { x: 600, y: 360 },
|
||||
desc: "One of my brands: open-source software for privacy, independence and opsec. Home to Enclave, Saturn and the cyberdecks.",
|
||||
tags: ["brand", "oss", "privacy"],
|
||||
map: { x: 340, y: 240 },
|
||||
label: "r",
|
||||
},
|
||||
{
|
||||
id: "saturn",
|
||||
name: "Saturn",
|
||||
group: "core",
|
||||
status: "ARCHIVED",
|
||||
desc: "Minecraft client. Did most of what I set out to do with it.",
|
||||
group: "orus",
|
||||
status: "BUILDING",
|
||||
desc: "Highly advanced and customizable Minecraft client with free emotes and cosmetics",
|
||||
tags: ["minecraft", "client"],
|
||||
map: { x: 200, y: 90 },
|
||||
map: { x: 190, y: 100 },
|
||||
},
|
||||
{
|
||||
id: "enclave",
|
||||
name: "Enclave",
|
||||
group: "core",
|
||||
group: "orus",
|
||||
status: "BUILDING",
|
||||
desc: "Discord-like chat platform, running on a custom transport protocol.",
|
||||
tags: ["chat", "realtime"],
|
||||
map: { x: 420, y: 70 },
|
||||
map: { x: 430, y: 80 },
|
||||
label: "r",
|
||||
},
|
||||
{
|
||||
id: "enclave-protocol",
|
||||
name: "Enclave protocol",
|
||||
group: "core",
|
||||
group: "orus",
|
||||
status: "CONCEPT",
|
||||
desc: "Transport protocol replacing HTTP and WSS; Enclave’s chat protocol runs on top of it. Concept and design notes.",
|
||||
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",
|
||||
@@ -94,7 +128,7 @@ export const projects: Project[] = [
|
||||
id: "algo-trading",
|
||||
name: "Algo trading",
|
||||
group: "ventures",
|
||||
status: "BUILDING",
|
||||
status: "ARCHIVED",
|
||||
desc: "Several repos exploring automated crypto trading strategies.",
|
||||
tags: ["trading", "crypto"],
|
||||
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.",
|
||||
tags: ["gitea", "docker", "coolify"],
|
||||
href: "https://git.selimaj.dev",
|
||||
map: { x: 190, y: 450 },
|
||||
map: { x: 190, y: 470 },
|
||||
},
|
||||
{
|
||||
id: "homelab",
|
||||
@@ -118,41 +152,28 @@ export const projects: Project[] = [
|
||||
tags: ["infra", "docker"],
|
||||
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",
|
||||
name: "RC planes",
|
||||
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.",
|
||||
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);
|
||||
|
||||
/** 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 = [
|
||||
{ id: "orus", x: 18, y: 50 },
|
||||
{ id: "saturn", x: 46, y: 22 },
|
||||
{ id: "enclave", x: 68, y: 52 },
|
||||
{ id: "equitysmp", x: 44, y: 80 },
|
||||
{ id: "orus", x: 42, y: 16 },
|
||||
{ id: "enclave", x: 66, y: 36 },
|
||||
{ id: "saturn", x: 62, y: 64 },
|
||||
{ id: "osias", x: 40, y: 84 },
|
||||
] as const;
|
||||
|
||||
Reference in New Issue
Block a user