diff --git a/.gitignore b/.gitignore index 6460862..5cc98c8 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ Thumbs.db # pygienium run-state and check artifacts .pygienium/ scripts/ +.cache diff --git a/public/nook/cam-recording.mp4 b/public/nook/cam-recording.mp4 new file mode 100644 index 0000000..1000b43 Binary files /dev/null and b/public/nook/cam-recording.mp4 differ diff --git a/src/app.css b/src/app.css index 9f37863..af27226 100644 --- a/src/app.css +++ b/src/app.css @@ -749,3 +749,9 @@ a.hover-underline-animation:hover::after { 10% { opacity: 1; } 100% { transform: translate(var(--sx, 20px), var(--sy, 30px)) scale(0.4); opacity: 0; } } + +/* Demo pulse: battery bolt */ +@keyframes nook-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } +} diff --git a/src/components/nook/FeatureBreakdowns.tsx b/src/components/nook/FeatureBreakdowns.tsx index a4d28c0..ef1f7a8 100644 --- a/src/components/nook/FeatureBreakdowns.tsx +++ b/src/components/nook/FeatureBreakdowns.tsx @@ -1,10 +1,17 @@ -import { For, createSignal, onCleanup, onMount, type JSX } from "solid-js"; +import { + For, + createEffect, + createSignal, + onCleanup, + onMount, + type JSX +} from "solid-js"; import { IslandPill, - IslandExpanded, AgentDotGrid, STATUS, CALM, + WARM, ACCENT, CRITICAL, ModuleCard @@ -13,9 +20,9 @@ import { Campfire, type CampfireState } from "./Campfire"; /** * Detailed feature breakdowns: alternating copy + live-demo sections. - * Each demo animates when scrolled into view (IntersectionObserver + - * motion's animate), staging the real interaction loop: attention - * flares, approval resolves, fans throttle, sessions stream. + * Each demo stages the real interaction loop: attention flares, approval + * resolves, fans throttle, the charge ceiling holds, meetings surface, + * the camera wakes, the hook installs. */ /* Scroll-triggered reveal: adds `in` once the element crosses 25%. */ @@ -131,9 +138,15 @@ function FleetDemo() {
@@ -191,7 +204,9 @@ function ApprovalDemo() { {allowed() ? "allowed" : "approval"} @@ -318,49 +333,49 @@ function FansDemo() { "--fan-speed": `${(0.4 - shakeIntensity() * 0.28).toFixed(3)}s` }} > -
- - {(f, i) => ( -
- - Fan {i() + 1} - -
+
+ + {(f, i) => ( +
+ + Fan {i() + 1} +
+ class="h-1 flex-1 overflow-hidden rounded-full" + style={{ background: "rgba(255,255,255,0.08)" }} + > +
+
+ + {Math.round(f.rpm)} + + RPM
- - {Math.round(f.rpm)} - - RPM -
- )} -
-
-
- - {maxed() ? "MAX" : "Auto"} - - - {maxed() ? "cooling session active" : "curve in control"} - + )} + +
+
+ + {maxed() ? "MAX" : "Auto"} + + + {maxed() ? "cooling session active" : "curve in control"} + +
-
+ props.phase === "press" + ? { ...props.at, scale: 0.62, o: 1 } + : props.phase === "idle" + ? { ...props.at, scale: 1, o: 1 } + : { ...(props.exit ?? { x: 106, y: 8 }), scale: 1, o: 0 }; + return ( + + ); +} + +/* ── Demo 4: the charge ceiling holds, top-up overrules ───── */ + +function BatteryDemo() { + const [level, setLevel] = createSignal(62); + const [phase, setPhase] = createSignal<"charge" | "held" | "topup">("charge"); + onMount(() => { + let frame = 0; + const timers: ReturnType[] = []; + const ramp = (target: number, durationMs: number) => { + cancelAnimationFrame(frame); + const start = level(); + const startTime = performance.now(); + const tick = (now: number) => { + const t = Math.min((now - startTime) / durationMs, 1); + setLevel(start + (target - start) * t); + if (t < 1) frame = requestAnimationFrame(tick); + }; + frame = requestAnimationFrame(tick); + }; + const loop = () => { + setLevel(62); + setPhase("charge"); + ramp(80, 2600); + timers.push(setTimeout(() => setPhase("held"), 2800)); + timers.push( + setTimeout(() => { + setPhase("topup"); + ramp(100, 2600); + }, 5200) + ); + }; + loop(); + const interval = setInterval(loop, 9600); + onCleanup(() => { + clearInterval(interval); + timers.forEach(clearTimeout); + cancelAnimationFrame(frame); + }); + }); + + const pct = () => Math.round(level()); + const watts = () => (phase() === "held" ? null : pct() >= 100 ? 9 : 38); + const subline = () => + phase() === "held" + ? "charging paused — at the limit" + : pct() >= 100 + ? "topped up — re-arms on unplug" + : phase() === "topup" + ? `${watts()} W · top-up past the limit` + : `${watts()} W · ${Math.max(1, Math.round((100 - pct()) * 1.5))} min to full`; + return ( + +
+
+ + Battery + + + Limit 80% + +
+
+ + {pct()}% + + {subline()} +
+
+
80 ? WARM : CALM, + transition: "background 0.4s" + }} + /> + +
+
+
+

Top-up mode

+

+ charge past the limit while plugged in +

+
+ + + +
+
+
+ + + + + {phase() === "held" + ? "holding — the pack stays young longer" + : phase() === "topup" + ? "topping up — re-arms when you unplug" + : "on the way to the ceiling"} + +
+
+ + ); +} + +/* ── Demo 5: the slot counts down, then opens the day ──────── */ + +const WEEK_LETTERS = ["S", "M", "T", "W", "T", "F", "S"]; +const DAY_EVENTS = [ + { time: "09:30–09:45", title: "Standup", loc: "Daily", meeting: true }, + { time: "15:30–16:00", title: "Design review", loc: "4th floor", meeting: false } ]; -function RemoteDemo() { - const [tick, setTick] = createSignal(0); +function CalendarDemo() { + const [mins, setMins] = createSignal(14); + const [today, setToday] = createSignal(new Date()); onMount(() => { + setToday(new Date()); const interval = setInterval( - () => setTick((t) => (t + 1) % REMOTE_TICKS.length), - 2600 + () => setMins((m) => (m <= 0 ? 14 : m - 1)), + 900 ); onCleanup(() => clearInterval(interval)); }); + const imminent = () => mins() <= 5; + const dialDays = () => { + const base = today(); + return [-4, -3, -2, -1, 0, 1, 2, 3, 4].map((off) => { + const d = new Date( + base.getFullYear(), + base.getMonth(), + base.getDate() + off + ); + return { + letter: WEEK_LETTERS[d.getDay()], + num: d.getDate(), + today: off === 0, + past: off < 0, + dots: (d.getDate() * 7 + d.getDay() * 3) % 4 + }; + }); + }; return (
- + + + + + {mins() === 0 ? "now" : `${mins()}m`} + +
+ } + /> + {/* the island opening onto the day panel */} +
-
- - {(host, i) => ( -
-
- - - {host.name} +
+
+ + Calendar + + + + + +
+
+ + {(d) => ( +
+ + {d.letter} - {host.via} + {d.num} + + + + {() => ( + + )} +
-

{host.line}

+ )} +
+
+
+ + {(ev) => ( +
+
+

{ev.time}

+

{ev.title}

+ {ev.loc &&

{ev.loc}

} +
+ {ev.meeting && ( + + Open + + )}
)}
-

- same island, same glance — agents two networks away -

- +
); } -/* ── Demo 5: campfire states ───────────────────────────────────────── */ +/* ── Demo 6: the lens only wakes for a click ──────────────── */ + +function CameraDemo() { + const [phase, setPhase] = createSignal<"idle" | "press" | "live">("idle"); + let videoRef: HTMLVideoElement | undefined; + onMount(() => { + const timers: ReturnType[] = []; + const loop = () => { + setPhase("idle"); + timers.push(setTimeout(() => setPhase("press"), 2600)); + timers.push(setTimeout(() => setPhase("live"), 3000)); + }; + loop(); + const interval = setInterval(loop, 8800); + onCleanup(() => { + clearInterval(interval); + timers.forEach(clearTimeout); + }); + }); + createEffect(() => { + if (phase() === "live") videoRef?.play(); + else videoRef?.pause(); + }); + return ( + +
+
+ Camera + + FaceTime HD Camera ▾ + +
+
+ {/* inert dashed start button */} +
+ + + + + + Start camera + +
+ {/* live preview */} +
+

+ {phase() === "live" + ? "live — stops the moment you leave the panel" + : "inert — the lens stays dark until you click"} +

+
+
+ ); +} + +/* ── Demo 7: one click copies, sets up, probes — pong ─────── */ + +const SSH_LINES = [ + { at: 1, text: "scp nook-hook gpu-rig:~/.nook/bin/" }, + { at: 2, text: "setup: bake NOOK_HOST, write ~/.nook/endpoint" }, + { at: 3, text: "nook-hook --probe → pong" } +]; + +function SshInstallDemo() { + const [step, setStep] = createSignal(0); + onMount(() => { + const timers: ReturnType[] = []; + const loop = () => { + setStep(0); + timers.push(setTimeout(() => setStep(1), 1600)); + timers.push(setTimeout(() => setStep(2), 3000)); + timers.push(setTimeout(() => setStep(3), 4200)); + timers.push(setTimeout(() => setStep(4), 5400)); + }; + loop(); + const interval = setInterval(loop, 11000); + onCleanup(() => { + clearInterval(interval); + timers.forEach(clearTimeout); + }); + }); + const cursorPhase = () => + step() === 0 + ? ("idle" as const) + : step() === 1 + ? ("press" as const) + : ("live" as const); + return ( + +
+
+
+

gpu-rig

+

+ mike@192.168.64.3 · linux +

+
+ = 4 ? 0 : 1, + transform: step() === 1 ? "scale(0.94)" : "scale(1)" + }} + > + {step() === 0 ? "Install hook" : "installing…"} + +
+
+
+
= 4 ? 0 : 1 }} + > + {step() === 0 && ( +

+ one click: copy → setup → probe +

+ )} + + {(l) => ( +
= l.at ? 1 : 0, + transform: + step() >= l.at ? "translateY(0)" : "translateY(4px)" + }} + > + l.at + ? "rgba(150,220,150,0.9)" + : "rgba(255,255,255,0.45)" + }} + > + {step() > l.at ? "✓" : "…"} + + + {l.text} + +
+ )} +
+
+
= 4 ? 1 : 0, + transform: step() >= 4 ? "translateY(0)" : "translateY(6px)" + }} + > +
+
+ + + gpu-rig + + + remote + +
+

+ You: benchmark fp16 +

+
+

+ agents join the same dot grid +

+
+
+
+ + + ); +} + +/* ── Demo 8: campfire states ───────────────────────────────────────── */ function CampfireDemo() { const states: CampfireState[] = [ @@ -489,7 +992,9 @@ function CampfireDemo() { >
-

{labels[i()]}

+

+ {labels[i()]} +

)}
@@ -507,7 +1012,8 @@ export default function FeatureBreakdowns() { bullets={[ "12 agents: Claude Code, Codex, Gemini CLI, OpenCode, Pi, and more", "The campfire dances while work runs, simmers when all is calm", - "Slot rankings — you choose which data earns the notch" + "Slot rankings — you choose which data earns the notch", + "You don't need a notch — external displays get the same island, floating top-center" ]} > @@ -530,8 +1036,9 @@ export default function FeatureBreakdowns() { - + + + + + + + + + + + + + - tailscale + ssh

Ready

@@ -330,7 +330,7 @@ export default function FeatureCollage() {