From 8790cc155ecaba3dc902f4dcb8081328dd8c2120 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Mon, 9 Nov 2026 10:40:19 -0500 Subject: [PATCH] site: refresh Nook demos and landing copy [gpt-6.1-sol] The meetings, mixer and charging previews had drifted from the current app, while slogans obscured what the modules do. Match the shipped controls and behavior, reuse the existing demo machinery, and explain the landing-page features plainly without changing routing or purchase flows. Tests: bun test src/routes/nook src/components/nook (36 passed) NOOK-1037 --- src/app.css | 9 + src/components/nook/FeatureBreakdowns.tsx | 424 ++------------- src/components/nook/FeatureCollage.tsx | 28 +- src/components/nook/IslandExtras.tsx | 51 +- src/components/nook/ModuleMocks.tsx | 6 + src/components/nook/MoreDemos.tsx | 126 +---- src/components/nook/PhoneBeta.tsx | 16 +- src/components/nook/UtilityDemos.tsx | 627 ++++++++++++++++++++++ src/routes/nook/index.tsx | 24 +- src/routes/nook/landing.test.ts | 78 +++ 10 files changed, 838 insertions(+), 551 deletions(-) create mode 100644 src/components/nook/UtilityDemos.tsx create mode 100644 src/routes/nook/landing.test.ts 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); + }); +});