0
TOOLS#a7897f66
Calculator
@Owner·deposited 12h ago·updated 8h ago·14 views
TOOLS#a7897f66
Calculator
OW
@Owner
14Views
0Comments
0Forks
0Saves
SHARE · REMIX
Calculator — a HTML Tools widget by @Owner.
CONTROLS
calculatorglassmorphismmathminimaldark
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: "Calculator" by @Owner]
Source: https://itjustvibes.com/Owner/glassmorphism-calculator
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>Calculator</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
overflow: hidden;
}
body::before {
content: '';
position: fixed;
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(120, 80, 255, 0.3) 0%, transparent 70%);
top: -100px;
left: -100px;
pointer-events: none;
}
body::after {
content: '';
position: fixed;
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(255, 80, 180, 0.2) 0%, transparent 70%);
bottom: -100px;
right: -100px;
pointer-events: none;
}
.calculator {
background: rgba(255, 255, 255, 0.07);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 28px;
padding: 24px;
width: 320px;
box-shadow:
0 32px 64px rgba(0,0,0,0.4),
0 0 0 1px rgba(255,255,255,0.05) inset,
0 1px 0 rgba(255,255,255,0.2) inset;
}
.display {
background: rgba(0,0,0,0.3);
border-radius: 18px;
padding: 20px 22px 16px;
margin-bottom: 20px;
border: 1px solid rgba(255,255,255,0.06);
min-height: 100px;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
overflow: hidden;
}
.display .expression {
font-size: 14px;
color: rgba(255,255,255,0.4);
margin-bottom: 6px;
min-height: 18px;
word-break: break-all;
text-align: right;
letter-spacing: 0.5px;
}
.display .value {
font-size: 44px;
font-weight: 300;
color: #fff;
letter-spacing: -1px;
line-height: 1;
word-break: break-all;
text-align: right;
transition: font-size 0.15s ease;
}
.display .value.small { font-size: 28px; }
.display .value.xsmall { font-size: 20px; }
.buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
button {
border: none;
border-radius: 14px;
height: 62px;
font-size: 20px;
font-weight: 400;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.15s ease;
position: relative;
outline: none;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
button:active { transform: scale(0.92); }
.btn-func {
background: rgba(255,255,255,0.18);
color: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.2), 0 1px 0 rgba(255,255,255,0.15) inset;
font-size: 18px;
}
.btn-func:hover { background: rgba(255,255,255,0.25); }
.btn-op {
background: linear-gradient(145deg, #a855f7, #7c3aed);
color: #fff;
box-shadow: 0 4px 12px rgba(139,92,246,0.4), 0 1px 0 rgba(255,255,255,0.2) inset;
font-size: 24px;
font-weight: 300;
}
.btn-op:hover { background: linear-gradient(145deg, #b975f9, #8b5cf6); }
.btn-op.active-op {
background: #fff;
color: #7c3aed;
box-shadow: 0 4px 16px rgba(255,255,255,0.3);
}
.btn-num {
background: rgba(255,255,255,0.1);
color: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.2), 0 1px 0 rgba(255,255,255,0.1) inset;
}
.btn-num:hover { background: rgba(255,255,255,0.16); }
.btn-zero {
grid-column: span 2;
border-radius: 20px;
text-align: left;
padding-left: 24px;
font-size: 20px;
}
.btn-eq {
background: linear-gradient(145deg, #f472b6, #ec4899);
color: #fff;
box-shadow: 0 4px 12px rgba(236,72,153,0.45), 0 1px 0 rgba(255,255,255,0.2) inset;
font-size: 24px;
font-weight: 300;
}
.btn-eq:hover { background: linear-gradient(145deg, #f687c8, #f05aaa); }
.ripple {
position: absolute;
border-radius: 50%;
background: rgba(255,255,255,0.25);
transform: scale(0);
animation: ripple-anim 0.4s linear;
pointer-events: none;
}
@keyframes ripple-anim { to { transform: scale(4); opacity: 0; } }
@keyframes flash { 0% { opacity: 0.5; } 100% { opacity: 1; } }
.flash { animation: flash 0.2s ease; }
</style>
</head>
<body>
<div class="calculator">
<div class="display">
<div class="expression" id="expression"></div>
<div class="value" id="display">0</div>
</div>
<div class="buttons">
<button class="btn-func" onclick="handleInput('AC')">AC</button>
<button class="btn-func" onclick="handleInput('±')">+/−</button>
<button class="btn-func" onclick="handleInput('%')">%</button>
<button class="btn-op" id="op-div" onclick="handleInput('÷')">÷</button>
<button class="btn-num" onclick="handleInput('7')">7</button>
<button class="btn-num" onclick="handleInput('8')">8</button>
<button class="btn-num" onclick="handleInput('9')">9</button>
<button class="btn-op" id="op-mul" onclick="handleInput('×')">×</button>
<button class="btn-num" onclick="handleInput('4')">4</button>
<button class="btn-num" onclick="handleInput('5')">5</button>
<button class="btn-num" onclick="handleInput('6')">6</button>
<button class="btn-op" id="op-sub" onclick="handleInput('−')">−</button>
<button class="btn-num" onclick="handleInput('1')">1</button>
<button class="btn-num" onclick="handleInput('2')">2</button>
<button class="btn-num" onclick="handleInput('3')">3</button>
<button class="btn-op" id="op-add" onclick="handleInput('+')">+</button>
<button class="btn-num btn-zero" onclick="handleInput('0')">0</button>
<button class="btn-num" onclick="handleInput('.')">.</button>
<button class="btn-eq" onclick="handleInput('=')">=</button>
</div>
</div>
<script>
let current = '0', previous = null, operator = null;
let waitingForOperand = false, expression = '', justCalculated = false;
const displayEl = document.getElementById('display');
const expressionEl = document.getElementById('expression');
const opMap = { 'div': '÷', 'mul': '×', 'sub': '−', 'add': '+' };
function updateDisplay() {
displayEl.textContent = current;
displayEl.className = 'value';
if (current.length > 10) displayEl.classList.add('xsmall');
else if (current.length > 7) displayEl.classList.add('small');
}
function clearActiveOps() {
Object.keys(opMap).forEach(k => document.getElementById('op-'+k).classList.remove('active-op'));
}
function setActiveOp(op) {
clearActiveOps();
const entry = Object.entries(opMap).find(([k, v]) => v === op);
if (entry) document.getElementById('op-'+entry[0]).classList.add('active-op');
}
function toNum(s) { return parseFloat(s); }
function formatNum(n) {
if (isNaN(n) || !isFinite(n)) return 'Error';
return parseFloat(n.toPrecision(12)).toString();
}
function calculate(a, op, b) {
if (op === '+') return a + b;
if (op === '−') return a - b;
if (op === '×') return a * b;
if (op === '÷') return b === 0 ? NaN : a / b;
}
function handleInput(val) {
const ops = ['÷', '×', '−', '+'];
if (ops.includes(val)) {
setActiveOp(val);
if (operator && !waitingForOperand) {
const result = calculate(toNum(previous), operator, toNum(current));
const fmt = formatNum(result);
expression = expression + ' ' + current + ' ' + val;
previous = fmt; current = fmt;
} else {
if (justCalculated) expression = current + ' ' + val;
else { expression = (previous !== null ? expression : current) + ' ' + val; if (previous === null) previous = current; }
}
operator = val; waitingForOperand = true; justCalculated = false;
expressionEl.textContent = expression;
updateDisplay(); return;
}
if (val === '=') {
clearActiveOps();
if (operator && previous !== null) {
expressionEl.textContent = expression + ' ' + current + ' =';
const result = calculate(toNum(previous), operator, toNum(current));
current = formatNum(result);
displayEl.classList.add('flash');
setTimeout(() => displayEl.classList.remove('flash'), 200);
previous = null; operator = null; waitingForOperand = false; justCalculated = true; expression = '';
updateDisplay();
} return;
}
if (val === 'AC') {
current = '0'; previous = null; operator = null;
waitingForOperand = false; expression = ''; justCalculated = false;
clearActiveOps(); expressionEl.textContent = ''; updateDisplay(); return;
}
if (val === '±') { current = formatNum(-toNum(current)); updateDisplay(); return; }
if (val === '%') { current = formatNum(toNum(current) / 100); updateDisplay(); return; }
clearActiveOps();
if (justCalculated) { current = '0'; expression = ''; justCalculated = false; }
if (waitingForOperand) { current = val === '.' ? '0.' : val; waitingForOperand = false; }
else {
if (val === '.' && current.includes('.')) return;
if (current === '0' && val !== '.') current = val;
else { if (current.replace('.','').length >= 12) return; current += val; }
}
updateDisplay();
}
document.addEventListener('keydown', e => {
const map = {'0':'0','1':'1','2':'2','3':'3','4':'4','5':'5','6':'6','7':'7','8':'8','9':'9',
'.':'.', '+':'+', '-':'−', '*':'×', '/':'÷', 'Enter':'=', '=':'=', 'Backspace':'AC', 'Escape':'AC', '%':'%'};
if (map[e.key]) { e.preventDefault(); handleInput(map[e.key]); }
});
document.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', function(e) {
const r = document.createElement('span');
r.className = 'ripple';
const rect = this.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
r.style.cssText = `width:${size}px;height:${size}px;left:${e.clientX-rect.left-size/2}px;top:${e.clientY-rect.top-size/2}px`;
this.appendChild(r);
setTimeout(() => r.remove(), 400);
});
});
</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.setState()` 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. **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/glassmorphism-calculator */