0
TOOLS#645a2d39
SBA DealLens
@Owner·deposited 2w ago·updated 2w ago·58 views
TOOLS#645a2d39
SBA DealLens
OW
@Owner
58Views
0Comments
0Forks
0Saves
SHARE · REMIX
SBA DealLens — a HTML tools widget by @Owner.
CONTROLS
SBAloansfinancecalculatorbusiness
No comments yet. Be the first!
✦ Remix with AI
SDK in this widget
- raw fetch(): Raw fetch() detected. Prefer vibes.fetch for proxied requests.
Generated prompt
You are helping me modify a vibe-coded widget from itjustvibes.com.
[VIBE CODE: "SBA DealLens" by @Owner]
Source: https://itjustvibes.com/Owner/sba-deallens
Type: HTML
--- SOURCE CODE ---
```html
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SBA DealLens</title><style>
body{margin:0;background:#111814;color:#18201c;font-family:Inter,system-ui,Segoe UI,sans-serif}#app{min-height:100vh;padding:14px}.w{max-width:1320px;margin:auto}.hero,.sec{background:#f7efe2;border:1px solid #ffffff44;border-radius:24px;box-shadow:0 18px 50px #0006;margin:0 0 14px;overflow:hidden}.hero{display:grid;grid-template-columns:1.4fr .8fr}.pad{padding:22px}.ey{font-size:12px;letter-spacing:.14em;text-transform:uppercase;font-weight:900;color:#536058}.ttl{font-size:clamp(34px,6vw,58px);line-height:.92;letter-spacing:-.06em;font-weight:950;margin:9px 0}.sub{color:#4b574f;line-height:1.5}.dark{background:#101512;color:#f8efdd}.k{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:13px}.k div{background:#ffffff12;border:1px solid #ffffff1f;border-radius:16px;padding:12px}.k span,.mut{font-size:12px;color:#6b766f}.dark span{color:#c3cec7}.k b{display:block;font-size:23px;margin-top:5px}button{border:1px solid #1a241d22;background:#fffaf0;border-radius:999px;padding:10px 13px;font-weight:850;cursor:pointer}button.p{background:#17201b;color:#fff4df}.head{padding:15px 18px;border-bottom:1px solid #18201c22;display:flex;justify-content:space-between;gap:10px}.head h2{margin:0;font-size:18px}.g2{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:12px}.g3{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.g4{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;padding:12px}.card{background:#fffaf0;border:1px solid #18201c22;border-radius:18px;padding:13px}label{display:block;font-size:12px;font-weight:850;color:#4e5a53;margin:0 0 5px}input,select{width:100%;min-height:38px;border:1px solid #18201c22;border-radius:11px;background:#fffdf8;padding:8px 9px;font:inherit}h3{margin:0 0 9px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;padding:12px}.m{background:#fffaf0;border:1px solid #18201c22;border-radius:18px;padding:13px;border-left:5px solid #497b9b}.m.g{border-left-color:#5b8f68}.m.a{border-left-color:#b78a34}.m.r{border-left-color:#a95151}.m b{display:block;font-size:24px;letter-spacing:-.04em;margin-top:6px}.charts{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:12px}.chart{background:#fffaf0;border:1px solid #18201c22;border-radius:18px;padding:13px}.chart svg{width:100%;height:205px}.axis{stroke:#c8bfae}.a{fill:none;stroke:#497b9b;stroke-width:3}.b{fill:none;stroke:#5b8f68;stroke-width:3}.ba{fill:#497b9b}.bb{fill:#5b8f68}text{font-size:10px;fill:#66736c}.notes{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:12px}li{margin:7px 0}.tbl{padding:12px;overflow:auto}table{width:100%;border-collapse:collapse;background:#fffaf0;border-radius:14px;overflow:hidden}th,td{padding:9px;border-bottom:1px solid #18201c18;text-align:right;white-space:nowrap}th:first-child,td:first-child{text-align:left}th{background:#eee4d4}.foot{color:#c8d0ca;font-size:12px;padding:5px 4px 20px}.tabs{display:none;padding:0 12px 12px;gap:8px}.tabs button{flex:1}.on{background:#17201b!important;color:#fff4df!important}@media(max-width:900px){.hero,.g2,.charts,.notes{grid-template-columns:1fr}.metrics,.g4{grid-template-columns:1fr 1fr}}@media(max-width:640px){#app{padding:10px}.metrics,.g3,.g4{grid-template-columns:1fr}.tabs{display:flex}.offer{display:none}.offer.show{display:block}.ttl{font-size:38px}}
</style></head><body><div id="app">Loading...</div><script>
(()=>{const $=q=>document.querySelector(q),M=300,Y=[5,10,15,20,25],money=n=>isFinite(n)?n.toLocaleString(undefined,{style:'currency',currency:'USD',maximumFractionDigits:0}):'—';let tab='A',C,timer,base=JSON.parse(JSON.stringify({cost:2500000,hor:10,shock:0,prime:7.5,t5:4.3,t10:4.45,cdcRef:6.35,status:'manual',A:{name:'SBA 7(a) offer',type:'7a',eq:10,fees:55000,fin:35000,base:'prime',spread:2.75,manual:10.25,term:25,amort:25,fee:.55},B:{name:'SBA 504 structure',type:'504',eq:10,fees:70000,fin:30000,bankPct:50,cdcPct:40,bankBase:'t5',bankSpread:2.35,bankRate:7.15,bankTerm:10,bankAmort:25,cdcRate:6.35,cdcTerm:25,cdcAmort:25}})),S=JSON.parse(JSON.stringify(base));function V(){if(!window.vibes)window.vibes={onReady:f=>document.readyState=='loading'?document.addEventListener('DOMContentLoaded',f):f(),save:async()=>{},load:async()=>null,fetch:(u,o)=>fetch(u,o)};return window.vibes}function pmt(p,r,n){r/=1200;n*=12;return r?p*r/(1-Math.pow(1+r,-n)):p/n}function r(o,k){if(k=='cdc')return +o.cdcRate||S.cdcRef;if(k=='bank'&&o.bankBase=='manual')return +o.bankRate||0;if(k=='main'&&o.base=='manual')return +o.manual||0;let key=k=='bank'?o.bankBase:o.base;return (+S[key]||0)+(k=='bank'?+o.bankSpread:+o.spread||0)+(+S.shock||0)}function rows(pr,rate,term,amort,fee,id,comp,pre){let bal=pr,mo=pmt(pr,rate,amort),a=[];for(let m=1;m<=M;m++){if(m>term*12||bal<=.01){a.push({m,id,comp,pay:0,int:0,end:0});continue}let intr=bal*rate/1200,ff=bal*(fee||0)/10000/12,pri=Math.min(mo-intr,bal),pay=pri+intr+ff;if(m==term*12&&amort>term){pri=bal;pay=bal+intr+ff}bal=Math.max(0,bal-pri);a.push({m,id,comp,pay,int:intr,end:bal,pre})}return{pr,rate,term,amort,mo:mo+pr*(fee||0)/10000/12,rows:a,pre}}function pen(c,bal,m){if(!bal)return 0;if(c.pre=='7a'&&m<=36)return bal*({1:.05,2:.03,3:.01}[Math.ceil(m/12)]||0);if(c.pre=='504'&&m<=120)return bal*.03*(121-m)/120;return 0}function model(id,o){let cs=[],up=S.cost*o.eq/100+(+o.fees||0);if(o.type=='7a'){let pr=S.cost*(1-o.eq/100)+(+o.fin||0);cs=[rows(pr,r(o,'main'),+o.term,+o.amort,+o.fee,id,'7(a)','7a')]}else{let bp=S.cost*o.bankPct/100+o.fin*.5,cp=S.cost*o.cdcPct/100+o.fin*.5;cs=[rows(bp,r(o,'bank'),+o.bankTerm,+o.bankAmort,0,id,'Bank',''),rows(cp,r(o,'cdc'),+o.cdcTerm,+o.cdcAmort,0,id,'CDC/SBA','504')]}let h={};Y.forEach(y=>h[y]=cost(cs,up,y*12));return{id,o,up,mo:cs.reduce((s,c)=>s+c.mo,0),pr:cs.reduce((s,c)=>s+c.pr,0),cs,h}}function cost(cs,up,m){let pay=0,int=0,bal=0,pp=0;cs.forEach(c=>{let last=c.rows[m-1];c.rows.slice(0,m).forEach(x=>{pay+=x.pay;int+=x.int});bal+=last.end;pp+=pen(c,last.end,m)});return{total:up+pay+bal+pp,pay,int,bal,pp}}function calc(){let A=model('A',S.A),B=model('B',S.B),be=null,prev;for(let m=1;m<=M;m++){let d=cost(A.cs,A.up,m).total-cost(B.cs,B.up,m).total;if(prev!=null&&((prev<0&&d>0)||(prev>0&&d<0))){be=m;break}prev=d}let warn=[];if(A.o.type=='7a'&&A.pr>5e6)warn.push('Offer A 7(a) principal exceeds $5M.');['A','B'].forEach(id=>{let o=S[id];if(o.type=='504'&&Math.abs(+o.bankPct+ +o.cdcPct+ +o.eq-100)>.1)warn.push('Offer '+id+' 504 percentages do not total 100%.')});return{A,B,be,warn}}function F(l,p,t='number'){let v=p.split('.').reduce((a,k)=>a&&a[k],S);return`<div><label>${l}</label><input data-p="${p}" type="${t}" step="any" value="${v}"></div>`}function Sel(l,p,ops){let v=p.split('.').reduce((a,k)=>a&&a[k],S);return`<div><label>${l}</label><select data-p="${p}">${ops.map(o=>`<option value="${o[0]}" ${v==o[0]?'selected':''}>${o[1]}</option>`).join('')}</select></div>`}function offer(id){let o=S[id],i=id;return`<div class="card offer ${tab==id?'show':''}"><h3>Offer ${id}</h3>${F('Name',i+'.name','text')}<div class="g3">${Sel('Structure',i+'.type',[['7a','SBA 7(a)'],['504','SBA 504']])}${F('Equity %',i+'.eq')}${F('Cash fees',i+'.fees')}</div><div class="g3">${F('Financed fees',i+'.fin')}${o.type=='7a'?Sel('Base',i+'.base',[['prime','Prime'],['t5','5Y Treasury'],['t10','10Y Treasury'],['manual','Manual']])+F('Spread %',i+'.spread')+F('Manual rate %',i+'.manual')+F('Term yrs',i+'.term')+F('Amort yrs',i+'.amort')+F('Annual fee bps',i+'.fee'):F('Bank %',i+'.bankPct')+F('CDC %',i+'.cdcPct')+Sel('Bank base',i+'.bankBase',[['t5','5Y Treasury'],['t10','10Y Treasury'],['prime','Prime'],['manual','Manual']])+F('Bank spread %',i+'.bankSpread')+F('Bank fixed %',i+'.bankRate')+F('CDC rate %',i+'.cdcRate')+F('Bank term',i+'.bankTerm')+F('Bank amort',i+'.bankAmort')+F('CDC term',i+'.cdcTerm')+F('CDC amort',i+'.cdcAmort')}</div></div>`}function path(A,B,bal=false){let max=0,pa=[],pb=[];for(let y=0;y<=25;y++){let m=Math.max(1,y*12),a=bal?A.cs.reduce((s,c)=>s+c.rows[m-1].end,0):A.h[Y.includes(y)?y:25].total,b=bal?B.cs.reduce((s,c)=>s+c.rows[m-1].end,0):B.h[Y.includes(y)?y:25].total;max=Math.max(max,a,b);pa.push([30+y*20,180-a/max*145]);pb.push([30+y*20,180-b/max*145])}return`<svg viewBox="0 0 560 205"><line class="axis" x1="30" y1="180" x2="535" y2="180"/><path class="a" d="M${pa.map(x=>x.join(' ')).join('L')}"/><path class="b" d="M${pb.map(x=>x.join(' ')).join('L')}"/><text x="36" y="18">Offer A blue / Offer B green</text></svg>`}function render(){C=calc();let A=C.A,B=C.B,h=+S.hor||10,d=A.h[h].total-B.h[h].total,w=Math.abs(d)<1?'Tie':d<0?'A':'B';$('#app').innerHTML=`<div class="w"><div class="hero"><div class="pad"><div class="ey">SBA DealLens</div><div class="ttl">SBA loan comparison, without the spreadsheet fog.</div><div class="sub">Compare 7(a) and 504 financing structures over time with cash-to-close, payment, payoff cost, balance, penalties, and break-even logic.</div><p><button class="p" data-a="rates">Refresh Fed H.15 rates</button> <button data-a="csv">Export CSV</button> <button data-a="reset">Reset</button></p></div><div class="pad dark"><h2>${w=='Tie'?'Rough tie':'Offer '+w+' leads'}</h2><p>At ${h} years, spread is ${money(Math.abs(d))}. ${C.be?'Break-even near month '+C.be+'.':'No break-even through 25 years.'}</p><div class="k"><div><span>A payment</span><b>${money(A.mo)}</b></div><div><span>B payment</span><b>${money(B.mo)}</b></div><div><span>A cash due</span><b>${money(A.up)}</b></div><div><span>B cash due</span><b>${money(B.up)}</b></div></div></div></div><section class="sec"><div class="head"><h2>Rates and scenario</h2><span class="mut">${S.status}</span></div><div class="g4">${F('Project cost','cost')}${F('Horizon yrs','hor')}${F('Rate shock %','shock')}${F('Prime %','prime')}${F('5Y Treasury %','t5')}${F('10Y Treasury %','t10')}${F('504 CDC effective %','cdcRef')}</div></section><section class="sec"><div class="head"><h2>Offer inputs</h2></div><div class="tabs"><button data-t="A" class="${tab=='A'?'on':''}">Offer A</button><button data-t="B" class="${tab=='B'?'on':''}">Offer B</button></div><div class="g2">${offer('A')}${offer('B')}</div></section><section class="sec"><div class="head"><h2>Decision lens</h2></div><div class="metrics"><div class="m g"><span>Lower payment</span><b>${A.mo<B.mo?'A':'B'} · ${money(Math.min(A.mo,B.mo))}</b></div><div class="m"><span>Lower cash</span><b>${A.up<B.up?'A':'B'} · ${money(Math.min(A.up,B.up))}</b></div><div class="m a"><span>${h}-yr lower cost</span><b>${w} · ${money(Math.min(A.h[h].total,B.h[h].total))}</b></div><div class="m r"><span>Break-even</span><b>${C.be?'Month '+C.be:'None'}</b></div></div></section><section class="sec"><div class="head"><h2>Visuals</h2></div><div class="charts"><div class="chart"><h3>Total cost curve</h3>${path(A,B,false)}</div><div class="chart"><h3>Balance curve</h3>${path(A,B,true)}</div></div></section><section class="sec"><div class="head"><h2>Warnings and table</h2></div><div class="notes"><div class="card"><b>Warnings</b><ul>${(C.warn.length?C.warn:['No major structural warnings.']).map(x=>`<li>${x}</li>`).join('')}</ul></div><div class="card"><b>Interpretation</b><ul><li>Do not pick a deal from payment alone.</li><li>504 is modeled as bank + CDC/SBA notes.</li><li>Confirm fees, eligibility, balloons, and legal terms before relying on output.</li></ul></div></div><div class="tbl"><table><tr><th>Horizon</th><th>A cost</th><th>B cost</th><th>A balance</th><th>B balance</th><th>Winner</th></tr>${Y.map(y=>`<tr><td>${y} years</td><td>${money(A.h[y].total)}</td><td>${money(B.h[y].total)}</td><td>${money(A.h[y].bal)}</td><td>${money(B.h[y].bal)}</td><td>${A.h[y].total<B.h[y].total?'A':'B'}</td></tr>`).join('')}</table></div></section><div class="foot">Educational model only. Not financial, legal, tax, SBA, or lending advice.</div></div>`;bind()}function bind(){document.querySelectorAll('[data-p]').forEach(e=>e.onchange=()=>{let a=S,ks=e.dataset.p.split('.');ks.slice(0,-1).forEach(k=>a=a[k]);let k=ks[ks.length-1];a[k]=typeof a[k]=='number'?+e.value:e.value;save();render()});document.querySelectorAll('[data-t]').forEach(b=>b.onclick=()=>{tab=b.dataset.t;render()});document.querySelectorAll('[data-a]').forEach(b=>b.onclick=()=>act(b.dataset.a))}function save(){clearTimeout(timer);timer=setTimeout(()=>V().save('sba_deallens',S).catch(()=>{}),200)}async function act(a){if(a=='reset'){S=JSON.parse(JSON.stringify(base));save();render()}if(a=='csv'){let s='horizon,A_cost,B_cost,A_payment,B_payment\n'+Y.map(y=>[y,C.A.h[y].total,C.B.h[y].total,C.A.mo,C.B.mo].join(',')).join('\n'),u=URL.createObjectURL(new Blob([s],{type:'text/csv'})),x=document.createElement('a');x.href=u;x.download='sba-deallens.csv';x.click()}if(a=='rates'){try{S.status='refreshing';render();let tx=await(await V().fetch('https://www.federalreserve.gov/releases/h15/')).text(),txt=tx.replace(/<[^>]+>/g,' '),f=n=>{let m=txt.match(new RegExp(n+'[^0-9]*(\\d+\\.\\d+)','i'));return m?+m[1]:null};S.prime=f('Bank prime loan')||S.prime;S.t5=f('5-year')||S.t5;S.t10=f('10-year')||S.t10;S.status='live or cached Fed H.15';save();render()}catch(e){S.status='manual / fetch unavailable';render()}}}V().onReady(async()=>{let x=await V().load('sba_deallens').catch(()=>null);if(x&&x.A)S=x;render()})})();
</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/sba-deallens */