forked from Mike/freno-dev
[gpt-6.1-sol] A blanket restriction on remote claims hid the working setup flow for Tailscale and authorized known hosts and left the opening grid unbalanced. Restore the host-labelled session card and setup demo, and make the copy safeguards reflect the supported scope rather than deny the feature. Tests: 4 restoration checks fail without the fix; 38 Nook tests pass; bun run build; Playwright at 1440/768/375px in both themes NOOK-1038
599 lines
22 KiB
TypeScript
599 lines
22 KiB
TypeScript
import { For, Show, createSignal, type JSX } from "solid-js";
|
||
import {
|
||
ModuleCard,
|
||
FakeCursor,
|
||
IslandPill,
|
||
AgentDotGrid,
|
||
STATUS,
|
||
ACCENT,
|
||
CALM,
|
||
CARD_STROKE
|
||
} from "./IslandMock";
|
||
import { useDemoLoop } from "./useDemoLoop";
|
||
import { Glyph, WingCapsule, SessionRowMock, CODEX, type GlyphName } from "./ModuleMocks";
|
||
|
||
/**
|
||
* Live demos for the breakdowns added with the 0.6–0.10 feature set:
|
||
* terminal replies, remote setup, 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, update and run the tests.";
|
||
|
||
export function TerminalReplyDemo() {
|
||
const [typed, setTyped] = createSignal(0);
|
||
const [phase, setPhase] = createSignal<
|
||
"asking" | "typing" | "sent" | "working" | "jump"
|
||
>("asking");
|
||
|
||
useDemoLoop(10000, [
|
||
[0, () => (setTyped(0), setPhase("asking"))],
|
||
[900, () => setPhase("typing")],
|
||
...REPLY.split("").map(
|
||
(_, i) => [1000 + i * 70, () => setTyped(i + 1)] as [number, () => void]
|
||
),
|
||
[2900, () => setPhase("sent")],
|
||
[3300, () => setPhase("working")],
|
||
[6600, () => setPhase("jump")]
|
||
]);
|
||
|
||
const delivered = () => phase() === "working" || phase() === "jump";
|
||
const cursorPhase = () =>
|
||
phase() === "sent" || phase() === "jump"
|
||
? ("press" as const)
|
||
: phase() === "typing" || phase() === "asking"
|
||
? ("idle" as const)
|
||
: ("live" as const);
|
||
const cursorAt = () =>
|
||
phase() === "jump" ? { x: 88, y: 14 } : { x: 88, y: 80 };
|
||
|
||
return (
|
||
<div class="mx-auto flex w-full max-w-[340px] flex-col gap-3">
|
||
<ModuleCard class="relative">
|
||
<div class="space-y-2 text-left">
|
||
<div class="flex items-center gap-2">
|
||
<span
|
||
class="inline-block h-[7px] w-[7px] rounded-full transition-colors duration-500"
|
||
style={{
|
||
background: delivered() ? STATUS.running : STATUS.ready
|
||
}}
|
||
/>
|
||
<span class="text-[13px] font-semibold text-white">the-nook</span>
|
||
<span class="font-mono text-[10px] text-white/40">main</span>
|
||
<span
|
||
class="ml-auto rounded-md px-2 py-0.5 text-[10px] font-semibold text-white/80 transition-colors duration-300"
|
||
style={{
|
||
background:
|
||
phase() === "jump" ? ACCENT : "rgba(255,255,255,0.08)"
|
||
}}
|
||
>
|
||
Jump ↗
|
||
</span>
|
||
</div>
|
||
<p class="text-[11px] leading-snug text-white/60">
|
||
{delivered()
|
||
? "Running the test suite…"
|
||
: "Done. Want me to update the tests too?"}
|
||
</p>
|
||
<div
|
||
class="flex items-center gap-2 rounded-lg px-2 py-1.5"
|
||
style={{ background: "rgba(255,255,255,0.06)" }}
|
||
>
|
||
<span class="min-h-[14px] flex-1 truncate font-mono text-[11px] text-white/85">
|
||
{delivered() ? "" : REPLY.slice(0, typed())}
|
||
<Show when={phase() === "typing"}>
|
||
<span class="nook-caret">▍</span>
|
||
</Show>
|
||
<Show when={!typed() || delivered()}>
|
||
<span class="text-white/30">Reply…</span>
|
||
</Show>
|
||
</span>
|
||
<span
|
||
class="rounded-md px-2 py-0.5 text-[10px] font-semibold text-white transition-transform duration-200"
|
||
style={{
|
||
background: ACCENT,
|
||
transform: phase() === "sent" ? "scale(0.9)" : "scale(1)"
|
||
}}
|
||
>
|
||
Send
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<FakeCursor phase={cursorPhase()} at={cursorAt()} />
|
||
</ModuleCard>
|
||
|
||
{/* the terminal the session actually runs in */}
|
||
<div
|
||
class="overflow-hidden rounded-lg text-left transition-shadow duration-500"
|
||
style={{
|
||
background: "#16161a",
|
||
"box-shadow":
|
||
phase() === "jump"
|
||
? `0 0 0 2px ${ACCENT}, 0 8px 30px rgba(72,151,178,0.35)`
|
||
: `inset 0 0 0 1px ${CARD_STROKE}`
|
||
}}
|
||
>
|
||
<div class="flex items-center gap-1.5 px-2.5 py-1.5">
|
||
<span class="h-2 w-2 rounded-full bg-[#ff5f57]" />
|
||
<span class="h-2 w-2 rounded-full bg-[#febc2e]" />
|
||
<span class="h-2 w-2 rounded-full bg-[#28c840]" />
|
||
<span class="ml-2 font-mono text-[10px] text-white/40">
|
||
tmux · the-nook:2
|
||
</span>
|
||
<span
|
||
class="ml-auto text-[9px] font-semibold tracking-wide uppercase transition-opacity duration-300"
|
||
style={{ color: ACCENT, opacity: phase() === "jump" ? 1 : 0 }}
|
||
>
|
||
focused
|
||
</span>
|
||
</div>
|
||
<div class="space-y-0.5 px-2.5 pb-2.5 font-mono text-[10.5px] leading-relaxed">
|
||
<p class="text-white/40">⏺ Updated 3 files</p>
|
||
<p class="text-white/70">
|
||
>{" "}
|
||
<span
|
||
class="transition-opacity duration-300"
|
||
style={{ opacity: delivered() ? 1 : 0 }}
|
||
>
|
||
{REPLY}
|
||
</span>
|
||
</p>
|
||
<p
|
||
class="transition-opacity duration-500"
|
||
style={{ color: CALM, opacity: delivered() ? 1 : 0 }}
|
||
>
|
||
⏺ Bash(swift test)
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** RemoteMirror's progress steps, in order. */
|
||
const SETUP_STEPS = [
|
||
"connecting",
|
||
"pushing the hook binary",
|
||
"configuring agents",
|
||
"checking the host can reach this Mac"
|
||
];
|
||
|
||
export function RemoteHostDemo() {
|
||
/* -1 not set up · 0…3 setting up · 4 live */
|
||
const [step, setStep] = createSignal(-1);
|
||
const [pressed, setPressed] = createSignal(false);
|
||
useDemoLoop(11000, [
|
||
[0, () => (setStep(-1), setPressed(false))],
|
||
[1400, () => setPressed(true)],
|
||
[1700, () => (setPressed(false), setStep(0))],
|
||
[2700, () => setStep(1)],
|
||
[3700, () => setStep(2)],
|
||
[4700, () => setStep(3)],
|
||
[5800, () => setStep(4)]
|
||
]);
|
||
const live = () => step() === 4;
|
||
const working = () => step() >= 0 && step() < 4;
|
||
const chip = () =>
|
||
live()
|
||
? { color: STATUS.ready, label: "Live" }
|
||
: working()
|
||
? { color: "rgba(255,255,255,0.55)", label: "Setting up" }
|
||
: { color: "rgba(255,255,255,0.35)", label: "Not set up" };
|
||
|
||
return (
|
||
<div class="relative mx-auto w-full max-w-[360px] space-y-3" role="img" aria-label="One-click remote setup preview for an authorized Tailscale host">
|
||
{/* the host's card in Settings ▸ Agents ▸ Remote hosts */}
|
||
<ModuleCard>
|
||
<div class="space-y-2 text-left">
|
||
<div class="flex items-center gap-2.5">
|
||
<div class="min-w-0 flex-1">
|
||
<p class="text-[13px] font-semibold text-white">gpu-rig</p>
|
||
<p class="truncate text-[11px] text-white/45">tailnet peer</p>
|
||
</div>
|
||
<Show when={working()}>
|
||
<span class="h-3 w-3 animate-spin rounded-full border-[1.5px] border-white/25 border-t-white/80" />
|
||
</Show>
|
||
<span class="flex items-center gap-1.5 text-[10.5px] font-medium text-white/85">
|
||
<span
|
||
class="h-[7px] w-[7px] rounded-full transition-colors duration-300"
|
||
style={{ background: chip().color }}
|
||
/>
|
||
{chip().label}
|
||
</span>
|
||
<Show
|
||
when={live()}
|
||
fallback={
|
||
<span
|
||
class="rounded-md px-2.5 py-1 text-[11px] font-semibold text-white transition-all duration-200"
|
||
style={{
|
||
background: ACCENT,
|
||
opacity: working() ? 0 : 1,
|
||
transform: pressed() ? "scale(0.92)" : "scale(1)"
|
||
}}
|
||
>
|
||
Set up
|
||
</span>
|
||
}
|
||
>
|
||
<Glyph name="refresh" size={11} class="text-white/50" />
|
||
<span class="rounded-md bg-white/10 px-2 py-1 text-[11px] font-semibold text-[#ff6961]">
|
||
Remove
|
||
</span>
|
||
</Show>
|
||
</div>
|
||
<p class="min-h-8 text-[11px] text-white/50">
|
||
{working() ? SETUP_STEPS[step()] : " "}
|
||
</p>
|
||
</div>
|
||
</ModuleCard>
|
||
|
||
{/* The host name stays visible in the session badge. */}
|
||
<div
|
||
class="transition-all duration-500"
|
||
style={{
|
||
opacity: live() ? 1 : 0,
|
||
transform: live() ? "translateY(0)" : "translateY(6px)"
|
||
}}
|
||
>
|
||
<SessionRowMock
|
||
title="trainer · fp16"
|
||
prompt="benchmark fp16"
|
||
state="Running python bench.py…"
|
||
status="running"
|
||
agent={CODEX}
|
||
host="gpu-rig"
|
||
age="now"
|
||
/>
|
||
</div>
|
||
<FakeCursor
|
||
phase={pressed() ? "press" : step() === -1 ? "idle" : "live"}
|
||
at={{ x: 88, y: 16 }}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── Capture: the module, and the Wings while a recording runs ────── */
|
||
|
||
/** CapturePalette.recording and the NookPalette paper/ink pair. */
|
||
const RECORDING_RED = "rgb(242,69,69)";
|
||
const PAPER = "#f1ead9";
|
||
const INK = "#0d0d0f";
|
||
|
||
function CaptureChip(props: {
|
||
glyph: GlyphName;
|
||
label: string;
|
||
menu?: boolean;
|
||
pressed?: boolean;
|
||
}) {
|
||
return (
|
||
<span
|
||
class="flex flex-1 items-center justify-center gap-1 rounded-full py-1.5 text-[11px] font-medium whitespace-nowrap text-white transition-transform duration-150"
|
||
style={{
|
||
background: props.pressed ? "rgba(255,255,255,0.16)" : "rgba(255,255,255,0.08)",
|
||
transform: props.pressed ? "scale(0.94)" : "scale(1)"
|
||
}}
|
||
>
|
||
<Glyph name={props.glyph} size={11} />
|
||
{props.label}
|
||
<Show when={props.menu}>
|
||
<Glyph name="chevron" size={8} class="opacity-50" />
|
||
</Show>
|
||
</span>
|
||
);
|
||
}
|
||
|
||
/** CaptureAudioToggles: paper capsule when on, faint when off. */
|
||
function AudioToggles(props: { system: boolean; mic: boolean }) {
|
||
const toggle = (on: boolean, glyph: GlyphName, label: string) => (
|
||
<span
|
||
class="flex items-center gap-1 rounded-full px-2 py-1 text-[10.5px] font-medium whitespace-nowrap transition-colors duration-300"
|
||
style={{
|
||
background: on ? PAPER : "rgba(255,255,255,0.08)",
|
||
color: on ? INK : "rgba(255,255,255,0.7)"
|
||
}}
|
||
>
|
||
<Glyph name={glyph} size={10} />
|
||
{label}
|
||
</span>
|
||
);
|
||
return (
|
||
<div class="flex gap-1.5">
|
||
{toggle(props.system, props.system ? "speaker" : "speakerOff", "System audio")}
|
||
{toggle(props.mic, props.mic ? "mic" : "micOff", "Microphone")}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function RowLabel(props: { glyph: GlyphName; label: string }) {
|
||
return (
|
||
<p class="flex items-center gap-1 text-[11px] font-semibold text-white/50">
|
||
<Glyph name={props.glyph} size={11} />
|
||
{props.label}
|
||
</p>
|
||
);
|
||
}
|
||
|
||
export function CaptureDemo() {
|
||
const [phase, setPhase] = createSignal<"idle" | "press" | "rec" | "paused" | "saved">("idle");
|
||
const [secs, setSecs] = createSignal(0);
|
||
const [mic, setMic] = createSignal(true);
|
||
const [wingPress, setWingPress] = createSignal<"" | "mic" | "pause" | "stop">("");
|
||
|
||
useDemoLoop(12400, [
|
||
[0, () => (setPhase("idle"), setSecs(0), setMic(true), setWingPress(""))],
|
||
[1500, () => setPhase("press")],
|
||
[1800, () => setPhase("rec")],
|
||
...[1, 2, 3, 4, 5].map((s) => [1800 + s * 1000, () => setSecs(s)] as [number, () => void]),
|
||
[4300, () => setWingPress("mic")],
|
||
[4550, () => (setMic(false), setWingPress(""))],
|
||
[7100, () => setWingPress("pause")],
|
||
[7350, () => (setPhase("paused"), setWingPress(""))],
|
||
[9000, () => setWingPress("stop")],
|
||
[9250, () => (setPhase("saved"), setWingPress(""))]
|
||
]);
|
||
|
||
const recording = () => phase() === "rec" || phase() === "paused";
|
||
const paused = () => phase() === "paused";
|
||
/* Island row is drawn at half size; Wing controls are as tall as it. */
|
||
const wingWidth = 2 * 60 + 12;
|
||
const cursor = () => {
|
||
switch (wingPress()) {
|
||
case "mic":
|
||
return { phase: "press" as const, at: { x: 21.8, y: 7 } };
|
||
case "pause":
|
||
return { phase: "press" as const, at: { x: 78.2, y: 7 } };
|
||
case "stop":
|
||
return { phase: "press" as const, at: { x: 86.5, y: 7 } };
|
||
}
|
||
if (phase() === "idle" || phase() === "press")
|
||
return { phase: phase() === "press" ? ("press" as const) : ("idle" as const), at: { x: 50, y: 58 } };
|
||
if (phase() === "saved") return { phase: "live" as const, at: { x: 86.5, y: 7 } };
|
||
return { phase: "idle" as const, at: { x: 21.8, y: 30 } };
|
||
};
|
||
|
||
return (
|
||
<div class="relative mx-auto w-full max-w-[360px]">
|
||
{/* the Compact island, flanked by the Wings while recording */}
|
||
<div class="relative h-[30px]">
|
||
<div
|
||
class="absolute top-0 left-1/2 flex items-start"
|
||
style={{ transform: "translateX(-50%) scale(0.5)", "transform-origin": "top center" }}
|
||
>
|
||
<div
|
||
class="flex justify-end overflow-hidden transition-all duration-500"
|
||
style={{ width: recording() ? `${wingWidth}px` : "0px", opacity: recording() ? 1 : 0 }}
|
||
>
|
||
<WingCapsule
|
||
height={60}
|
||
controls={[
|
||
{ glyph: "speaker" },
|
||
{ glyph: mic() ? "mic" : "micOff", on: mic(), pressed: wingPress() === "mic" }
|
||
]}
|
||
/>
|
||
<span class="w-3 shrink-0" />
|
||
</div>
|
||
<IslandPill
|
||
campfire="running"
|
||
right={<AgentDotGrid states={["running", "ready", "running"]} />}
|
||
/>
|
||
<div
|
||
class="flex overflow-hidden transition-all duration-500"
|
||
style={{ width: recording() ? `${wingWidth}px` : "0px", opacity: recording() ? 1 : 0 }}
|
||
>
|
||
<span class="w-3 shrink-0" />
|
||
<WingCapsule
|
||
height={60}
|
||
controls={[
|
||
{ glyph: paused() ? "play" : "pause", pressed: wingPress() === "pause" },
|
||
{ glyph: "stop", pressed: wingPress() === "stop" }
|
||
]}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* the Capture module */}
|
||
<ModuleCard class="mt-5">
|
||
<div class="min-h-[150px] text-left">
|
||
<Show
|
||
when={recording()}
|
||
fallback={
|
||
<div class="space-y-2.5">
|
||
<div class="space-y-1.5">
|
||
<RowLabel glyph="viewfinder" label="Screenshot" />
|
||
<div class="flex gap-1.5">
|
||
<CaptureChip glyph="dashed" label="Area" />
|
||
<CaptureChip glyph="display" label="Screen" />
|
||
<CaptureChip glyph="window" label="App" menu />
|
||
</div>
|
||
</div>
|
||
<div class="space-y-1.5">
|
||
<RowLabel glyph="record" label="Record" />
|
||
<div class="flex gap-1.5">
|
||
<CaptureChip glyph="dashed" label="Area" />
|
||
<CaptureChip glyph="display" label="Screen" pressed={phase() === "press"} />
|
||
<CaptureChip glyph="window" label="App" menu />
|
||
</div>
|
||
<AudioToggles system mic={mic()} />
|
||
</div>
|
||
<Show when={phase() === "saved"}>
|
||
<div class="flex items-center gap-1.5 text-[10.5px]">
|
||
<Glyph name="film" size={11} class="text-white/50" />
|
||
<span class="min-w-0 flex-1 truncate text-white/85">
|
||
Screen Recording 2026-10-13 at 9.41.12.mov
|
||
</span>
|
||
<span class="shrink-0 text-white/50">Show in Finder</span>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
}
|
||
>
|
||
<div class="space-y-2.5">
|
||
<div class="flex items-center gap-2">
|
||
<span
|
||
class="h-2 w-2 rounded-full transition-colors"
|
||
style={{ background: paused() ? "rgba(255,255,255,0.4)" : RECORDING_RED }}
|
||
/>
|
||
<span
|
||
class="font-mono text-[22px] font-semibold transition-colors"
|
||
style={{ color: paused() ? "rgba(255,255,255,0.5)" : "white" }}
|
||
>
|
||
0:0{secs()}
|
||
</span>
|
||
<span class="ml-auto font-mono text-[10px] font-medium text-white/50">
|
||
{paused() ? "Paused" : "Recording"}
|
||
</span>
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<CaptureChip glyph={paused() ? "play" : "pause"} label={paused() ? "Resume" : "Pause"} />
|
||
<CaptureChip glyph="stop" label="Stop" />
|
||
</div>
|
||
<AudioToggles system mic={mic()} />
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</ModuleCard>
|
||
|
||
{/* cursor plotted on a fixed 360 px frame centered over the demo */}
|
||
<div class="pointer-events-none absolute inset-y-0 left-1/2 w-[360px] -translate-x-1/2">
|
||
<FakeCursor phase={cursor().phase} at={cursor().at} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── Floating actions bloom around the open island ────────────────── */
|
||
|
||
/* Tiny monochrome glyphs for the action kinds the app offers. */
|
||
const GLYPH: Record<string, () => JSX.Element> = {
|
||
leaf: () => (
|
||
<path d="M3 13C3 7 7 3 13 3c0 6-4 10-10 10Zm0 0 6-6" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" />
|
||
),
|
||
bolt: () => <path d="M9 1 3 9h4l-1 6 6-8H8l1-6Z" fill="currentColor" />,
|
||
timer: () => (
|
||
<g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
|
||
<circle cx="8" cy="9" r="5.5" />
|
||
<path d="M8 9V6M6.5 1.5h3" />
|
||
</g>
|
||
),
|
||
speaker: () => (
|
||
<g fill="currentColor">
|
||
<path d="M2 6h3l4-3v10l-4-3H2Z" />
|
||
<path d="M11 5.5a3.5 3.5 0 0 1 0 5" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" />
|
||
</g>
|
||
),
|
||
shell: () => (
|
||
<path d="m3 5 3 3-3 3m5 0h5" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
||
),
|
||
stack: () => (
|
||
<g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round">
|
||
<path d="m8 2 6 3-6 3-6-3 6-3Z" />
|
||
<path d="m2 8.5 6 3 6-3M2 11.5l6 3 6-3" />
|
||
</g>
|
||
),
|
||
link: () => (
|
||
<path d="M7 9a3 3 0 0 0 4 0l2-2a3 3 0 0 0-4-4l-1 1m1 3a3 3 0 0 0-4 0l-2 2a3 3 0 0 0 4 4l1-1" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" />
|
||
)
|
||
};
|
||
|
||
interface FloatAction {
|
||
glyph: keyof typeof GLYPH;
|
||
label: string;
|
||
x: number;
|
||
y: number;
|
||
}
|
||
|
||
/* Positions mirror the app's slots: Left ·1/·2, Right ·1/·2, Bottom ·1–3. */
|
||
const ACTIONS: FloatAction[] = [
|
||
{ glyph: "leaf", label: "Low Power Mode on", x: -128, y: -18 },
|
||
{ glyph: "bolt", label: "Top-up to 100%", x: -128, y: 26 },
|
||
{ glyph: "speaker", label: "Output → Headphones", x: 128, y: -18 },
|
||
{ glyph: "stack", label: "Shortcut: Focus Time", x: 128, y: 26 },
|
||
{ glyph: "timer", label: "Timer · 25 min", x: -50, y: 78 },
|
||
{ glyph: "shell", label: "$ make deploy", x: 0, y: 78 },
|
||
{ glyph: "link", label: "Opened dashboard", x: 50, y: 78 }
|
||
];
|
||
|
||
export function FloatingActionsDemo() {
|
||
const [open, setOpen] = createSignal(false);
|
||
const [pressed, setPressed] = createSignal(-1);
|
||
const [lit, setLit] = createSignal<number[]>([]);
|
||
|
||
useDemoLoop(9600, [
|
||
[0, () => (setOpen(false), setPressed(-1), setLit([]))],
|
||
[400, () => setOpen(true)],
|
||
[2200, () => setPressed(0)],
|
||
[2500, () => setLit([0])],
|
||
[4000, () => setPressed(4)],
|
||
[4300, () => setLit([0, 4])],
|
||
[5800, () => setPressed(2)],
|
||
[6100, () => setLit([0, 4, 2])],
|
||
[8800, () => (setOpen(false), setPressed(-1))]
|
||
]);
|
||
|
||
const lastLit = () => lit()[lit().length - 1];
|
||
|
||
return (
|
||
<div class="relative mx-auto flex h-[240px] w-full max-w-[340px] items-start justify-center pt-6">
|
||
{/* the expanded island */}
|
||
<div
|
||
class="relative z-10 rounded-b-[18px] bg-black transition-all duration-500"
|
||
style={{
|
||
width: open() ? "190px" : "120px",
|
||
height: open() ? "96px" : "26px",
|
||
"transition-timing-function": "cubic-bezier(0.34,1.56,0.64,1)",
|
||
"box-shadow": `inset 0 0 0 1px ${CARD_STROKE}`
|
||
}}
|
||
>
|
||
<div
|
||
class="space-y-1.5 p-3 transition-opacity duration-300"
|
||
style={{ opacity: open() ? 1 : 0 }}
|
||
>
|
||
<div class="h-1.5 w-2/3 rounded bg-white/20" />
|
||
<div class="h-1.5 w-1/2 rounded bg-white/10" />
|
||
<div class="h-1.5 w-3/4 rounded" style={{ background: `${CALM}66` }} />
|
||
</div>
|
||
<For each={ACTIONS}>
|
||
{(a, i) => (
|
||
<span
|
||
class="absolute flex h-8 w-8 items-center justify-center rounded-full transition-all"
|
||
style={{
|
||
left: "50%",
|
||
top: "50%",
|
||
transform: open()
|
||
? `translate(calc(-50% + ${a.x}px), calc(-50% + ${a.y - 6}px)) scale(${pressed() === i() ? 0.82 : 1})`
|
||
: "translate(-50%, -50%) scale(0.2)",
|
||
opacity: open() ? 1 : 0,
|
||
background: lit().includes(i()) ? ACCENT : "#1c1c20",
|
||
color: lit().includes(i()) ? "white" : "rgba(255,255,255,0.7)",
|
||
"box-shadow": `inset 0 0 0 1px rgba(255,255,255,0.12), 0 4px 14px rgba(0,0,0,0.5)`,
|
||
"transition-duration": "520ms",
|
||
"transition-delay": open() ? `${i() * 45}ms` : `${(6 - i()) * 25}ms`,
|
||
"transition-timing-function": "cubic-bezier(0.34,1.56,0.64,1)"
|
||
}}
|
||
>
|
||
<svg width="15" height="15" viewBox="0 0 16 16">
|
||
{GLYPH[a.glyph]()}
|
||
</svg>
|
||
</span>
|
||
)}
|
||
</For>
|
||
</div>
|
||
|
||
{/* what the last press did */}
|
||
<div
|
||
class="absolute bottom-1 left-1/2 -translate-x-1/2 rounded-full px-3 py-1 text-[10px] font-semibold whitespace-nowrap text-white transition-all duration-300"
|
||
style={{
|
||
background: "rgba(72,151,178,0.22)",
|
||
opacity: lastLit() !== undefined && open() ? 1 : 0
|
||
}}
|
||
>
|
||
{lastLit() !== undefined ? ACTIONS[lastLit()].label : " "}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|