+
+
+ {/* its agents arrive as ordinary rows, the machine named in the chip */}
+
+
-
-
+
+
);
}
@@ -1019,8 +959,8 @@ export default function FeatureBreakdowns() {
title="Your whole fleet, in one pixel strip"
body="Every session, every agent, one square each. The pill never wraps, never grows — an arriving question flashes the fire and swaps the slot, then hands the notch back."
bullets={[
- "12 agents: Claude Code, Codex, Gemini CLI, OpenCode, Pi, and more",
- "The campfire dances while work runs, simmers when all is calm",
+ "13 agents: Claude Code, Codex, Gemini CLI, OpenCode, Pi, Muse, and more",
+ "Custom agents: your own coding agent can wire itself in from the published contract",
"Slot rankings — you choose which data earns the notch",
"You don't need a notch — external displays get the same island, floating top-center"
]}
@@ -1031,10 +971,11 @@ export default function FeatureBreakdowns() {
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1067,11 +1049,38 @@ export default function FeatureBreakdowns() {
"Thermal protection pauses charging when the pack runs hot",
"Energy flow figure: adapter → battery → system, live watts"
]}
- reversed
>
+
+
+
+
+
+
+
+
@@ -1096,35 +1106,23 @@ export default function FeatureBreakdowns() {
"A device picker for every connected camera",
"Opt-in module — in no default panel"
]}
- reversed
>
-
-
-
-
-
+
);
diff --git a/src/components/nook/FeatureCollage.tsx b/src/components/nook/FeatureCollage.tsx
index 759c167..a2076d4 100644
--- a/src/components/nook/FeatureCollage.tsx
+++ b/src/components/nook/FeatureCollage.tsx
@@ -1,13 +1,12 @@
-import { For, type JSX } from "solid-js";
+import { For, createSignal, onCleanup, onMount, type JSX } from "solid-js";
+import { IslandPill, STATUS, CALM, CRITICAL, ModuleCard } from "./IslandMock";
import {
- IslandPill,
- AgentDotGrid,
- STATUS,
- CALM,
- CRITICAL,
- ACCENT,
- ModuleCard
-} from "./IslandMock";
+ ApprovalCardMock,
+ SessionRowMock,
+ DieLine,
+ CLAUDE,
+ CODEX
+} from "./ModuleMocks";
/**
* Feature collage: five cells sold as the app's actual UI. The hero cell
@@ -16,7 +15,7 @@ import {
* module chrome, status palette, and metric language.
*/
-function CollageCell(props: {
+export function CollageCell(props: {
class?: string;
kicker: string;
title: string;
@@ -44,7 +43,7 @@ function CollageCell(props: {
* is always dark (preferredColorScheme .dark), so the mocks read correctly
* in both site themes.
*/
-function InkStage(props: { children: JSX.Element; class?: string }) {
+export function InkStage(props: { children: JSX.Element; class?: string }) {
return (
-
-
-
- the-nook · bridge
-
-
- approval
-
-
-
Run Edit
-
- server/routes.ts +12 −4
-
-
-
- Allow
-
-
- Deny
-
-
- Deny with reason…
-
-
-
- );
-}
-
/** Fans panel mock: RPM gauge rows in the app's card chrome. */
function FansPanelMock() {
const fans = [
@@ -150,99 +105,62 @@ function FansPanelMock() {
);
}
-/** Remote agents: two session rows like the expanded agents panel. */
+/**
+ * Remote agents as the panel draws them: ordinary session rows whose agent
+ * chip names the machine ("Claude Code · build-box").
+ */
function RemoteMock() {
return (
-
-
-
- build-box
-
- ssh
-
-
-
You: fix flaky test
-
-
-
-
- gpu-rig
-
- ssh
-
-
-
Ready
-
+
+
);
}
-/** System gauges: temp die blocks + memory, in module cards. */
+/* Deterministic usage wander for the traces. */
+const wander = (n: number, k: number) =>
+ 0.42 + 0.22 * Math.sin(n * 0.45 + k * 1.7) + 0.12 * Math.sin(n * 1.3 + k);
+
+/**
+ * The Temperatures module: one line per die — label, its 60 s usage trace,
+ * its temperature. The GPU climbs past the 70 ° warn line and tints amber.
+ */
function GaugesMock() {
+ const [n, setN] = createSignal(0);
+ onMount(() => {
+ const interval = setInterval(() => setN((v) => v + 1), 700);
+ onCleanup(() => clearInterval(interval));
+ });
+ const trace = (k: number) =>
+ Array.from({ length: 30 }, (_, i) => Math.max(0.04, Math.min(0.97, wander(n() + i, k))));
+ const gpuCelsius = () => 62 + 12 * (0.5 + 0.5 * Math.sin(n() * 0.18));
return (
-
@@ -340,7 +263,7 @@ export default function FeatureCollage() {
body="Run agents on Linux boxes and SSH servers — one click installs the nook-hook, and their sessions join the same island."
>
-
+
@@ -350,7 +273,7 @@ export default function FeatureCollage() {
@@ -361,8 +284,13 @@ export default function FeatureCollage() {
- Plus calendar, reminders, calls, and Now Playing — all in the same
- island, all one-time purchase.
+ Plus a mixer, capture, timers, weather, and a dozen more modules —{" "}
+
+ see them all ↓
+
diff --git a/src/components/nook/IslandExtras.tsx b/src/components/nook/IslandExtras.tsx
new file mode 100644
index 0000000..cfde9d5
--- /dev/null
+++ b/src/components/nook/IslandExtras.tsx
@@ -0,0 +1,900 @@
+import {
+ For,
+ Index,
+ Show,
+ createSignal,
+ onCleanup,
+ onMount,
+ type JSX
+} from "solid-js";
+import {
+ ModuleCard,
+ STATUS,
+ ACCENT,
+ CALM,
+ WARM,
+ CRITICAL,
+ CARD_FILL,
+ CARD_STROKE
+} from "./IslandMock";
+import { CollageCell, InkStage } from "./FeatureCollage";
+import { useDemoLoop } from "./useDemoLoop";
+import { Glyph, TimerTapeMock, TIMER_ORANGE, type GlyphName } from "./ModuleMocks";
+
+/**
+ * "Everything else": a bento of small live tiles for the modules that do
+ * not get a full breakdown, headed by a two-way marquee naming the rest.
+ */
+
+/** Steady tick for tiles that animate continuously rather than in steps. */
+function useTick(everyMs: number) {
+ const [tick, setTick] = createSignal(0);
+ onMount(() => {
+ const interval = setInterval(() => setTick((t) => t + 1), everyMs);
+ onCleanup(() => clearInterval(interval));
+ });
+ return tick;
+}
+
+/** A compact black island strip for tiles that show a slot reading. */
+function MiniPill(props: { children: JSX.Element; class?: string }) {
+ return (
+
+ {props.children}
+
+ );
+}
+
+/* ── Now Playing + the waveform slot ─────────────────────────────── */
+
+function NowPlayingMock() {
+ return (
+
+
+
+
+
+
+ Night Drive
+
+
+ Synthwave Radio · Browser
+
+
+
+
+
+
+
+ ⏮
+ ⏸
+ ⏭
+
+
+
+
+
+
+ {(i) => (
+
+ )}
+
+
+
+ waveform slot
+
+
+ );
+}
+
+/* ── Timers: the module's face — readout over the tick tape ──────── */
+
+/** TimerService.format: m:ss, rounding up like the app. */
+function clock(seconds: number) {
+ const s = Math.ceil(seconds);
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
+}
+
+/** The face's capsule control: 26 px, mono 11, white 8 %. */
+function TimerPill(props: { glyph?: GlyphName; label?: string }) {
+ return (
+
+
+
+
+ {props.label}
+
+ );
+}
+
+/*
+ * A 3:00 lead timer drains (in fast-forward), rings with its tape pulsing,
+ * and a 25:00 Focus timer keeps running in the stacked row beneath.
+ */
+const LEAD_SECONDS = 180;
+const LEAD_MS = 9000;
+const TIMER_PERIOD = 14000;
+
+function TimersMock() {
+ const tick = useTick(50);
+ const t = () => (tick() * 50) % TIMER_PERIOD;
+ const leadLeft = () => Math.max(0, 1 - t() / LEAD_MS);
+ const ringing = () => t() >= LEAD_MS;
+ const focusLeft = () => 0.64 - (t() / TIMER_PERIOD) * 0.06;
+ return (
+
+
+ );
+}
+
+/* ── Wings ───────────────────────────────────────────────────────── */
+
+export interface WingControlMock {
+ glyph: GlyphName;
+ on?: boolean;
+ pressed?: boolean;
+}
+
+/**
+ * One edge's capsule: black at 94 %, a 1 px white-18 % stroke, and each
+ * control a circle as tall as the Compact island, icon at 42 %.
+ */
+export function WingCapsule(props: { controls: WingControlMock[]; height: number }) {
+ return (
+
+
+ {(c) => (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/nook/MoreDemos.tsx b/src/components/nook/MoreDemos.tsx
new file mode 100644
index 0000000..b2bd870
--- /dev/null
+++ b/src/components/nook/MoreDemos.tsx
@@ -0,0 +1,617 @@
+import { For, Show, createSignal, type JSX } from "solid-js";
+import {
+ ModuleCard,
+ FakeCursor,
+ IslandPill,
+ AgentDotGrid,
+ STATUS,
+ ACCENT,
+ CALM,
+ CRITICAL,
+ CARD_STROKE
+} from "./IslandMock";
+import { useDemoLoop } from "./useDemoLoop";
+import { Glyph, WingCapsule, type GlyphName } from "./ModuleMocks";
+
+/**
+ * Live demos for the breakdowns added with the 0.6–0.10 feature set:
+ * terminal replies, the volume mixer, Capture (module + Wings), and
+ * floating actions. Same ink chrome and status palette as the originals.
+ */
+
+/* ── Reply lands in the agent's own terminal ──────────────────────── */
+
+const REPLY = "yes — and run them";
+
+export function TerminalReplyDemo() {
+ const [typed, setTyped] = createSignal(0);
+ const [phase, setPhase] = createSignal<
+ "asking" | "typing" | "sent" | "working" | "jump"
+ >("asking");
+
+ useDemoLoop(10000, [
+ [0, () => (setTyped(0), setPhase("asking"))],
+ [900, () => setPhase("typing")],
+ ...REPLY.split("").map(
+ (_, i) => [1000 + i * 70, () => setTyped(i + 1)] as [number, () => void]
+ ),
+ [2900, () => setPhase("sent")],
+ [3300, () => setPhase("working")],
+ [6600, () => setPhase("jump")]
+ ]);
+
+ const delivered = () => phase() === "working" || phase() === "jump";
+ const cursorPhase = () =>
+ phase() === "sent" || phase() === "jump"
+ ? ("press" as const)
+ : phase() === "typing" || phase() === "asking"
+ ? ("idle" as const)
+ : ("live" as const);
+ const cursorAt = () =>
+ phase() === "jump" ? { x: 88, y: 14 } : { x: 88, y: 80 };
+
+ return (
+
+
+
+
+
+ the-nook
+ main
+
+ Jump ↗
+
+
+
+ {delivered()
+ ? "Running the test suite…"
+ : "Done. Want me to update the tests too?"}
+
+ The iPhone and Apple Watch companion is in beta. Approvals,
+ questions, and replies reach you away from the desk, and your
+ answer goes straight back to the agent.
+
+
+
+ {(bullet) => (
+
+
+ {bullet}
+
+ )}
+
+
+
+
+
Request beta access
+
+ Leave your email to get a TestFlight invite. We only receive your
+ email address.
+
+
+
+
+ Thanks. Watch your inbox for a TestFlight invite.
+
+
+
+
+ Too many requests from this network. Try again later.
+
+
+
+
+ Couldn't send the request. Check your connection and try again.
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/nook/betaRequest.ts b/src/components/nook/betaRequest.ts
new file mode 100644
index 0000000..4e970b7
--- /dev/null
+++ b/src/components/nook/betaRequest.ts
@@ -0,0 +1,12 @@
+/**
+ * The iPhone beta-access request the landing page posts to
+ * `/api/the-nook/report` — the same title the app's Settings ▸ Phone card
+ * sends, so requests from both land together. Email only, no device facts.
+ */
+export function betaRequestPayload(email: string) {
+ return {
+ title: "iPhone app beta access request",
+ description: "Beta access requested from nook.freno.me.",
+ contact: email
+ };
+}
diff --git a/src/components/nook/useDemoLoop.ts b/src/components/nook/useDemoLoop.ts
new file mode 100644
index 0000000..ed5ec61
--- /dev/null
+++ b/src/components/nook/useDemoLoop.ts
@@ -0,0 +1,28 @@
+import { onCleanup, onMount } from "solid-js";
+
+/**
+ * Drives a looping landing-page demo: each `[atMs, step]` pair fires at its
+ * offset, and the whole schedule restarts every `periodMs`. Timers are torn
+ * down with the owning component.
+ */
+export function useDemoLoop(
+ periodMs: number,
+ steps: [atMs: number, step: () => void][]
+): void {
+ onMount(() => {
+ const timers: ReturnType[] = [];
+ const run = () => {
+ timers.length = 0;
+ for (const [at, step] of steps) {
+ if (at === 0) step();
+ else timers.push(setTimeout(step, at));
+ }
+ };
+ run();
+ const interval = setInterval(run, periodMs);
+ onCleanup(() => {
+ clearInterval(interval);
+ timers.forEach(clearTimeout);
+ });
+ });
+}
diff --git a/src/routes/api/the-nook/report.test.ts b/src/routes/api/the-nook/report.test.ts
index 04bad4f..9271566 100644
--- a/src/routes/api/the-nook/report.test.ts
+++ b/src/routes/api/the-nook/report.test.ts
@@ -5,6 +5,7 @@ import { describe, expect, it, mock, beforeEach } from "bun:test";
// and description, HTML-escaped.
// 2. Invalid payloads never reach the mailer.
// 3. The rate limiter halts floods per fingerprint (falling back to IP).
+// 4. The landing page's iPhone beta request (email only) gets through.
// sendEmail and the limiter are mocked so the test sends no real mail.
const sentEmails: { to: string; subject: string; html: string }[] = [];
@@ -22,6 +23,9 @@ mock.module("~/server/bug-report-rate-limit", () => ({
}));
const { POST } = await import("./report");
+const { betaRequestPayload } = await import(
+ "~/components/nook/betaRequest"
+);
/** A minimal APIEvent double: JSON body plus optional headers. */
function request(body: unknown, headers: Record = {}) {
@@ -134,4 +138,17 @@ describe("POST /api/the-nook/report", () => {
expect(res.status).toBe(200);
expect(sentEmails).toHaveLength(1);
});
+
+ it("delivers the landing page's beta request with only the email", async () => {
+ const res = await POST(request(betaRequestPayload("tester@example.com")));
+ expect(res.status).toBe(200);
+
+ expect(sentEmails).toHaveLength(1);
+ const email = sentEmails[0]!;
+ expect(email.subject).toBe(
+ "The Nook bug report: iPhone app beta access request"
+ );
+ expect(email.html).toContain("Reply-to: tester@example.com");
+ expect(email.html).not.toContain("
Displays
");
+ });
});
diff --git a/src/routes/nook/index.tsx b/src/routes/nook/index.tsx
index fcd624e..a21dea4 100644
--- a/src/routes/nook/index.tsx
+++ b/src/routes/nook/index.tsx
@@ -3,6 +3,8 @@ import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
import SubdomainHeader from "~/components/SubdomainHeader";
import FeatureCollage from "~/components/nook/FeatureCollage";
import FeatureBreakdowns from "~/components/nook/FeatureBreakdowns";
+import IslandExtras from "~/components/nook/IslandExtras";
+import PhoneBeta from "~/components/nook/PhoneBeta";
import Button from "~/components/ui/Button";
import Input from "~/components/ui/Input";
import { createSignal, Show } from "solid-js";
@@ -149,6 +151,12 @@ export default function NookLanding() {
@@ -156,10 +164,10 @@ export default function NookLanding() {
Try it free for 14 days
- Orchestrate your coding agents, watch temperatures, take control of
- your fans, and keep background services quiet. When the trial ends,
- unlock everything with a single one-time payment — no subscription,
- ever.
+ Orchestrate your coding agents, take control of your fans and
+ battery, give every app its own volume, capture your screen, and
+ keep your day in the notch. When the trial ends, unlock everything
+ with a single one-time payment — no subscription, ever.