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
This commit is contained in:
@@ -761,6 +761,14 @@ a.hover-underline-animation:hover::after {
|
|||||||
animation: nook-pulse 0.8s steps(2, jump-none) infinite;
|
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 */
|
/* Live signal inside a mixer fader */
|
||||||
.nook-meter {
|
.nook-meter {
|
||||||
transform-origin: left center;
|
transform-origin: left center;
|
||||||
@@ -840,6 +848,7 @@ a.hover-underline-animation:hover::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.nook-power-flow,
|
||||||
.nook-meter,
|
.nook-meter,
|
||||||
.nook-wave,
|
.nook-wave,
|
||||||
.nook-progress,
|
.nook-progress,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
IslandPill,
|
IslandPill,
|
||||||
AgentDotGrid,
|
AgentDotGrid,
|
||||||
CALM,
|
CALM,
|
||||||
WARM,
|
|
||||||
ACCENT,
|
ACCENT,
|
||||||
CRITICAL,
|
CRITICAL,
|
||||||
ModuleCard,
|
ModuleCard,
|
||||||
@@ -24,9 +23,9 @@ import {
|
|||||||
CODEX
|
CODEX
|
||||||
} from "./ModuleMocks";
|
} from "./ModuleMocks";
|
||||||
import { useDemoLoop } from "./useDemoLoop";
|
import { useDemoLoop } from "./useDemoLoop";
|
||||||
|
import { BatteryDemo, CalendarDemo, MixerDemo } from "./UtilityDemos";
|
||||||
import {
|
import {
|
||||||
TerminalReplyDemo,
|
TerminalReplyDemo,
|
||||||
MixerDemo,
|
|
||||||
CaptureDemo,
|
CaptureDemo,
|
||||||
FloatingActionsDemo
|
FloatingActionsDemo
|
||||||
} from "./MoreDemos";
|
} from "./MoreDemos";
|
||||||
@@ -168,11 +167,11 @@ function FleetDemo() {
|
|||||||
{phase() === "flare"
|
{phase() === "flare"
|
||||||
? "Codex needs your input"
|
? "Codex needs your input"
|
||||||
: phase() === "resolve"
|
: phase() === "resolve"
|
||||||
? "Answer delivered — agents back to work"
|
? "Reply sent. Agents running."
|
||||||
: "Fleet running calmly"}
|
: "Agents running"}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-0.5 text-[11px] text-white/50">
|
<p class="mt-0.5 text-[11px] text-white/50">
|
||||||
{phase() === "flare" ? "question · in the island now" : "\u00a0"}
|
{phase() === "flare" ? "Question waiting in the island" : "\u00a0"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,7 +233,7 @@ function ApprovalDemo() {
|
|||||||
prompt="harden the bridge"
|
prompt="harden the bridge"
|
||||||
state={
|
state={
|
||||||
asking()
|
asking()
|
||||||
? "Approve Bash — swift test --filter Bridge?"
|
? "Approve Bash: swift test --filter Bridge?"
|
||||||
: "Running swift test --filter Bridge…"
|
: "Running swift test --filter Bridge…"
|
||||||
}
|
}
|
||||||
status={asking() ? "attention" : "running"}
|
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<typeof setTimeout>[] = [];
|
|
||||||
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 (
|
|
||||||
<ModuleCard class="mx-auto w-full max-w-[320px]">
|
|
||||||
<div class="space-y-3 text-left">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-[10px] font-semibold tracking-wide text-white/40 uppercase">
|
|
||||||
Battery
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="ml-auto rounded-md px-2 py-0.5 font-mono text-[10px] font-semibold transition-transform duration-300"
|
|
||||||
style={{
|
|
||||||
color: "rgba(255,255,255,0.75)",
|
|
||||||
background: "rgba(255,255,255,0.08)",
|
|
||||||
transform: phase() === "held" ? "scale(1.12)" : "scale(1)"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Limit 80%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-baseline gap-1.5">
|
|
||||||
<span class="font-mono text-[22px] font-semibold text-white">
|
|
||||||
{pct()}%
|
|
||||||
</span>
|
|
||||||
<span class="text-[11px] text-white/50">{subline()}</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="relative h-[6px] overflow-hidden rounded-full"
|
|
||||||
style={{ background: "rgba(255,255,255,0.08)" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="h-full rounded-full"
|
|
||||||
style={{
|
|
||||||
width: `${pct()}%`,
|
|
||||||
background: pct() > 80 ? WARM : CALM,
|
|
||||||
transition: "background 0.4s"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="absolute top-0 h-full w-px"
|
|
||||||
style={{ left: "80%", background: "rgba(255,255,255,0.5)" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-2 rounded-lg px-2 py-1.5 transition-colors duration-500"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
phase() === "topup" ? "rgba(110,167,255,0.10)" : "transparent"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="flex-1">
|
|
||||||
<p class="text-[11px] font-semibold text-white/85">Top-up mode</p>
|
|
||||||
<p class="text-[9px] text-white/40">
|
|
||||||
charge past the limit while plugged in
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="relative inline-flex h-[16px] w-[30px] items-center rounded-full transition-colors duration-300"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
phase() === "topup" ? ACCENT : "rgba(255,255,255,0.15)"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="absolute h-[12px] w-[12px] rounded-full bg-white transition-all duration-300"
|
|
||||||
style={{ left: phase() === "topup" ? "16px" : "2px" }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<svg
|
|
||||||
width="10"
|
|
||||||
height="13"
|
|
||||||
viewBox="0 0 10 13"
|
|
||||||
style={{
|
|
||||||
animation: watts()
|
|
||||||
? "nook-pulse 1.1s ease-in-out infinite"
|
|
||||||
: "none",
|
|
||||||
opacity: watts() ? 1 : 0.3
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.2 0 0.8 7h2.6l-1 5.2L7.8 5.4H5l1.2-5.4z"
|
|
||||||
fill="#FFD23F"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="text-[10px] text-white/50">
|
|
||||||
{phase() === "held"
|
|
||||||
? "holding — the pack stays young longer"
|
|
||||||
: phase() === "topup"
|
|
||||||
? "topping up — re-arms when you unplug"
|
|
||||||
: "on the way to the ceiling"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModuleCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── 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). */
|
|
||||||
<div class="flex h-[316px] flex-col items-center gap-4">
|
|
||||||
<IslandPill
|
|
||||||
campfire="running"
|
|
||||||
right={
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<svg width="10" height="8" viewBox="0 0 12 9" class="shrink-0">
|
|
||||||
<path
|
|
||||||
d="M0 2A1.4 1.4 0 0 1 1.4 0.6h5.6A1.4 1.4 0 0 1 8.4 2v5A1.4 1.4 0 0 1 7 8.4H1.4A1.4 1.4 0 0 1 0 7z M12 2.6 9 4.5l3 1.9z"
|
|
||||||
fill="rgba(255,255,255,0.75)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span
|
|
||||||
class="whitespace-nowrap font-mono text-[9.5px] font-semibold text-white/90"
|
|
||||||
style={{ color: imminent() ? "#ff9e42" : "rgba(255,255,255,0.9)" }}
|
|
||||||
>
|
|
||||||
{mins() === 0 ? "now" : `${mins()}m`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{/* the island opening onto the day panel */}
|
|
||||||
<div
|
|
||||||
class="w-full max-w-[380px] overflow-hidden rounded-[14px] bg-black transition-all duration-700"
|
|
||||||
style={{
|
|
||||||
"max-height": imminent() ? "240px" : "0px",
|
|
||||||
opacity: imminent() ? 1 : 0,
|
|
||||||
transform: imminent() ? "translateY(0)" : "translateY(-8px)"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="px-4 pt-3 pb-3 text-left">
|
|
||||||
<div class="mb-2 flex items-center">
|
|
||||||
<span class="text-[11px] font-semibold text-white/50">
|
|
||||||
Calendar
|
|
||||||
</span>
|
|
||||||
<span class="ml-auto flex h-4 w-4 items-center justify-center text-[12px] font-semibold text-white/50">
|
|
||||||
+
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2 flex gap-1">
|
|
||||||
<For each={dialDays()}>
|
|
||||||
{(d) => (
|
|
||||||
<div
|
|
||||||
class="flex flex-1 flex-col items-center gap-[3px]"
|
|
||||||
style={{ opacity: d.today ? 1 : d.past ? 0.55 : 0.8 }}
|
|
||||||
>
|
|
||||||
<span class="text-[8px] font-semibold text-white/45">
|
|
||||||
{d.letter}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="flex h-[20px] w-[20px] items-center justify-center rounded-full text-[10px]"
|
|
||||||
style={{
|
|
||||||
background: d.today
|
|
||||||
? "rgb(255,158,66)"
|
|
||||||
: "rgba(255,255,255,0.06)",
|
|
||||||
color: "rgba(255,255,255,0.95)",
|
|
||||||
"font-weight": d.today ? 700 : 500
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{d.num}
|
|
||||||
</span>
|
|
||||||
<span class="flex h-[4px] gap-[2px]">
|
|
||||||
<For each={Array.from({ length: Math.min(d.dots, 3) })}>
|
|
||||||
{() => (
|
|
||||||
<span
|
|
||||||
class="rounded-full"
|
|
||||||
style={{
|
|
||||||
width: "3.5px",
|
|
||||||
height: "3.5px",
|
|
||||||
background: d.today
|
|
||||||
? "rgb(255,158,66)"
|
|
||||||
: "rgba(255,255,255,0.55)"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mb-1 h-px"
|
|
||||||
style={{ background: "rgba(255,255,255,0.06)" }}
|
|
||||||
/>
|
|
||||||
<For each={DAY_EVENTS}>
|
|
||||||
{(ev) => (
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-2 rounded-md px-1.5 py-1.5 transition-colors duration-500"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
ev.meeting && imminent()
|
|
||||||
? "rgba(72,151,178,0.16)"
|
|
||||||
: "transparent"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p class="font-mono text-[10px] text-white/45">{ev.time}</p>
|
|
||||||
<p class="text-[11px] font-semibold text-white">{ev.title}</p>
|
|
||||||
{ev.loc && <p class="text-[10px] text-white/35">{ev.loc}</p>}
|
|
||||||
</div>
|
|
||||||
{ev.meeting && (
|
|
||||||
<span
|
|
||||||
class="ml-auto rounded-md px-2 py-0.5 text-[10px] font-semibold text-white transition-colors duration-500"
|
|
||||||
style={{
|
|
||||||
background: imminent() ? ACCENT : "rgba(255,255,255,0.10)"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Open
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Demo 6: the lens only wakes for a click ──────────────── */
|
/* ── Demo 6: the lens only wakes for a click ──────────────── */
|
||||||
|
|
||||||
function CameraDemo() {
|
function CameraDemo() {
|
||||||
@@ -797,8 +489,8 @@ function CameraDemo() {
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-[10px] text-white/40">
|
<p class="text-[10px] text-white/40">
|
||||||
{phase() === "live"
|
{phase() === "live"
|
||||||
? "live — stops the moment you leave the panel"
|
? "Camera on while this panel is open"
|
||||||
: "inert — the lens stays dark until you click"}
|
: "Camera off until you click Start"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ModuleCard>
|
</ModuleCard>
|
||||||
@@ -816,11 +508,11 @@ function CampfireDemo() {
|
|||||||
"error"
|
"error"
|
||||||
];
|
];
|
||||||
const labels = [
|
const labels = [
|
||||||
"fleet running",
|
"Running",
|
||||||
"pending question",
|
"Question",
|
||||||
"ready to review",
|
"Ready",
|
||||||
"all quiet",
|
"Idle",
|
||||||
"something failed"
|
"Error"
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-5">
|
<div class="grid grid-cols-2 gap-4 sm:grid-cols-5">
|
||||||
@@ -848,13 +540,13 @@ export default function FeatureBreakdowns() {
|
|||||||
<div class="space-y-24">
|
<div class="space-y-24">
|
||||||
<BreakdownSection
|
<BreakdownSection
|
||||||
kicker="The island"
|
kicker="The island"
|
||||||
title="Your whole fleet, in one pixel strip"
|
title="See which agents need your attention"
|
||||||
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."
|
body="Each square represents an agent session. Its color shows whether the agent is working, finished, or waiting for you. Questions appear in the island so you can respond without switching windows."
|
||||||
bullets={[
|
bullets={[
|
||||||
"Claude Code, Codex, Gemini CLI, OpenCode, Pi, Muse, Cursor and more",
|
"Claude Code, Codex, Gemini CLI, OpenCode, Pi, Muse, Cursor and more",
|
||||||
"Custom agents: your own coding agent can wire itself in from the published contract",
|
"Connect custom agents using the published integration contract",
|
||||||
"Slot rankings — you choose which data earns the notch",
|
"Choose which information appears in the compact view",
|
||||||
"You don't need a notch — external displays get the same island, floating top-center"
|
"Works on displays without a notch, centered at the top of the screen"
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<FleetDemo />
|
<FleetDemo />
|
||||||
@@ -862,12 +554,12 @@ export default function FeatureBreakdowns() {
|
|||||||
|
|
||||||
<BreakdownSection
|
<BreakdownSection
|
||||||
kicker="Approvals"
|
kicker="Approvals"
|
||||||
title="Answer from the island — or your phone"
|
title="Approve agent requests"
|
||||||
body="A gated tool call surfaces the island: the fire turns to a question, the card pins above your sessions, and the row says what it's waiting on. Allow, Allow for session, Deny, or deny with a reason, and the run picks up where it stopped."
|
body="When an agent needs permission, the island opens with a request card above your sessions. Review the tool and its input, then allow or deny it. You can include a reason when you deny a request."
|
||||||
bullets={[
|
bullets={[
|
||||||
"Approval cards pin above the session list; questions answer inside their row",
|
"Review permissions above the session list and answer questions in the session row",
|
||||||
"Allow for session waves through the rest of that session's requests",
|
"Allow for session approves subsequent requests in that session",
|
||||||
"Answer from iPhone or Apple Watch too, now in TestFlight beta",
|
"Respond from iPhone or Apple Watch with the TestFlight beta",
|
||||||
]}
|
]}
|
||||||
reversed
|
reversed
|
||||||
>
|
>
|
||||||
@@ -876,8 +568,8 @@ export default function FeatureBreakdowns() {
|
|||||||
|
|
||||||
<BreakdownSection
|
<BreakdownSection
|
||||||
kicker="Terminals"
|
kicker="Terminals"
|
||||||
title="Reply without hunting for the tab"
|
title="Reply to agents and open their terminals"
|
||||||
body="Every session knows the terminal it runs in. Type a reply in the island and it lands in that exact pane; click Jump and the right window comes forward."
|
body="Send a reply from the island to the terminal pane running that session. Use Jump to bring its window forward when you want to work in the terminal."
|
||||||
bullets={[
|
bullets={[
|
||||||
"Replies reach agents in tmux, iTerm2, Terminal, and kitty",
|
"Replies reach agents in tmux, iTerm2, Terminal, and kitty",
|
||||||
"Jump focuses the exact window, even for sessions over SSH",
|
"Jump focuses the exact window, even for sessions over SSH",
|
||||||
@@ -889,10 +581,10 @@ export default function FeatureBreakdowns() {
|
|||||||
|
|
||||||
<BreakdownSection
|
<BreakdownSection
|
||||||
kicker="The campfire"
|
kicker="The campfire"
|
||||||
title="A hearth that tells the truth"
|
title="Agent status in the campfire"
|
||||||
body="Stop-motion pixel art, four frames a step. It dances when tokens stream, putters blue when an agent asks, and goes to dead embers the moment something breaks."
|
body="The pixel-art campfire changes with your agents' activity. Different animations indicate work in progress, a question waiting for you, or an error."
|
||||||
bullets={[
|
bullets={[
|
||||||
"Idle, running, ready, question, and error — at a glance",
|
"Separate animations for idle, running, ready, question, and error states",
|
||||||
"Hand-drawn 14×14 sprites, no tweening",
|
"Hand-drawn 14×14 sprites, no tweening",
|
||||||
"Six chiptunes you can assign to any agent event"
|
"Six chiptunes you can assign to any agent event"
|
||||||
]}
|
]}
|
||||||
@@ -902,13 +594,13 @@ export default function FeatureBreakdowns() {
|
|||||||
|
|
||||||
<BreakdownSection
|
<BreakdownSection
|
||||||
kicker="Cooling"
|
kicker="Cooling"
|
||||||
title="Fans that answer to you, not the scheduler"
|
title="Monitor and control your Mac's fans"
|
||||||
body="Live RPM for every fan, and the curve is yours. Apple's stock curve waits until you're already cooking — swap to Aggressive and it climbs earlier and harder, or slam MAX before the compile."
|
body="Check each fan's speed and choose how cooling responds to load. Keep the stock fan curve, use Aggressive for earlier cooling, or start a MAX session to run the fans at full speed."
|
||||||
bullets={[
|
bullets={[
|
||||||
"Stock, Aggressive, and MAX — swap curves in one click",
|
"Switch between Stock, Aggressive, and MAX",
|
||||||
"MAX and COOL sessions surface right in the pill",
|
"See active MAX and COOL sessions in the compact view",
|
||||||
"CPU/GPU die temps tier-color before things get loud",
|
"CPU and GPU temperature readings change color as they rise",
|
||||||
"All local: your hardware data never leaves the device"
|
"Hardware monitoring runs locally on your Mac"
|
||||||
]}
|
]}
|
||||||
reversed
|
reversed
|
||||||
>
|
>
|
||||||
@@ -917,14 +609,14 @@ export default function FeatureBreakdowns() {
|
|||||||
|
|
||||||
<BreakdownSection
|
<BreakdownSection
|
||||||
kicker="Power"
|
kicker="Power"
|
||||||
title="Wall power, on your terms"
|
title="Set a battery charge limit"
|
||||||
body="Live watts from the adapter to the battery to the chassis — and a charge ceiling you set yourself. On recent macOS The Nook drives the system's own limiter; below that it enforces the limit itself. One slider, 50–100%, any version."
|
body="Set the charge limit on the battery bar, or enter a percentage. The power diagram shows how much power goes to your Mac and battery. Turn on Top-up mode when you need a full charge."
|
||||||
bullets={[
|
bullets={[
|
||||||
"One slider, 50–100%, on any macOS version",
|
"Drag the limit marker or type a percentage; available limits depend on your Mac",
|
||||||
"Hooks the system limiter where it exists, enforces elsewhere",
|
"Uses macOS charge limits where available, with helper-based control on supported Macs",
|
||||||
"Top-up mode charges past, then re-arms on unplug",
|
"Top-up mode allows charging past the limit and turns off when you unplug",
|
||||||
"Thermal protection pauses charging when the pack runs hot",
|
"Overheat protection can pause charging when the battery gets hot",
|
||||||
"Energy flow figure: adapter → battery → system, live watts"
|
"Separate readings for system draw and battery charging, not a wall-power measurement"
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<BatteryDemo />
|
<BatteryDemo />
|
||||||
@@ -932,12 +624,12 @@ export default function FeatureBreakdowns() {
|
|||||||
|
|
||||||
<BreakdownSection
|
<BreakdownSection
|
||||||
kicker="Sound"
|
kicker="Sound"
|
||||||
title="Every app gets its own fader"
|
title="Adjust volume and output for each app"
|
||||||
body="A real volume mixer: set each app's level, mute the noisy one, and send any app to its own output — music to headphones while the meeting stays on the speakers."
|
body="Set an app's volume without changing the others, mute it, or choose a different output. You can play music through headphones and keep a meeting on the speakers."
|
||||||
bullets={[
|
bullets={[
|
||||||
"Per-app volume, mute, and output routing",
|
"Per-app volume, mute, and output routing",
|
||||||
"Master output, system sounds, and microphone pickers in one panel",
|
"Master output, system sounds, and microphone pickers in one panel",
|
||||||
"Catches players that aren't apps, like command-line podcast tools"
|
"Includes audio from command-line players"
|
||||||
]}
|
]}
|
||||||
reversed
|
reversed
|
||||||
>
|
>
|
||||||
@@ -946,8 +638,8 @@ export default function FeatureBreakdowns() {
|
|||||||
|
|
||||||
<BreakdownSection
|
<BreakdownSection
|
||||||
kicker="Capture"
|
kicker="Capture"
|
||||||
title="Screenshots and recordings from the notch"
|
title="Take screenshots and record your screen"
|
||||||
body="Drag out an area, pick a screen, or choose one app. While a recording runs, Wings unfold beside the island: pause and stop on the right, system audio and microphone on the left."
|
body="Capture an area, a whole screen, or one app. During a recording, controls beside the island let you pause, stop, and toggle system audio or your microphone."
|
||||||
bullets={[
|
bullets={[
|
||||||
"Area, screen, or single-app capture",
|
"Area, screen, or single-app capture",
|
||||||
"Screenshots save to your screenshot folder and copy to the clipboard",
|
"Screenshots save to your screenshot folder and copy to the clipboard",
|
||||||
@@ -960,13 +652,13 @@ export default function FeatureBreakdowns() {
|
|||||||
|
|
||||||
<BreakdownSection
|
<BreakdownSection
|
||||||
kicker="Your day"
|
kicker="Your day"
|
||||||
title="Meetings come to the notch"
|
title="Check your calendar and join meetings"
|
||||||
body="Today sits in a dial — days behind, days ahead, dots where things happen. The slot counts your next call down, and when it gets close the island opens the day on its own."
|
body="Browse your calendar from the day strip and open a meeting from its event row. The compact view shows today's date, a countdown before an event, and an ON AIR indicator while a meeting is in progress."
|
||||||
bullets={[
|
bullets={[
|
||||||
"Next meeting counts down right in the slot",
|
"Date, countdown, and meeting progress in the compact view",
|
||||||
"Near a call, the island opens the day itself",
|
"Choose how early the island opens before a meeting",
|
||||||
"Meeting links get an Open button in the row",
|
"Join meetings from the video button beside an event",
|
||||||
"Real calendars and reminders — no sync service"
|
"Uses your Mac's calendars and reminders"
|
||||||
]}
|
]}
|
||||||
reversed
|
reversed
|
||||||
>
|
>
|
||||||
@@ -975,13 +667,13 @@ export default function FeatureBreakdowns() {
|
|||||||
|
|
||||||
<BreakdownSection
|
<BreakdownSection
|
||||||
kicker="Camera"
|
kicker="Camera"
|
||||||
title="A lens that stays asleep"
|
title="Preview your camera before a call"
|
||||||
body="A live camera preview in the island for the seconds before a call. It never wakes on its own: permission-gated, inert until you click Start, and dead the moment you leave the panel."
|
body="Check your camera in the island before joining a call. The preview starts only when you click Start camera and stops when you leave the panel."
|
||||||
bullets={[
|
bullets={[
|
||||||
"Nothing watches until Start camera gets a click",
|
"Requires camera permission and a click on Start camera",
|
||||||
"Live only while the panel sits open",
|
"Preview runs only while the panel is open",
|
||||||
"A device picker for every connected camera",
|
"A device picker for every connected camera",
|
||||||
"Opt-in module — in no default panel"
|
"Add the camera module to a panel when you want it"
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CameraDemo />
|
<CameraDemo />
|
||||||
@@ -989,8 +681,8 @@ export default function FeatureBreakdowns() {
|
|||||||
|
|
||||||
<BreakdownSection
|
<BreakdownSection
|
||||||
kicker="Floating actions"
|
kicker="Floating actions"
|
||||||
title="Your buttons, orbiting the island"
|
title="Add shortcuts beside the island"
|
||||||
body="Pin up to seven one-tap actions around the expanded island. They ride its expansion in and out, so the things you reach for most are always one click away."
|
body="Add up to seven actions beside the expanded island. Use them to open apps, run shortcuts, or change settings without opening a panel."
|
||||||
bullets={[
|
bullets={[
|
||||||
"Run an Apple Shortcut, open an app or URL, or run a shell command",
|
"Run an Apple Shortcut, open an app or URL, or run a shell command",
|
||||||
"Toggle Low Power Mode, top-up, or overheat protection",
|
"Toggle Low Power Mode, top-up, or overheat protection",
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export default function FeatureCollage() {
|
|||||||
Why the Nook
|
Why the Nook
|
||||||
</p>
|
</p>
|
||||||
<h2 class="text-text mb-12 text-center text-3xl font-bold">
|
<h2 class="text-text mb-12 text-center text-3xl font-bold">
|
||||||
One glance. Everything your agents are doing.
|
Keep track of your agents while you work
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-3">
|
<div class="grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||||
@@ -148,12 +148,12 @@ export default function FeatureCollage() {
|
|||||||
The island
|
The island
|
||||||
</p>
|
</p>
|
||||||
<h3 class="text-text mb-2 text-2xl font-bold tracking-tight">
|
<h3 class="text-text mb-2 text-2xl font-bold tracking-tight">
|
||||||
Every agent, one glance
|
Agent status at the top of your screen
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-subtext0 max-w-sm text-sm leading-relaxed">
|
<p class="text-subtext0 max-w-sm text-sm leading-relaxed">
|
||||||
A pill tucked into your notch — a campfire for your whole fleet
|
The island sits around your Mac's notch, with agent activity on
|
||||||
on the left, live data on the right. Blue running, green ready,
|
the left and your choice of information on the right. Blue means
|
||||||
amber needs you.
|
running, green means ready, and amber means an agent needs you.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-10 flex w-full justify-center">
|
<div class="my-10 flex w-full justify-center">
|
||||||
@@ -186,15 +186,15 @@ export default function FeatureCollage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-subtext0 -mb-1 text-center text-xs">
|
<p class="text-subtext0 -mb-1 text-center text-xs">
|
||||||
expand for the full panel ↓
|
Open the island to see your sessions
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Approvals */}
|
{/* Approvals */}
|
||||||
<CollageCell
|
<CollageCell
|
||||||
kicker="Approvals"
|
kicker="Approvals"
|
||||||
title="Unblock agents, anywhere"
|
title="Review permission requests"
|
||||||
body="Permission prompts pin as cards above your sessions — Allow, Allow for session, or Deny. Away from the desk, answer from your iPhone or Apple Watch (beta)."
|
body="Review permission requests above your sessions and choose Allow, Allow for session, or Deny. The iPhone and Apple Watch beta lets you respond away from your Mac."
|
||||||
>
|
>
|
||||||
<InkStage>
|
<InkStage>
|
||||||
<div class="w-full max-w-sm px-3">
|
<div class="w-full max-w-sm px-3">
|
||||||
@@ -211,8 +211,8 @@ export default function FeatureCollage() {
|
|||||||
{/* Fans */}
|
{/* Fans */}
|
||||||
<CollageCell
|
<CollageCell
|
||||||
kicker="Cooling"
|
kicker="Cooling"
|
||||||
title="Silence it or max it"
|
title="Fan speeds and cooling modes"
|
||||||
body="Read both fans live, then take over the curve completely. Flip to MAX before a compile, whisper-quiet when the fleet idles."
|
body="Monitor each fan's speed and choose a cooling mode. Keep automatic control or start a MAX session before a demanding task."
|
||||||
>
|
>
|
||||||
<InkStage class="w-full">
|
<InkStage class="w-full">
|
||||||
<div class="w-full max-w-[260px] text-left">
|
<div class="w-full max-w-[260px] text-left">
|
||||||
@@ -224,8 +224,8 @@ export default function FeatureCollage() {
|
|||||||
{/* Gauges */}
|
{/* Gauges */}
|
||||||
<CollageCell
|
<CollageCell
|
||||||
kicker="Your Mac"
|
kicker="Your Mac"
|
||||||
title="The gauges that matter"
|
title="CPU and GPU monitoring"
|
||||||
body="Each die's temperature beside its 60-second usage trace, tinted amber from 70° and red at 85°, so you see a compile heating up before the fans do."
|
body="See CPU and GPU temperatures beside their last 60 seconds of usage. Temperature readings turn amber at 70°C and red at 85°C."
|
||||||
>
|
>
|
||||||
<InkStage class="w-full">
|
<InkStage class="w-full">
|
||||||
<div class="w-full max-w-[300px] text-left">
|
<div class="w-full max-w-[300px] text-left">
|
||||||
@@ -236,12 +236,12 @@ export default function FeatureCollage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-subtext1 mt-6 text-center text-xs">
|
<p class="text-subtext1 mt-6 text-center text-xs">
|
||||||
Plus a mixer, capture, timers, weather, and a dozen more modules —{" "}
|
Also includes a volume mixer, screen capture, timers, weather, and more.{" "}
|
||||||
<a
|
<a
|
||||||
href="#island-extras"
|
href="#island-extras"
|
||||||
class="underline decoration-dotted hover:opacity-80"
|
class="underline decoration-dotted hover:opacity-80"
|
||||||
>
|
>
|
||||||
see them all ↓
|
Browse the other modules
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
ACCENT,
|
ACCENT,
|
||||||
CALM,
|
CALM,
|
||||||
WARM,
|
WARM,
|
||||||
CRITICAL,
|
|
||||||
CARD_FILL,
|
CARD_FILL,
|
||||||
CARD_STROKE
|
CARD_STROKE
|
||||||
} from "./IslandMock";
|
} from "./IslandMock";
|
||||||
@@ -571,8 +570,8 @@ function KeepAwakeMock() {
|
|||||||
|
|
||||||
const MODES = [
|
const MODES = [
|
||||||
{ name: "Auto", note: "follows each agent's own settings", card: "asks", tint: "rgba(234,179,8,0.10)" },
|
{ 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: "Yolo", note: "automatically allows received requests", 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: "Verify", note: "received requests wait for approval", card: "held", tint: "rgba(229,72,77,0.12)" }
|
||||||
];
|
];
|
||||||
|
|
||||||
function ApprovalModesMock() {
|
function ApprovalModesMock() {
|
||||||
@@ -773,14 +772,14 @@ export default function IslandExtras() {
|
|||||||
<section class="bg-base relative z-20 py-16" id="island-extras">
|
<section class="bg-base relative z-20 py-16" id="island-extras">
|
||||||
<div class="px-4 md:px-8">
|
<div class="px-4 md:px-8">
|
||||||
<p class="text-subtext1 mb-2 text-center text-xs font-semibold tracking-[0.18em] uppercase">
|
<p class="text-subtext1 mb-2 text-center text-xs font-semibold tracking-[0.18em] uppercase">
|
||||||
And everything else
|
More modules
|
||||||
</p>
|
</p>
|
||||||
<h2 class="text-text mb-3 text-center text-3xl font-bold">
|
<h2 class="text-text mb-3 text-center text-3xl font-bold">
|
||||||
One island instead of a menu bar full of utilities
|
Other tools in the island
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-subtext0 mx-auto mb-10 max-w-2xl text-center leading-relaxed">
|
<p class="text-subtext0 mx-auto mb-10 max-w-2xl text-center leading-relaxed">
|
||||||
Every module below lives in the same notch, arranged however you
|
Add the modules you need and arrange them in your panels.
|
||||||
like, under the same one-time license.
|
They're all included with your license.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -793,8 +792,8 @@ export default function IslandExtras() {
|
|||||||
<CollageCell
|
<CollageCell
|
||||||
class="md:col-span-2"
|
class="md:col-span-2"
|
||||||
kicker="Now Playing"
|
kicker="Now Playing"
|
||||||
title="Music in the notch"
|
title="Playback controls and track information"
|
||||||
body="Track, source, and transport controls in a module. Sources without artwork, like some browsers, get a live waveform in the slot instead."
|
body="See what's playing and control playback. For sources without artwork, including some browsers, the compact view shows an audio waveform."
|
||||||
>
|
>
|
||||||
<InkStage class="h-full w-full items-center">
|
<InkStage class="h-full w-full items-center">
|
||||||
<NowPlayingMock />
|
<NowPlayingMock />
|
||||||
@@ -803,8 +802,8 @@ export default function IslandExtras() {
|
|||||||
|
|
||||||
<CollageCell
|
<CollageCell
|
||||||
kicker="Timers"
|
kicker="Timers"
|
||||||
title="Timers that stack"
|
title="Run several timers at once"
|
||||||
body="A readout over a tick tape that drains with a soft crest. Run several at once; finished timers ring and hold the island open until you dismiss them."
|
body="Keep multiple timers running and check their progress in the island. When a timer finishes, it rings and keeps the island open until you dismiss it."
|
||||||
>
|
>
|
||||||
<InkStage class="h-full w-full items-center px-3">
|
<InkStage class="h-full w-full items-center px-3">
|
||||||
<TimersMock />
|
<TimersMock />
|
||||||
@@ -813,8 +812,8 @@ export default function IslandExtras() {
|
|||||||
|
|
||||||
<CollageCell
|
<CollageCell
|
||||||
kicker="Weather"
|
kicker="Weather"
|
||||||
title="The sky, in the slot"
|
title="Weather for your saved places"
|
||||||
body="Saved places with hourly and daily forecasts. The compact slot stacks the sky over the temperature."
|
body="Check hourly and daily forecasts for saved places. The compact view shows the current conditions and temperature."
|
||||||
>
|
>
|
||||||
<InkStage class="h-full w-full items-center px-3">
|
<InkStage class="h-full w-full items-center px-3">
|
||||||
<WeatherMock />
|
<WeatherMock />
|
||||||
@@ -824,8 +823,8 @@ export default function IslandExtras() {
|
|||||||
<CollageCell
|
<CollageCell
|
||||||
class="md:col-span-2"
|
class="md:col-span-2"
|
||||||
kicker="Interceptions"
|
kicker="Interceptions"
|
||||||
title="Volume and brightness, under the notch"
|
title="Volume and brightness controls"
|
||||||
body="An optional takeover of the system HUD: levels peek out below the island. Extra brightness spends an XDR panel's HDR headroom past the usual maximum, and external displays get real brightness control."
|
body="Show volume and brightness changes below the island instead of the system overlay. Adjust supported external displays and use extra brightness on compatible XDR displays."
|
||||||
>
|
>
|
||||||
<InkStage class="h-full w-full items-center">
|
<InkStage class="h-full w-full items-center">
|
||||||
<div class="flex min-h-[110px] w-full justify-center pt-2">
|
<div class="flex min-h-[110px] w-full justify-center pt-2">
|
||||||
@@ -836,8 +835,8 @@ export default function IslandExtras() {
|
|||||||
|
|
||||||
<CollageCell
|
<CollageCell
|
||||||
kicker="Approval modes"
|
kicker="Approval modes"
|
||||||
title="One stance for every agent"
|
title="Choose how approvals are handled"
|
||||||
body="Auto follows each agent's own settings, Yolo waves gated tools through, and Verify holds every one until you decide."
|
body="Auto follows each agent's settings. Yolo automatically allows requests that reach The Nook. Verify waits for your decision on each request it receives."
|
||||||
>
|
>
|
||||||
<InkStage class="h-full w-full items-center px-3">
|
<InkStage class="h-full w-full items-center px-3">
|
||||||
<ApprovalModesMock />
|
<ApprovalModesMock />
|
||||||
@@ -846,8 +845,8 @@ export default function IslandExtras() {
|
|||||||
|
|
||||||
<CollageCell
|
<CollageCell
|
||||||
kicker="Network"
|
kicker="Network"
|
||||||
title="Know your connection"
|
title="Network information and speed tests"
|
||||||
body="Public IP, ISP, gateway, and DNS one click from your clipboard. VPN routes get flagged, and a speed test is always a click away."
|
body="View and copy your public IP, ISP, gateway, and DNS details. Check for VPN routes or run a speed test."
|
||||||
>
|
>
|
||||||
<InkStage class="h-full w-full items-center px-3">
|
<InkStage class="h-full w-full items-center px-3">
|
||||||
<NetworkMock />
|
<NetworkMock />
|
||||||
@@ -856,8 +855,8 @@ export default function IslandExtras() {
|
|||||||
|
|
||||||
<CollageCell
|
<CollageCell
|
||||||
kicker="Disks"
|
kicker="Disks"
|
||||||
title="Storage, measured"
|
title="Disk usage and drive health"
|
||||||
body="Live read and write throughput, capacity that matches Finder, and SMART health for your drives."
|
body="Check read and write speeds, free space, and SMART health information for supported drives."
|
||||||
>
|
>
|
||||||
<InkStage class="h-full w-full items-center px-3">
|
<InkStage class="h-full w-full items-center px-3">
|
||||||
<DiskMock />
|
<DiskMock />
|
||||||
@@ -866,8 +865,8 @@ export default function IslandExtras() {
|
|||||||
|
|
||||||
<CollageCell
|
<CollageCell
|
||||||
kicker="Keep Awake"
|
kicker="Keep Awake"
|
||||||
title="Sleep when you say"
|
title="Keep your Mac awake"
|
||||||
body="Stay awake for a set time, while plugged in, with the lid closed, or only while your agents are working."
|
body="Keep your Mac awake for a set time, while plugged in, with the lid closed, or while your agents are working."
|
||||||
>
|
>
|
||||||
<InkStage class="h-full w-full items-center px-3">
|
<InkStage class="h-full w-full items-center px-3">
|
||||||
<KeepAwakeMock />
|
<KeepAwakeMock />
|
||||||
@@ -876,7 +875,7 @@ export default function IslandExtras() {
|
|||||||
|
|
||||||
<CollageCell
|
<CollageCell
|
||||||
kicker="Lock Screen"
|
kicker="Lock Screen"
|
||||||
title="Visible while locked"
|
title="Agent status on the Lock Screen"
|
||||||
body="The island stays on your Lock Screen, so you can see how many agents are working or waiting without unlocking."
|
body="The island stays on your Lock Screen, so you can see how many agents are working or waiting without unlocking."
|
||||||
>
|
>
|
||||||
<InkStage class="h-full w-full items-center px-3">
|
<InkStage class="h-full w-full items-center px-3">
|
||||||
@@ -886,8 +885,8 @@ export default function IslandExtras() {
|
|||||||
|
|
||||||
<CollageCell
|
<CollageCell
|
||||||
kicker="Layout"
|
kicker="Layout"
|
||||||
title="Panels you shape"
|
title="Arrange your panels"
|
||||||
body="Drag and resize modules in the mosaic editor, pick a density, and let slot rankings switch with your Focus mode."
|
body="Drag and resize modules, choose how much detail they show, and change compact-view priorities with your Focus mode."
|
||||||
>
|
>
|
||||||
<InkStage class="h-full w-full items-center px-3">
|
<InkStage class="h-full w-full items-center px-3">
|
||||||
<LayoutMock />
|
<LayoutMock />
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ const GLYPHS: Record<string, () => JSX.Element> = {
|
|||||||
<path d="M3.5 8a4.5 4.5 0 1 0 1.4-3.3M3.2 2.2v2.8H6" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
<path d="M3.5 8a4.5 4.5 0 1 0 1.4-3.3M3.2 2.2v2.8H6" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
),
|
),
|
||||||
plus: () => <path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />,
|
plus: () => <path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />,
|
||||||
|
video: () => <path d="M2 4h8v8H2Z m9 3 3-2v6l-3-2Z" fill="currentColor" />,
|
||||||
|
pin: () => <path d="m6 2 6 6-3 1-2 3-1-2-4 4m2-8 2-2Z" stroke="currentColor" stroke-width="1.2" fill="none" stroke-linejoin="round" />,
|
||||||
|
eye: () => <><path d="M1 8s2.5-4 7-4 7 4 7 4-2.5 4-7 4-7-4-7-4Z" stroke="currentColor" stroke-width="1.2" fill="none" /><circle cx="8" cy="8" r="2" fill="currentColor" /></>,
|
||||||
|
plug: () => <path d="M5 1v4m6-4v4M3 5h10v3a5 5 0 0 1-4 4.9V16H7v-3.1A5 5 0 0 1 3 8Z" stroke="currentColor" stroke-width="1.3" fill="none" />,
|
||||||
|
battery: () => <><rect x="1" y="4" width="12" height="8" rx="1.5" stroke="currentColor" stroke-width="1.2" fill="none" /><path d="M14 6v4M3 6h5v4H3Z" stroke="currentColor" stroke-width="1.2" /></>,
|
||||||
|
laptop: () => <path d="M3 2.5h10v9H3Z M1 13.5h14" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linejoin="round" />,
|
||||||
refresh: () => (
|
refresh: () => (
|
||||||
<path d="M12.5 8A4.5 4.5 0 1 1 11 4.6M11.6 2v2.8H8.8" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
<path d="M12.5 8A4.5 4.5 0 1 1 11 4.6M11.6 2v2.8H8.8" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
STATUS,
|
STATUS,
|
||||||
ACCENT,
|
ACCENT,
|
||||||
CALM,
|
CALM,
|
||||||
CRITICAL,
|
|
||||||
CARD_STROKE
|
CARD_STROKE
|
||||||
} from "./IslandMock";
|
} from "./IslandMock";
|
||||||
import { useDemoLoop } from "./useDemoLoop";
|
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:
|
* 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.
|
* floating actions. Same ink chrome and status palette as the originals.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ── Reply lands in the agent's own terminal ──────────────────────── */
|
/* ── 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() {
|
export function TerminalReplyDemo() {
|
||||||
const [typed, setTyped] = createSignal(0);
|
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 (
|
|
||||||
<ModuleCard class="mx-auto w-full max-w-[340px]">
|
|
||||||
<div class="space-y-2.5 text-left">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="text-[11px] font-semibold text-white/50">
|
|
||||||
Volume mixer
|
|
||||||
</span>
|
|
||||||
<span class="ml-auto font-mono text-[10px] text-white/40">
|
|
||||||
out · Speakers
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<For each={MIXER_APPS}>
|
|
||||||
{(app, i) => (
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
class="flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-[5px] text-[8px] font-bold text-black/70"
|
|
||||||
style={{ background: app.color }}
|
|
||||||
>
|
|
||||||
{app.glyph}
|
|
||||||
</span>
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<div class="flex items-center gap-1.5">
|
|
||||||
<span class="truncate text-[11px] font-semibold text-white/85">
|
|
||||||
{app.name}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="ml-auto shrink-0 rounded px-1.5 text-[9px] text-white/55 transition-colors duration-500"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
i() === 0 && step() === 2
|
|
||||||
? `${ACCENT}55`
|
|
||||||
: "rgba(255,255,255,0.07)"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{route(i())}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="relative mt-1 h-[5px] overflow-hidden rounded-full"
|
|
||||||
style={{ background: "rgba(255,255,255,0.08)" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="h-full rounded-full"
|
|
||||||
style={{
|
|
||||||
width: `${level(i()) * 100}%`,
|
|
||||||
background: muted(i()) ? "rgba(255,255,255,0.18)" : CALM,
|
|
||||||
transition: "width 0.9s cubic-bezier(0.22,1,0.36,1), background 0.3s"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* live signal riding inside the level */}
|
|
||||||
<div
|
|
||||||
class="nook-meter absolute top-0 left-0 h-full rounded-full"
|
|
||||||
style={{
|
|
||||||
width: `${level(i()) * 100}%`,
|
|
||||||
background: "rgba(255,255,255,0.35)",
|
|
||||||
opacity: muted(i()) ? 0 : 1,
|
|
||||||
"animation-duration": `${0.5 + i() * 0.17}s`,
|
|
||||||
transition: "width 0.9s cubic-bezier(0.22,1,0.36,1), opacity 0.3s"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="w-4 shrink-0 text-center text-[11px] transition-colors duration-300"
|
|
||||||
style={{ color: muted(i()) ? CRITICAL : "rgba(255,255,255,0.4)" }}
|
|
||||||
>
|
|
||||||
{muted(i()) ? "✕" : "♪"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<div class="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
|
|
||||||
<p class="text-[10px] text-white/40">
|
|
||||||
{
|
|
||||||
[
|
|
||||||
"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()]
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</ModuleCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Capture: the module, and the Wings while a recording runs ────── */
|
/* ── Capture: the module, and the Wings while a recording runs ────── */
|
||||||
|
|
||||||
/** CapturePalette.recording and the NookPalette paper/ink pair. */
|
/** CapturePalette.recording and the NookPalette paper/ink pair. */
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function CompanionDemo() {
|
|||||||
{allowed() ? "Allowed ✓" : "Claude Code · the-nook"}
|
{allowed() ? "Allowed ✓" : "Claude Code · the-nook"}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-[9px] leading-snug text-white/60">
|
<p class="text-[9px] leading-snug text-white/60">
|
||||||
{allowed() ? "Back to work." : "Run Bash: swift test"}
|
{allowed() ? "Agent running." : "Run Bash: swift test"}
|
||||||
</p>
|
</p>
|
||||||
<Show when={!allowed()}>
|
<Show when={!allowed()}>
|
||||||
<div class="mt-1.5 flex gap-1">
|
<div class="mt-1.5 flex gap-1">
|
||||||
@@ -139,7 +139,7 @@ function CompanionDemo() {
|
|||||||
>
|
>
|
||||||
<p class="text-[8px] font-semibold text-white/50">Codex asks</p>
|
<p class="text-[8px] font-semibold text-white/50">Codex asks</p>
|
||||||
<p class="mt-0.5 text-[9.5px] leading-snug font-semibold text-white">
|
<p class="mt-0.5 text-[9.5px] leading-snug font-semibold text-white">
|
||||||
Ship it to staging?
|
Deploy to staging?
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-2 space-y-1">
|
<div class="mt-2 space-y-1">
|
||||||
<For each={["Yes", "Not yet"]}>
|
<For each={["Yes", "Not yet"]}>
|
||||||
@@ -194,19 +194,19 @@ export default function PhoneBeta(props: { brandColor: string }) {
|
|||||||
TestFlight beta
|
TestFlight beta
|
||||||
</span>
|
</span>
|
||||||
<h2 class="text-text mb-3 text-3xl font-bold tracking-tight">
|
<h2 class="text-text mb-3 text-3xl font-bold tracking-tight">
|
||||||
Your fleet, in your pocket
|
Use The Nook on iPhone and Apple Watch
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-subtext0 mb-5 leading-relaxed">
|
<p class="text-subtext0 mb-5 leading-relaxed">
|
||||||
The iPhone and Apple Watch companion is in beta. Approvals,
|
The companion app is in TestFlight beta. Check your agents,
|
||||||
questions, and replies reach you away from the desk, and your
|
respond to permission requests, and answer questions when you're
|
||||||
answer goes straight back to the agent.
|
away from your Mac.
|
||||||
</p>
|
</p>
|
||||||
<ul class="mb-8 space-y-2.5">
|
<ul class="mb-8 space-y-2.5">
|
||||||
<For
|
<For
|
||||||
each={[
|
each={[
|
||||||
"Allow, deny, answer, and reply from iPhone or Apple Watch",
|
"Allow, deny, answer, and reply from iPhone or Apple Watch",
|
||||||
"Connects over an end-to-end encrypted relay, your tailnet, or your LAN",
|
"Connects over an end-to-end encrypted relay, your tailnet, or your LAN",
|
||||||
"Home Screen, Lock Screen, and watch-face widgets for fleet status",
|
"Agent status widgets for your Home Screen, Lock Screen, and watch face",
|
||||||
"Mute notifications for a while, or while you're active at the Mac"
|
"Mute notifications for a while, or while you're active at the Mac"
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -255,7 +255,7 @@ export default function PhoneBeta(props: { brandColor: string }) {
|
|||||||
</form>
|
</form>
|
||||||
<Show when={state() === "sent"}>
|
<Show when={state() === "sent"}>
|
||||||
<p class="text-subtext1 mt-4 text-sm">
|
<p class="text-subtext1 mt-4 text-sm">
|
||||||
Thanks. Watch your inbox for a TestFlight invite.
|
Thanks. We'll email you a TestFlight invite.
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={state() === "limited"}>
|
<Show when={state() === "limited"}>
|
||||||
|
|||||||
627
src/components/nook/UtilityDemos.tsx
Normal file
627
src/components/nook/UtilityDemos.tsx
Normal file
@@ -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 (
|
||||||
|
<div class="relative h-5 min-w-0 flex-1 rounded-full bg-white/10">
|
||||||
|
<div
|
||||||
|
class="absolute inset-y-0 left-0 overflow-hidden rounded-full transition-[width] duration-700"
|
||||||
|
style={{ width: `calc(${position()} + 9px)`, background: PAPER }}
|
||||||
|
>
|
||||||
|
<Show when={props.tint && !props.muted}>
|
||||||
|
<div
|
||||||
|
class="nook-meter h-full w-full rounded-full"
|
||||||
|
style={{ background: props.tint }}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={props.tint}>
|
||||||
|
<span
|
||||||
|
class="absolute top-[7.5px] h-[5px] w-[5px] rounded-full border border-white/55 bg-[#0d0d0f]"
|
||||||
|
style={{ left: "calc(9px + (100% - 18px) * 0.8 - 2.5px)" }}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<span
|
||||||
|
class="absolute top-px h-[18px] w-[18px] rounded-full border-[1.5px] shadow-sm transition-[left] duration-700"
|
||||||
|
style={{
|
||||||
|
left: `calc(${position()} - 9px)`,
|
||||||
|
background: "#0d0d0f",
|
||||||
|
"border-color": PAPER
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RoutePicker(props: { label: string }) {
|
||||||
|
return (
|
||||||
|
<span class="inline-flex max-w-[155px] items-center gap-1 rounded-full bg-white/10 px-2 py-1 text-[11px] text-white/85">
|
||||||
|
<span class="truncate">{props.label}</span>
|
||||||
|
<Glyph name="chevron" size={8} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
role="img"
|
||||||
|
aria-label="Volume mixer preview with output, system sounds, microphone, and per-app volume, mute and routing controls"
|
||||||
|
>
|
||||||
|
<ModuleCard class="mx-auto w-full max-w-[360px]">
|
||||||
|
<div class="space-y-3 text-left text-white">
|
||||||
|
<div class="flex items-center gap-2" aria-label="Output volume">
|
||||||
|
<Glyph name="speaker" class="text-white/50" />
|
||||||
|
<MixerSlider level={65} />
|
||||||
|
<span class="w-[34px] text-right font-mono text-[11px] text-white/60">
|
||||||
|
65%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<For
|
||||||
|
each={[
|
||||||
|
{
|
||||||
|
label: "Output",
|
||||||
|
icon: "speaker",
|
||||||
|
device: "MacBook Pro Speakers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "System sounds",
|
||||||
|
icon: "bell",
|
||||||
|
device: "MacBook Pro Speakers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Microphone",
|
||||||
|
icon: "mic",
|
||||||
|
device: "MacBook Pro Microphone"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{(route) => (
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Glyph name={route.icon} class="text-white/50" />
|
||||||
|
<span class="flex-1 text-[12px] font-medium">
|
||||||
|
{route.label}
|
||||||
|
</span>
|
||||||
|
<RoutePicker label={route.device} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<div class="h-px bg-white/[0.06]" />
|
||||||
|
<For each={MIXER_APPS}>
|
||||||
|
{(app, i) => (
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-[4px] text-[10px] text-white"
|
||||||
|
style={{ background: app.color }}
|
||||||
|
>
|
||||||
|
{app.glyph}
|
||||||
|
</span>
|
||||||
|
<span class="min-w-0 flex-1 truncate text-[12px] font-medium">
|
||||||
|
{app.name}
|
||||||
|
</span>
|
||||||
|
<RoutePicker
|
||||||
|
label={i() === 0 && step() >= 2 ? "Headphones" : "Default"}
|
||||||
|
/>
|
||||||
|
<span aria-label="Pin app" class="text-white/50">
|
||||||
|
<Glyph name="pin" size={10} />
|
||||||
|
</span>
|
||||||
|
<span aria-label="Hide app" class="text-white/50">
|
||||||
|
<Glyph name="eye" size={10} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 pl-[26px]">
|
||||||
|
<MixerSlider
|
||||||
|
level={volume(i())}
|
||||||
|
tint={app.color}
|
||||||
|
muted={muted(i())}
|
||||||
|
/>
|
||||||
|
<span class="w-[34px] text-right font-mono text-[11px] text-white/60">
|
||||||
|
{muted(i()) ? 0 : volume(i())}%
|
||||||
|
</span>
|
||||||
|
<Glyph
|
||||||
|
name={muted(i()) ? "speakerOff" : "speaker"}
|
||||||
|
size={14}
|
||||||
|
style={{
|
||||||
|
color: muted(i()) ? WARM : "rgba(255,255,255,0.5)"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</ModuleCard>
|
||||||
|
<p class="mt-3 min-h-8 text-center text-[11px] text-white/55">
|
||||||
|
{
|
||||||
|
[
|
||||||
|
"Adjust each app separately",
|
||||||
|
"Browser muted",
|
||||||
|
"Music playing through headphones",
|
||||||
|
"Music volume lowered to 38%"
|
||||||
|
][step()]
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PowerNode(props: {
|
||||||
|
label: string;
|
||||||
|
glyph: GlyphName;
|
||||||
|
watts: number;
|
||||||
|
tint: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div class="flex h-9 items-center gap-[7px]">
|
||||||
|
<Glyph name={props.glyph} size={18} style={{ color: props.tint }} />
|
||||||
|
<div>
|
||||||
|
<p class="text-[9px] text-white/55">{props.label}</p>
|
||||||
|
<p class="font-mono text-[11px] text-white">
|
||||||
|
{props.watts.toFixed(1)} W
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<div
|
||||||
|
role="img"
|
||||||
|
aria-label="Battery preview showing system draw, adapter power split between the Mac and battery, an 80 percent charge limit, and top-up mode"
|
||||||
|
>
|
||||||
|
<ModuleCard class="mx-auto w-full max-w-[340px]">
|
||||||
|
<div class="space-y-3 text-left text-white">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-1.5 font-mono text-[18px]">
|
||||||
|
<span style={{ color: CALM }}>ϟ</span> 18.2 W
|
||||||
|
</div>
|
||||||
|
<p class="text-[9px] font-semibold text-white/55">System draw</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<PowerNode
|
||||||
|
label="Adapter"
|
||||||
|
glyph="plug"
|
||||||
|
watts={18.2 + batteryWatts()}
|
||||||
|
tint={CALM}
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
class="h-[82px] min-w-4 flex-1"
|
||||||
|
viewBox="0 0 100 82"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 41H25C55 41 40 18 70 18H100"
|
||||||
|
stroke={CALM}
|
||||||
|
stroke-opacity="0.3"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0 41H25C55 41 40 64 70 64H100"
|
||||||
|
stroke={BATTERY}
|
||||||
|
stroke-opacity="0.3"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="nook-power-flow"
|
||||||
|
d="M0 41H25C55 41 40 18 70 18H100"
|
||||||
|
stroke={CALM}
|
||||||
|
stroke-width="1.8"
|
||||||
|
stroke-dasharray="3 18"
|
||||||
|
/>
|
||||||
|
<Show when={batteryWatts() > 0}>
|
||||||
|
<path
|
||||||
|
class="nook-power-flow"
|
||||||
|
d="M0 41H25C55 41 40 64 70 64H100"
|
||||||
|
stroke={BATTERY}
|
||||||
|
stroke-width="1.8"
|
||||||
|
stroke-dasharray="3 18"
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<path d="m96 15 4 3-4 3" stroke={CALM} stroke-width="1.5" />
|
||||||
|
<path d="m96 61 4 3-4 3" stroke={BATTERY} stroke-width="1.5" />
|
||||||
|
<Show when={held()}>
|
||||||
|
<path
|
||||||
|
d="m81 60 8 8m0-8-8 8"
|
||||||
|
stroke={BATTERY}
|
||||||
|
stroke-width="1.8"
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</svg>
|
||||||
|
<div class="space-y-2.5">
|
||||||
|
<PowerNode label="Mac" glyph="laptop" watts={18.2} tint={CALM} />
|
||||||
|
<PowerNode
|
||||||
|
label="Battery"
|
||||||
|
glyph="battery"
|
||||||
|
watts={batteryWatts()}
|
||||||
|
tint={BATTERY}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-baseline gap-2">
|
||||||
|
<span class="flex-1 text-[9px] font-semibold text-white/55 uppercase">
|
||||||
|
Battery
|
||||||
|
</span>
|
||||||
|
<span class="font-mono text-[15px]">{pct()}%</span>
|
||||||
|
<span class="ml-2 flex items-center gap-1 font-mono text-[10px] text-white/70">
|
||||||
|
Limit{" "}
|
||||||
|
<span class="rounded border border-white/15 bg-white/5 px-2 py-0.5 text-[12px] text-white/90">
|
||||||
|
80
|
||||||
|
</span>
|
||||||
|
%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex h-[18px] items-center">
|
||||||
|
<div class="h-1 w-full overflow-hidden rounded-full bg-white/[0.08]">
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full transition-[width] duration-[1600ms]"
|
||||||
|
style={{ width: `${pct()}%`, background: CALM }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="absolute h-3 w-1 rounded-sm bg-white/85"
|
||||||
|
style={{ left: "calc(80% - 2px)" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<For
|
||||||
|
each={[
|
||||||
|
{
|
||||||
|
label: "Top-up mode",
|
||||||
|
note: "Charges past the limit; off on unplug",
|
||||||
|
on: topUp()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Overheat protection",
|
||||||
|
note: topUp()
|
||||||
|
? "Suspended while Top-up mode is on"
|
||||||
|
: "Pauses at 95°F",
|
||||||
|
on: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{(control) => (
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="text-[12px] font-medium">{control.label}</p>
|
||||||
|
<p class="text-[10px] leading-snug text-white/55">
|
||||||
|
{control.note}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="relative h-4 w-[30px] shrink-0 rounded-full transition-colors"
|
||||||
|
style={{
|
||||||
|
background: control.on ? CALM : "rgba(255,255,255,0.15)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute top-0.5 h-3 w-3 rounded-full bg-white transition-[left]"
|
||||||
|
style={{ left: control.on ? "16px" : "2px" }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</ModuleCard>
|
||||||
|
<p class="mt-3 min-h-8 text-center text-[11px] text-white/55">
|
||||||
|
{held()
|
||||||
|
? "Charging paused at the 80% limit"
|
||||||
|
: topUp()
|
||||||
|
? "Top-up mode allows a full charge"
|
||||||
|
: "Charging with an 80% limit"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
role="img"
|
||||||
|
aria-label="Calendar preview showing the date, a meeting countdown, an on-air indicator, and the day's events with a join meeting button"
|
||||||
|
class="flex min-h-[360px] min-w-0 flex-col items-center gap-4"
|
||||||
|
>
|
||||||
|
<div class="flex max-w-full justify-center" style={{ zoom: 0.8 }}>
|
||||||
|
<IslandPill
|
||||||
|
right={
|
||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" aria-hidden="true">
|
||||||
|
<Show when={step() === 0}>
|
||||||
|
<rect
|
||||||
|
x="6"
|
||||||
|
y="4"
|
||||||
|
width="28"
|
||||||
|
height="27"
|
||||||
|
rx="5"
|
||||||
|
fill="#ededed"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11 4h18a5 5 0 0 1 5 5v3H6V9a5 5 0 0 1 5-5"
|
||||||
|
fill={CALENDAR}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x="20"
|
||||||
|
y="10"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-size="5.5"
|
||||||
|
font-weight="700"
|
||||||
|
fill="#1f1f1f"
|
||||||
|
>
|
||||||
|
THU
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
x="20"
|
||||||
|
y="28"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-size="17"
|
||||||
|
font-weight="700"
|
||||||
|
fill="#1f1f1f"
|
||||||
|
>
|
||||||
|
12
|
||||||
|
</text>
|
||||||
|
<circle cx="20" cy="36" r="2.25" fill={CALENDAR} />
|
||||||
|
</Show>
|
||||||
|
<Show when={step() === 1 || step() === 2}>
|
||||||
|
<text
|
||||||
|
x="20"
|
||||||
|
y="10"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-size="7"
|
||||||
|
font-weight="700"
|
||||||
|
fill="white"
|
||||||
|
opacity="0.65"
|
||||||
|
>
|
||||||
|
IN
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
x="20"
|
||||||
|
y="30"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-size="20"
|
||||||
|
font-weight="700"
|
||||||
|
fill="white"
|
||||||
|
>
|
||||||
|
{step() === 1 ? "14m" : "5m"}
|
||||||
|
</text>
|
||||||
|
<rect
|
||||||
|
x="6"
|
||||||
|
y="34"
|
||||||
|
width="28"
|
||||||
|
height="2"
|
||||||
|
rx="1"
|
||||||
|
fill="white"
|
||||||
|
opacity="0.15"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="6"
|
||||||
|
y="34"
|
||||||
|
width={step() === 1 ? 2 : 19}
|
||||||
|
height="2"
|
||||||
|
rx="1"
|
||||||
|
fill={CALENDAR}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<Show when={onAir()}>
|
||||||
|
<rect
|
||||||
|
x="1"
|
||||||
|
y="3"
|
||||||
|
width="38"
|
||||||
|
height="22"
|
||||||
|
rx="5"
|
||||||
|
fill={CALENDAR}
|
||||||
|
opacity="0.18"
|
||||||
|
/>
|
||||||
|
<circle cx="4.7" cy="14.5" r="2.1" fill={CALENDAR} />
|
||||||
|
<text
|
||||||
|
x="22.6"
|
||||||
|
y="17"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-size="7.8"
|
||||||
|
font-weight="700"
|
||||||
|
fill={CALENDAR}
|
||||||
|
>
|
||||||
|
ON AIR
|
||||||
|
</text>
|
||||||
|
<rect
|
||||||
|
x="1"
|
||||||
|
y="31"
|
||||||
|
width="38"
|
||||||
|
height="5"
|
||||||
|
rx="2.5"
|
||||||
|
fill={CALENDAR}
|
||||||
|
opacity="0.7"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="1"
|
||||||
|
y="31"
|
||||||
|
width={step() === 4 ? 25 : 8}
|
||||||
|
height="5"
|
||||||
|
rx="2.5"
|
||||||
|
fill="#333"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x={step() === 4 ? 25 : 8}
|
||||||
|
y="29"
|
||||||
|
width="2.6"
|
||||||
|
height="9"
|
||||||
|
rx="1.3"
|
||||||
|
fill="#ededed"
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full max-w-[360px]">
|
||||||
|
<ModuleCard>
|
||||||
|
<div class="space-y-2.5 text-left text-white">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] text-white/55">Calendar</p>
|
||||||
|
<p class="text-[14px] font-semibold">Thu, Nov 12</p>
|
||||||
|
</div>
|
||||||
|
<span class="ml-auto rounded-lg bg-white/[0.08] p-[7px] text-white/55">
|
||||||
|
<Glyph name="plus" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex h-[60px] gap-[3px]">
|
||||||
|
<For each={DAYS}>
|
||||||
|
{(day) => (
|
||||||
|
<div
|
||||||
|
class="flex min-w-0 flex-1 flex-col items-center justify-center gap-[5px] rounded-[10px]"
|
||||||
|
style={{
|
||||||
|
background: day.day === 12 ? CALENDAR : "transparent",
|
||||||
|
color: day.day === 12 ? "#0d0d0f" : "white",
|
||||||
|
opacity: day.day < 12 ? 0.6 : 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="text-[9px] opacity-60">{day.letter}</span>
|
||||||
|
<span
|
||||||
|
class="text-[13px]"
|
||||||
|
style={{ "font-weight": day.day === 12 ? 700 : 500 }}
|
||||||
|
>
|
||||||
|
{day.day}
|
||||||
|
</span>
|
||||||
|
<span class="flex h-1 gap-0.5">
|
||||||
|
<For each={Array.from({ length: day.dots })}>
|
||||||
|
{() => (
|
||||||
|
<span
|
||||||
|
class="h-1 w-1 rounded-full"
|
||||||
|
style={{ background: CALENDAR }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<For each={EVENTS}>
|
||||||
|
{(event) => (
|
||||||
|
<div class="flex items-center gap-2.5 rounded-[10px] bg-white/[0.035] p-2.5">
|
||||||
|
<div class="w-[40px] shrink-0 text-right font-mono text-[10px]">
|
||||||
|
<p>{event.start}</p>
|
||||||
|
<p class="mt-[3px] text-white/55">{event.end}</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="w-[3px] self-stretch rounded-sm"
|
||||||
|
style={{ background: event.color }}
|
||||||
|
/>
|
||||||
|
<div class="min-w-0 flex-1 space-y-1">
|
||||||
|
<p class="text-[12px] leading-snug font-semibold">
|
||||||
|
{event.title}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-[10px] font-medium"
|
||||||
|
style={{ color: event.color }}
|
||||||
|
>
|
||||||
|
{event.calendar}
|
||||||
|
</p>
|
||||||
|
<p class="text-[10px] text-white/55">{event.location}</p>
|
||||||
|
</div>
|
||||||
|
<Show when={event.meeting}>
|
||||||
|
<span
|
||||||
|
aria-label="Join meeting"
|
||||||
|
class="rounded-lg bg-white/[0.08] p-2 text-white/65"
|
||||||
|
>
|
||||||
|
<Glyph name="video" />
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</ModuleCard>
|
||||||
|
</div>
|
||||||
|
<p class="min-h-4 text-center text-[11px] text-white/55">
|
||||||
|
{step() === 0
|
||||||
|
? "Today's date and calendar events"
|
||||||
|
: onAir()
|
||||||
|
? "Meeting in progress"
|
||||||
|
: "Countdown to your next meeting"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -59,9 +59,9 @@ export default function NookLanding() {
|
|||||||
<PageHead
|
<PageHead
|
||||||
title="Agent control for your Mac"
|
title="Agent control for your Mac"
|
||||||
ogTitle="The Nook — agent control for your Mac"
|
ogTitle="The Nook — agent control for your Mac"
|
||||||
description="The Nook — a native macOS utility for coding-agent orchestration, fan and thermal control."
|
description="Monitor coding agents, respond to permission requests, and manage your Mac\'s cooling, battery, and audio with The Nook."
|
||||||
ogImage={`https://${site().domain}/nook/og.png`}
|
ogImage={`https://${site().domain}/nook/og.png`}
|
||||||
ogDescription="A one-time-purchase macOS app for coding-agent orchestration, fan control, and thermal insight."
|
ogDescription="A macOS app for coding agents, fan control, battery charging, and per-app audio. Includes a 14-day trial and a one-time license."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SubdomainHeader />
|
<SubdomainHeader />
|
||||||
@@ -96,11 +96,11 @@ export default function NookLanding() {
|
|||||||
The Nook
|
The Nook
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-text mb-4 text-4xl font-bold tracking-tight sm:text-5xl">
|
<h1 class="text-text mb-4 text-4xl font-bold tracking-tight sm:text-5xl">
|
||||||
Your Mac and Agents, under control
|
Manage your coding agents and your Mac
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-subtext0 mb-2 max-w-xl text-xl">
|
<p class="text-subtext0 mb-2 max-w-xl text-xl">
|
||||||
Agent orchestration, fan and thermal control in a beautiful native
|
Follow your agents, respond to requests, and manage cooling,
|
||||||
UI.
|
charging, and audio from the top of your screen.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-subtext1 mb-8 text-sm">
|
<p class="text-subtext1 mb-8 text-sm">
|
||||||
macOS 14.2+ · 14-day free trial · up to 3 devices
|
macOS 14.2+ · 14-day free trial · up to 3 devices
|
||||||
@@ -145,7 +145,7 @@ export default function NookLanding() {
|
|||||||
In action
|
In action
|
||||||
</p>
|
</p>
|
||||||
<h2 class="text-text mb-16 text-center text-3xl font-bold">
|
<h2 class="text-text mb-16 text-center text-3xl font-bold">
|
||||||
Built for the way you actually run agents
|
What you can do with The Nook
|
||||||
</h2>
|
</h2>
|
||||||
<FeatureBreakdowns />
|
<FeatureBreakdowns />
|
||||||
</div>
|
</div>
|
||||||
@@ -164,10 +164,9 @@ export default function NookLanding() {
|
|||||||
Try it free for 14 days
|
Try it free for 14 days
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-subtext0 mx-auto mb-10 max-w-2xl leading-relaxed">
|
<p class="text-subtext0 mx-auto mb-10 max-w-2xl leading-relaxed">
|
||||||
Orchestrate your coding agents, take control of your fans and
|
The trial includes every module. After 14 days, a one-time
|
||||||
battery, give every app its own volume, capture your screen, and
|
purchase lets you keep using The Nook on up to three of your Macs.
|
||||||
keep your day in the notch. When the trial ends, unlock everything
|
There is no subscription.
|
||||||
with a single one-time payment — no subscription, ever.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col items-center justify-center gap-4 sm:flex-row sm:space-x-4">
|
<div class="flex flex-col items-center justify-center gap-4 sm:flex-row sm:space-x-4">
|
||||||
<Button
|
<Button
|
||||||
@@ -187,7 +186,7 @@ export default function NookLanding() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-subtext1 mt-6 text-xs">
|
<p class="text-subtext1 mt-6 text-xs">
|
||||||
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.
|
your own Macs.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,8 +228,7 @@ export default function NookLanding() {
|
|||||||
</form>
|
</form>
|
||||||
<Show when={resendSent()}>
|
<Show when={resendSent()}>
|
||||||
<p class="text-subtext1 mt-4 text-center text-sm">
|
<p class="text-subtext1 mt-4 text-center text-sm">
|
||||||
Check your inbox — if a license is registered to that email, your
|
If a license is registered to that email, we'll send you the key.
|
||||||
key is on its way.
|
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={resendError()}>
|
<Show when={resendError()}>
|
||||||
|
|||||||
78
src/routes/nook/landing.test.ts
Normal file
78
src/routes/nook/landing.test.ts
Normal file
@@ -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*</);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the power circuit and current battery controls", () => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user