@ghostystudio/boxes
v0.1.5
Published
Ghosty's animated box robots: a canvas TV-head with moods, screens, themes and finishes, plus the box detail sheet and hover card. Web component, React components, or a plain <script>.
Maintainers
Readme
@ghostystudio/boxes
Ghosty's animated box robots: a TV-headed robot drawn on a 2D canvas, with moods, screens, themes, finishes and wear. Use one box, or a whole summary row with drag-and-drop reordering, in any page: plain HTML, any framework, or React.
Based on robot-heads by Fayaz Ahmed (MIT).
Plain <script>
<script src="https://unpkg.com/@ghostystudio/boxes/dist/boxes.iife.js"></script>
<ghosty-box theme="mailmask" state="working" size="120"></ghosty-box>Bundler
import { defineGhostyBox, defineGhostyBoxes } from '@ghostystudio/boxes';
defineGhostyBox(); // <ghosty-box>
defineGhostyBoxes(); // <ghosty-boxes>React
@ghostystudio/boxes/react ships "use client", so it works as-is in Next.js App Router.
import { useState } from 'react';
import { RobotHead, GhostyBoxes, type SummaryItem } from '@ghostystudio/boxes/react';
export function OneBox() {
return <RobotHead theme="mailmask" state="happy" size={96} />;
}
export function Mailboxes({ initial }: { initial: SummaryItem[] }) {
const [items, setItems] = useState(initial);
return (
<GhostyBoxes
theme="mailmask"
data={{ title: 'Mailboxes', count: `${items.length}/6`, items }}
onReorder={({ order }) => setItems(order.map((id) => items.find((it) => it.id === id)!))}
onOpen={({ id }) => console.log('open', id)}
/>
);
}Props are the attributes in camelCase (trimColor, screenIcon, floorShadow).
<ghosty-box> attributes (RobotHead props in camelCase)
| Attribute | Values | Default |
|---|---|---|
| theme | ghosty, mailmask, mailmaskGreen, or one from registerTheme | none |
| shape | rectangle, square, circle, hexagon, tree, pumpkin | rectangle |
| state | idle, thinking, searching, listening, speaking, working, happy, success, error, sleeping | idle |
| size | px | 160 |
| color / trim-color / screen-color | any CSS hex or rgb() | blue / steel / white |
| face | eyes, terminal, off | eyes |
| screen-icon | with face="terminal": mail, inbox, preview, build, ci, desktop, web, or one from registerScreenIcon | none |
| texture | grain, smooth, paper, fabric, metal, leather, wood, ribs | grain |
| wear | 0–1 | 0 |
| speed | multiplier, 0 freezes | 1 |
| seed | 0–1, so a row does not blink in unison (seedOf(id)) | random |
| paused, interactive="false", antenna="false", floor-shadow="false" | flags | |
Explicit attributes win over the theme. It respects prefers-reduced-motion.
<ghosty-boxes>: the summary row
const row = document.querySelector('ghosty-boxes');
row.data = {
title: 'Mailboxes', count: '4/6', sub: '12 unread', note: 'Drag to reorder.',
items: [
{ id: 'hello', name: 'hello@', label: '8 unread', tone: 'talk', dots: { on: 8, cap: 10 }, head: { state: 'happy' } },
{ id: 'free', name: 'free', off: true },
],
};
row.addEventListener('box-reorder', (e) => save(e.detail.order)); // { id, from, to, order }
row.addEventListener('box-open', (e) => open(e.detail.id));Or declare it in plain HTML with the data attribute (JSON):
<ghosty-boxes theme="mailmask" data='{"title":"Mailboxes","items":[{"id":"a","name":"hello@","head":{"state":"happy"}}]}'></ghosty-boxes>Grab a box and it lifts, swings with the drag and smiles; drop it and it lands with a hop. Items
only swap within their group; off items are free slots and fixed items open on click but never move. Keyboard: Enter opens, Alt+←/→ moves.
Style it with CSS variables on .gbx: --gbx-bg, --gbx-border, --gbx-fg, --gbx-muted,
--gbx-work, --gbx-talk, --gbx-error, --gbx-dot, --gbx-dot-off, --gbx-radius, --gbx-font.
Box detail, hover card and the big group (React)
The same sheet, hover card and "open a group big" Ghosty Studio uses, driven only by data and
callbacks (no fetches, no app types). Needs react-dom for the portals.
import { useState } from 'react';
import { BoxDetail, BoxHoverCard, BoxesSheet, GhostyBoxes, lookFor, useHoverCard, type BoxDetailData } from '@ghostystudio/boxes/react';
const look = lookFor(agent.id, { color: agent.color }); // same robot for the same id, everywhere
const detail: BoxDetailData = {
name: agent.name, idLabel: box.id.slice(0, 14), subtitle: owner.email,
head: { ...look, state: 'sleeping', antenna: false },
title: agent.name, chips: ['Agent'], state: { label: 'asleep', tone: 'sleep' },
hint: 'Asleep 19 h ago: costs disk, not RAM.',
conversations: { active: 1, capacity: 4, titles: ['WhatsApp · Ana'] },
rows: [{ label: 'Model', value: 'Haiku 5.5 · medium' }, { label: 'RAM', value: '4 GB' }],
chats: [{ key: 'wa:ana', heading: <><b>WhatsApp</b> · Ana</>, time: '3 min ago', details: 'Asked: a quote' }],
};
{open && <BoxDetail data={detail} light onClose={() => setOpen(false)}><button>Release</button></BoxDetail>}BoxDetail({ data, light, onClose, stage?, children }): modal in a portal; Esc and a click outside close it.childrenare the actions at the bottom;stagereplaces the robot.BoxDetailCard({ data, stage?, onClose?, children }): just the card's content, for your own overlay (wrap it in<div class="gbd"><article class="card">…). CSS:boxDetailCssis injected.ConvDots({ active, capacity, titles, big, hot, onHot }): the conversation dots.BoxHoverCard({ anchor, title, subtitle, chips, stats, footer, children, onEnter, onLeave })withuseHoverCard()→{ anchor, show, hide, keep, close }: putonMouseEnter={show} onMouseLeave={hide}on the robot and render the card whileanchoris set.BagRowdraws a usage bar.BoxesSheet({ title, count, hint, light, onClose, children }): a group opened big; put a<GhostyBoxes>inside and it reorders by dragging.lookFor(id, chosen?)→{ color, shape, seed },colorFor(id),agentPalette: the deterministic look Ghosty Studio gives every agent.
BoxDetailData: name, idLabel?, subtitle?, head? (RobotHead props), title, chips?,
state?: { label, tone } (work, talk, waste, error, calm, sleep), hint?,
conversations?: { active, capacity, titles?, label? }, rows?: { label, value }[],
chats?: { key, heading, details?, time? }[], chatsTitle?. Dark by default; light for the light card.
API
mount(el, options)→{ update, set, poke, destroy }: a box on any element, no custom element.prewarm(shapes, { size, antenna }): bake each shape's lighting ahead of time (the first box of a shape and size is slow to draw).registerTheme(name, theme),registerScreenIcon(name, rows, { always }).
Licenses
MIT (see LICENSE). Bulb and star art: Google Noto Emoji, Apache-2.0 (NOTO-LICENSE). Shell
textures: ambientCG, CC0.
