0
GAMES#b455075c
Mead Chug 2
@princess-peach·deposited 3w ago·updated 3w ago·36 views
GAMES#b455075c
Mead Chug 2
PR
@princess-peach
36Views
0Comments
0Forks
0Saves
SHARE · REMIX
Mead Chug 2 — a HTML games widget by @princess-peach.
CONTROLS
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: "Mead Chug 2" by @princess-peach]
Source: https://itjustvibes.com/princess-peach/mead-chug-2
Type: HTML
--- SOURCE CODE ---
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Mead Chug</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=UnifrakturMaguntia&family=Cormorant+Garamond:ital,wght@0,400;0,600;1,500&display=swap" rel="stylesheet">
<style>
:root {
--foam-top: #fff8e6;
--mead-top: #ffd866;
--mead-mid: #d99220;
--mead-bot: #5a3805;
--brass: #e3b347;
--brass-glow: #ffc961;
--parchment: #f5e2b8;
}
- {
box-sizing: border-box;
margin: 0; padding: 0;
-webkit-tap-highlight-color: transparent;
-webkit-user-select: none;
user-select: none;
}
html, body {
height: 100%;
overflow: hidden;
overscroll-behavior: none;
touch-action: none;
font-family: 'Cormorant Garamond', Georgia, serif;
background: #0a0503;
color: var(-parchment);
}
.void {
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 80% 30% at 50% 0%, rgba(60,30,10,0.6) 0%, transparent 70%),
linear-gradient(180deg, #150a04 0%, #050201 100%);
z-index: 0;
}
.grain {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 10;
opacity: 0.18;
mix-blend-mode: overlay;
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 240 240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.65 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
.liquid {
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 100% 50% at 30% 30%, rgba(255,240,160,0.18) 0%, transparent 60%),
linear-gradient(180deg,
var(-mead-top) 0%,
#f0b430 8%,
var(-mead-mid) 30%,
#a47008 60%,
var(-mead-bot) 100%);
clip-path: polygon(0 30%, 100% 30%, 100% 100%, 0 100%);
z-index: 2;
}
.liquid::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 60% 12% at 25% 35%, rgba(255,255,200,0.25), transparent 70%),
radial-gradient(ellipse 40% 8% at 75% 55%, rgba(255,240,160,0.18), transparent 70%);
animation: shimmer 9s ease-in-out infinite alternate;
pointer-events: none;
}
@keyframes shimmer {
from { transform: translate(-2%, 0) scale(1); }
to { transform: translate(2%, -1%) scale(1.05); }
}
.liquid::after {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(90deg, rgba(0,0,0,0.32) 0%, transparent 12%, transparent 88%, rgba(0,0,0,0.32) 100%);
pointer-events: none;
}
.bubbles {
position: absolute;
inset: 0;
pointer-events: none;
}
.bubble {
position: absolute;
border-radius: 50%;
background: radial-gradient(circle at 32% 30%,
rgba(255,255,255,0.9) 0%,
rgba(255,255,255,0.4) 35%,
rgba(255,240,180,0.1) 65%,
transparent 85%);
box-shadow: inset 0 0 2px rgba(255,255,255,0.5);
}
@keyframes rise {
0% { transform: translate(0, 30px) scale(0.6); opacity: 0; }
8% { opacity: 0.9; }
100% { transform: translate(var(-drift, 0), -120vh) scale(1.1); opacity: 0; }
}
.foam {
position: fixed;
left: -20%;
right: -20%;
width: 140%;
height: 56px;
top: 30%;
transform-origin: 50% 50%;
will-change: transform, top;
z-index: 3;
pointer-events: none;
}
.foam-bumps {
position: absolute;
left: 0; right: 0; top: 0;
height: 32px;
background:
radial-gradient(circle at 4% 70%, var(-foam-top) 11px, transparent 12px),
radial-gradient(circle at 12% 35%, var(-foam-top) 14px, transparent 15px),
radial-gradient(circle at 22% 65%, var(-foam-top) 12px, transparent 13px),
radial-gradient(circle at 32% 25%, var(-foam-top) 16px, transparent 17px),
radial-gradient(circle at 42% 60%, var(-foam-top) 13px, transparent 14px),
radial-gradient(circle at 52% 30%, var(-foam-top) 15px, transparent 16px),
radial-gradient(circle at 62% 65%, var(-foam-top) 12px, transparent 13px),
radial-gradient(circle at 72% 28%, var(-foam-top) 14px, transparent 15px),
radial-gradient(circle at 82% 60%, var(-foam-top) 13px, transparent 14px),
radial-gradient(circle at 90% 35%, var(-foam-top) 15px, transparent 16px),
radial-gradient(circle at 97% 65%, var(-foam-top) 11px, transparent 12px);
filter: drop-shadow(0 -2px 2px rgba(255,255,255,0.4))
drop-shadow(0 3px 6px rgba(120,80,20,0.5));
}
.foam-body {
position: absolute;
left: 0; right: 0;
top: 22px;
height: 34px;
background:
linear-gradient(180deg,
#fff5d8 0%,
#fbe9b8 50%,
#d9bf78 100%);
box-shadow:
inset 0 2px 4px rgba(255,255,255,0.55),
inset 0 -3px 6px rgba(150,110,40,0.4);
}
.foam-fizz {
position: absolute;
inset: 22px 0 0 0;
background:
radial-gradient(circle at 8% 45%, rgba(255,255,255,0.7) 1.5px, transparent 2px),
radial-gradient(circle at 18% 75%, rgba(255,255,255,0.6) 1px, transparent 2px),
radial-gradient(circle at 28% 50%, rgba(255,255,255,0.7) 1.5px, transparent 2px),
radial-gradient(circle at 36% 85%, rgba(255,255,255,0.5) 1px, transparent 2px),
radial-gradient(circle at 48% 55%, rgba(255,255,255,0.7) 1.5px, transparent 2px),
radial-gradient(circle at 58% 70%, rgba(255,255,255,0.5) 1px, transparent 2px),
radial-gradient(circle at 68% 50%, rgba(255,255,255,0.7) 1.5px, transparent 2px),
radial-gradient(circle at 78% 80%, rgba(255,255,255,0.5) 1px, transparent 2px),
radial-gradient(circle at 88% 55%, rgba(255,255,255,0.6) 1.5px, transparent 2px);
}
.hud {
position: fixed;
inset: 0;
z-index: 5;
pointer-events: none;
display: flex;
flex-direction: column;
align-items: center;
padding: max(env(safe-area-inset-top), 16px) 20px max(env(safe-area-inset-bottom), 20px);
}
.hud-top { text-align: center; }
.hud-title {
font-family: 'UnifrakturMaguntia', serif;
font-size: clamp(22px, 6.5vw, 32px);
color: var(-brass-glow);
letter-spacing: 3px;
text-shadow:
0 2px 0 #2a1808,
0 4px 14px rgba(0,0,0,0.85),
0 0 24px rgba(255,156,64,0.5);
line-height: 1;
}
.hud-sub {
font-style: italic;
font-size: 11px;
color: rgba(245,226,184,0.7);
margin-top: 4px;
letter-spacing: 3px;
text-transform: uppercase;
text-shadow: 0 1px 2px rgba(0,0,0,0.9);
}
.hud-spacer { flex: 1; }
.hud-bottom {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.timer {
font-family: 'UnifrakturMaguntia', serif;
font-size: 40px;
color: var(-brass-glow);
font-variant-numeric: tabular-nums;
letter-spacing: 2px;
text-shadow:
0 2px 0 #2a1808,
0 0 20px rgba(255,201,97,0.7),
0 4px 12px rgba(0,0,0,0.9);
min-width: 130px;
text-align: center;
}
.hint {
font-size: 13px;
color: rgba(245,226,184,0.7);
font-style: italic;
letter-spacing: 1px;
text-shadow: 0 1px 2px rgba(0,0,0,0.85);
min-height: 18px;
text-align: center;
}
button {
pointer-events: auto;
font-family: 'UnifrakturMaguntia', serif;
font-size: 24px;
letter-spacing: 2px;
padding: 16px 40px;
background:
linear-gradient(180deg, #f0c860 0%, #c89020 45%, #8a5808 100%);
color: #2a1808;
border: 2px solid #2a1808;
border-radius: 12px;
box-shadow:
inset 0 2px 0 rgba(255,250,210,0.7),
inset 0 -3px 8px rgba(80,40,0,0.4),
0 6px 20px rgba(0,0,0,0.7),
0 0 40px rgba(255,156,64,0.2);
cursor: pointer;
transition: transform 0.08s, box-shadow 0.08s;
text-shadow: 0 1px 0 rgba(255,250,210,0.5);
}
button:active {
transform: translateY(2px);
box-shadow:
inset 0 2px 0 rgba(255,250,210,0.7),
inset 0 -2px 4px rgba(80,40,0,0.4),
0 3px 10px rgba(0,0,0,0.7);
}
.drinking .timer {
animation: timer-pulse 0.25s ease infinite alternate;
}
@keyframes timer-pulse {
from { text-shadow: 0 2px 0 #2a1808, 0 0 20px rgba(255,201,97,0.7), 0 4px 12px rgba(0,0,0,0.9); }
to { text-shadow: 0 2px 0 #2a1808, 0 0 36px rgba(255,201,97,1), 0 4px 12px rgba(0,0,0,0.9); }
}
.overlay {
position: fixed;
inset: 0;
z-index: 20;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 36px;
opacity: 0;
pointer-events: none;
transition: opacity 0.5s;
background: radial-gradient(ellipse at center, rgba(40,20,8,0.65) 0%, rgba(8,4,2,0.96) 75%);
}
.overlay.show {
opacity: 1;
pointer-events: auto;
}
.overlay h1 {
font-family: 'UnifrakturMaguntia', serif;
font-size: clamp(44px, 14vw, 72px);
color: var(-brass-glow);
text-shadow: 0 0 36px rgba(255,180,80,0.6), 0 4px 14px rgba(0,0,0,0.9);
letter-spacing: 5px;
line-height: 1;
margin-bottom: 6px;
}
.deco {
color: var(-brass);
font-size: 18px;
letter-spacing: 6px;
opacity: 0.5;
margin-bottom: 22px;
}
.overlay p {
color: var(-parchment);
font-size: 18px;
font-style: italic;
max-width: 340px;
line-height: 1.55;
margin-bottom: 32px;
letter-spacing: 0.5px;
}
.overlay h2 {
font-family: 'UnifrakturMaguntia', serif;
font-size: clamp(80px, 26vw, 150px);
color: var(-brass-glow);
text-shadow: 0 0 40px rgba(255,180,80,0.8), 0 6px 16px rgba(0,0,0,0.9);
letter-spacing: 6px;
animation: cheer-pop 0.9s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes cheer-pop {
0% { transform: scale(0.2) rotate(-12deg); opacity: 0; }
55% { transform: scale(1.15) rotate(3deg); opacity: 1; }
100% { transform: scale(1) rotate(0); opacity: 1; }
}
.result {
font-size: 22px;
color: var(-parchment);
margin-top: 14px;
font-style: italic;
letter-spacing: 1px;
}
.best {
font-size: 16px;
color: rgba(245,226,184,0.75);
margin-top: 6px;
letter-spacing: 1px;
min-height: 22px;
}
.overlay button { margin-top: 28px; }
.spark {
position: fixed;
bottom: 0;
width: 8px;
height: 8px;
border-radius: 50%;
background: radial-gradient(circle, #ffd970, rgba(255,180,80,0.4) 55%, transparent 80%);
pointer-events: none;
z-index: 30;
animation: spark-rise 2.4s ease-out forwards;
}
@keyframes spark-rise {
0% { transform: translate(0, 30px) scale(0); opacity: 0; }
15% { transform: translate(var(-sx, 0), -50px) scale(1); opacity: 1; }
100% { transform: translate(var(-sx2, 0), -115vh) scale(0.3); opacity: 0; }
}
.hidden { display: none !important; }
</style>
</head>
<body>
<div class="void"></div>
<div class="liquid" id="liquid">
<div class="bubbles" id="bubbles"></div>
</div>
<div class="foam" id="foam">
<div class="foam-bumps"></div>
<div class="foam-body"></div>
<div class="foam-fizz"></div>
</div>
<div class="grain"></div>
<div class="hud" id="hud">
<div class="hud-top">
<div class="hud-title">Mead Chug</div>
<div class="hud-sub">⚜ tilt to drink ⚜</div>
</div>
<div class="hud-spacer"></div>
<div class="hud-bottom">
<div class="timer" id="timer">0.00s</div>
<div class="hint" id="hint">Tilt phone sideways past 35°</div>
</div>
</div>
<div class="overlay show" id="startOverlay">
<h1>Mead Chug</h1>
<div class="deco">❦ ✦ ❦</div>
<p>Hoist thy phone. Tilt it sharply to one side -- left or right -- to drink the mead. Be swift, the bards are watching.</p>
<button id="startBtn">Begin the Feast</button>
</div>
<div class="overlay" id="cheersOverlay">
<h2>Skål!</h2>
<div class="result" id="finalTime">0.00 seconds</div>
<div class="best" id="bestTime"></div>
<button id="refillBtn">Pour Another</button>
</div>
<script>
(() => {
const liquid = document.getElementById('liquid');
const foam = document.getElementById('foam');
const bubblesEl = document.getElementById('bubbles');
const timerEl = document.getElementById('timer');
const hintEl = document.getElementById('hint');
const hud = document.getElementById('hud');
const startOv = document.getElementById('startOverlay');
const cheersOv = document.getElementById('cheersOverlay');
const startBtn = document.getElementById('startBtn');
const refillBtn = document.getElementById('refillBtn');
const finalTimeEl = document.getElementById('finalTime');
const bestEl = document.getElementById('bestTime');
let level = 92;
let isPlaying = false;
let startTime = 0;
let elapsed = 0;
let rafId = null;
let lastGlugTime = 0;
let audioCtx = null;
let currentGamma = 0;
let keyTilt = 0;
let best = null;
// Single axis only. High threshold = no accidental sips.
const DRINK_THRESHOLD = 35;
const VISUAL_TILT_CAP = 28;
function createBubbles() {
for (let i = 0; i < 22; i++) {
const b = document.createElement('div');
b.className = 'bubble';
const size = 3 + Math.random() * 9;
b.style.width = b.style.height = size + 'px';
b.style.left = (3 + Math.random() * 94) + '%';
b.style.top = (40 + Math.random() * 55) + '%';
b.style.setProperty('--drift', (Math.random() * 50 - 25) + 'px');
b.style.animation = `rise ${5 + Math.random() * 5}s linear ${Math.random() * 6}s infinite`;
bubblesEl.appendChild(b);
}
}
createBubbles();
function ensureAudio() {
if (!audioCtx) {
const AC = window.AudioContext || window.webkitAudioContext;
if (AC) audioCtx = new AC();
}
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
return audioCtx;
}
function playGlug() {
const ctx = ensureAudio();
if (!ctx) return;
const t = ctx.currentTime;
const osc = ctx.createOscillator();
osc.type = 'sine';
const startFreq = 520 + Math.random() * 380;
osc.frequency.setValueAtTime(startFreq, t);
osc.frequency.exponentialRampToValueAtTime(65 + Math.random() * 45, t + 0.14);
const og = ctx.createGain();
og.gain.setValueAtTime(0, t);
og.gain.linearRampToValueAtTime(0.4, t + 0.008);
og.gain.exponentialRampToValueAtTime(0.001, t + 0.24);
osc.connect(og).connect(ctx.destination);
osc.start(t); osc.stop(t + 0.3);
const sz = Math.floor(ctx.sampleRate * 0.14);
const buf = ctx.createBuffer(1, sz, ctx.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < sz; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / sz);
const n = ctx.createBufferSource();
n.buffer = buf;
const bp = ctx.createBiquadFilter();
bp.type = 'bandpass';
bp.frequency.value = 220 + Math.random() * 220;
bp.Q.value = 4;
const ng = ctx.createGain();
ng.gain.setValueAtTime(0.2, t);
ng.gain.exponentialRampToValueAtTime(0.001, t + 0.13);
n.connect(bp).connect(ng).connect(ctx.destination);
n.start(t);
if (navigator.vibrate) navigator.vibrate(30);
}
function playCheers() {
const ctx = ensureAudio();
if (!ctx) return;
const t = ctx.currentTime;
[523.25, 659.25, 783.99, 1046.50].forEach((f, i) => {
const o = ctx.createOscillator();
o.type = 'triangle';
o.frequency.value = f;
const g = ctx.createGain();
const s = t + i * 0.09;
g.gain.setValueAtTime(0, s);
g.gain.linearRampToValueAtTime(0.22, s + 0.02);
g.gain.exponentialRampToValueAtTime(0.001, s + 1.4);
o.connect(g).connect(ctx.destination);
o.start(s); o.stop(s + 1.5);
});
if (navigator.vibrate) navigator.vibrate([60, 60, 60, 60, 250]);
}
// Only gamma -- single axis
function handleOrientation(event) {
currentGamma = event.gamma || 0;
}
async function requestOrientationPermission() {
if (typeof DeviceOrientationEvent !== 'undefined' &&
typeof DeviceOrientationEvent.requestPermission === 'function') {
try {
const perm = await DeviceOrientationEvent.requestPermission();
if (perm === 'granted') {
window.addEventListener('deviceorientation', handleOrientation);
return true;
}
hintEl.textContent = 'Motion denied -- use ←/→ arrows';
return false;
} catch (e) {
hintEl.textContent = 'Use ←/→ arrows to drink';
return false;
}
} else {
window.addEventListener('deviceorientation', handleOrientation);
return true;
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') { keyTilt = -55; ensureAudio(); }
if (e.key === 'ArrowRight') { keyTilt = 55; ensureAudio(); }
});
document.addEventListener('keyup', () => { keyTilt = 0; });
function renderSurface() {
const rawTilt = keyTilt || currentGamma;
const tilt = Math.max(-VISUAL_TILT_CAP, Math.min(VISUAL_TILT_CAP, rawTilt));
const w = window.innerWidth;
const h = window.innerHeight || 1;
const angleRad = tilt * Math.PI / 180;
const dyPx = Math.tan(angleRad) * w / 2;
const dyPct = (dyPx / h) * 100;
const topPct = 100 - level;
const leftTop = topPct + dyPct;
const rightTop = topPct - dyPct;
liquid.style.clipPath =
`polygon(0 ${leftTop}%, 100% ${rightTop}%, 100% 100%, 0 100%)`;
const centerTop = (leftTop + rightTop) / 2;
foam.style.top = centerTop + '%';
const risePx = (rightTop - leftTop) / 100 * h;
const foamAngleDeg = Math.atan2(risePx, w) * 180 / Math.PI;
foam.style.transform = `translateY(-50%) rotate(${foamAngleDeg}deg)`;
}
function tick(now) {
if (!isPlaying) { renderSurface(); return; }
elapsed = (now - startTime) / 1000;
timerEl.textContent = elapsed.toFixed(2) + 's';
const rawTilt = keyTilt || currentGamma;
const absTilt = Math.abs(rawTilt);
if (absTilt > DRINK_THRESHOLD && level > 0) {
const factor = Math.min(1, (absTilt - DRINK_THRESHOLD) / 30);
const drinkRate = 6 + factor * 28;
const dt = 1 / 60;
level = Math.max(0, level - drinkRate * dt);
const interval = 340 - factor * 160;
if (now - lastGlugTime > interval) {
playGlug();
lastGlugTime = now;
}
hud.classList.add('drinking');
hintEl.textContent = 'CHUG! CHUG! CHUG!';
if (level <= 0) { finish(); return; }
} else {
hud.classList.remove('drinking');
if (absTilt > 8) {
hintEl.textContent = `Tilt more -- ${Math.round(absTilt)}° / ${DRINK_THRESHOLD}°`;
} else {
hintEl.textContent = 'Tilt phone sideways past 35°';
}
}
renderSurface();
rafId = requestAnimationFrame(tick);
}
function startGame() {
level = 92;
isPlaying = true;
startTime = performance.now();
elapsed = 0;
lastGlugTime = 0;
cheersOv.classList.remove('show');
timerEl.textContent = '0.00s';
rafId = requestAnimationFrame(tick);
}
function finish() {
isPlaying = false;
cancelAnimationFrame(rafId);
hud.classList.remove('drinking');
finalTimeEl.textContent = elapsed.toFixed(2) + ' seconds';
if (best === null || elapsed < best) {
best = elapsed;
saveBest(elapsed);
bestEl.textContent = '⚜ A new record! ⚜';
} else {
bestEl.textContent = `Best: ${best.toFixed(2)}s`;
}
cheersOv.classList.add('show');
playCheers();
celebrate();
}
function celebrate() {
for (let i = 0; i < 28; i++) {
const s = document.createElement('div');
s.className = 'spark';
s.style.left = Math.random() * 100 + '%';
s.style.setProperty('--sx', (Math.random() * 80 - 40) + 'px');
s.style.setProperty('--sx2', (Math.random() * 160 - 80) + 'px');
s.style.animationDelay = (Math.random() * 0.6) + 's';
document.body.appendChild(s);
setTimeout(() => s.remove(), 3400);
}
}
async function loadBest() {
try {
if (window.storage && window.storage.get) {
const r = await window.storage.get('mead-best');
if (r && r.value) {
const v = parseFloat(r.value);
if (!isNaN(v)) best = v;
}
}
} catch (e) {}
}
async function saveBest(t) {
try {
if (window.storage && window.storage.set) {
await window.storage.set('mead-best', String(t));
}
} catch (e) {}
}
loadBest();
startBtn.addEventListener('click', async () => {
ensureAudio();
await requestOrientationPermission();
startOv.classList.remove('show');
setTimeout(() => startOv.classList.add('hidden'), 500);
startGame();
});
refillBtn.addEventListener('click', () => {
ensureAudio();
cheersOv.classList.remove('show');
startGame();
});
renderSurface();
(function preLoop() {
if (!isPlaying) renderSurface();
requestAnimationFrame(preLoop);
})();
window.addEventListener('resize', renderSurface);
})();
</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);
```
### 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"`.
### 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/princess-peach/mead-chug-2 */