Spaces:
Running
Running
| // The macOS desktop scene: menu bar, dock, draggable app windows | |
| // and their simulated contents (what Puck patrols). | |
| import * as React from "react"; | |
| import type { EffectId } from "../engine"; | |
| import { type Pos, useDrag } from "../lib/useDrag"; | |
| export interface WinState { | |
| claude: "running" | "done"; | |
| build: "running" | "done" | "fail"; | |
| mail: string[]; | |
| discordNoise: boolean; | |
| discordMention: boolean; | |
| calSoon: boolean; | |
| } | |
| export const initialWinState = (): WinState => ({ | |
| claude: "running", | |
| build: "running", | |
| mail: ["hf"], | |
| discordNoise: true, | |
| discordMention: false, | |
| calSoon: false, | |
| }); | |
| /** How an engine effect mutates the simulated windows. Pure; colocated with WinState. */ | |
| export function applyEffect(w: WinState, eff: EffectId): WinState { | |
| switch (eff) { | |
| case "claude_done": | |
| return { ...w, claude: "done" }; | |
| case "build_done": | |
| return { ...w, build: "done" }; | |
| case "build_fail": | |
| return { ...w, build: "fail" }; | |
| case "mail_unread": | |
| return { ...w, mail: Array.from(new Set([...w.mail, w.mail.includes("hf") ? "fin" : "hf"])) }; | |
| case "discord_mention": | |
| return { ...w, discordMention: true }; | |
| case "discord_noise": | |
| return { ...w, discordNoise: true }; | |
| case "calendar_soon": | |
| return { ...w, calSoon: true }; | |
| case "none": | |
| return w; | |
| } | |
| } | |
| // ---- menu bar --------------------------------------------------------------- | |
| export function MenuBar({ | |
| moodLabel, | |
| puckActive, | |
| badge, | |
| onPuck, | |
| clock, | |
| }: { | |
| moodLabel: string; | |
| puckActive: boolean; | |
| badge: number; | |
| onPuck: () => void; | |
| clock: string; | |
| }) { | |
| return ( | |
| <div className="menubar"> | |
| <span className="mb-apple"></span> | |
| <span className="mb-app">Finder</span> | |
| <span className="mb-menu">File</span> | |
| <span className="mb-menu">Edit</span> | |
| <span className="mb-menu">View</span> | |
| <span className="mb-menu">Go</span> | |
| <span className="mb-menu">Window</span> | |
| <div className="mb-right"> | |
| <button type="button" className={`mb-puck${puckActive ? " active" : ""}`} onClick={onPuck}> | |
| <span className="mb-puck-dot" /> | |
| <span className="mb-puck-mood">Puck · {moodLabel}</span> | |
| {badge > 0 && <span className="badge">{badge}</span>} | |
| </button> | |
| <span className="mb-ico"></span> | |
| <span className="mb-ico"></span> | |
| <span className="mb-ico"></span> | |
| <span className="mb-clock">{clock}</span> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // ---- dock --------------------------------------------------------------------- | |
| export interface DockApp { | |
| id: string; | |
| name: string; | |
| glyph: string; | |
| running?: boolean; | |
| badge?: number; | |
| tint?: string; | |
| } | |
| export function Dock({ apps, onOpen }: { apps: DockApp[]; onOpen: (id: string) => void }) { | |
| return ( | |
| <div className="dock"> | |
| {apps.map((a) => ( | |
| <div | |
| key={a.id} | |
| className={`dock-ico${a.running ? " run" : ""}`} | |
| title={a.name} | |
| onClick={() => onOpen(a.id)} | |
| style={a.tint ? { color: a.tint } : undefined} | |
| > | |
| <span>{a.glyph}</span> | |
| {(a.badge ?? 0) > 0 && <span className="dk-badge">{a.badge}</span>} | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| // ---- generic draggable window ------------------------------------------------- | |
| function AppWindow({ | |
| id, | |
| pos, | |
| onPos, | |
| w, | |
| h, | |
| title, | |
| icon, | |
| sub, | |
| attn, | |
| dimmed, | |
| onFocus, | |
| z, | |
| children, | |
| }: { | |
| id: string; | |
| pos: Pos; | |
| onPos: (id: string, p: Pos) => void; | |
| w: number; | |
| h: number; | |
| title: string; | |
| icon: string; | |
| sub?: string; | |
| attn: boolean; | |
| dimmed: boolean; | |
| onFocus: (id: string) => void; | |
| z: number; | |
| children: React.ReactNode; | |
| }) { | |
| const drag = useDrag(pos, (p) => onPos(id, p)); | |
| return ( | |
| <div | |
| id={`win-${id}`} | |
| className={`win${attn ? " attn" : ""}${dimmed ? " dimmed" : ""}`} | |
| style={{ left: pos.x, top: pos.y, width: w, height: h, zIndex: z }} | |
| onMouseDown={() => onFocus(id)} | |
| > | |
| <div | |
| className="win-head" | |
| onMouseDown={(e) => { | |
| onFocus(id); | |
| drag(e); | |
| }} | |
| > | |
| <div className="traffic"> | |
| <i className="r" /> | |
| <i className="y" /> | |
| <i className="g" /> | |
| </div> | |
| <div className="win-title"> | |
| <span className="wt-ico">{icon}</span> | |
| {title} | |
| </div> | |
| {sub && <div className="win-sub">{sub}</div>} | |
| </div> | |
| <div className="win-body">{children}</div> | |
| </div> | |
| ); | |
| } | |
| // ---- window bodies -------------------------------------------------------- | |
| function ClaudeBody({ st }: { st: WinState }) { | |
| const done = st.claude === "done"; | |
| return ( | |
| <div> | |
| <div className="cc-line tok-dim">⏵ implement the memory garden panel</div> | |
| <div className="cc-line"> | |
| <span className="tok-fn">edit</span> src/garden/Plot.tsx <span className="tok-dim">+48 −6</span> | |
| </div> | |
| <div className="cc-line"> | |
| <span className="tok-fn">edit</span> src/garden/seed.ts <span className="tok-dim">+12 −0</span> | |
| </div> | |
| <div className="cc-line"> | |
| <span className="tok-fn">write</span> src/garden/decay.ts <span className="tok-dim">new</span> | |
| </div> | |
| <div className={`cc-task${done ? " done" : ""}`}> | |
| {done ? <span className="spin">✓</span> : <span className="spin" />} | |
| <span>{done ? "Memory Garden — completed · 7 files" : "Memory Garden — running…"}</span> | |
| <span className="pct">{done ? "done" : "····"}</span> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function TermBody({ st }: { st: WinState }) { | |
| const s = st.build; | |
| return ( | |
| <div className="term"> | |
| <div> | |
| <span className="prompt">vu@workshop</span> ~/puck $ npm run build | |
| </div> | |
| <div className="tok-dim">vite v5.3.1 building for production…</div> | |
| <div> | |
| transforming modules <span className="tok-dim">·····</span> | |
| </div> | |
| {s === "running" && ( | |
| <> | |
| <div>bundling…</div> | |
| <div className="bar"> | |
| <i style={{ width: "64%" }} /> | |
| </div> | |
| </> | |
| )} | |
| {s === "done" && ( | |
| <> | |
| <div className="ok">✓ built in 2m 14s — 0 errors</div> | |
| <div className="bar"> | |
| <i style={{ width: "100%" }} /> | |
| </div> | |
| </> | |
| )} | |
| {s === "fail" && ( | |
| <> | |
| <div className="err">✗ 3 tests failing — auth.test.ts</div> | |
| <div className="warn"> expected 200, received 401</div> | |
| </> | |
| )} | |
| </div> | |
| ); | |
| } | |
| interface ListRow { | |
| from: string; | |
| sub: string; | |
| time: string; | |
| unread?: boolean; | |
| mention?: boolean; | |
| noise?: boolean; | |
| muted?: boolean; | |
| av: string; | |
| } | |
| function MsgList({ rows }: { rows: ListRow[] }) { | |
| return ( | |
| <div className="list"> | |
| {rows.map((r) => ( | |
| <div key={r.from + r.sub} className={`list-row${r.unread || r.mention ? " unread" : ""}`}> | |
| <div className={`avatar${r.muted || (r.noise && !r.mention) ? " muted" : ""}`}>{r.av}</div> | |
| <div className="lr-main"> | |
| <div className="lr-top"> | |
| <span className="lr-from">{r.from}</span> | |
| <span className="lr-time">{r.time}</span> | |
| </div> | |
| <div className="lr-sub">{r.sub}</div> | |
| </div> | |
| <div className={`lr-dot${r.unread || r.mention ? "" : " hidden"}`} /> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| function MailBody({ st }: { st: WinState }) { | |
| return ( | |
| <MsgList | |
| rows={[ | |
| { | |
| from: "Hugging Face", | |
| sub: "Build Small — submissions close Sunday", | |
| time: "9:41", | |
| unread: st.mail.includes("hf"), | |
| av: "H", | |
| }, | |
| { | |
| from: "Fidelity", | |
| sub: "Your June statement is ready", | |
| time: "8:02", | |
| unread: st.mail.includes("fin"), | |
| av: "F", | |
| muted: true, | |
| }, | |
| { from: "Mira Okafor", sub: "re: puck demo video", time: "Yest", av: "M" }, | |
| { from: "GitHub", sub: "[puck] CI passed on main", time: "Yest", av: "G", muted: true }, | |
| ]} | |
| /> | |
| ); | |
| } | |
| function DiscordBody({ st }: { st: WinState }) { | |
| return ( | |
| <MsgList | |
| rows={[ | |
| { | |
| from: "#general", | |
| sub: st.discordNoise ? "12 new messages · gif wars again" : "quiet for now", | |
| time: "now", | |
| noise: true, | |
| av: "#", | |
| }, | |
| { | |
| from: "Mira", | |
| sub: "@you can you push the demo branch?", | |
| time: "now", | |
| mention: st.discordMention, | |
| av: "M", | |
| }, | |
| { from: "#puck-build", sub: "anyone seen the holo weights?", time: "2m", av: "P" }, | |
| { from: "#random", sub: "🐸🐸🐸", time: "5m", noise: true, av: "R" }, | |
| ]} | |
| /> | |
| ); | |
| } | |
| function CalBody({ st }: { st: WinState }) { | |
| return ( | |
| <div> | |
| <div className="cal-day">Today · Thu Jun 5</div> | |
| <div className="cal-ev" style={st.calSoon ? { borderLeftColor: "#ff5b54" } : undefined}> | |
| <div className="ce-t">Demo sync</div> | |
| <div className="ce-time">{st.calSoon ? "in 10 min · 11:00" : "11:00 – 11:30"}</div> | |
| </div> | |
| <div className="cal-ev" style={{ opacity: 0.6 }}> | |
| <div className="ce-t">Deep work — Puck UI</div> | |
| <div className="ce-time">14:00 – 16:00</div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // ---- the set of windows ----------------------------------------------------- | |
| interface WinDef { | |
| id: string; | |
| title: string; | |
| icon: string; | |
| sub: string; | |
| w: number; | |
| h: number; | |
| Body: React.ComponentType<{ st: WinState }>; | |
| } | |
| export const WIN_DEFS: WinDef[] = [ | |
| { id: "claude", title: "Claude Code", icon: "✳", sub: "Desktop 2", w: 432, h: 296, Body: ClaudeBody }, | |
| { id: "terminal", title: "workshop — build", icon: "▦", sub: "zsh", w: 404, h: 224, Body: TermBody }, | |
| { id: "mail", title: "Mail", icon: "✉", sub: "Inbox", w: 364, h: 318, Body: MailBody }, | |
| { id: "discord", title: "Discord", icon: "◍", sub: "Social Bog", w: 332, h: 236, Body: DiscordBody }, | |
| { id: "calendar", title: "Calendar", icon: "◷", sub: "", w: 252, h: 184, Body: CalBody }, | |
| ]; | |
| // memo: SimApp re-renders every sprite flight; the window scene only depends on these props | |
| export const DesktopWindows = React.memo(function DesktopWindows({ | |
| positions, | |
| onPos, | |
| onFocus, | |
| focused, | |
| attnId, | |
| winState, | |
| }: { | |
| positions: Record<string, Pos>; | |
| onPos: (id: string, p: Pos) => void; | |
| onFocus: (id: string) => void; | |
| focused: string | null; | |
| attnId: string | null; | |
| winState: WinState; | |
| }) { | |
| return ( | |
| <> | |
| {WIN_DEFS.map((d, i) => ( | |
| <AppWindow | |
| key={d.id} | |
| id={d.id} | |
| pos={positions[d.id]} | |
| onPos={onPos} | |
| w={d.w} | |
| h={d.h} | |
| title={d.title} | |
| icon={d.icon} | |
| sub={d.sub || undefined} | |
| attn={attnId === d.id} | |
| dimmed={!!focused && focused !== d.id && attnId !== d.id} | |
| onFocus={onFocus} | |
| z={focused === d.id ? 30 : 10 + i} | |
| > | |
| <d.Body st={winState} /> | |
| </AppWindow> | |
| ))} | |
| </> | |
| ); | |
| }); | |