Instructions for AI agents (and humans) working in this repo. Read this before creating or modifying anything. It encodes the hard constraints of the Rabbit R1 platform and the conventions this repo follows. Following it is how you avoid breaking things.
This file is also included by CLAUDE.md via @AGENTS.md.
creations/<name>/ as three files: index.html (markup + references), styles.css, app.js (split for legibility — never inline <style>/<script>). Sized exactly 240×282 px.<link rel="stylesheet" href="../../lib/fonts/fonts.css"> and set font-family: "Power Grotesk", -apple-system, sans-serif. Never CDN fonts, never absolute URLs.creationStorage values must be base64-encoded (btoa/atob).transform/opacity.#FE5000 accent on #0a0a0a/#000) — see Brand colors. No light theme, no toggle.<link href="../../lib/fonts/fonts.css"> → <link href="../../lib/shared/reset.css"> → its own styles.css, in that order. reset.css provides the box-model reset, 240×282 dark viewport, font stack, and palette tokens — don’t duplicate them.creations/<name>/ one Creation per folder, split into three files:
├── index.html markup + <link>/<script> references
├── styles.css all CSS
└── app.js all JS
lib/fonts/ shared Power Grotesk WOFF2 + fonts.css (3 weights)
lib/shared/ shared reset.css (CSS base + tokens) + core.js / store.js (window.R1 helpers)
qr-generator/ self-host QR generator tool — NOT a Creation, leave at root
README.md human-facing overview
AGENTS.md this file (source of truth)
CLAUDE.md contains `@AGENTS.md`
CNAME optional GitHub Pages custom domain
lib/ (e.g. lib/fonts/, lib/shared/). Don’t copy fonts or shared CSS into a Creation.creations/. If you’re tempted to add a tool or generator, ask first.<base>/creations/<name>/, where <base> is whatever static host serves the repo root.Every Creation is three files in one folder — keep markup, styles, and logic separate so it’s legible and reviewable:
creations/<name>/
├── index.html # structure only: <link>/<script> references + DOM. No inline <style>/<script>.
├── styles.css # all CSS
└── app.js # all JS
index.html references its siblings with relative paths: <link rel="stylesheet" href="styles.css"> and <script src="app.js"></script>.lib/ assets with the deeper relative path (../../lib/...) — the Creation is two levels deep: ../../lib/fonts/fonts.css and ../../lib/shared/reset.css.<body>, in dependency order: core.js (+ store.js if you persist data) → your app.js. core.js exposes window.R1 (hasSDK, $, toast, bindControls) — use bindControls() instead of hand-wiring the hardware events + dev harness.mkdir creations/<name> and create three files: index.html, styles.css, app.js.index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=240, initial-scale=1.0, user-scalable=no">
<title><Name></title>
<link rel="stylesheet" href="../../lib/fonts/fonts.css">
<link rel="stylesheet" href="../../lib/shared/reset.css">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="app"><!-- your UI --></div>
<!-- on-screen controls for desktop testing; auto-hidden on device -->
<div class="devbar" id="devbar"></div>
<script src="../../lib/shared/core.js"></script>
<script src="../../lib/shared/store.js"></script> <!-- only if you persist data -->
<script src="app.js"></script>
</body>
</html>
styles.css (component styles only — the reset, viewport sizing, dark base, font stack, palette tokens, and .devbar show/hide all come from reset.css):
#app {
width: 240px; height: 282px;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center; gap: 10px;
}
/* palette tokens (--accent, --bg, --surface, --text, …) are defined in reset.css;
override an individual value here only if this Creation needs to deviate. */
app.js:
(function () {
"use strict";
// hardware → handler wiring (also called by the dev controls)
function onWheel(dir) {} // dir = +1 (up) or -1 (down)
function onPTT() {} // side button click = primary action
function onLongPress() {} // long press = mode switch / destructive
// lib/shared/core.js exposes window.R1 — bindControls() wires the 4 hardware
// events AND, when there's no SDK, the desktop dev harness (devbar + arrow/space/g keys).
R1.bindControls({ wheel: onWheel, ptt: onPTT, longPress: onLongPress, devbar: "devbar" });
// other R1 helpers: R1.hasSDK, R1.$(id), R1.toast(el, msg, ms). load store.js for R1.store.
window.onPluginMessage = function (data) {
// data.data is a JSON string when useLLM was true
};
})();
The complete documented API. pluginId is injected/overridden by the system — never set it yourself.
PluginMessageHandler.postMessage(JSON.stringify({
message: "text",
useLLM: true, // optional: get an LLM reply (arrives via window.onPluginMessage)
wantsR1Response: true, // optional (default false): speak the reply through the R1 speaker
wantsJournalEntry: true // optional (default false): log to the journal
}));
closeWebView.postMessage("");
TouchEventHandler.postMessage(JSON.stringify({ type: "tap"|"down"|"up"|"move"|"cancel", x: 100, y: 200 }));
All values must be base64. Returns null if missing.
await window.creationStorage.plain.setItem('k', btoa(val)); // unencrypted
const v = atob(await window.creationStorage.plain.getItem('k')); // throws if null → guard it
await window.creationStorage.plain.removeItem('k');
await window.creationStorage.plain.clear();
// .secure.* has the same API (hardware-encrypted, Android M+)
const ok = await window.creationSensors.accelerometer.isAvailable();
window.creationSensors.accelerometer.start((d) => {
// d = { x, y, z } normalized -1..1
}, { frequency: 60 });
window.creationSensors.accelerometer.stop();
window.onPluginMessage = function (data) {
// data.message (string), data.pluginId, data.data (JSON string when useLLM)
if (data.data) { const parsed = JSON.parse(data.data); /* use it */ }
};
window.addEventListener)scrollUp, scrollDown, sideClick, longPressStart, longPressEnd.
A double side-button press fires two
sideClickevents ~50 ms apart.
Available via getUserMedia etc. in the WebView. The motorized rotating camera has no SDK control — to change framing, switch the active videoinput device / facingMode.
The R1’s reliable inputs are the scroll wheel and the side button (PTT). Design for those, not touch.
longPressEnd, not longPressStart.hold: gallery, etc.).#FE5000) accent on a dark field reads best on the small display — see Brand colors.Every Creation ships dark mode only — no light theme, no toggle. The R1’s small, dim-friendly screen reads best on a dark field, and one shared palette keeps creations recognizably on-brand.
#FE5000. The signature color. Use it sparingly: the primary action, the active/selected item, focus rings, capture flash, and toasts. Never as a large fill or full background — on a 240×282 screen it fatigues the eye.#0a0a0a; media/camera surfaces #000.#141414–#161616 (cards, top/shutter/dev bars, thumbnails).#ffffff; meta #b8b8b8; hints/empty states #9a9a9a–#6a6a6a.#161616 or a low-alpha white (e.g. rgba(255,255,255,.06)).These live in lib/shared/reset.css as CSS custom properties (so every Creation shares them). Reference var(--…) everywhere; override an individual value in your styles.css only if a Creation needs to deviate:
:root {
--accent: #FE5000; /* R1 orange — primary action, selection, brand */
--bg: #0a0a0a; /* app background */
--bg-media: #000; /* camera / image surfaces */
--surface: #141414; /* cards, bars, thumbs */
--text: #ffffff;
--text-muted: #b8b8b8;
--text-dim: #6a6a6a;
--border: #161616;
}
The R1’s own interface is lowercase end to end — match it. All user-facing copy is lowercase: buttons, labels, topbar tags, toasts, hints, section headers, and empty-state text. No title case, no ALL CAPS.
hold: gallery, wheel▲, saved (3), 1 camera only, deleted, no photos yet.Hold: Gallery, Saved (3), CAM, GALLERY.<link rel="stylesheet" href="../../lib/fonts/fonts.css"> (note the ../../ — creations live two levels deep).font-family: "Power Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;.lib/fonts/, add an @font-face block to lib/fonts/fonts.css with the correct font-weight, and only do so if a Creation actually uses it. Never bundle unused weights.transform and opacity; avoid animating layout properties.innerHTML rebuilds).track.stop(), accelerometer.stop()) when leaving a screen.font-display: swap is already set — don’t change it.A desktop browser has no SDK bridges and (in most dev setups) cannot grant camera/mic permission (NotAllowedError). Design for this.
python3 -m http.server 8000 from the repo root.http://localhost:8000/creations/<name>/ in a browser resized to 240×282.g = long press) drive the same handlers as the hardware events.getUserMedia.For storage testing without the device, fall back to localStorage when window.creationStorage is absent (see creations/camera/app.js for the pattern).
npm run check passes clean (Biome lint + format). Stage and it auto-fixes via lint-staged; never commit with outstanding errors.index.html, styles.css, app.js — no inline <style>/<script>.../../lib/fonts/fonts.css and ../../lib/shared/reset.css (in that order) before its own styles.css.../../lib/shared/core.js before app.js and wires input via R1.bindControls() (not hand-rolled addEventListener).user-scalable=no, no scrollbars.../../lib/fonts/fonts.css; computed font-family starts with "Power Grotesk".#FE5000 accent on #0a0a0a/#000).creationStorage reads guard against null; all writes are base64.creations/<name>/.<style>/<script>. Split into styles.css and app.js — see File structure.../fonts/fonts.css or /fonts/... is wrong from creations/<name>/. It must be ../../lib/fonts/fonts.css.PluginMessageHandler.postMessage unconditionally, the page throws in every desktop test. Guard with typeof PluginMessageHandler !== 'undefined'.creationStorage values must be btoa‘d, and reads must handle null.deviceId/facingMode instead.reset.css already sets the box model, the 240×282 dark viewport, the font stack, and the --accent/--bg/… tokens. Reference var(--…); don’t copy the :root block or *{} reset into a Creation’s styles.css.qr-generator/ is vendored from rabbit-hmi-oss/creations-sdk (MIT).