diff --git a/src/app.css b/src/app.css index 553ce1b..9e92cf1 100644 --- a/src/app.css +++ b/src/app.css @@ -761,6 +761,14 @@ a.hover-underline-animation:hover::after { animation: nook-pulse 0.8s steps(2, jump-none) infinite; } +/* Directional dashes in the power-flow preview. */ +.nook-power-flow { + animation: nook-power-flow 1.6s linear infinite; +} +@keyframes nook-power-flow { + to { stroke-dashoffset: -21; } +} + /* Live signal inside a mixer fader */ .nook-meter { transform-origin: left center; @@ -840,6 +848,7 @@ a.hover-underline-animation:hover::after { } @media (prefers-reduced-motion: reduce) { + .nook-power-flow, .nook-meter, .nook-wave, .nook-progress, diff --git a/src/components/nook/FeatureBreakdowns.tsx b/src/components/nook/FeatureBreakdowns.tsx index cfbae76..2fd08c2 100644 --- a/src/components/nook/FeatureBreakdowns.tsx +++ b/src/components/nook/FeatureBreakdowns.tsx @@ -10,7 +10,6 @@ import { IslandPill, AgentDotGrid, CALM, - WARM, ACCENT, CRITICAL, ModuleCard, @@ -24,9 +23,9 @@ import { CODEX } from "./ModuleMocks"; import { useDemoLoop } from "./useDemoLoop"; +import { BatteryDemo, CalendarDemo, MixerDemo } from "./UtilityDemos"; import { TerminalReplyDemo, - MixerDemo, CaptureDemo, FloatingActionsDemo } from "./MoreDemos"; @@ -168,11 +167,11 @@ function FleetDemo() { {phase() === "flare" ? "Codex needs your input" : phase() === "resolve" - ? "Answer delivered — agents back to work" - : "Fleet running calmly"} + ? "Reply sent. Agents running." + : "Agents running"}

- {phase() === "flare" ? "question · in the island now" : "\u00a0"} + {phase() === "flare" ? "Question waiting in the island" : "\u00a0"}

@@ -234,7 +233,7 @@ function ApprovalDemo() { prompt="harden the bridge" state={ asking() - ? "Approve Bash — swift test --filter Bridge?" + ? "Approve Bash: swift test --filter Bridge?" : "Running swift test --filter Bridge…" } status={asking() ? "attention" : "running"} @@ -408,313 +407,6 @@ function FansDemo() { ); } -/* ── 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 CalendarDemo() { - const [mins, setMins] = createSignal(14); - const [today, setToday] = createSignal(new Date()); - onMount(() => { - setToday(new Date()); - const interval = setInterval( - () => 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 ( - /* Fixed height: the day panel expands into reserved space, never - shifts the page (matters most on mobile's stacked layout). */ -
- - - - - - {mins() === 0 ? "now" : `${mins()}m`} - -
- } - /> - {/* the island opening onto the day panel */} -
-
-
- - Calendar - - - + - -
-
- - {(d) => ( -
- - {d.letter} - - - {d.num} - - - - {() => ( - - )} - - -
- )} -
-
-
- - {(ev) => ( -
-
-

{ev.time}

-

{ev.title}

- {ev.loc &&

{ev.loc}

} -
- {ev.meeting && ( - - Open - - )} -
- )} -
-
-
-
- ); -} - /* ── Demo 6: the lens only wakes for a click ──────────────── */ function CameraDemo() { @@ -797,8 +489,8 @@ function CameraDemo() {

{phase() === "live" - ? "live — stops the moment you leave the panel" - : "inert — the lens stays dark until you click"} + ? "Camera on while this panel is open" + : "Camera off until you click Start"}

@@ -816,11 +508,11 @@ function CampfireDemo() { "error" ]; const labels = [ - "fleet running", - "pending question", - "ready to review", - "all quiet", - "something failed" + "Running", + "Question", + "Ready", + "Idle", + "Error" ]; return (
@@ -848,13 +540,13 @@ export default function FeatureBreakdowns() {
@@ -862,12 +554,12 @@ export default function FeatureBreakdowns() { @@ -876,8 +568,8 @@ export default function FeatureBreakdowns() { @@ -917,14 +609,14 @@ export default function FeatureBreakdowns() { @@ -932,12 +624,12 @@ export default function FeatureBreakdowns() { @@ -946,8 +638,8 @@ export default function FeatureBreakdowns() { @@ -975,13 +667,13 @@ export default function FeatureBreakdowns() { @@ -989,8 +681,8 @@ export default function FeatureBreakdowns() {

- One glance. Everything your agents are doing. + Keep track of your agents while you work

@@ -148,12 +148,12 @@ export default function FeatureCollage() { The island

- Every agent, one glance + Agent status at the top of your screen

- A pill tucked into your notch — a campfire for your whole fleet - on the left, live data on the right. Blue running, green ready, - amber needs you. + The island sits around your Mac's notch, with agent activity on + the left and your choice of information on the right. Blue means + running, green means ready, and amber means an agent needs you.

@@ -186,15 +186,15 @@ export default function FeatureCollage() { />

- expand for the full panel ↓ + Open the island to see your sessions

{/* Approvals */}
@@ -211,8 +211,8 @@ export default function FeatureCollage() { {/* Fans */}
@@ -224,8 +224,8 @@ export default function FeatureCollage() { {/* Gauges */}
@@ -236,12 +236,12 @@ export default function FeatureCollage() {

- Plus a mixer, capture, timers, weather, and a dozen more modules —{" "} + Also includes a volume mixer, screen capture, timers, weather, and more.{" "} - see them all ↓ + Browse the other modules

diff --git a/src/components/nook/IslandExtras.tsx b/src/components/nook/IslandExtras.tsx index dd06b16..118e081 100644 --- a/src/components/nook/IslandExtras.tsx +++ b/src/components/nook/IslandExtras.tsx @@ -13,7 +13,6 @@ import { ACCENT, CALM, WARM, - CRITICAL, CARD_FILL, CARD_STROKE } from "./IslandMock"; @@ -571,8 +570,8 @@ function KeepAwakeMock() { const MODES = [ { name: "Auto", note: "follows each agent's own settings", card: "asks", tint: "rgba(234,179,8,0.10)" }, - { name: "Yolo", note: "gated tools pass straight through", card: "passed", tint: "rgba(111,185,130,0.12)" }, - { name: "Verify", note: "every gated tool waits for you", card: "held", tint: "rgba(229,72,77,0.12)" } + { name: "Yolo", note: "automatically allows received requests", card: "passed", tint: "rgba(111,185,130,0.12)" }, + { name: "Verify", note: "received requests wait for approval", card: "held", tint: "rgba(229,72,77,0.12)" } ]; function ApprovalModesMock() { @@ -773,14 +772,14 @@ export default function IslandExtras() {

- And everything else + More modules

- One island instead of a menu bar full of utilities + Other tools in the island

- Every module below lives in the same notch, arranged however you - like, under the same one-time license. + Add the modules you need and arrange them in your panels. + They're all included with your license.

@@ -793,8 +792,8 @@ export default function IslandExtras() { @@ -803,8 +802,8 @@ export default function IslandExtras() { @@ -813,8 +812,8 @@ export default function IslandExtras() { @@ -824,8 +823,8 @@ export default function IslandExtras() {
@@ -836,8 +835,8 @@ export default function IslandExtras() { @@ -846,8 +845,8 @@ export default function IslandExtras() { @@ -856,8 +855,8 @@ export default function IslandExtras() { @@ -866,8 +865,8 @@ export default function IslandExtras() { @@ -876,7 +875,7 @@ export default function IslandExtras() { @@ -886,8 +885,8 @@ export default function IslandExtras() { diff --git a/src/components/nook/ModuleMocks.tsx b/src/components/nook/ModuleMocks.tsx index 92e0e18..fade43f 100644 --- a/src/components/nook/ModuleMocks.tsx +++ b/src/components/nook/ModuleMocks.tsx @@ -77,6 +77,12 @@ const GLYPHS: Record JSX.Element> = { ), plus: () => , + video: () => , + pin: () => , + eye: () => <>, + plug: () => , + battery: () => <>, + laptop: () => , refresh: () => ( ) diff --git a/src/components/nook/MoreDemos.tsx b/src/components/nook/MoreDemos.tsx index b2bd870..261c6d5 100644 --- a/src/components/nook/MoreDemos.tsx +++ b/src/components/nook/MoreDemos.tsx @@ -7,7 +7,6 @@ import { STATUS, ACCENT, CALM, - CRITICAL, CARD_STROKE } from "./IslandMock"; import { useDemoLoop } from "./useDemoLoop"; @@ -15,13 +14,13 @@ 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 + * terminal replies, 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"; +const REPLY = "Yes, update and run the tests."; export function TerminalReplyDemo() { const [typed, setTyped] = createSignal(0); @@ -153,127 +152,6 @@ export function TerminalReplyDemo() { ); } -/* ── Volume mixer: every app its own level and output ─────────────── */ - -interface MixerApp { - name: string; - color: string; - glyph: string; -} - -const MIXER_APPS: MixerApp[] = [ - { name: "Music", color: "#fa5c7c", glyph: "♫" }, - { name: "Browser", color: "#6EA7FF", glyph: "◎" }, - { name: "Meeting", color: "#6FB982", glyph: "●" }, - { name: "podcast-cli", color: "#E7A762", glyph: ">_" } -]; - -export function MixerDemo() { - const [step, setStep] = createSignal(0); - useDemoLoop(9600, [ - [0, () => setStep(0)], - [1800, () => setStep(1)], - [3800, () => setStep(2)], - [5800, () => setStep(3)] - ]); - - const level = (i: number) => { - if (i === 0) return step() >= 3 ? 0.38 : 0.72; - if (i === 1) return 0.55; - if (i === 2) return 0.86; - return 0.46; - }; - const muted = (i: number) => i === 1 && step() >= 1; - const route = (i: number) => - i === 0 && step() >= 2 ? "Headphones" : i === 2 ? "Display" : "Speakers"; - - return ( - -
-
- - Volume mixer - - - out · Speakers - -
- - {(app, i) => ( -
- - {app.glyph} - -
-
- - {app.name} - - - {route(i())} - -
-
-
- {/* live signal riding inside the level */} -
-
-
- - {muted(i()) ? "✕" : "♪"} - -
- )} - -
-

- { - [ - "every app on its own fader", - "browser muted — the rest keep playing", - "music routed to headphones, meeting stays on the display", - "music down, nothing else moved" - ][step()] - } -

-
- - ); -} - /* ── Capture: the module, and the Wings while a recording runs ────── */ /** CapturePalette.recording and the NookPalette paper/ink pair. */ diff --git a/src/components/nook/PhoneBeta.tsx b/src/components/nook/PhoneBeta.tsx index 3d80e13..270133b 100644 --- a/src/components/nook/PhoneBeta.tsx +++ b/src/components/nook/PhoneBeta.tsx @@ -96,7 +96,7 @@ function CompanionDemo() { {allowed() ? "Allowed ✓" : "Claude Code · the-nook"}

- {allowed() ? "Back to work." : "Run Bash: swift test"} + {allowed() ? "Agent running." : "Run Bash: swift test"}

@@ -139,7 +139,7 @@ function CompanionDemo() { >

Codex asks

- Ship it to staging? + Deploy to staging?

@@ -194,19 +194,19 @@ export default function PhoneBeta(props: { brandColor: string }) { TestFlight beta

- Your fleet, in your pocket + Use The Nook on iPhone and Apple Watch

- 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. + The companion app is in TestFlight beta. Check your agents, + respond to permission requests, and answer questions when you're + away from your Mac.

    @@ -255,7 +255,7 @@ export default function PhoneBeta(props: { brandColor: string }) {

    - Thanks. Watch your inbox for a TestFlight invite. + Thanks. We'll email you a TestFlight invite.

    diff --git a/src/components/nook/UtilityDemos.tsx b/src/components/nook/UtilityDemos.tsx new file mode 100644 index 0000000..4de75b7 --- /dev/null +++ b/src/components/nook/UtilityDemos.tsx @@ -0,0 +1,627 @@ +import { For, Show, createSignal } from "solid-js"; +import { IslandPill, ModuleCard, CALM, WARM } from "./IslandMock"; +import { Glyph, type GlyphName } from "./ModuleMocks"; +import { useDemoLoop } from "./useDemoLoop"; + +const PAPER = "#f1ead9"; +const BATTERY = "#8dc9b3"; +const CALENDAR = "#ff9e42"; + +/** SettingsSlider: paper fill, dark knob, and a unity notch on app sliders. */ +function MixerSlider(props: { level: number; tint?: string; muted?: boolean }) { + const fraction = () => props.level / (props.tint ? 125 : 100); + const position = () => `calc(9px + (100% - 18px) * ${fraction()})`; + return ( +
    +
    + +
    + +
    + + + + +
    + ); +} + +function RoutePicker(props: { label: string }) { + return ( + + {props.label} + + + ); +} + +const MIXER_APPS = [ + { name: "Music", color: "#fa5c7c", glyph: "♫" }, + { name: "Browser", color: CALM, glyph: "◎" }, + { name: "Meeting", color: BATTERY, glyph: "▰" } +]; + +/** VolumeMixerPanelView and MixerAppRow, including the current route controls. */ +export function MixerDemo() { + const [step, setStep] = createSignal(0); + useDemoLoop(10800, [ + [0, () => setStep(0)], + [2400, () => setStep(1)], + [4800, () => setStep(2)], + [7200, () => setStep(3)] + ]); + const muted = (i: number) => i === 1 && step() >= 1; + const volume = (i: number) => + i === 0 ? (step() >= 3 ? 38 : 100) : i === 1 ? 55 : 86; + return ( +
    + +
    +
    + + + + 65% + +
    + + {(route) => ( +
    + + + {route.label} + + +
    + )} +
    +
    + + {(app, i) => ( +
    +
    + + {app.glyph} + + + {app.name} + + = 2 ? "Headphones" : "Default"} + /> + + + + + + +
    +
    + + + {muted(i()) ? 0 : volume(i())}% + + +
    +
    + )} +
    +
    + +

    + { + [ + "Adjust each app separately", + "Browser muted", + "Music playing through headphones", + "Music volume lowered to 38%" + ][step()] + } +

    +
    + ); +} + +function PowerNode(props: { + label: string; + glyph: GlyphName; + watts: number; + tint: string; +}) { + return ( +
    + +
    +

    {props.label}

    +

    + {props.watts.toFixed(1)} W +

    +
    +
    + ); +} + +/** PowerPanelView: system draw, parallel power circuit, battery bar and controls. */ +export function BatteryDemo() { + const [step, setStep] = createSignal(0); + useDemoLoop(14000, [ + [0, () => setStep(0)], + [1800, () => setStep(1)], + [3600, () => setStep(2)], + [6400, () => setStep(3)], + [8400, () => setStep(4)], + [10400, () => setStep(5)] + ]); + const pct = () => [62, 71, 80, 80, 92, 100][step()]; + const topUp = () => step() >= 3; + const held = () => step() === 2; + const batteryWatts = () => (held() || pct() === 100 ? 0 : 26.4); + return ( +
    + +
    +
    +
    + ϟ 18.2 W +
    +

    System draw

    +
    +
    + + +
    + + +
    +
    +
    + + Battery + + {pct()}% + + Limit{" "} + + 80 + + % + +
    +
    +
    +
    +
    + +
    + + {(control) => ( +
    +
    +

    {control.label}

    +

    + {control.note} +

    +
    + + + +
    + )} +
    +
    + +

    + {held() + ? "Charging paused at the 80% limit" + : topUp() + ? "Top-up mode allows a full charge" + : "Charging with an 80% limit"} +

    +
    + ); +} + +const DAYS = [ + { letter: "M", day: 9, dots: 1 }, + { letter: "T", day: 10, dots: 2 }, + { letter: "W", day: 11, dots: 1 }, + { letter: "T", day: 12, dots: 2 }, + { letter: "F", day: 13, dots: 3 }, + { letter: "S", day: 14, dots: 0 }, + { letter: "S", day: 15, dots: 0 } +]; +const EVENTS = [ + { + start: "09:30", + end: "09:45", + title: "Standup", + calendar: "Work", + color: CALENDAR, + location: "Video call", + meeting: true + }, + { + start: "15:30", + end: "16:00", + title: "Design review", + calendar: "Work", + color: CALENDAR, + location: "4th floor", + meeting: false + } +]; + +/** CalendarSlotFace's daymark / next-up / on-air states, and CalendarPanelView. */ +export function CalendarDemo() { + const [step, setStep] = createSignal(0); + useDemoLoop(16000, [ + [0, () => setStep(0)], + [2400, () => setStep(1)], + [4800, () => setStep(2)], + [9000, () => setStep(3)], + [12000, () => setStep(4)] + ]); + const onAir = () => step() >= 3; + return ( + + ); +} diff --git a/src/routes/nook/index.tsx b/src/routes/nook/index.tsx index ea10c08..2546353 100644 --- a/src/routes/nook/index.tsx +++ b/src/routes/nook/index.tsx @@ -59,9 +59,9 @@ export default function NookLanding() { @@ -96,11 +96,11 @@ export default function NookLanding() { The Nook

    - Your Mac and Agents, under control + Manage your coding agents and your Mac

    - Agent orchestration, fan and thermal control in a beautiful native - UI. + Follow your agents, respond to requests, and manage cooling, + charging, and audio from the top of your screen.

    macOS 14.2+ · 14-day free trial · up to 3 devices @@ -145,7 +145,7 @@ export default function NookLanding() { In action

    - Built for the way you actually run agents + What you can do with The Nook

    @@ -164,10 +164,9 @@ export default function NookLanding() { Try it free for 14 days

    - 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. + The trial includes every module. After 14 days, a one-time + purchase lets you keep using The Nook on up to three of your Macs. + There is no subscription.

    - Beta pricing — full price will be $15. One license covers up to 3 of + The beta price is $10. The regular price will be $15. One license covers up to 3 of your own Macs.

    @@ -229,8 +228,7 @@ export default function NookLanding() {

    - Check your inbox — if a license is registered to that email, your - key is on its way. + If a license is registered to that email, we'll send you the key.

    diff --git a/src/routes/nook/landing.test.ts b/src/routes/nook/landing.test.ts new file mode 100644 index 0000000..1a91a75 --- /dev/null +++ b/src/routes/nook/landing.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const root = join(import.meta.dir, "../../.."); +const read = (path: string) => readFileSync(join(root, path), "utf8"); +const landingCopy = [ + "src/routes/nook/index.tsx", + ...[ + "FeatureCollage", + "FeatureBreakdowns", + "IslandExtras", + "PhoneBeta", + "MoreDemos", + "UtilityDemos" + ].map((name) => `src/components/nook/${name}.tsx`) +] + .map(read) + .join("\n"); + +// Like remote-claims.test.ts, this pins an editorial decision, not a tone score. +describe("Nook landing copy (NOOK-1037)", () => { + for (const phrase of [ + "Your fleet, in your pocket", + "Timers that stack", + "Volume and brightness, under the notch", + "The sky, in the slot", + "Storage, measured", + "Wall power, on your terms", + "Fans that answer to you, not the scheduler", + "Your whole fleet, in one pixel strip", + "A hearth that tells the truth", + "A lens that stays asleep", + "Your buttons, orbiting the island", + "Silence it or max it", + "Sleep when you say", + "Panels you shape" + ]) { + it(`replaces “${phrase}”`, () => { + expect(landingCopy).not.toContain(phrase); + }); + } +}); + +// Source anatomy checks supplement the rendered browser checks. +const demos = read("src/components/nook/UtilityDemos.tsx"); + +describe("current utility demo anatomy (NOOK-1037)", () => { + it("shows the three calendar slot states and calendar-colored agenda rows", () => { + for (const text of ["ON AIR", "Work", "Thu, Nov 12", "Join meeting"]) + expect(demos).toContain(text); + expect(demos).toMatch(/>\s*IN\s* { + for (const text of [ + "System draw", + "Adapter", + "Mac", + "Overheat protection", + "Charges past the limit; off on unplug" + ]) + expect(demos).toContain(text); + expect(demos).not.toContain("the pack stays young longer"); + }); + + it("shows master routing, app percentages, pin and hide controls", () => { + for (const text of [ + "System sounds", + "Microphone", + "Output volume", + "Pin app", + "Hide app", + "Default" + ]) + expect(demos).toContain(text); + }); +});