0
GAMES#93c17568
Tiniest Violin
@Owner·deposited 3w ago·updated 2w ago·49 views
GAMES#93c17568
Tiniest Violin
OW
@Owner
49Views
0Comments
0Forks
0Saves
SHARE · REMIX
Tiniest Violin — a HTML Games widget by @Owner.
CONTROLS
#music#game
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: "Tiniest Violin" by @Owner]
Source: https://itjustvibes.com/Owner/tiniest-violin
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, user-scalable=no" />
<title>Tiniest Violin</title>
<style>
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html, body {
margin: 0;
height: 100%;
overflow: hidden;
touch-action: none;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background:
radial-gradient(circle at 20% 15%, rgba(255, 211, 106, 0.24), transparent 32%),
radial-gradient(circle at 80% 25%, rgba(111, 219, 255, 0.2), transparent 34%),
linear-gradient(145deg, #16071f, #35104a 55%, #130818);
color: white;
}
body {
display: flex;
align-items: center;
justify-content: center;
}
.app {
width: min(100vw, 900px);
height: 100svh;
max-height: 820px;
padding: 16px;
display: grid;
grid-template-rows: auto 1fr auto;
gap: 12px;
}
.top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
h1 {
margin: 0;
font-size: clamp(34px, 8vw, 64px);
line-height: .9;
letter-spacing: -2px;
}
.sub {
margin-top: 8px;
color: rgba(255,255,255,.72);
font-weight: 700;
}
.badge {
padding: 10px 14px;
border-radius: 999px;
background: rgba(255,255,255,.1);
border: 1px solid rgba(255,255,255,.18);
box-shadow: 0 12px 30px rgba(0,0,0,.28);
font-weight: 900;
white-space: nowrap;
}
.stage {
position: relative;
overflow: hidden;
border-radius: 34px;
border: 1px solid rgba(255,255,255,.16);
background:
radial-gradient(circle at center, rgba(255,255,255,.12), transparent 42%),
linear-gradient(180deg, rgba(255,255,255,.09), rgba(255,255,255,.03));
box-shadow:
inset 0 0 70px rgba(255,255,255,.06),
0 24px 70px rgba(0,0,0,.38);
}
.hint {
position: absolute;
top: 14px;
left: 50%;
z-index: 20;
transform: translateX(-50%);
width: calc(100% - 28px);
max-width: 620px;
text-align: center;
padding: 10px 14px;
border-radius: 999px;
font-weight: 900;
color: rgba(255,255,255,.88);
background: rgba(0,0,0,.25);
border: 1px solid rgba(255,255,255,.14);
pointer-events: none;
}
.violin {
position: absolute;
left: 50%;
top: 54%;
width: min(82vw, 470px);
height: min(82vw, 470px);
transform: translate(-50%, -50%);
pointer-events: none;
filter: drop-shadow(0 34px 30px rgba(0,0,0,.45));
z-index: 3;
}
.bow {
position: absolute;
z-index: 10;
width: 280px;
height: 12px;
border-radius: 999px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) rotate(-18deg);
transform-origin: center;
background: linear-gradient(90deg, #fff7d1, #6d321b 35%, #fff7d1 50%, #6d321b 65%, #fff7d1);
box-shadow: 0 0 24px rgba(255,211,106,.75);
opacity: 0;
pointer-events: none;
}
.bow.on {
opacity: 1;
}
.finger {
position: absolute;
z-index: 11;
width: 38px;
height: 38px;
border-radius: 50%;
transform: translate(-50%, -50%);
background: radial-gradient(circle at 35% 30%, white, #78e7ff 42%, #ff6db4 80%);
box-shadow: 0 0 30px rgba(120,231,255,.9);
opacity: 0;
pointer-events: none;
}
.finger.on {
opacity: 1;
}
.controls {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
align-items: center;
}
.meter {
height: 18px;
border-radius: 999px;
overflow: hidden;
background: rgba(255,255,255,.12);
border: 1px solid rgba(255,255,255,.16);
}
.fill {
height: 100%;
width: 0%;
border-radius: inherit;
background: linear-gradient(90deg, #78e7ff, #ff6db4, #ffd36a);
box-shadow: 0 0 18px rgba(255,211,106,.75);
}
.buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
button {
appearance: none;
border: 1px solid rgba(255,255,255,.2);
border-radius: 16px;
background: rgba(255,255,255,.11);
color: white;
font-weight: 950;
padding: 12px 14px;
cursor: pointer;
box-shadow: 0 10px 24px rgba(0,0,0,.25);
}
button.primary {
background: linear-gradient(135deg, rgba(255,109,180,.45), rgba(120,231,255,.32));
border-color: rgba(255,255,255,.35);
}
button.active {
background: rgba(255, 211, 106, .28);
}
.status {
margin-top: 7px;
color: rgba(255,255,255,.7);
font-size: 13px;
font-weight: 700;
}
@media (max-width: 640px) {
.app {
padding: 12px;
}
.top {
flex-direction: column;
}
.controls {
grid-template-columns: 1fr;
}
.buttons {
justify-content: stretch;
}
button {
flex: 1;
}
.hint {
font-size: 12px;
}
}
</style>
</head>
<body>
<main class="app">
<section class="top">
<div>
<h1>tiny violin</h1>
<div class="sub">Drag your finger across the strings to play tragic violin music.</div>
</div>
<div class="badge" id="moodLabel">😭 Sad Mode</div>
</section>
<section class="stage" id="stage">
<div class="hint" id="hint">Tap "Enable Sound", then drag across the violin.</div>
<svg class="violin" viewBox="0 0 500 500" aria-hidden="true">
<defs>
<radialGradient id="bodyGlow" cx="35%" cy="18%" r="70%">
<stop offset="0" stop-color="#ffd99b"/>
<stop offset=".42" stop-color="#be5c27"/>
<stop offset="1" stop-color="#4b170d"/>
</radialGradient>
<linearGradient id="wood" x1="0" x2="1">
<stop offset="0" stop-color="#72301b"/>
<stop offset=".5" stop-color="#df7b32"/>
<stop offset="1" stop-color="#693019"/>
</linearGradient>
</defs>
<rect x="236" y="44" width="28" height="188" rx="14" fill="#3a170e"/>
<rect x="219" y="18" width="62" height="44" rx="17" fill="#5b2515"/>
<circle cx="209" cy="36" r="12" fill="#251008"/>
<circle cx="291" cy="36" r="12" fill="#251008"/>
<circle cx="209" cy="65" r="12" fill="#251008"/>
<circle cx="291" cy="65" r="12" fill="#251008"/>
<path d="M250 166
C196 112 126 137 139 206
C147 249 102 250 94 318
C84 405 188 448 250 391
C312 448 416 405 406 318
C398 250 353 249 361 206
C374 137 304 112 250 166Z"
fill="url(#bodyGlow)"
stroke="#2b0d07"
stroke-width="10"/>
<path d="M250 189
C220 158 170 167 174 216
C178 260 132 267 128 324
C124 382 201 410 250 353
C299 410 376 382 372 324
C368 267 322 260 326 216
C330 167 280 158 250 189Z"
fill="url(#wood)"
opacity=".68"/>
<path d="M190 248 C161 275 166 326 197 343" fill="none" stroke="#1e0905" stroke-width="12" stroke-linecap="round"/>
<path d="M310 248 C339 275 334 326 303 343" fill="none" stroke="#1e0905" stroke-width="12" stroke-linecap="round"/>
<circle cx="225" cy="238" r="6" fill="#1e0905"/>
<circle cx="275" cy="238" r="6" fill="#1e0905"/>
<path d="M224 266 C239 280 261 280 276 266" fill="none" stroke="#1e0905" stroke-width="6" stroke-linecap="round"/>
<path d="M210 298 C226 276 274 276 290 298 L280 326 C264 316 236 316 220 326 Z"
fill="#ffe0a0" stroke="#4a1d0f" stroke-width="5"/>
<path d="M216 383 C224 339 276 339 284 383 C275 406 225 406 216 383Z" fill="#1f0b07"/>
<line x1="238" y1="42" x2="226" y2="392" stroke="#fff8d6" stroke-width="3.5"/>
<line x1="246" y1="42" x2="242" y2="397" stroke="#fff8d6" stroke-width="3.5"/>
<line x1="254" y1="42" x2="258" y2="397" stroke="#fff8d6" stroke-width="3.5"/>
<line x1="262" y1="42" x2="274" y2="392" stroke="#fff8d6" stroke-width="3.5"/>
</svg>
<div class="bow" id="bow"></div>
<div class="finger" id="finger"></div>
</section>
<section class="controls">
<div>
<div class="meter"><div class="fill" id="fill"></div></div>
<div class="status" id="status">Sound is off until you tap Enable Sound.</div>
</div>
<div class="buttons">
<button class="primary" id="enableBtn">Enable Sound</button>
<button id="testBtn">Test Sound</button>
<button class="active" id="sadBtn">Sad</button>
<button id="epicBtn">Epic</button>
<button id="sillyBtn">Silly</button>
</div>
</section>
</main>
<script>
(() => {
const stage = document.getElementById("stage");
const bow = document.getElementById("bow");
const finger = document.getElementById("finger");
const fill = document.getElementById("fill");
const status = document.getElementById("status");
const hint = document.getElementById("hint");
const moodLabel = document.getElementById("moodLabel");
const enableBtn = document.getElementById("enableBtn");
const testBtn = document.getElementById("testBtn");
const sadBtn = document.getElementById("sadBtn");
const epicBtn = document.getElementById("epicBtn");
const sillyBtn = document.getElementById("sillyBtn");
let ctx = null;
let master = null;
let mainGain = null;
let scratchGain = null;
let filter = null;
let osc1 = null;
let osc2 = null;
let osc3 = null;
let vibrato = null;
let vibratoGain = null;
let playing = false;
let audioReady = false;
let lastX = 0;
let lastY = 0;
let lastT = 0;
let drama = 0;
let mood = "sad";
const scales = {
sad: [196, 220, 246.94, 261.63, 293.66, 329.63, 349.23, 392, 440, 493.88, 523.25],
epic: [146.83, 196, 220, 246.94, 293.66, 329.63, 392, 440, 493.88, 587.33, 659.25],
silly: [261.63, 293.66, 329.63, 392, 466.16, 523.25, 587.33, 659.25, 783.99, 987.77]
};
function getAudioContext() {
return new (window.AudioContext || window.webkitAudioContext)();
}
function makeNoiseBuffer(audioCtx) {
const length = audioCtx.sampleRate * 1.5;
const buffer = audioCtx.createBuffer(1, length, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < length; i++) {
data[i] = Math.random() * 2 - 1;
}
return buffer;
}
async function enableAudio() {
if (!ctx) {
ctx = getAudioContext();
master = ctx.createGain();
master.gain.value = 0.95;
master.connect(ctx.destination);
mainGain = ctx.createGain();
mainGain.gain.value = 0.0001;
scratchGain = ctx.createGain();
scratchGain.gain.value = 0.0001;
filter = ctx.createBiquadFilter();
filter.type = "bandpass";
filter.frequency.value = 1000;
filter.Q.value = 2.2;
const lowpass = ctx.createBiquadFilter();
lowpass.type = "lowpass";
lowpass.frequency.value = 4200;
osc1 = ctx.createOscillator();
osc2 = ctx.createOscillator();
osc3 = ctx.createOscillator();
osc1.type = "sawtooth";
osc2.type = "triangle";
osc3.type = "sine";
osc1.frequency.value = 220;
osc2.frequency.value = 440;
osc3.frequency.value = 660;
vibrato = ctx.createOscillator();
vibrato.type = "sine";
vibrato.frequency.value = 6;
vibratoGain = ctx.createGain();
vibratoGain.gain.value = 7;
const noise = ctx.createBufferSource();
noise.buffer = makeNoiseBuffer(ctx);
noise.loop = true;
const delay = ctx.createDelay();
delay.delayTime.value = 0.105;
const delayGain = ctx.createGain();
delayGain.gain.value = 0.16;
vibrato.connect(vibratoGain);
vibratoGain.connect(osc1.frequency);
vibratoGain.connect(osc2.frequency);
osc1.connect(mainGain);
osc2.connect(mainGain);
osc3.connect(mainGain);
noise.connect(scratchGain);
mainGain.connect(filter);
scratchGain.connect(filter);
filter.connect(lowpass);
lowpass.connect(master);
lowpass.connect(delay);
delay.connect(delayGain);
delayGain.connect(master);
osc1.start();
osc2.start();
osc3.start();
vibrato.start();
noise.start();
}
await ctx.resume();
audioReady = true;
status.textContent = "Sound enabled. Drag across the violin strings.";
hint.textContent = "Drag your finger across the strings.";
enableBtn.textContent = "Sound On";
enableBtn.classList.add("active");
playBlip();
}
function playBlip() {
if (!ctx || ctx.state !== "running") return;
const now = ctx.currentTime;
const blipOsc = ctx.createOscillator();
const blipGain = ctx.createGain();
blipOsc.type = "triangle";
blipOsc.frequency.setValueAtTime(660, now);
blipOsc.frequency.exponentialRampToValueAtTime(440, now + 0.16);
blipGain.gain.setValueAtTime(0.0001, now);
blipGain.gain.exponentialRampToValueAtTime(0.45, now + 0.02);
blipGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.22);
blipOsc.connect(blipGain);
blipGain.connect(master);
blipOsc.start(now);
blipOsc.stop(now + 0.24);
}
function setMood(next) {
mood = next;
sadBtn.classList.toggle("active", mood === "sad");
epicBtn.classList.toggle("active", mood === "epic");
sillyBtn.classList.toggle("active", mood === "silly");
moodLabel.textContent =
mood === "sad" ? "😭 Sad Mode" :
mood === "epic" ? "⚔️ Epic Mode" :
"🤡 Silly Mode";
}
function nearestNote(freq) {
const scale = scales[mood];
let best = scale[0];
let bestDiff = Infinity;
for (const note of scale) {
const diff = Math.abs(note - freq);
if (diff < bestDiff) {
best = note;
bestDiff = diff;
}
}
return best;
}
function getPoint(e) {
if (e.touches && e.touches.length) {
return {
x: e.touches[0].clientX,
y: e.touches[0].clientY
};
}
if (e.changedTouches && e.changedTouches.length) {
return {
x: e.changedTouches[0].clientX,
y: e.changedTouches[0].clientY
};
}
return {
x: e.clientX,
y: e.clientY
};
}
function moveVisuals(x, y, angle) {
const rect = stage.getBoundingClientRect();
const px = x - rect.left;
const py = y - rect.top;
bow.style.left = px + "px";
bow.style.top = py + "px";
bow.style.transform = `translate(-50%, -50%) rotate(${angle}deg)`;
finger.style.left = px + "px";
finger.style.top = py + "px";
}
function updateSound(x, y, speed) {
if (!ctx || !audioReady) return;
const rect = stage.getBoundingClientRect();
const nx = Math.max(0, Math.min(1, (x - rect.left) / rect.width));
const ny = Math.max(0, Math.min(1, (y - rect.top) / rect.height));
let base = mood === "sad" ? 175 : mood === "epic" ? 145 : 260;
let range = mood === "sad" ? 420 : mood === "epic" ? 620 : 900;
let freq = base + (1 - ny) * range + Math.sin(nx * Math.PI * 6) * 34;
freq = nearestNote(freq);
const now = ctx.currentTime;
const loudness = Math.min(0.72, 0.16 + speed * 0.011);
const scratch = Math.min(0.12, 0.025 + speed * 0.0025);
osc1.frequency.setTargetAtTime(freq, now, 0.025);
osc2.frequency.setTargetAtTime(freq * 2.002, now, 0.03);
osc3.frequency.setTargetAtTime(freq * 3.004, now, 0.035);
filter.frequency.setTargetAtTime(700 + speed * 22 + nx * 1500, now, 0.035);
filter.Q.setTargetAtTime(2 + speed * 0.035, now, 0.05);
vibrato.frequency.setTargetAtTime(
mood === "silly" ? 9.2 : mood === "epic" ? 6.8 : 5.8,
now,
0.06
);
vibratoGain.gain.setTargetAtTime(
mood === "silly" ? 13 : mood === "epic" ? 9 : 7,
now,
0.06
);
mainGain.gain.setTargetAtTime(loudness, now, 0.025);
scratchGain.gain.setTargetAtTime(scratch, now, 0.025);
}
async function startPlay(e) {
e.preventDefault();
await enableAudio();
const p = getPoint(e);
playing = true;
lastX = p.x;
lastY = p.y;
lastT = performance.now();
bow.classList.add("on");
finger.classList.add("on");
moveVisuals(p.x, p.y, -18);
updateSound(p.x, p.y, 1);
}
function movePlay(e) {
if (!playing) return;
e.preventDefault();
const p = getPoint(e);
const now = performance.now();
const dx = p.x - lastX;
const dy = p.y - lastY;
const dt = Math.max(12, now - lastT);
const dist = Math.hypot(dx, dy);
const speed = Math.min(80, dist / dt * 16.67);
const angle = Math.atan2(dy, dx) * 180 / Math.PI;
moveVisuals(p.x, p.y, angle);
updateSound(p.x, p.y, speed);
drama = Math.max(drama * 0.92, Math.min(100, speed * 2.1));
fill.style.width = Math.round(drama) + "%";
lastX = p.x;
lastY = p.y;
lastT = now;
}
function stopPlay(e) {
if (e) e.preventDefault();
playing = false;
bow.classList.remove("on");
finger.classList.remove("on");
if (ctx && mainGain && scratchGain) {
const now = ctx.currentTime;
mainGain.gain.setTargetAtTime(0.0001, now, 0.07);
scratchGain.gain.setTargetAtTime(0.0001, now, 0.05);
}
}
function decayMeter() {
if (!playing) {
drama *= 0.94;
fill.style.width = Math.round(drama) + "%";
}
requestAnimationFrame(decayMeter);
}
enableBtn.addEventListener("click", async (e) => {
e.stopPropagation();
await enableAudio();
});
testBtn.addEventListener("click", async (e) => {
e.stopPropagation();
await enableAudio();
playBlip();
if (ctx && mainGain) {
const now = ctx.currentTime;
osc1.frequency.setTargetAtTime(440, now, 0.01);
osc2.frequency.setTargetAtTime(880, now, 0.01);
osc3.frequency.setTargetAtTime(1320, now, 0.01);
mainGain.gain.setTargetAtTime(0.45, now, 0.02);
scratchGain.gain.setTargetAtTime(0.04, now, 0.02);
mainGain.gain.setTargetAtTime(0.0001, now + 0.5, 0.08);
scratchGain.gain.setTargetAtTime(0.0001, now + 0.5, 0.08);
}
});
sadBtn.addEventListener("click", () => setMood("sad"));
epicBtn.addEventListener("click", () => setMood("epic"));
sillyBtn.addEventListener("click", () => setMood("silly"));
stage.addEventListener("touchstart", startPlay, { passive: false });
stage.addEventListener("touchmove", movePlay, { passive: false });
window.addEventListener("touchend", stopPlay, { passive: false });
window.addEventListener("touchcancel", stopPlay, { passive: false });
stage.addEventListener("mousedown", startPlay);
window.addEventListener("mousemove", movePlay);
window.addEventListener("mouseup", stopPlay);
window.addEventListener("blur", stopPlay);
decayMeter();
})();
</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
### 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. **Do NOT import external JS libraries via script tags** — they will be blocked. Include library code inline or use a CDN link in a `<link>` tag for CSS only.
6. **Keep total code under 80KB** — 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/tiniest-violin */