Skip to content

Commit c64500a

Browse files
committed
feat(website): editorial/blueprint visual redesign of docs
Replaces the generic neutral preset + Inter homepage with a distinct engineering-blueprint aesthetic: warm paper / deep ink palette with a single safety-orange accent, Fraunces (display) + IBM Plex Sans (body) + JetBrains Mono (labels) via next/font, blueprint-grid hero with monospace meta bar and italic-orange "bimanual" accent, numbered spec sheet, mono-indexed routes list, serif docs titles with a "§ Category" section pip, paper-card Mermaid frames with corner registration marks, and a [ Haller ] serif+brackets nav wordmark. Also fixes silent Mermaid failure on docs pages: browsers resolved the oklch() palette vars to lab() strings which Mermaid 11's khroma color parser can't read, so initialize() threw and the diagram stuck on its skeleton. Mermaid now uses a hardcoded hex palette keyed by resolvedTheme.
1 parent e4e53fe commit c64500a

6 files changed

Lines changed: 763 additions & 68 deletions

File tree

website/app/(home)/page.tsx

Lines changed: 248 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,259 @@
11
import Link from 'next/link';
2+
import { ArrowUpRight } from 'lucide-react';
23

34
export default function HomePage() {
45
return (
5-
<main className="flex flex-1 flex-col">
6-
<section className="flex flex-col items-center justify-center px-6 py-24 text-center md:py-32">
7-
<p className="mb-4 font-mono text-xs uppercase tracking-[0.18em] text-fd-muted-foreground">
8-
open hardware + software
9-
</p>
10-
<h1 className="max-w-3xl text-4xl font-semibold leading-tight tracking-tight md:text-6xl">
11-
Haller — a hackable bimanual mobile manipulator.
12-
</h1>
13-
<p className="mt-6 max-w-2xl text-base text-fd-muted-foreground md:text-lg">
14-
A four-wheel differential-drive base carrying two SO-101 arms.
15-
ROS 2 for the base, LeRobot for the arms, a unified FastAPI + Next.js HMI
16-
that ties it all together. Build it, calibrate it in the browser, teleoperate it
17-
across the room.
18-
</p>
19-
<div className="mt-10 flex flex-wrap items-center justify-center gap-3">
20-
<Link
21-
href="/docs"
22-
className="inline-flex h-10 items-center rounded-md bg-fd-primary px-5 text-sm font-medium text-fd-primary-foreground transition hover:opacity-90"
23-
>
24-
Read the docs
25-
</Link>
26-
<Link
27-
href="https://github.com/oscardvs/haller_ws"
28-
className="inline-flex h-10 items-center rounded-md border border-fd-border px-5 text-sm font-medium transition hover:bg-fd-accent"
29-
>
30-
View on GitHub
31-
</Link>
32-
</div>
33-
</section>
34-
35-
<section className="mx-auto grid w-full max-w-5xl gap-4 px-6 pb-24 md:grid-cols-3">
36-
<Feature
37-
title="ROS 2 base"
38-
body="Differential drive on LK-TECH MF5010 BLDCs over CAN. RPLIDAR A1M8 for 2D mapping. Runs on Jetson Orin Nano."
39-
/>
40-
<Feature
41-
title="Two SO-101 arms"
42-
body="Open hardware design from TheRobotStudio, 6× Feetech STS3215 servos each. Symmetric build — either arm can lead or follow."
43-
/>
44-
<Feature
45-
title="Browser HMI"
46-
body="One unified web interface for both arms and the base. Joint sliders, teleop launcher, calibration wizard, dataset recording."
47-
/>
48-
</section>
6+
<main className="relative flex flex-1 flex-col overflow-hidden">
7+
{/* full-bleed blueprint grid sits behind the hero only */}
8+
<div className="pointer-events-none absolute inset-x-0 top-0 h-[120vh] [mask-image:linear-gradient(to_bottom,black_0%,black_55%,transparent_100%)]">
9+
<div className="h-grid h-full w-full" />
10+
</div>
11+
12+
<Hero />
13+
<SpecSheet />
14+
<RoutesIndex />
15+
<Footnote />
4916
</main>
5017
);
5118
}
5219

53-
function Feature({ title, body }: { title: string; body: string }) {
20+
/* ────────────────────────────────────────────────────────────────────────── */
21+
22+
function Hero() {
23+
return (
24+
<section className="relative mx-auto w-full max-w-6xl px-6 pt-14 pb-24 sm:pt-20 md:pt-28">
25+
{/* registration marks pinned to the hero block */}
26+
<span className="h-cross" style={{ top: 24, left: 8 }} />
27+
<span className="h-cross" style={{ top: 24, right: 8 }} />
28+
29+
{/* meta bar */}
30+
<div
31+
className="h-rise flex flex-wrap items-center gap-x-5 gap-y-2 text-fd-muted-foreground h-mono"
32+
style={{ animationDelay: '0ms' }}
33+
>
34+
<span className="inline-flex items-center gap-2 text-[11px]">
35+
<span className="h-status-dot" />
36+
<span>HALLER / V0.1 / OPEN HARDWARE</span>
37+
</span>
38+
<span className="hidden h-3 w-px bg-fd-border sm:block" />
39+
<span className="text-[11px]">N 50°50′ · E 04°21′ · BRUSSELS</span>
40+
<span className="hidden h-3 w-px bg-fd-border sm:block" />
41+
<span className="text-[11px]">DOCS · LAST PUSH 2026-05-23</span>
42+
</div>
43+
44+
{/* title — serif display, italic accent, mono pip */}
45+
<h1
46+
className="h-rise mt-10 text-balance text-[clamp(2.6rem,7.4vw,6.4rem)] leading-[0.96] text-fd-foreground h-display"
47+
style={{ animationDelay: '80ms' }}
48+
>
49+
A hackable{' '}
50+
<span className="italic text-fd-primary" style={{ fontVariationSettings: "'opsz' 144, 'SOFT' 100" }}>
51+
bimanual
52+
</span>{' '}
53+
mobile manipulator,
54+
<br className="hidden sm:block" />
55+
engineered in the open.
56+
</h1>
57+
58+
{/* deck / lede */}
59+
<p
60+
className="h-rise mt-8 max-w-2xl text-pretty text-[1.05rem] leading-[1.55] text-fd-muted-foreground sm:text-[1.12rem]"
61+
style={{ animationDelay: '160ms' }}
62+
>
63+
Haller is a four-wheel differential-drive base carrying two SO-101 arms.
64+
ROS&nbsp;2 for the base, LeRobot for the arms, a unified FastAPI&nbsp;+&nbsp;Next.js HMI
65+
that ties it all together —{' '}
66+
<span className="italic text-fd-foreground">build it, calibrate it in the browser,
67+
teleoperate it across the room.</span>
68+
</p>
69+
70+
{/* CTAs */}
71+
<div
72+
className="h-rise mt-10 flex flex-wrap items-center gap-3"
73+
style={{ animationDelay: '240ms' }}
74+
>
75+
<Link
76+
href="/docs"
77+
className="group inline-flex h-11 items-center gap-2 rounded-md bg-fd-primary px-5 text-[13px] font-medium tracking-wide text-fd-primary-foreground shadow-[0_1px_0_0_oklch(0%_0_0_/_8%)] transition hover:translate-y-[-1px] hover:shadow-[0_4px_18px_-6px_oklch(63%_0.21_42_/_55%)]"
78+
>
79+
<span className="h-mono text-[10px] opacity-80"></span>
80+
Read the documentation
81+
</Link>
82+
<Link
83+
href="https://github.com/oscardvs/haller_ws"
84+
className="group inline-flex h-11 items-center gap-2 rounded-md border border-fd-border bg-fd-card/40 px-5 text-[13px] font-medium tracking-wide text-fd-foreground backdrop-blur transition hover:border-fd-primary/60 hover:bg-fd-card"
85+
>
86+
View on GitHub
87+
<ArrowUpRight className="h-3.5 w-3.5 opacity-70 transition group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
88+
</Link>
89+
<Link
90+
href="/docs/architecture"
91+
className="ml-1 inline-flex h-11 items-center text-[13px] text-fd-muted-foreground underline underline-offset-4 decoration-fd-border transition hover:text-fd-primary hover:decoration-fd-primary/60"
92+
>
93+
See the architecture diagram
94+
</Link>
95+
</div>
96+
</section>
97+
);
98+
}
99+
100+
/* ────────────────────────────────────────────────────────────────────────── */
101+
102+
const SPECS = [
103+
{
104+
n: '01',
105+
tag: 'BASE',
106+
title: 'ROS 2 differential drive',
107+
body: 'LK-TECH MF5010 BLDCs over CAN at 1 Mbps. RPLIDAR A1M8 publishes 2D scans into Nav2. The whole stack runs on a Jetson Orin Nano with a 0.5 s cmd_vel watchdog as the secondary safety.',
108+
stat: ['/cmd_vel', '50 Hz odom'],
109+
},
110+
{
111+
n: '02',
112+
tag: 'ARMS',
113+
title: 'Two symmetric SO-101 arms',
114+
body: 'Open hardware from TheRobotStudio — 6× Feetech STS3215 servos each, on half-duplex USB-TTL buses. Either arm can lead or follow. Calibration sidecars are saved and backed up on every write.',
115+
stat: ['6 DoF × 2', 'in-browser calib'],
116+
},
117+
{
118+
n: '03',
119+
tag: 'HMI',
120+
title: 'One browser, every loop closed',
121+
body: 'A unified Next.js + FastAPI dashboard: joint sliders, pose presets, MJPEG feeds, leader↔follower teleop at 60 Hz, webcam pose-teleop with a SPACE dead-man, dataset recording — all behind a single E-STOP.',
122+
stat: ['60 Hz teleop', '20 Hz telemetry'],
123+
},
124+
];
125+
126+
function SpecSheet() {
54127
return (
55-
<div className="rounded-lg border border-fd-border bg-fd-card p-5">
56-
<h3 className="mb-2 font-medium">{title}</h3>
57-
<p className="text-sm text-fd-muted-foreground">{body}</p>
58-
</div>
128+
<section className="relative mx-auto w-full max-w-6xl px-6 pt-6 pb-24">
129+
<header className="mb-10 flex items-baseline justify-between">
130+
<span className="h-label">[ Spec Sheet · 03 ]</span>
131+
<span className="h-mono text-[10px] text-fd-muted-foreground">
132+
§01—03 / overview
133+
</span>
134+
</header>
135+
136+
<div className="h-rule mb-8" />
137+
138+
<ol className="grid gap-px overflow-hidden rounded-md border border-fd-border bg-fd-border md:grid-cols-3">
139+
{SPECS.map((s) => (
140+
<li
141+
key={s.n}
142+
className="group relative flex flex-col bg-fd-background p-7 transition hover:bg-fd-card"
143+
>
144+
<div className="flex items-baseline justify-between">
145+
<span className="h-mono text-[11px] tracking-widest text-fd-primary">
146+
[{s.n}]
147+
</span>
148+
<span className="h-label">{s.tag}</span>
149+
</div>
150+
151+
<h3
152+
className="mt-6 text-[1.55rem] leading-[1.1] text-fd-foreground h-display"
153+
>
154+
{s.title}
155+
</h3>
156+
157+
<p className="mt-4 text-[0.95rem] leading-[1.55] text-fd-muted-foreground">
158+
{s.body}
159+
</p>
160+
161+
<ul className="mt-6 flex flex-wrap gap-x-4 gap-y-1 border-t border-fd-border pt-4">
162+
{s.stat.map((k) => (
163+
<li
164+
key={k}
165+
className="h-mono text-[10.5px] uppercase tracking-[0.14em] text-fd-muted-foreground"
166+
>
167+
· {k}
168+
</li>
169+
))}
170+
</ul>
171+
</li>
172+
))}
173+
</ol>
174+
</section>
175+
);
176+
}
177+
178+
/* ────────────────────────────────────────────────────────────────────────── */
179+
180+
const ROUTES: { label: string; href: string; hint: string }[] = [
181+
{ label: 'Architecture', href: '/docs/architecture', hint: 'one-screen mental model' },
182+
{ label: 'Bill of materials', href: '/docs/hardware/bom', hint: 'parts & sourcing' },
183+
{ label: 'Build', href: '/docs/hardware/build', hint: 'mechanical assembly' },
184+
{ label: 'Mobile base (ROS 2)', href: '/docs/base/overview', hint: 'bringup & drive' },
185+
{ label: 'Vision pipeline', href: '/docs/base/vision', hint: 'detection + seg + costmap' },
186+
{ label: 'LeRobot environment', href: '/docs/setup/lerobot-environment', hint: 'python tooling' },
187+
{ label: 'SO-101 arms', href: '/docs/setup/so101-arm', hint: 'calibrate & test' },
188+
{ label: 'HMI overview', href: '/docs/hmi/overview', hint: 'web frontend' },
189+
{ label: 'Calibration wizard', href: '/docs/hmi/calibration-wizard', hint: 'browser flow' },
190+
{ label: 'Leader ↔ follower teleop', href: '/docs/hmi/teleop', hint: '60 Hz mirror' },
191+
{ label: 'Human-pose teleop', href: '/docs/hmi/human-teleop', hint: 'webcam → joints' },
192+
{ label: 'Dataset collection', href: '/docs/data/dataset-collection', hint: 'record & push' },
193+
{ label: 'RunPod inference', href: '/docs/data/runpod-inference', hint: 'cloud GPU' },
194+
{ label: 'Jetson deployment', href: '/docs/deployment/jetson', hint: 'on-robot install' },
195+
{ label: 'Wi-Fi AP fallback', href: '/docs/deployment/wifi-ap', hint: 'HallerRobot SSID' },
196+
{ label: 'Troubleshooting', href: '/docs/troubleshooting', hint: 'when it lies to you' },
197+
];
198+
199+
function RoutesIndex() {
200+
return (
201+
<section className="relative mx-auto w-full max-w-6xl px-6 pb-28">
202+
<header className="mb-6 flex items-baseline justify-between">
203+
<span className="h-label">[ Index ]</span>
204+
<span className="h-mono text-[10px] text-fd-muted-foreground">
205+
{ROUTES.length.toString().padStart(2, '0')} entries
206+
</span>
207+
</header>
208+
<div className="h-rule mb-6" />
209+
210+
<ul className="divide-y divide-fd-border border-y border-fd-border">
211+
{ROUTES.map((r, i) => (
212+
<li key={r.href}>
213+
<Link
214+
href={r.href}
215+
className="group flex items-baseline gap-4 py-3.5 transition hover:bg-fd-card hover:pl-2"
216+
>
217+
<span className="h-mono w-8 shrink-0 text-[11px] text-fd-muted-foreground">
218+
{(i + 1).toString().padStart(2, '0')}
219+
</span>
220+
<span className="flex-1 text-[1rem] text-fd-foreground transition group-hover:text-fd-primary">
221+
{r.label}
222+
</span>
223+
<span className="hidden sm:inline h-mono text-[10.5px] uppercase tracking-[0.14em] text-fd-muted-foreground">
224+
{r.hint}
225+
</span>
226+
<ArrowUpRight className="h-3.5 w-3.5 -translate-x-1 opacity-0 transition group-hover:translate-x-0 group-hover:opacity-70" />
227+
</Link>
228+
</li>
229+
))}
230+
</ul>
231+
</section>
232+
);
233+
}
234+
235+
/* ────────────────────────────────────────────────────────────────────────── */
236+
237+
function Footnote() {
238+
return (
239+
<section className="mx-auto w-full max-w-6xl px-6 pb-20">
240+
<div className="h-rule mb-8" />
241+
<div className="flex flex-wrap items-end justify-between gap-6">
242+
<p className="max-w-xl text-sm leading-relaxed text-fd-muted-foreground">
243+
Apache-2.0. The SO-101 mechanical design is licensed under its own
244+
terms by{' '}
245+
<Link
246+
href="https://github.com/TheRobotStudio/SO-ARM100"
247+
className="text-fd-foreground underline underline-offset-4 decoration-fd-border hover:text-fd-primary hover:decoration-fd-primary/60"
248+
>
249+
TheRobotStudio
250+
</Link>
251+
.
252+
</p>
253+
<p className="h-mono text-[10px] uppercase tracking-[0.18em] text-fd-muted-foreground">
254+
Set in Fraunces · IBM Plex Sans · JetBrains Mono
255+
</p>
256+
</div>
257+
</section>
59258
);
60259
}

website/app/docs/[[...slug]]/page.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,21 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
2121
const MDX = page.data.body;
2222
const markdownUrl = getPageMarkdownUrl(page).url;
2323

24+
/* "§ HMI / Calibration" — section pip above the title.
25+
Skips for the docs index where slugs is empty. */
26+
const slugs = page.slugs;
27+
const sectionLabel = formatSectionLabel(slugs);
28+
2429
return (
2530
<DocsPage toc={page.data.toc} full={page.data.full}>
31+
{sectionLabel && (
32+
<div className="mb-3 flex items-center gap-2">
33+
<span className="h-mono text-[10.5px] tracking-[0.18em] uppercase text-fd-primary">
34+
§
35+
</span>
36+
<span className="h-label">{sectionLabel}</span>
37+
</div>
38+
)}
2639
<DocsTitle>{page.data.title}</DocsTitle>
2740
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
2841
<div className="flex flex-row gap-2 items-center border-b pb-6">
@@ -35,7 +48,6 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
3548
<DocsBody>
3649
<MDX
3750
components={getMDXComponents({
38-
// this allows you to link to other pages with relative file paths
3951
a: createRelativeLink(source, page),
4052
})}
4153
/>
@@ -44,6 +56,22 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
4456
);
4557
}
4658

59+
function formatSectionLabel(slugs: string[]): string | null {
60+
/* Only render the label for nested pages — for top-level entries the H1
61+
already shows the same word, so a "Architecture / Architecture" pair
62+
would be noise. */
63+
if (slugs.length < 2) return null;
64+
const pretty = (s: string) =>
65+
s
66+
.replace(/-/g, ' ')
67+
.replace(/\b\w/g, (c) => c.toUpperCase())
68+
.replace(/Hmi/g, 'HMI')
69+
.replace(/Ros/g, 'ROS')
70+
.replace(/\bSo101\b/gi, 'SO-101')
71+
.replace(/\bBom\b/gi, 'BOM');
72+
return pretty(slugs[0]);
73+
}
74+
4775
export async function generateStaticParams() {
4876
return source.generateParams();
4977
}

0 commit comments

Comments
 (0)