0
ART-VISUAL#655e4785
Freedom Fireworks Studio
@Owner·deposited 0mo ago·updated 0mo ago·32 views
ART-VISUAL#655e4785
Freedom Fireworks Studio
OW
@Owner
32Views
0Comments
0Forks
0Saves
SHARE · REMIX
Freedom Fireworks Studio — a HTML art-visual widget by @Owner.
CONTROLS
4th of Julyfireworkspartysummerholiday
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: "Freedom Fireworks Studio" by @Owner]
Source: https://itjustvibes.com/Owner/freedom-fireworks-studio
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" />
<title>Freedom Fireworks Studio</title>
<style>
:root{
--bg:#07111f;--panel:#0d1d35;--panel2:#102747;--text:#f8fbff;--muted:#9fb6d7;--red:#ff405d;--blue:#3aa8ff;--white:#ffffff;--gold:#ffd166;--green:#42e6a4;--shadow:0 24px 70px rgba(0,0,0,.4);--radius:24px;
}
*{box-sizing:border-box}html,body{margin:0;min-height:100%;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:radial-gradient(circle at 15% 10%,rgba(58,168,255,.28),transparent 28%),radial-gradient(circle at 80% 5%,rgba(255,64,93,.26),transparent 25%),linear-gradient(145deg,#050913 0%,#07111f 48%,#111827 100%);color:var(--text);overflow-x:hidden}
body:before{content:"";position:fixed;inset:0;pointer-events:none;background-image:linear-gradient(rgba(255,255,255,.04) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.035) 1px,transparent 1px);background-size:44px 44px;mask-image:linear-gradient(to bottom,#000,transparent 85%);z-index:0}.app{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:18px 18px 28px}.hero{position:relative;overflow:hidden;min-height:420px;border:1px solid rgba(255,255,255,.12);border-radius:32px;background:linear-gradient(135deg,rgba(13,29,53,.88),rgba(10,20,37,.72));box-shadow:var(--shadow)}
.hero:after{content:"";position:absolute;inset:auto -20% -44% -20%;height:260px;background:conic-gradient(from 180deg,var(--red),var(--white),var(--blue),var(--gold),var(--red));filter:blur(90px);opacity:.22}.hero-grid{position:relative;display:grid;grid-template-columns:1fr 1.05fr;min-height:420px;z-index:2}.hero-copy{padding:38px 34px 30px;display:flex;flex-direction:column;justify-content:space-between;gap:28px}.eyebrow{display:inline-flex;align-items:center;gap:9px;width:max-content;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:#dcecff;border-radius:999px;padding:9px 13px;font-size:13px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.pulse{width:9px;height:9px;border-radius:50%;background:var(--red);box-shadow:0 0 0 0 rgba(255,64,93,.68);animation:pulse 1.8s infinite}@keyframes pulse{70%{box-shadow:0 0 0 11px rgba(255,64,93,0)}100%{box-shadow:0 0 0 0 rgba(255,64,93,0)}}h1{font-size:clamp(42px,7vw,84px);line-height:.9;margin:16px 0 12px;letter-spacing:-.07em}.gradient{background:linear-gradient(90deg,var(--red),#fff,var(--blue));-webkit-background-clip:text;background-clip:text;color:transparent;text-shadow:none}.lead{margin:0;max-width:560px;color:#c7d7ed;font-size:clamp(16px,2vw,19px);line-height:1.55}.actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:24px}.btn{border:0;border-radius:16px;padding:13px 16px;font-weight:900;color:#07111f;background:#fff;cursor:pointer;box-shadow:0 14px 35px rgba(0,0,0,.22);transition:transform .15s ease,filter .15s ease,background .2s ease}.btn:hover{transform:translateY(-2px);filter:brightness(1.05)}.btn.primary{color:#fff;background:linear-gradient(135deg,var(--red),var(--blue))}.btn.ghost{color:#dcecff;background:rgba(255,255,255,.09);border:1px solid rgba(255,255,255,.14);box-shadow:none}.btn.small{padding:9px 12px;border-radius:12px;font-size:13px}.scoreboard{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.score{padding:13px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.06);border-radius:18px}.score b{display:block;font-size:22px}.score span{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;font-weight:800}.sky{position:relative;min-height:420px;border-left:1px solid rgba(255,255,255,.08);background:radial-gradient(circle at 50% 40%,rgba(255,255,255,.08),transparent 32%)}canvas{position:absolute;inset:0;width:100%;height:100%;display:block}.tap-hint{position:absolute;right:18px;bottom:18px;z-index:3;padding:12px 14px;border-radius:16px;background:rgba(3,7,18,.58);border:1px solid rgba(255,255,255,.12);backdrop-filter:blur(14px);color:#dbeafe;font-weight:800}.section-grid{display:grid;grid-template-columns:1.04fr .96fr;gap:18px;margin-top:18px}.card{border:1px solid rgba(255,255,255,.11);background:linear-gradient(180deg,rgba(16,39,71,.8),rgba(9,20,37,.74));box-shadow:var(--shadow);border-radius:var(--radius);padding:20px}.card h2{margin:0 0 8px;font-size:24px;letter-spacing:-.03em}.card p{color:var(--muted);line-height:1.5}.controls{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:16px}.field{display:flex;flex-direction:column;gap:7px}.field label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#c5d8f4;font-weight:900}input,select{width:100%;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:#fff;border-radius:14px;padding:12px;font-weight:800;outline:none}select option{color:#06101f}input[type=range]{accent-color:var(--blue);padding:0}.meter-wrap{height:18px;border-radius:999px;overflow:hidden;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.11);margin:16px 0}.meter{height:100%;width:50%;border-radius:999px;background:linear-gradient(90deg,var(--red),var(--white),var(--blue));transition:width .3s ease}.result{min-height:118px;border-radius:20px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.11);padding:16px;margin-top:14px}.result strong{color:#fff}.result .big{font-size:30px;display:block;margin-bottom:7px}.checklist{display:grid;gap:10px;margin-top:14px}.check{display:flex;align-items:center;gap:12px;padding:13px;border-radius:17px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.06);cursor:pointer;user-select:none}.box{display:grid;place-items:center;flex:0 0 24px;width:24px;height:24px;border-radius:8px;border:2px solid rgba(255,255,255,.36);font-weight:900;color:#07111f;background:transparent}.check.done .box{background:linear-gradient(135deg,var(--red),var(--blue));border-color:transparent;color:white}.check.done .text{text-decoration:line-through;color:#8ea4c1}.text{font-weight:850}.mini{font-size:12px;color:var(--muted);margin-left:auto}.chips{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0}.chip{border:1px solid rgba(255,255,255,.13);background:rgba(255,255,255,.07);color:#dcecff;border-radius:999px;padding:8px 10px;font-size:13px;font-weight:850}.timeline{display:grid;gap:9px;margin-top:14px}.time-row{display:grid;grid-template-columns:78px 1fr;gap:12px;align-items:center;padding:12px;border-radius:16px;background:rgba(255,255,255,.055);border:1px solid rgba(255,255,255,.09)}.time-row b{color:#fff}.time-row span{color:#bdd0e9}.toast{position:fixed;left:50%;bottom:18px;transform:translateX(-50%) translateY(22px);opacity:0;pointer-events:none;background:rgba(3,7,18,.82);color:#fff;border:1px solid rgba(255,255,255,.14);border-radius:16px;padding:12px 14px;box-shadow:var(--shadow);z-index:20;transition:.22s ease}.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}.footer-note{text-align:center;color:#88a2c7;font-size:12px;margin:18px 0 0}.sparkle{position:absolute;width:5px;height:5px;border-radius:50%;background:#fff;opacity:.7;animation:fall linear infinite}.sparkle:nth-child(1){left:12%;animation-duration:6s}.sparkle:nth-child(2){left:28%;animation-duration:8s;animation-delay:1s}.sparkle:nth-child(3){left:62%;animation-duration:7s;animation-delay:2s}.sparkle:nth-child(4){left:82%;animation-duration:9s;animation-delay:.8s}@keyframes fall{from{top:-8px;transform:translateY(0) scale(.8)}to{top:110%;transform:translateY(20px) scale(1.5);opacity:0}}@media(max-width:860px){.hero-grid,.section-grid{grid-template-columns:1fr}.sky{min-height:360px;border-left:0;border-top:1px solid rgba(255,255,255,.08)}.hero-copy{padding:28px 22px}.scoreboard{grid-template-columns:1fr 1fr 1fr}.controls{grid-template-columns:1fr}.app{padding:12px}.hero{border-radius:24px}.card{padding:17px}}@media(max-width:520px){.actions{display:grid}.btn{width:100%}.scoreboard{grid-template-columns:1fr}.time-row{grid-template-columns:64px 1fr}h1{font-size:46px}}
</style>
</head>
<body>
<div class="app" id="app">
<section class="hero">
<i class="sparkle"></i><i class="sparkle"></i><i class="sparkle"></i><i class="sparkle"></i>
<div class="hero-grid">
<div class="hero-copy">
<div>
<div class="eyebrow"><span class="pulse"></span> Fourth of July mode</div>
<h1>Freedom <span class="gradient">Fireworks</span> Studio</h1>
<p class="lead">Tap the sky to launch fireworks, tune the party vibe, and build a quick cookout plan that saves to your It Just Vibes state.</p>
<div class="actions">
<button class="btn primary" id="launchBurst">Launch grand finale</button>
<button class="btn ghost" id="randomPlan">Spark a party plan</button>
<button class="btn ghost" id="soundToggle">Sound: off</button>
</div>
</div>
<div class="scoreboard">
<div class="score"><b id="burstCount">0</b><span>Bursts</span></div>
<div class="score"><b id="vibeScore">76%</b><span>Vibe score</span></div>
<div class="score"><b id="doneCount">0/6</b><span>Ready list</span></div>
</div>
</div>
<div class="sky" id="sky">
<canvas id="fireworks"></canvas>
<div class="tap-hint">Tap / click the sky ✦</div>
</div>
</div>
</section>
<div class="section-grid">
<section class="card">
<h2>Party command center</h2>
<p>Adjust the sliders and the app builds a tiny "host brief" for your cookout, fireworks watch, or backyard hang.</p>
<div class="controls">
<div class="field"><label for="guests">Guests</label><input id="guests" type="range" min="2" max="40" value="12" /></div>
<div class="field"><label for="intensity">Firework intensity</label><input id="intensity" type="range" min="1" max="10" value="6" /></div>
<div class="field"><label for="cookout">Cookout style</label><select id="cookout"><option>Classic grill</option><option>Potluck heroes</option><option>Beach cooler</option><option>Porch snacks</option><option>Late-night desserts</option></select></div>
<div class="field"><label for="palette">Sky palette</label><select id="palette"><option value="patriot">Red, white & blue</option><option value="gold">Gold finale</option><option value="neon">Neon parade</option><option value="random">Surprise me</option></select></div>
</div>
<div class="meter-wrap"><div class="meter" id="meter"></div></div>
<div class="result" id="brief"></div>
<div class="chips" id="chips"></div>
</section>
<section class="card">
<h2>Host-ready checklist</h2>
<p>Tap items as you knock them out. Progress persists for each user on the widget.</p>
<div class="checklist" id="checklist"></div>
</section>
</div>
<div class="section-grid">
<section class="card">
<h2>Instant timeline</h2>
<p>A simple flow that works for a casual holiday hang without overplanning it.</p>
<div class="timeline" id="timeline"></div>
</section>
<section class="card">
<h2>Vibe card</h2>
<p>Generate a reusable mini plan for the group chat.</p>
<div class="result" id="vibeCard"></div>
<div class="actions"><button class="btn primary" id="newVibe">Generate new card</button><button class="btn ghost" id="copyVibe">Copy card</button></div>
</section>
</div>
<p class="footer-note">Built for summer nights, safe celebrations, and wildly unnecessary amounts of sparkle.</p>
</div>
<div class="toast" id="toast">Saved</div>
<script>
(function(){
"use strict";
const fallbackStore = {};
const sdk = window.vibes || {
onReady: cb => Promise.resolve().then(cb),
load: async key => fallbackStore[key] || null,
save: async (key, value) => { fallbackStore[key] = value; },
delete: async key => { delete fallbackStore[key]; },
listKeys: async () => Object.keys(fallbackStore)
};
const KEY = "july4_state";
const $ = id => document.getElementById(id);
const state = {
bursts:0, guests:12, intensity:6, cookout:"Classic grill", palette:"patriot", sound:false,
checks:[false,false,false,false,false,false], vibeIndex:0
};
const checklistItems = [
["Cooler stocked","Ice, water, and backup drinks"],
["Grill zone ready","Tools, fuel, tray, and safe distance"],
["Bug plan","Spray, citronella, or fans"],
["Playlist queued","One patriotic banger, then no rules"],
["Sunset seats","Chairs, blankets, porch spots"],
["Clean exit","Trash bags and a last sweep"]
];
const palettes = {
patriot:["#ff405d","#ffffff","#3aa8ff","#ffd166"],
gold:["#ffd166","#fff3b0","#fca311","#ff7b00"],
neon:["#ff2bd6","#39ff14","#00e5ff","#faff00"],
random:["#ff405d","#ffffff","#3aa8ff","#ffd166","#42e6a4","#c084fc","#fb7185"]
};
const vibeCards = [
"Backyard glow-up: grill first, sparklers at sunset, dessert after the big finale.",
"Porch parade: snacks on arrival, playlist in the middle, fireworks watch when the sky turns navy.",
"Easy host mode: potluck mains, cooler station, lawn games, and a no-stress cleanup basket.",
"Late summer cinema: burgers, blankets, glow sticks, then a patriotic movie under the stars.",
"Golden-hour hang: lemonade, hot dogs, corn, watermelon, and the loudest grand finale allowed."
];
const safeSave = (() => { let t; return function(){ clearTimeout(t); t = setTimeout(async () => { try { await sdk.save(KEY, state); } catch(e){} }, 120); }; })();
function toast(msg){ const el=$("toast"); el.textContent=msg; el.classList.add("show"); setTimeout(()=>el.classList.remove("show"),1300); }
function clamp(n,min,max){ return Math.max(min,Math.min(max,n)); }
function score(){ const done=state.checks.filter(Boolean).length; return clamp(Math.round(36 + state.guests*.85 + state.intensity*4 + done*6), 10, 100); }
function renderChecklist(){
const wrap=$("checklist"); wrap.innerHTML="";
checklistItems.forEach((item,i)=>{
const row=document.createElement("div"); row.className="check"+(state.checks[i]?" done":""); row.innerHTML='<span class="box">'+(state.checks[i]?"✓":"")+'</span><span class="text">'+item[0]+'</span><span class="mini">'+item[1]+'</span>';
row.addEventListener("click",()=>{state.checks[i]=!state.checks[i]; renderAll(); safeSave();});
wrap.appendChild(row);
});
}
function renderBrief(){
const done=state.checks.filter(Boolean).length, sc=score();
$("guests").value=state.guests; $("intensity").value=state.intensity; $("cookout").value=state.cookout; $("palette").value=state.palette;
$("burstCount").textContent=String(state.bursts); $("vibeScore").textContent=sc+"%"; $("doneCount").textContent=done+"/6"; $("meter").style.width=sc+"%";
const food=Math.ceil(state.guests*1.25), drinks=Math.ceil(state.guests*2.5), glow=state.intensity>6?"grand finale energy":"chill sparkle energy";
$("brief").innerHTML='<span class="big">'+state.guests+' guests · '+sc+'% ready</span><strong>'+state.cookout+'</strong> with '+food+' food portions, about '+drinks+' drinks, and '+glow+'. Keep a water station visible and give the sky show a clear viewing zone.';
const chips=[state.guests+" people", state.intensity+"/10 sparkle", state.cookout, (palettes[state.palette]||palettes.patriot).length+" sky colors"];
$("chips").innerHTML=chips.map(c=>'<span class="chip">'+c+'</span>').join("");
}
function renderTimeline(){
const rows=[["5:30","Guests arrive, cooler opens, easy snacks out."],["6:15","Grill or potluck table goes live."],["7:15","Lawn game, porch chat, or music reset."],["8:30","Glow sticks, dessert, and sunset seats."],["9:15","Fireworks watch with cleanup bags ready."]];
$("timeline").innerHTML=rows.map(r=>'<div class="time-row"><b>'+r[0]+'</b><span>'+r[1]+'</span></div>').join("");
}
function renderVibe(){ $("vibeCard").innerHTML='<span class="big">Group-chat card</span>'+vibeCards[state.vibeIndex % vibeCards.length]; }
function renderAll(){ renderBrief(); renderChecklist(); renderTimeline(); renderVibe(); }
let audioCtx=null;
function popSound(){
if(!state.sound) return;
try{
audioCtx = audioCtx || new (window.AudioContext||window.webkitAudioContext)();
const osc=audioCtx.createOscillator(), gain=audioCtx.createGain();
osc.type="triangle"; osc.frequency.setValueAtTime(180+Math.random()*260,audioCtx.currentTime);
gain.gain.setValueAtTime(.05,audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+.16);
osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime+.17);
}catch(e){}
}
const canvas=$("fireworks"), ctx=canvas.getContext("2d");
let w=0,h=0,dpr=1,particles=[],rockets=[];
function resize(){ const rect=canvas.getBoundingClientRect(); dpr=Math.min(window.devicePixelRatio||1,2); w=Math.max(1,Math.floor(rect.width*dpr)); h=Math.max(1,Math.floor(rect.height*dpr)); canvas.width=w; canvas.height=h; }
function color(){ const arr=palettes[state.palette]||palettes.patriot; return arr[Math.floor(Math.random()*arr.length)]; }
function launch(x,y,mega){
const count=mega?96:42+state.intensity*4; const c=color(); state.bursts += mega?5:1; popSound();
for(let i=0;i<count;i++){
const a=(Math.PI*2/count)*i+Math.random()*.16, sp=(mega?4.2:2.5)+Math.random()*(state.intensity*.34+2.2), life=45+Math.random()*38;
particles.push({x,y,vx:Math.cos(a)*sp*dpr,vy:Math.sin(a)*sp*dpr,life,max:life,size:(mega?2.4:1.8)*dpr+Math.random()*1.6*dpr,color:c});
}
renderBrief(); safeSave();
}
function autoRocket(){ rockets.push({x:(.18+Math.random()*.64)*w,y:h+20*dpr,vx:(Math.random()-.5)*.9*dpr,vy:-(6+Math.random()*3.2)*dpr,target:(.18+Math.random()*.48)*h,color:color()}); }
function draw(){
ctx.globalCompositeOperation="source-over"; ctx.fillStyle="rgba(7,17,31,.22)"; ctx.fillRect(0,0,w,h); ctx.globalCompositeOperation="lighter";
for(let i=rockets.length-1;i>=0;i--){ const r=rockets[i]; r.x+=r.vx; r.y+=r.vy; r.vy+=.025*dpr; ctx.fillStyle=r.color; ctx.beginPath(); ctx.arc(r.x,r.y,2.2*dpr,0,Math.PI*2); ctx.fill(); if(r.y<r.target){ launch(r.x,r.y,false); rockets.splice(i,1); } }
for(let i=particles.length-1;i>=0;i--){ const p=particles[i]; p.life--; p.x+=p.vx; p.y+=p.vy; p.vy+=.045*dpr; p.vx*=.986; p.vy*=.986; const alpha=Math.max(0,p.life/p.max); ctx.globalAlpha=alpha; ctx.fillStyle=p.color; ctx.beginPath(); ctx.arc(p.x,p.y,p.size*alpha+.3,0,Math.PI*2); ctx.fill(); if(p.life<=0) particles.splice(i,1); }
ctx.globalAlpha=1; requestAnimationFrame(draw);
}
async function start(){
try{ const saved=await sdk.load(KEY); if(saved && typeof saved==="object") Object.assign(state,saved); }catch(e){}
renderAll(); resize(); draw(); setInterval(()=>{ if(document.visibilityState!=="hidden") autoRocket(); }, 1800);
window.addEventListener("resize",resize);
canvas.addEventListener("pointerdown",e=>{ const r=canvas.getBoundingClientRect(); launch((e.clientX-r.left)*dpr,(e.clientY-r.top)*dpr,false); });
$("launchBurst").addEventListener("click",()=>{ for(let i=0;i<5;i++) setTimeout(()=>launch((.18+Math.random()*.64)*w,(.2+Math.random()*.45)*h,true),i*140); toast("Grand finale launched"); });
$("randomPlan").addEventListener("click",()=>{ state.guests=2+Math.floor(Math.random()*34); state.intensity=1+Math.floor(Math.random()*10); state.cookout=["Classic grill","Potluck heroes","Beach cooler","Porch snacks","Late-night desserts"][Math.floor(Math.random()*5)]; state.palette=["patriot","gold","neon","random"][Math.floor(Math.random()*4)]; state.vibeIndex++; renderAll(); safeSave(); toast("Party plan sparked"); });
$("soundToggle").addEventListener("click",()=>{ state.sound=!state.sound; $("soundToggle").textContent="Sound: "+(state.sound?"on":"off"); popSound(); safeSave(); });
["guests","intensity","cookout","palette"].forEach(id=>$(id).addEventListener("input",e=>{ state[id]=id==="guests"||id==="intensity"?Number(e.target.value):e.target.value; renderAll(); safeSave(); }));
$("newVibe").addEventListener("click",()=>{ state.vibeIndex++; renderVibe(); safeSave(); launch((.25+Math.random()*.5)*w,(.25+Math.random()*.35)*h,true); });
$("copyVibe").addEventListener("click",async()=>{ const text=$("vibeCard").textContent.trim(); try{ await navigator.clipboard.writeText(text); toast("Copied vibe card"); }catch(e){ toast("Copy unavailable here"); } });
$("soundToggle").textContent="Sound: "+(state.sound?"on":"off");
for(let i=0;i<3;i++) setTimeout(autoRocket,i*500);
}
sdk.onReady(start);
})();
</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/Owner/freedom-fireworks-studio */