0
ART-VISUAL#73247c51
Busy Meter GTA VI
@Owner·deposited 3w ago·updated 3w ago·109 views
ART-VISUAL#73247c51
Busy Meter GTA VI
OW
@Owner
109Views
0Comments
0Forks
0Saves
SHARE · REMIX
Busy Meter GTA VI — a HTML art-visual widget by @Owner.
CONTROLS
canvasanimationgametimerform
No comments yet. Be the first!
✦ Remix with AI
SDK in this widgetNo Vibes SDK features detected yet
Generated prompt
You are helping me modify a vibe-coded widget from itjustvibes.com.
[VIBE CODE: "Busy Meter GTA VI" by @Owner]
Source: https://itjustvibes.com/Owner/busy-meter-gta-vi
Type: HTML
--- SOURCE CODE ---
```html
<!doctype html><html><head><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1,viewport-fit=cover"><style>:root{color-scheme:light;box-sizing:border-box;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)}html{scroll-padding-top:env(safe-area-inset-top,0px)}body{margin:0;padding:0;font:14px -apple-system,BlinkMacSystemFont,sans-serif;background:#faf9f5;color:#141413}img{max-width:100%}[hidden]:not([hidden=until-found i]){display:none!important}</style></head><body>
<title>Busy Meter</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Big+Shoulders+Display:wght@700;900&family=Figtree:wght@400;500;600;700&family=JetBrains+Mono:wght@500&display=swap">
<style>
/* Layout: night-shift signage. One big 3D hero with a live status dial, then a quiet scale, how-it-knows, mounts, editions, specs, FAQ. Single deliberate dark look. */
:root {
--night: #110e26;
--night-2: #19153a;
--night-3: #221d4a;
--line: #312a63;
--ink: #f5f0fd;
--muted: #aaa2cf;
--amber: #ffb22e;
--amber-ink: #241500;
--pink: #ff4fa3;
--siren-red: #ff3b55;
--siren-blue: #4a83ff;
--free: #5be38f;
--display: "Big Shoulders Display", "Arial Narrow", "Roboto Condensed", Impact, sans-serif;
--body: "Figtree", "Segoe UI", system-ui, -apple-system, sans-serif;
--mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
color-scheme: dark;
}
* { box-sizing: border-box; }
html { background: var(--night); }
body { background: var(--night); color: var(--ink); font-family: var(--body); font-size: 16px; line-height: 1.6; -webkit-font-smoothing: antialiased; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--amber); outline-offset: 3px; border-radius: 6px; }
.wrap { max-width: 1140px; margin: 0 auto; padding-inline: clamp(16px, 4vw, 40px); }
h1, h2, h3 { font-family: var(--display); text-transform: uppercase; letter-spacing: 0.01em; line-height: 0.95; margin: 0; text-wrap: balance; font-weight: 900; }
p { margin: 0; }
.eyebrow { font-family: var(--mono); font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--amber); }
.muted { color: var(--muted); }
svg.st { width: 1em; height: 1em; display: inline-block; vertical-align: -0.12em; }
/* top bar */
.bar { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 10; background: color-mix(in srgb, var(--night) 88%, transparent); backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); }
.bar .wrap { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-block: 12px; }
.mark { font-family: var(--display); font-weight: 900; font-size: 24px; letter-spacing: 0.04em; text-decoration: none; display: flex; align-items: center; gap: 8px; }
.mark .dots { display: inline-flex; gap: 2px; color: var(--amber); font-size: 13px; }
.mark .dots .off { color: var(--line); }
.nav { display: flex; gap: 22px; align-items: center; font-size: 14px; font-weight: 500; }
.nav a { text-decoration: none; color: var(--muted); }
.nav a:hover { color: var(--ink); }
.nav a.btn, .nav a.btn:hover { color: var(--amber-ink); }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; font: 600 15px/1 var(--body); border-radius: 999px; padding: 14px 22px; border: 1px solid transparent; cursor: pointer; text-decoration: none; transition: transform .15s ease, background .15s ease; }
.btn:active { transform: translateY(1px); }
.btn-amber { background: var(--amber); color: var(--amber-ink); }
.btn-amber:hover { background: #ffc35c; }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line); }
.btn-ghost:hover { border-color: var(--muted); }
.btn-sm { padding: 10px 16px; font-size: 14px; }
@media (max-width: 640px) { .nav a:not(.btn) { display: none; } }
/* hero */
.hero { position: relative; overflow: hidden; padding-block: clamp(32px, 6vw, 72px) clamp(40px, 6vw, 80px); }
.hero::before { content: ""; position: absolute; inset: auto -20% -40% -20%; height: 70%; background: radial-gradient(60% 60% at 60% 40%, color-mix(in srgb, var(--pink) 30%, transparent), transparent 70%); pointer-events: none; }
.hero-grid { position: relative; display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); gap: clamp(24px, 4vw, 56px); align-items: center; }
@media (max-width: 900px) { .hero-grid { grid-template-columns: minmax(0, 1fr); } }
.hero h1 { font-size: clamp(54px, 8.4vw, 112px); margin-block: 14px 20px; }
.hero h1 em { font-style: normal; color: var(--amber); }
.lede { font-size: clamp(17px, 1.6vw, 19px); color: var(--muted); max-width: 46ch; }
.lede strong { color: var(--ink); font-weight: 600; }
.buy { display: grid; gap: 16px; margin-top: 28px; justify-items: start; }
.ctas { display: flex; flex-wrap: wrap; gap: 10px; }
.price { font-family: var(--display); font-size: 40px; font-weight: 900; line-height: 1; }
.price small { font-family: var(--body); font-size: 13px; font-weight: 500; color: var(--muted); display: block; letter-spacing: 0; margin-top: 4px; }
.facts { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 22px; font-family: var(--mono); font-size: 12px; color: var(--muted); letter-spacing: 0.04em; }
.facts span::before { content: "■ "; color: var(--pink); font-size: 9px; vertical-align: 2px; }
.stage { position: relative; border-radius: 22px; background: radial-gradient(80% 70% at 50% 45%, var(--night-3), var(--night-2) 70%); border: 1px solid var(--line); overflow: hidden; }
.stage canvas { display: block; width: 100%; aspect-ratio: 4 / 3; max-width: 100%; cursor: grab; touch-action: pan-y; }
.stage canvas:active { cursor: grabbing; }
.stage-top { position: absolute; top: 14px; left: 16px; right: 16px; display: flex; justify-content: space-between; align-items: center; gap: 12px; pointer-events: none; }
.readout { font-family: var(--mono); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; background: color-mix(in srgb, var(--night) 70%, transparent); border: 1px solid var(--line); padding: 6px 10px; border-radius: 8px; }
.readout b { color: var(--amber); font-weight: 500; }
.readout.siren b { color: var(--siren-red); }
.readout.free b { color: var(--free); }
.flip { pointer-events: auto; font: 500 12px/1 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink); background: color-mix(in srgb, var(--night) 70%, transparent); border: 1px solid var(--line); border-radius: 8px; padding: 7px 10px; cursor: pointer; }
.flip:hover { border-color: var(--muted); }
.hint { position: absolute; bottom: 12px; left: 0; right: 0; text-align: center; font-size: 12px; color: var(--muted); pointer-events: none; }
@media (max-width: 560px) { .readout { font-size: 10.5px; padding: 5px 8px; white-space: nowrap; } .flip { font-size: 10.5px; padding: 6px 8px; } .hint { display: none; } }
.nogl { display: none; position: absolute; inset: 0; place-items: center; text-align: center; padding: 24px; color: var(--muted); }
.dial { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; margin-top: 12px; }
@media (max-width: 560px) { .dial { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.chip { background: var(--night-2); border: 1px solid var(--line); color: var(--muted); border-radius: 12px; padding: 9px 4px 8px; cursor: pointer; font: 600 12px/1.2 var(--body); display: grid; justify-items: center; gap: 5px; transition: border-color .15s, color .15s, background .15s; }
.chip .n { font-family: var(--display); font-size: 22px; font-weight: 900; line-height: 1; color: var(--ink); }
.chip:hover { border-color: var(--muted); color: var(--ink); }
.chip[aria-pressed="true"] { border-color: var(--amber); color: var(--ink); background: color-mix(in srgb, var(--amber) 14%, var(--night-2)); }
.chip[data-mode="cam"] .n { background: linear-gradient(90deg, var(--siren-red), var(--siren-blue)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.chip[data-mode="cam"][aria-pressed="true"] { border-color: var(--siren-blue); background: color-mix(in srgb, var(--siren-blue) 14%, var(--night-2)); }
.chip[data-level="0"] .n { color: var(--free); }
/* sections */
section.block { padding-block: clamp(56px, 8vw, 104px); border-top: 1px solid var(--line); }
.head { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px 48px; align-items: end; margin-bottom: clamp(28px, 4vw, 48px); }
@media (max-width: 800px) { .head { grid-template-columns: minmax(0, 1fr); } }
.head h2 { font-size: clamp(40px, 5.4vw, 68px); margin-top: 10px; }
.head p { color: var(--muted); max-width: 52ch; }
/* heat scale */
.scale { display: grid; gap: 0; border: 1px solid var(--line); border-radius: 18px; overflow: hidden; }
.row { display: grid; grid-template-columns: 64px 150px minmax(0, 1fr) minmax(0, 1.1fr); gap: 16px; align-items: center; padding: 16px 22px; border-top: 1px solid var(--line); background: var(--night); }
.row:first-child { border-top: 0; }
.row:nth-child(even) { background: var(--night-2); }
.row .lvl { font-family: var(--display); font-weight: 900; font-size: 36px; line-height: 1; }
.row .stars { color: var(--amber); font-size: 20px; letter-spacing: 3px; white-space: nowrap; }
.row .stars .off { color: var(--line); }
.row .name { font-weight: 600; font-size: 17px; }
.row .name span { display: block; font-weight: 400; font-size: 14px; color: var(--muted); }
.row .trig { font-family: var(--mono); font-size: 12.5px; color: var(--muted); line-height: 1.55; }
.row.cam .stars { color: var(--siren-red); }
.row.cam .stars .b { color: var(--siren-blue); }
.row.free .lvl { color: var(--free); }
@media (max-width: 760px) {
.row { grid-template-columns: 52px minmax(0, 1fr); gap: 4px 14px; padding: 14px 16px; }
.row .lvl { grid-row: span 3; font-size: 30px; }
}
/* how it knows */
.ways { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
@media (max-width: 960px) { .ways { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .ways { grid-template-columns: minmax(0, 1fr); } }
.way { padding: 22px; border-radius: 16px; background: var(--night-2); border: 1px solid var(--line); display: grid; gap: 10px; align-content: start; }
.way .tag { font-family: var(--mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--pink); }
.way h3 { font-size: 28px; }
.way p { color: var(--muted); font-size: 15px; }
.way .setup { font-family: var(--mono); font-size: 12px; color: var(--ink); border-top: 1px dashed var(--line); padding-top: 10px; margin-top: 4px; }
/* mounts */
.mounts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
@media (max-width: 820px) { .mounts { grid-template-columns: minmax(0, 1fr); } }
.mount { border: 1px solid var(--line); border-radius: 16px; overflow: hidden; display: grid; grid-template-rows: auto 1fr; background: var(--night); }
.mount svg { display: block; width: 100%; height: auto; background: var(--night-2); border-bottom: 1px solid var(--line); }
.mount .txt { padding: 18px 20px 22px; display: grid; gap: 6px; }
.mount h3 { font-size: 26px; }
.mount p { color: var(--muted); font-size: 15px; }
/* editions */
.eds { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; align-items: stretch; }
@media (max-width: 980px) { .eds { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 540px) { .eds { grid-template-columns: minmax(0, 1fr); } }
.ed { border: 1px solid var(--line); border-radius: 18px; padding: 24px 22px; display: grid; grid-template-rows: auto auto auto 1fr auto; gap: 12px; background: var(--night); }
.ed.pick { border-color: var(--amber); background: color-mix(in srgb, var(--amber) 7%, var(--night)); }
.ed .badge { justify-self: start; font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--amber-ink); background: var(--amber); padding: 4px 8px; border-radius: 6px; }
.ed .badge.ghost { background: transparent; color: var(--muted); border: 1px solid var(--line); }
.ed h3 { font-size: 30px; }
.ed .p { font-family: var(--display); font-size: 46px; font-weight: 900; line-height: 1; }
.ed ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 8px; font-size: 14.5px; color: var(--muted); align-content: start; }
.ed li { padding-left: 22px; position: relative; }
.ed li::before { content: "★"; position: absolute; left: 0; color: var(--amber); font-size: 12px; top: 2px; }
.ed .btn { width: 100%; }
/* specs + box */
.specgrid { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: clamp(24px, 4vw, 56px); }
@media (max-width: 860px) { .specgrid { grid-template-columns: minmax(0, 1fr); } }
dl.specs { margin: 0; display: grid; grid-template-columns: minmax(110px, 0.6fr) minmax(0, 1.4fr); border-top: 1px solid var(--line); }
dl.specs dt, dl.specs dd { margin: 0; padding: 12px 0; border-bottom: 1px solid var(--line); }
dl.specs dt { font-family: var(--mono); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); padding-top: 15px; }
dl.specs dd { font-size: 15px; font-variant-numeric: tabular-nums; }
.box { border: 1px dashed var(--line); border-radius: 16px; padding: 24px; align-self: start; display: grid; gap: 14px; }
.box h3 { font-size: 28px; }
.box ol { margin: 0; padding: 0; list-style: none; display: grid; gap: 10px; counter-reset: b; }
.box li { counter-increment: b; display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 8px; font-size: 15px; }
.box li::before { content: counter(b, decimal-leading-zero); font-family: var(--mono); font-size: 12px; color: var(--pink); padding-top: 3px; }
/* faq */
.faq { display: grid; gap: 10px; max-width: 820px; }
details { border: 1px solid var(--line); border-radius: 14px; background: var(--night-2); }
summary { cursor: pointer; list-style: none; padding: 18px 22px; font-weight: 600; font-size: 17px; display: flex; justify-content: space-between; gap: 16px; }
summary::-webkit-details-marker { display: none; }
summary::after { content: "+"; font-family: var(--mono); color: var(--amber); font-size: 18px; line-height: 1; }
details[open] summary::after { content: "–"; }
details p { padding: 0 22px 20px; color: var(--muted); max-width: 68ch; }
footer { border-top: 1px solid var(--line); padding-block: 36px 48px; color: var(--muted); font-size: 13px; }
footer .wrap { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 16px; }
.toast { position: fixed; left: 50%; bottom: calc(24px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%) translateY(20px); background: var(--ink); color: var(--night); font-weight: 600; font-size: 14px; padding: 12px 18px; border-radius: 12px; opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; max-width: calc(100% - 32px); }
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
</style>
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="star" viewBox="0 0 24 24"><path fill="currentColor" d="M12 1.8l3.05 6.6 7.2.8-5.35 4.9 1.5 7.1L12 17.6l-6.4 3.6 1.5-7.1L1.75 9.2l7.2-.8z"/></symbol>
</svg>
<header class="bar">
<div class="wrap">
<a class="mark" href="#top" aria-label="Busy Meter home">BUSY METER
<span class="dots" aria-hidden="true"><svg class="st"><use href="#star"/></svg><svg class="st"><use href="#star"/></svg><svg class="st"><use href="#star"/></svg><svg class="st off"><use href="#star"/></svg><svg class="st off"><use href="#star"/></svg></span>
</a>
<nav class="nav" aria-label="Sections">
<a href="#scale">The scale</a>
<a href="#how">How it knows</a>
<a href="#editions">Editions</a>
<a href="#specs">Specs</a>
<a class="btn btn-amber btn-sm" href="#editions">Pre-order</a>
</nav>
</div>
</header>
<main id="top">
<section class="hero">
<div class="wrap hero-grid">
<div>
<p class="eyebrow">Desk status light · Wi-Fi · USB-C</p>
<h1>Five stars means <em>don't come in.</em></h1>
<p class="lede">Busy Meter sits on your desk and shows the room how busy you are, from <strong>zero stars</strong> when you're free to <strong>five</strong> when you're not to be disturbed. It follows your calendar or Teams status on its own, and when your camera goes live, it <strong>flashes red and blue</strong>.</p>
<div class="buy">
<div class="price">$49<small>Desk Edition · ships before the holidays</small></div>
<div class="ctas"><a class="btn btn-amber" href="#editions">Pre-order</a>
<a class="btn btn-ghost" href="#scale">See the scale</a></div>
</div>
<div class="facts"><span>Readable from the doorway</span><span>No account needed</span><span>Open API</span></div>
</div>
<div>
<div class="stage">
<canvas id="meter" aria-label="Interactive 3D model of Busy Meter. Drag to rotate."></canvas>
<div class="stage-top">
<div class="readout" id="readout" aria-live="polite">Status <b>3/5 · In a meeting</b></div>
<button class="flip" id="flip" type="button">Show back</button>
</div>
<p class="hint">Drag to rotate · pick a status below</p>
<div class="nogl" id="nogl">3D preview needs WebGL. The status buttons still describe each level.</div>
</div>
<div class="dial" role="group" aria-label="Choose a status to preview">
<button class="chip" type="button" data-level="0" aria-pressed="false"><span class="n">0</span>Free</button>
<button class="chip" type="button" data-level="1" aria-pressed="false"><span class="n">1</span>Focus</button>
<button class="chip" type="button" data-level="2" aria-pressed="false"><span class="n">2</span>Call</button>
<button class="chip" type="button" data-level="3" aria-pressed="true"><span class="n">3</span>Meeting</button>
<button class="chip" type="button" data-level="4" aria-pressed="false"><span class="n">4</span>Presenting</button>
<button class="chip" type="button" data-level="5" aria-pressed="false"><span class="n">5</span>DND</button>
<button class="chip" type="button" data-level="cam" data-mode="cam" aria-pressed="false"><span class="n">●</span>Camera</button>
</div>
</div>
</div>
</section>
<section class="block" id="scale">
<div class="wrap">
<div class="head">
<div><p class="eyebrow">The heat scale</p><h2>Everyone in the house can read it.</h2></div>
<p>Six levels plus one alarm. The stars line up with the presence states your work apps already track, so Busy Meter can switch levels without you touching it.</p>
</div>
<div class="scale" id="scaleRows"></div>
</div>
</section>
<section class="block" id="how">
<div class="wrap">
<div class="head">
<div><p class="eyebrow">How it knows</p><h2>Four ways to set the stars.</h2></div>
<p>Start with a tap on the top of the meter. Connect a calendar or Teams later if you want it to switch on its own. Setup runs from a page the meter serves on your Wi-Fi, so there's no app to download.</p>
</div>
<div class="ways">
<article class="way"><p class="tag">Manual</p><h3>Tap the top</h3><p>The touch pad on top adds a star with each tap. Hold it for two seconds to clear back to zero.</p><p class="setup">Setup: none. Plug in and tap.</p></article>
<article class="way"><p class="tag">Calendar</p><h3>Google or Outlook</h3><p>Busy Meter checks your calendar every minute and lights three stars for any event marked busy. It clears when the meeting ends.</p><p class="setup">Setup: paste your calendar's private iCal link.</p></article>
<article class="way"><p class="tag">Presence</p><h3>Teams and Slack</h3><p>Follows your live status: in a call, in a meeting, presenting, do not disturb. A small desktop helper adds the red and blue flash when your webcam turns on.</p><p class="setup">Setup: sign in once from the meter's setup page.</p></article>
<article class="way"><p class="tag">Tinkerers</p><h3>Home Assistant</h3><p>The firmware is WLED-compatible, so it shows up in Home Assistant and takes a plain JSON request from any script, shortcut or automation.</p><p class="setup">Setup: add it as a WLED device.</p></article>
</div>
</div>
</section>
<section class="block" id="mounts">
<div class="wrap">
<div class="head">
<div><p class="eyebrow">Built for the doorway</p><h2>The light faces the people walking in.</h2></div>
<p>A status light only works if the person at the door can see it. The Doorway Edition lights both faces at once and comes with three ways to mount it.</p>
</div>
<div class="mounts">
<article class="mount">
<svg viewBox="0 0 320 170" role="img" aria-label="Meter on a tilted desk stand">
<rect x="0" y="0" width="320" height="170" fill="#19153a"/>
<line x1="20" y1="140" x2="300" y2="140" stroke="#312a63" stroke-width="2"/>
<polygon points="70,140 250,140 240,122 80,122" fill="#2a2556"/>
<rect x="72" y="70" width="176" height="54" rx="14" fill="#221d4a" stroke="#ffb22e" stroke-width="1.5"/>
<g color="#ffb22e"><use href="#star" x="86" y="85" width="24" height="24"/><use href="#star" x="118" y="85" width="24" height="24"/><use href="#star" x="150" y="85" width="24" height="24"/></g>
<g fill="#3a3370" color="#3a3370"><use href="#star" x="182" y="85" width="24" height="24"/><use href="#star" x="214" y="85" width="24" height="24"/></g>
</svg>
<div class="txt"><h3>Desk wedge</h3><p>Weighted base tilted back 8° so the stars read from a chair across the room.</p></div>
</article>
<article class="mount">
<svg viewBox="0 0 320 170" role="img" aria-label="Meter clipped to the top of a monitor, facing backward">
<rect x="0" y="0" width="320" height="170" fill="#19153a"/>
<rect x="50" y="62" width="220" height="96" rx="6" fill="#0e0b20" stroke="#312a63" stroke-width="2"/>
<rect x="96" y="22" width="128" height="40" rx="10" fill="#221d4a" stroke="#ff4fa3" stroke-width="1.5"/>
<path d="M120 62 v-6 h80 v6" fill="none" stroke="#aaa2cf" stroke-width="3"/>
<g color="#ffb22e"><use href="#star" x="106" y="32" width="18" height="18"/><use href="#star" x="128" y="32" width="18" height="18"/><use href="#star" x="150" y="32" width="18" height="18"/><use href="#star" x="172" y="32" width="18" height="18"/><use href="#star" x="194" y="32" width="18" height="18"/></g>
</svg>
<div class="txt"><h3>Monitor clip</h3><p>Sits on top of any monitor 12 to 32 mm thick. The rear stars face the door while you face the screen.</p></div>
</article>
<article class="mount">
<svg viewBox="0 0 320 170" role="img" aria-label="Meter mounted on a wall plate beside a door">
<rect x="0" y="0" width="320" height="170" fill="#19153a"/>
<rect x="196" y="20" width="96" height="150" fill="#221d4a" stroke="#312a63" stroke-width="2"/>
<circle cx="276" cy="100" r="4" fill="#aaa2cf"/>
<rect x="28" y="58" width="150" height="44" rx="11" fill="#221d4a" stroke="#ff3b55" stroke-width="1.5"/>
<g color="#ff3b55"><use href="#star" x="38" y="69" width="22" height="22"/><use href="#star" x="94" y="69" width="22" height="22"/><use href="#star" x="150" y="69" width="22" height="22"/></g>
<g color="#4a83ff"><use href="#star" x="66" y="69" width="22" height="22"/><use href="#star" x="122" y="69" width="22" height="22"/></g>
</svg>
<div class="txt"><h3>Wall plate</h3><p>Hang it outside an office door with two screws or removable strips. Run power from a USB-C cable up to 3 m long.</p></div>
</article>
</div>
</div>
</section>
<section class="block" id="editions">
<div class="wrap">
<div class="head">
<div><p class="eyebrow">Editions</p><h2>Pick how you want it.</h2></div>
<p>Every finished meter is printed, wired and tested in our shop. Makers can build their own from the kit or the files.</p>
</div>
<div class="eds">
<article class="ed">
<span class="badge ghost">Finished</span><h3>Desk</h3><p class="p">$49</p>
<ul><li>Single-sided, 5 stars</li><li>Desk wedge stand</li><li>1.5 m USB-C cable</li><li>All four status sources</li></ul>
<button class="btn btn-ghost" type="button" data-buy="Desk Edition">Pre-order Desk</button>
</article>
<article class="ed pick">
<span class="badge">Most picked</span><h3>Doorway</h3><p class="p">$65</p>
<ul><li>Double-sided, stars on both faces</li><li>Desk wedge, monitor clip and wall plate</li><li>3 m USB-C cable</li><li>All four status sources</li></ul>
<button class="btn btn-amber" type="button" data-buy="Doorway Edition">Pre-order Doorway</button>
</article>
<article class="ed">
<span class="badge ghost">Build it</span><h3>DIY Kit</h3><p class="p">$25</p>
<ul><li>Printed housing and diffusers</li><li>Pre-flashed controller, LEDs, touch pad</li><li>About 30 minutes with a soldering iron</li><li>Printed wiring card</li></ul>
<button class="btn btn-ghost" type="button" data-buy="DIY Kit">Pre-order Kit</button>
</article>
<article class="ed">
<span class="badge ghost">Digital</span><h3>Print Files</h3><p class="p">$7</p>
<ul><li>STL and 3MF for housing and all mounts</li><li>Firmware and flashing guide</li><li>Parts list with links</li><li>Personal use license</li></ul>
<button class="btn btn-ghost" type="button" data-buy="Print Files">Get the files</button>
</article>
</div>
</div>
</section>
<section class="block" id="specs">
<div class="wrap">
<div class="head">
<div><p class="eyebrow">Specifications</p><h2>What's inside.</h2></div>
<p>Standard parts, open firmware, nothing proprietary. If a part ever fails, you can replace it yourself.</p>
</div>
<div class="specgrid">
<dl class="specs">
<dt>Size</dt><dd>240 × 70 × 52 mm on the desk wedge</dd>
<dt>Weight</dt><dd>About 140 g with stand</dd>
<dt>Light</dt><dd>WS2812B RGB LEDs: 5 on the Desk Edition, 10 on the Doorway Edition (5 per face)</dd>
<dt>Controller</dt><dd>ESP32-C3 with 2.4 GHz Wi-Fi and Bluetooth LE 5</dd>
<dt>Control</dt><dd>Capacitive touch pad on top; web setup page; WLED JSON API</dd>
<dt>Power</dt><dd>USB-C, 5 V. Brightness is capped in firmware to stay under 300 mA, so any phone charger or laptop port works.</dd>
<dt>Materials</dt><dd>Matte PLA housing, frosted PETG star diffusers</dd>
<dt>Colors</dt><dd>Night (black/amber), Sunset (pink/amber), Swamp (green/amber)</dd>
</dl>
<div class="box">
<h3>In the box</h3>
<ol>
<li>Busy Meter</li>
<li>Desk wedge stand</li>
<li>USB-C cable (1.5 m Desk, 3 m Doorway)</li>
<li>Monitor clip and wall plate (Doorway)</li>
<li>Quick-start card with setup QR code</li>
</ol>
<p class="muted" style="font-size:14px">Power adapter not included. Any USB charger rated 5 V / 1 A or more works.</p>
</div>
</div>
</div>
</section>
<section class="block" id="faq">
<div class="wrap">
<div class="head">
<div><p class="eyebrow">Questions</p><h2>Before you order.</h2></div>
<p>Something not covered here? Ask in the shop messages and we'll add it.</p>
</div>
<div class="faq">
<details><summary>Do I need an app or an account?</summary><p>No. The meter creates its own Wi-Fi setup page the first time you plug it in. Join it from your phone, pick your home network, and choose which status sources to turn on. Manual tap mode works with no setup at all.</p></details>
<details><summary>Does it work with Zoom or Google Meet?</summary><p>Yes, through your calendar. Any meeting on your Google or Outlook calendar lights three stars. For instant changes, tap the top or trigger it from a keyboard shortcut using the JSON API.</p></details>
<details><summary>Will it work on 5 GHz Wi-Fi?</summary><p>The ESP32-C3 uses 2.4 GHz only. Most home routers broadcast both bands under one name, so this usually just works. If yours separates them, connect the meter to the 2.4 GHz network.</p></details>
<details><summary>Can I change the colors?</summary><p>Yes. Each level's color and brightness can be changed on the setup page. Streamers often set it to light up when they go live.</p></details>
<details><summary>Is it bright enough for a sunny room?</summary><p>The default brightness reads clearly across an office in daylight. You can raise it on the setup page; the firmware still keeps total power within what a USB port can supply.</p></details>
<details><summary>Is this tied to a video game?</summary><p>No. Busy Meter is an original design. The star scale is ours, and the product isn't affiliated with or endorsed by any game publisher.</p></details>
</div>
</div>
</section>
</main>
<footer>
<div class="wrap"><span>Busy Meter · original design, printed and assembled by hand</span><span>Prices in USD · Draft product page</span></div>
</footer>
<div class="toast" id="toast" role="status"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function () {
const LEVELS = [
{ n: 0, name: "Free", sub: "Come on in", trig: "Teams: Available · Calendar: no event" },
{ n: 1, name: "Focus", sub: "Heads-down, knock first", trig: "Calendar: focus time block · Tap ×1" },
{ n: 2, name: "On a call", sub: "Quick questions by text", trig: "Teams: In a call · Slack: huddle" },
{ n: 3, name: "In a meeting", sub: "Wait until it clears", trig: "Teams: In a meeting · Calendar: busy event" },
{ n: 4, name: "Presenting", sub: "Screen is shared", trig: "Teams: Presenting" },
{ n: 5, name: "Do not disturb", sub: "Emergencies only", trig: "Teams/Slack: Do not disturb · Hold tap" },
{ n: "cam", name: "Camera live", sub: "Stay out of the shot", trig: "Webcam in use (desktop helper or Home Assistant) · Double-tap · Overrides any level" }
];
// Heat scale rows
const starSvg = (cls) => `<svg class="st ${cls || ""}"><use href="#star"/></svg>`;
const rows = document.getElementById("scaleRows");
rows.innerHTML = LEVELS.map(l => {
let stars = "";
if (l.n === "cam") { for (let i = 0; i < 5; i++) stars += starSvg(i % 2 ? "b" : ""); }
else { for (let i = 0; i < 5; i++) stars += starSvg(i < l.n ? "" : "off"); }
const cls = l.n === "cam" ? "cam" : l.n === 0 ? "free" : "";
const lvl = l.n === "cam" ? "!" : l.n;
return `<div class="row ${cls}"><div class="lvl">${lvl}</div><div class="stars" aria-label="${l.n === "cam" ? "flashing" : l.n + " of 5 stars"}">${stars}</div><div class="name">${l.name}<span>${l.sub}</span></div><div class="trig">${l.trig}</div></div>`;
}).join("");
// Toast for demo buy buttons
const toast = document.getElementById("toast");
let tt;
document.querySelectorAll("[data-buy]").forEach(b => b.addEventListener("click", () => {
toast.textContent = b.dataset.buy + " added. Checkout opens when pre-orders go live.";
toast.classList.add("show"); clearTimeout(tt); tt = setTimeout(() => toast.classList.remove("show"), 2600);
}));
// ---- 3D ----
const canvas = document.getElementById("meter");
const readout = document.getElementById("readout");
const chips = Array.from(document.querySelectorAll(".chip"));
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let current = 3, demo = true;
function setReadout(l) {
const L = LEVELS.find(x => String(x.n) === String(l));
readout.classList.toggle("siren", l === "cam");
readout.classList.toggle("free", String(l) === "0");
readout.innerHTML = l === "cam" ? `Status <b>${L.name}</b>` : `Status <b>${l}/5 · ${L.name}</b>`;
chips.forEach(c => c.setAttribute("aria-pressed", String(c.dataset.level === String(l))));
}
let api = { set: () => {} };
if (typeof THREE === "undefined" || !window.WebGLRenderingContext) {
document.getElementById("nogl").style.display = "grid";
} else {
try { api = build(); } catch (e) { document.getElementById("nogl").style.display = "grid"; }
}
function choose(l, fromUser) {
if (fromUser) demo = false;
current = l; setReadout(l); api.set(l);
}
chips.forEach(c => c.addEventListener("click", () => {
const v = c.dataset.level; choose(v === "cam" ? "cam" : Number(v), true);
}));
choose(3);
if (!reduce) {
const order = [0, 1, 2, 3, 4, 5, "cam"];
let i = 3;
setInterval(() => { if (!demo) return; i = (i + 1) % order.length; choose(order[i]); }, 2400);
}
function build() {
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(30, 4 / 3, 0.1, 100);
camera.position.set(0, 0.9, 8.2);
camera.lookAt(0, -0.1, 0);
scene.add(new THREE.HemisphereLight(0xb9a8ff, 0x1a0f2e, 0.45));
const key = new THREE.DirectionalLight(0xffffff, 0.9); key.position.set(3, 4, 6); scene.add(key);
const rim = new THREE.DirectionalLight(0xff4fa3, 1.3); rim.position.set(-5, 2, -4); scene.add(rim);
const rim2 = new THREE.DirectionalLight(0x4a83ff, 0.6); rim2.position.set(5, 1, -4); scene.add(rim2);
const meter = new THREE.Group(); scene.add(meter);
const body = new THREE.Group(); body.rotation.x = -0.14; body.position.y = 0.15; meter.add(body);
// rounded housing
function rrect(w, h, r) {
const s = new THREE.Shape(), x = -w / 2, y = -h / 2;
s.moveTo(x + r, y); s.lineTo(x + w - r, y); s.quadraticCurveTo(x + w, y, x + w, y + r);
s.lineTo(x + w, y + h - r); s.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
s.lineTo(x + r, y + h); s.quadraticCurveTo(x, y + h, x, y + h - r);
s.lineTo(x, y + r); s.quadraticCurveTo(x, y, x + r, y); return s;
}
const D = 0.9, BEV = 0.07;
const hGeo = new THREE.ExtrudeGeometry(rrect(4.8, 1.4, 0.32), { depth: D, bevelEnabled: true, bevelThickness: BEV, bevelSize: BEV, bevelSegments: 4, curveSegments: 10 });
hGeo.center();
const shell = new THREE.MeshStandardMaterial({ color: 0x100e1f, roughness: 0.6, metalness: 0.1 });
body.add(new THREE.Mesh(hGeo, shell));
const FRONT = D / 2 + BEV;
// face panels (slightly inset look)
const faceMat = new THREE.MeshStandardMaterial({ color: 0x0d0b1d, roughness: 0.9 });
const fGeo = new THREE.ShapeGeometry(rrect(4.4, 1.08, 0.22), 10);
const ff = new THREE.Mesh(fGeo, faceMat); ff.position.z = FRONT + 0.002; body.add(ff);
const fb = new THREE.Mesh(fGeo, faceMat); fb.position.z = -FRONT - 0.002; fb.rotation.y = Math.PI; body.add(fb);
// stars
function starShape(R, r) {
const s = new THREE.Shape();
for (let i = 0; i < 10; i++) {
const rad = i % 2 ? r : R, a = Math.PI / 2 + i * Math.PI / 5;
const px = Math.cos(a) * rad, py = Math.sin(a) * rad;
i ? s.lineTo(px, py) : s.moveTo(px, py);
}
s.closePath(); return s;
}
const sGeo = new THREE.ExtrudeGeometry(starShape(0.38, 0.165), { depth: 0.05, bevelEnabled: true, bevelThickness: 0.015, bevelSize: 0.015, bevelSegments: 2 });
// glow texture
const gc = document.createElement("canvas"); gc.width = gc.height = 128;
const g = gc.getContext("2d"); const grad = g.createRadialGradient(64, 64, 0, 64, 64, 64);
grad.addColorStop(0, "rgba(255,255,255,1)"); grad.addColorStop(0.25, "rgba(255,255,255,0.45)"); grad.addColorStop(1, "rgba(255,255,255,0)");
g.fillStyle = grad; g.fillRect(0, 0, 128, 128);
const glowTex = new THREE.CanvasTexture(gc);
const stars = [];
const xs = [-1.76, -0.88, 0, 0.88, 1.76];
[1, -1].forEach(side => {
xs.forEach((x, i) => {
const mat = new THREE.MeshStandardMaterial({ color: 0x2b2650, emissive: 0x000000, roughness: 0.35, metalness: 0 });
const m = new THREE.Mesh(sGeo, mat);
m.position.set(x, 0, side * (FRONT - 0.005));
if (side < 0) m.rotation.y = Math.PI;
body.add(m);
const sp = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTex, color: 0xffb22e, transparent: true, opacity: 0, blending: THREE.AdditiveBlending, depthWrite: false }));
sp.scale.set(1.5, 1.5, 1); sp.position.set(x, 0, side * (FRONT + 0.12)); body.add(sp);
stars.push({ i, side, mat, sp });
});
});
// touch pad on top
const pad = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 0.03, 40), new THREE.MeshStandardMaterial({ color: 0x2b2650, roughness: 0.4 }));
pad.position.set(1.7, 0.77, 0); body.add(pad);
const ringMat = new THREE.MeshStandardMaterial({ color: 0xff4fa3, emissive: 0xff4fa3, emissiveIntensity: 0.8 });
const ring = new THREE.Mesh(new THREE.TorusGeometry(0.22, 0.018, 10, 48), ringMat);
ring.rotation.x = Math.PI / 2; ring.position.set(1.7, 0.785, 0); body.add(ring);
// USB-C port
const port = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.09, 0.26), new THREE.MeshStandardMaterial({ color: 0x050409, roughness: 1 }));
port.position.set(2.47, -0.25, 0); body.add(port);
// wedge stand
const wedge = new THREE.Mesh(new THREE.BoxGeometry(5.0, 0.26, 1.5), new THREE.MeshStandardMaterial({ color: 0x1a1736, roughness: 0.7 }));
wedge.position.y = -0.78; meter.add(wedge);
const lipMat = new THREE.MeshStandardMaterial({ color: 0x1a3a2a, emissive: 0x000000 });
const lip = new THREE.Mesh(new THREE.BoxGeometry(0.36, 0.05, 0.02), lipMat);
lip.position.set(0, -0.78, 0.76); meter.add(lip);
const lipGlow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTex, color: 0x5be38f, transparent: true, opacity: 0, blending: THREE.AdditiveBlending, depthWrite: false }));
lipGlow.scale.set(1.2, 0.5, 1); lipGlow.position.set(0, -0.78, 0.85); meter.add(lipGlow);
// soft floor shadow
const sc = document.createElement("canvas"); sc.width = sc.height = 128;
const s2 = sc.getContext("2d"); const sg = s2.createRadialGradient(64, 64, 0, 64, 64, 64);
sg.addColorStop(0, "rgba(0,0,0,0.55)"); sg.addColorStop(1, "rgba(0,0,0,0)"); s2.fillStyle = sg; s2.fillRect(0, 0, 128, 128);
const floor = new THREE.Mesh(new THREE.PlaneGeometry(8, 3), new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(sc), transparent: true, depthWrite: false }));
floor.rotation.x = -Math.PI / 2; floor.position.y = -0.92; scene.add(floor);
const AMBER = new THREE.Color(0xffb22e), RED = new THREE.Color(0xff3b55), BLUE = new THREE.Color(0x4a83ff);
let level = 3;
function paint(t) {
const cam = level === "cam";
const phase = cam ? Math.floor(t * (reduce ? 1.5 : 5)) % 2 : 0;
stars.forEach(s => {
let on, col;
if (cam) { on = true; col = ((s.i + phase) % 2) ? BLUE : RED; }
else { on = s.i < level; col = AMBER; }
if (on) {
s.mat.color.setHex(0x000000); s.mat.emissive.copy(col); s.mat.emissiveIntensity = 0.85;
s.sp.material.color.copy(col); s.sp.material.opacity = 0.55;
} else {
s.mat.color.setHex(0x2b2650); s.mat.emissive.setHex(0x000000); s.sp.material.opacity = 0;
}
});
const free = level === 0;
lipMat.emissive.setHex(free ? 0x5be38f : 0x000000); lipMat.emissiveIntensity = free ? 1.4 : 0;
lipGlow.material.opacity = free ? 0.8 : 0;
}
// drag rotation
let targetY = -0.35, rotY = -0.35, dragging = false, lastX = 0, idle = 0, flipped = false;
canvas.addEventListener("pointerdown", e => { dragging = true; lastX = e.clientX; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener("pointermove", e => { if (!dragging) return; targetY += (e.clientX - lastX) * 0.01; lastX = e.clientX; idle = 0; });
const end = () => { dragging = false; };
canvas.addEventListener("pointerup", end); canvas.addEventListener("pointercancel", end);
const flipBtn = document.getElementById("flip");
flipBtn.addEventListener("click", () => {
flipped = !flipped; targetY += Math.PI; idle = 0;
flipBtn.textContent = flipped ? "Show front" : "Show back";
});
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight;
if (!w || !h) return;
renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
camera.position.z = w / h < 1.2 ? 10 : 8.2;
}
new ResizeObserver(resize).observe(canvas); resize();
const clock = new THREE.Clock();
function loop() {
const t = clock.getElapsedTime(); const dt = 0.016;
idle += dt;
let sway = 0;
if (!dragging && !reduce && idle > 2.5) sway = Math.sin(t * 0.45) * 0.28;
rotY += (targetY + sway - rotY) * 0.08;
meter.rotation.y = rotY;
if (!reduce) meter.position.y = Math.sin(t * 1.1) * 0.04;
paint(t);
renderer.render(scene, camera);
requestAnimationFrame(loop);
}
loop();
return { set: l => { level = l; } };
}
})();
</script>
</body></html>
```
[REQUESTED CHANGES]
(no specific request — apply your best judgment)
--- HOW TO RESPOND (READ FIRST) ---
Before writing any code, follow this exact process:
1. **ANALYZE** the widget source code provided above and identify:
a. Which Vibes SDK features it already uses (vibes.save, vibes.load, vibes.shared.join, etc.)
b. Which SDK features would genuinely benefit THIS specific widget — tailored to what it does, not a dump of everything available.
2. **PRESENT A NUMBERED LIST** covering:
- SDK features currently active in this widget
- New SDK features that would concretely improve this widget (be specific: why this widget, what it enables)
3. **WAIT** — do not write any code yet. Reply with your analysis and numbered list, then stop and ask the user which numbered items they want.
4. **IMPLEMENT ONLY** the items the user confirms, plus any explicit change they requested. Do not add unrequested features.
**IMPORTANT — Shared state room names:**
If you add vibes.shared.join(), do NOT use a hardcoded string literal as the room name (e.g. vibes.shared.join("lobby")) unless the user explicitly wants ALL viewers to share one single global state. A hardcoded room name means every person who visits this widget reads and writes the same shared state — it is a global room. For per-user or per-session isolation, derive the room name from a variable (e.g. a user ID, session token, or random value). When in doubt, use vibes.save/vibes.load for per-user persistence instead.
--- VIBES SDK CONTEXT ---
## Vibes SDK Reference
You are building an HTML widget for It Just Vibes (itjustvibes.com). The Vibes SDK is auto-injected — do NOT add a script tag. Just use `window.vibes` (or just `vibes`).
### Setup
Wrap your startup code in `vibes.onReady`:
```js
vibes.onReady(async () => {
const saved = await vibes.load("myKey");
// your widget logic here
});
```
### State (Per-User Persistence)
Every user gets their own isolated state per widget. All methods return Promises.
| Method | Description |
|--------|-------------|
| `await vibes.save(key, value)` | Save JSON-serializable data |
| `await vibes.load(key)` | Load saved data (returns `null` if not found) |
| `await vibes.delete(key)` | Delete a saved key |
| `await vibes.listKeys()` | Get array of all saved key names |
**Key rules:**
- Keys are strings, max 64 characters, alphanumeric + dashes/underscores
- Values must be JSON-serializable (objects, arrays, strings, numbers, booleans)
- Max 100KB per value, 500KB per widget per user, 5MB per user total
- Max 100 keys per widget per user
### Fetch Proxy
Use direct `fetch()` for APIs with permissive CORS headers (`Access-Control-Allow-Origin: *`). Use `vibes.fetch` for APIs without CORS headers (the proxy handles cross-origin requests):
```js
const resp = await vibes.fetch("https://api.example.com/data", {
method: "GET", // GET, POST, PUT, DELETE
headers: {}, // optional headers
body: null, // optional body (string)
timeout: null // optional timeout in ms
});
const data = await resp.json(); // or resp.text()
console.log(resp.status, resp.ok);
```
### API Keys (never put a key in your code)
The widget owner stores keys in **Manage → Secrets**. Never hard-code an API key — anyone can read widget source.
**Server keys** (OpenAI, Stripe secret, etc.): pass the secret's name as `useSecret`. The proxy adds the key to the request; your code never sees it. The owner must also add the API's domain under Allowed Domains.
```js
const resp = await vibes.fetch("https://api.openai.com/v1/models", { useSecret: "OPENAI_KEY" });
```
**Browser keys** (keys built to be public, e.g. a Google Maps JavaScript API key): some libraries need the key in the page. Read it at runtime — it resolves `null` if the owner hasn't set it, so always handle that case:
```js
vibes.onReady(async () => {
const key = typeof vibes.browserKey === "function" ? await vibes.browserKey("GOOGLE_MAPS_BROWSER_KEY") : null;
if (!key) { showSetupMessage(); return; } // e.g. "Owner: add a Browser secret in Manage → Secrets"
loadGoogleMaps(key);
});
```
For website-restricted keys (Google Maps), the owner restricts the key to `https://run.itjustvibes.com/*` and turns on **Manage → Settings → Send origin as referrer**. Browser keys are visible to every viewer; never use `browserKey` for a server key. Forks don't get the owner's keys.
### Multiplayer (Shared State)
Real-time shared state across all users viewing the same widget.
```js
// Join a room (call once at startup)
await vibes.shared.join("lobby", { persistent: true });
// Set shared state (broadcasts to all users)
await vibes.shared.set("score", { player1: 10, player2: 7 });
// Read shared state (synchronous, returns last known value)
const score = vibes.shared.get("score");
// Listen for changes to a specific key
vibes.shared.onChange("score", (newValue) => {
console.log("Score updated:", newValue);
});
// Listen for any shared state change
vibes.shared.onAny((key, value) => {
console.log(key, "changed to", value);
});
// Get number of connected users
const count = await vibes.shared.getUserCount();
// Leave room
vibes.shared.leave();
// Clear all shared state for this room
await vibes.shared.clear();
```
**Shared state options:**
- `{ persistent: true }` — state survives page reloads (stored server-side)
- Default room name is "__default__" if omitted
### Agent-Accessible State (vibes.ai.*)
State written with the standard `vibes.save` / `vibes.load` methods is private to each user and is **NOT readable by AI agents**. To share state with an AI agent (via the MCP connector), use the `vibes.ai` sub-namespace:
| Method | Description |
|--------|-------------|
| `await vibes.ai.setState(key, value)` | Write agent-readable state. Key is stored internally as `ai/<key>`. |
| `await vibes.ai.getState(key)` | Read agent-readable state. Returns `null` if key absent. |
| `await vibes.ai.listKeys()` | List all agent-readable keys (without the `ai/` prefix). |
**Important rules:**
- Regular `vibes.save()` / `vibes.load()` is **NOT agent-accessible** — use `vibes.ai.*` for state you want agents to read.
- The widget **owner** must enable agent access in **Manage → Agent tab** before any agent can read or write `vibes.ai.*` state.
- Keys are auto-prefixed to `ai/` internally; you supply just the short key (e.g. `'context'`).
```js
vibes.onReady(async () => {
// Write state an AI agent can later read
await vibes.ai.setState('context', { currentLevel: 3, score: 1500 });
// Read it back (same auto-prefix applies)
const ctx = await vibes.ai.getState('context');
// List all agent-accessible keys for this widget
const keys = await vibes.ai.listKeys(); // e.g. ['context']
});
```
### Rules
1. **Do NOT use localStorage, sessionStorage, or window.storage** — they are blocked or undefined in the sandbox. Use `vibes.save`/`vibes.load` instead.
2. **Do NOT add a script tag to import the SDK** — it is auto-injected.
3. **Wrap startup code in `vibes.onReady()`** — the SDK may not be ready immediately.
4. **Await all SDK calls** — every method (except `vibes.shared.get`) returns a Promise.
5. **External JS libraries are allowed and encouraged** — load them via a `<script src>` tag from a reputable CDN (jsDelivr, unpkg, or cdnjs) with a pinned version, or inline the library source. The only exception: do NOT add a script tag for the Vibes SDK itself — it is auto-injected (see Rule 2).
6. **Keep total code under 100KB** — that is the default widget size limit (your account limit may be higher).
### Rate Limits
| Operation | Limit |
|-----------|-------|
| Writes (save + delete combined) | 30/min |
| Reads (load) | 60/min |
| List keys | 30/min |
| Fetch proxy | Rate limited per widget |
### Error Handling
All SDK methods can reject. Wrap in try/catch:
```js
try {
await vibes.save("key", value);
} catch (err) {
console.error("Save failed:", err.message);
}
```
Fetch proxy errors include `err.code`: `"RATE_LIMITED"`, `"BLOCKED"`, `"FETCH_ERROR"`, and `"SECRET_NOT_FOUND"` (unknown or paused `useSecret` name). `vibes.browserKey` rejects only with `"RATE_LIMITED"` or `"FETCH_ERROR"`.
### Data Export
Export rows of data as CSV or Excel directly from your widget.
```js
// Register dataset for the platform's "Export data" button
// AND optionally trigger an immediate download
vibes.exportData(rows, { filename: 'results.csv' })
// rows: Array of arrays (each inner array is one row; first row = headers)
// options.filename: sets the download filename and format (csv or xlsx)
// options.directDownload: false to only register without downloading (default: true)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `filename` | string | `'data.csv'` | Download filename; extension determines format (`.csv` or `.xlsx`) |
| `directDownload` | boolean | `true` | Trigger immediate browser download in addition to registering the dataset |
**Notes:**
- First call registers the dataset so the widget chrome shows an "Export data" button
- Use `.csv` extension for CSV, `.xlsx` for Excel
- CSV injection safety is automatic (formula-starting cells prefixed with `'`)
- Data stays in the browser — no server round-trip
--- FORK & RESUBMIT INSTRUCTION ---
Return the complete, self-contained HTML file with all changes applied.
It should be ready to paste into itjustvibes.com/submit to create a new fork.
Include this comment at the top of the output:
/* Forked from: https://itjustvibes.com/Owner/busy-meter-gta-vi */