← Spreadsheet results

Spreadsheet

GPT 6 Luna · full

Initial view of Spreadsheet

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 23,492 bytes · SHA-256 b69f772661b7
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Grid — Spreadsheet</title>
<style>
/* Tunable visual constants */
:root{--blue:#1a73e8;--blue-soft:#e8f0fe;--ink:#202124;--muted:#697586;--line:#dadce0;--header:#f8f9fa;--cell-width:112px;--cell-height:27px}
*{box-sizing:border-box}body{margin:0;background:#fff;color:var(--ink);font:13px Arial,Helvetica,sans-serif;overflow:hidden}.app{height:100vh;display:flex;flex-direction:column;min-width:600px}.topbar{height:52px;display:flex;align-items:center;padding:0 18px;gap:12px;border-bottom:1px solid #e5e7eb;flex:none}.brand{font-size:16px;font-weight:600;letter-spacing:-.2px;margin-right:14px}.brand-mark{display:inline-grid;place-items:center;width:25px;height:25px;border-radius:6px;background:#188038;color:white;font-weight:700;margin-right:9px;font-size:14px}.toolbar{display:flex;align-items:center;gap:6px}.tool-btn{height:30px;padding:0 10px;border:1px solid #dadce0;border-radius:5px;background:#fff;color:#3c4043;cursor:pointer;font:inherit;white-space:nowrap}.tool-btn:hover{background:#f1f3f4}.tool-btn.primary{color:var(--blue);border-color:#c6dafc}.divider{width:1px;height:22px;background:#dadce0;margin:0 5px}.status{margin-left:auto;color:#737b84;font-size:12px}.formula-area{height:48px;display:flex;align-items:center;padding:0 14px;gap:10px;border-bottom:1px solid var(--line);flex:none;background:#fff}.name-box{width:76px;height:29px;border:1px solid #dadce0;border-radius:4px;display:flex;align-items:center;justify-content:center;font-weight:600;color:#3c4043;background:#fff}.fx{font-style:italic;font-weight:700;font-size:16px;color:#777}.formula-input{height:30px;flex:1;border:0;border-left:1px solid #e2e5e9;padding:0 10px;font:13px Arial,sans-serif;outline:none;color:#202124}.formula-input:focus{box-shadow:inset 0 -2px var(--blue)}.grid-wrap{overflow:auto;flex:1;position:relative;background:white}.sheet{border-collapse:separate;border-spacing:0;table-layout:fixed;width:max-content;min-width:100%}.sheet th,.sheet td{height:var(--cell-height);padding:0;border-right:1px solid var(--line);border-bottom:1px solid var(--line);white-space:nowrap;overflow:hidden}.sheet thead th{position:sticky;top:0;z-index:3;background:var(--header);height:27px;text-align:center;font-weight:500;color:#5f6368;min-width:var(--cell-width);border-top:1px solid var(--line)}.sheet thead th.corner{left:0;z-index:5;position:sticky;min-width:48px;width:48px}.sheet tbody th.row-head{position:sticky;left:0;z-index:2;width:48px;min-width:48px;background:var(--header);text-align:center;font-size:12px;color:#5f6368;font-weight:400}.sheet td.cell{width:var(--cell-width);min-width:var(--cell-width);max-width:var(--cell-width);position:relative;background:#fff;cursor:cell}.cell-text{display:block;padding:4px 7px;line-height:18px;overflow:hidden;text-overflow:clip;text-align:left;pointer-events:none}.cell.numeric .cell-text{ text-align:right}.cell.error .cell-text{color:#d93025}.cell.selected{z-index:1;outline:2px solid var(--blue);outline-offset:-2px}.cell.selected .cell-text{padding:3px 6px}.cell.editing{outline:2px solid var(--blue);outline-offset:-2px;z-index:4}.cell-editor{position:absolute;inset:0;width:100%;height:100%;border:0;outline:0;padding:3px 6px;font:13px Arial,sans-serif;background:white;color:#202124;z-index:5}.context-menu{position:fixed;z-index:20;display:none;min-width:185px;padding:5px;background:white;border:1px solid #dadce0;border-radius:7px;box-shadow:0 4px 16px #0002}.context-menu button{display:block;width:100%;border:0;background:transparent;text-align:left;padding:8px 10px;border-radius:4px;font:13px Arial;color:#333;cursor:pointer}.context-menu button:hover{background:#f1f3f4}.context-menu .menu-sep{height:1px;background:#eee;margin:4px}.hint{height:24px;padding:5px 15px;color:#737b84;border-top:1px solid #e9eaee;font-size:11px;flex:none}.hidden{display:none!important}
</style>
</head>
<body>
<div class="app">
  <header class="topbar">
    <div class="brand"><span class="brand-mark">▦</span>Grid</div>
    <div class="toolbar">
      <button class="tool-btn" id="undoBtn" title="Undo (Ctrl+Z)">↶ Undo</button><button class="tool-btn" id="redoBtn" title="Redo (Ctrl+Y)">↷ Redo</button><span class="divider"></span>
      <button class="tool-btn" id="insertRow">+ Row</button><button class="tool-btn" id="deleteRow">− Row</button><button class="tool-btn" id="insertCol">+ Column</button><button class="tool-btn" id="deleteCol">− Column</button>
    </div><div class="status" id="saveStatus">All changes saved</div>
  </header>
  <div class="formula-area"><div class="name-box" id="nameBox">A1</div><span class="fx">fx</span><input id="formulaInput" class="formula-input" aria-label="Formula bar" autocomplete="off" spellcheck="false"></div>
  <div class="grid-wrap" id="gridWrap"><table class="sheet" id="sheet" aria-label="Spreadsheet grid"><thead><tr id="headRow"><th class="corner"></th></tr></thead><tbody id="body"></tbody></table></div>
  <div class="hint">Tip: use arrow keys to move · Enter or Tab to commit · Right-click a row or column header for insert/delete</div>
</div>
<div class="context-menu" id="contextMenu"></div>
<script>
'use strict';
// Tunable grid and persistence constants
const ROWS=100, COLS=26, STORAGE_KEY='grid-spreadsheet-v1';
const ERR={REF:'#REF!',DIV:'#DIV/0!',PARSE:'#ERR!',CYCLE:'#CYCLE!'};

// Spreadsheet state and address helpers
const cells=new Map(), asts=new Map(), deps=new Map(), reverse=new Map();
let selected={r:0,c:0}, editInput=null, dirtyTimer=0, history=[],future=[];
const key=(r,c)=>`${r},${c}`;
function colName(c){let s='';for(c++;c;c=Math.floor((c-1)/26))s=String.fromCharCode(65+(c-1)%26)+s;return s}
function address(r,c){return colName(c)+(r+1)}
function parseAddress(s){const m=/^([A-Z]+)([1-9]\d*)$/i.exec(s);if(!m)return null;let c=0;for(const ch of m[1].toUpperCase())c=c*26+ch.charCodeAt(0)-64;return{r:Number(m[2])-1,c:c-1}}
function getCell(r,c){return cells.get(key(r,c))||{raw:'',value:'',error:''}}
function ensureCell(r,c){const k=key(r,c);let x=cells.get(k);if(!x){x={raw:'',value:'',error:''};cells.set(k,x)}return x}
function colRef(c){return c>=0&&c<COLS?colName(c):'#REF!'}

// Formula tokenizer: tokens references, numbers, names, punctuation and operators
function tokenize(source){const out=[];let i=0;while(i<source.length){const ch=source[i];if(/\s/.test(ch)){i++;continue}if(source.slice(i,i+5)==='#REF!'){out.push({t:'error',v:'#REF!'});i+=5;continue}if(/[0-9.]/.test(ch)){const m=/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/.exec(source.slice(i));if(!m)throw Error('number');out.push({t:'num',v:Number(m[0])});i+=m[0].length;continue}if(/[A-Za-z]/.test(ch)){const m=/^[A-Za-z]+[0-9]+/.exec(source.slice(i));if(m){const p=parseAddress(m[0]);out.push({t:'ref',v:m[0].toUpperCase(),p});i+=m[0].length;continue}const n=/^[A-Za-z]+/.exec(source.slice(i))[0];out.push({t:'name',v:n.toUpperCase()});i+=n.length;continue}if('+-*/():,'.includes(ch)){out.push({t:ch,v:ch});i++;continue}throw Error('token')}out.push({t:'eof'});return out}

// Recursive-descent parser with standard arithmetic precedence
function parseFormula(raw){const ts=tokenize(raw.slice(1));let i=0;const peek=()=>ts[i],take=t=>{if(peek().t!==t)throw Error('expected '+t);return ts[i++]};
 function expr(){let n=term();while(peek().t==='+'||peek().t==='-'){const op=ts[i++].t;n={type:'bin',op,left:n,right:term()}}return n}
 function term(){let n=unary();while(peek().t==='*'||peek().t==='/'){const op=ts[i++].t;n={type:'bin',op,left:n,right:unary()}}return n}
 function unary(){if(peek().t==='+'||peek().t==='-'){const op=ts[i++].t;return{type:'unary',op,arg:unary()}}return primary()}
 function primary(){const t=peek();if(t.t==='num'){i++;return{type:'num',value:t.v}}if(t.t==='error'){i++;return{type:'error',value:ERR.REF}}if(t.t==='ref'){i++;let a={type:'ref',p:t.p,source:t.v};if(peek().t===':'){i++;const end=take('ref');a={type:'range',a:a.p,b:end.p,as:a.source,bs:end.v}}return a}if(t.t==='('){i++;const n=expr();take(')');return n}if(t.t==='name'){i++;const name=t.v;if(!['SUM','AVG','AVERAGE','MIN','MAX','COUNT'].includes(name))throw Error('function');take('(');const args=[];if(peek().t!==')'){args.push(expr());while(peek().t===','){i++;args.push(expr())}}take(')');return{type:'call',name,args}}throw Error('expression')}
 const tree=expr();if(peek().t!=='eof')throw Error('trailing');return tree}

// Dependency extraction and incremental graph maintenance
function collectRefs(node,set){if(!node)return;if(node.type==='ref'){if(node.p)set.add(key(node.p.r,node.p.c))}else if(node.type==='range'){if(!node.a||!node.b)return;const r0=Math.max(0,Math.min(node.a.r,node.b.r)),r1=Math.min(ROWS-1,Math.max(node.a.r,node.b.r)),c0=Math.max(0,Math.min(node.a.c,node.b.c)),c1=Math.min(COLS-1,Math.max(node.a.c,node.b.c));for(let r=r0;r<=r1;r++)for(let c=c0;c<=c1;c++)set.add(key(r,c))}else if(node.type==='bin'){collectRefs(node.left,set);collectRefs(node.right,set)}else if(node.type==='unary')collectRefs(node.arg,set);else if(node.type==='call')node.args.forEach(x=>collectRefs(x,set))}
function detachDeps(k){for(const d of deps.get(k)||[]){const rev=reverse.get(d);if(rev){rev.delete(k);if(!rev.size)reverse.delete(d)}}deps.delete(k);asts.delete(k)}
function parseCell(k){const cell=cells.get(k);detachDeps(k);if(!cell||!cell.raw.startsWith('='))return;try{const ast=parseFormula(cell.raw);const ds=new Set();collectRefs(ast,ds);asts.set(k,ast);deps.set(k,ds);for(const d of ds){if(!reverse.has(d))reverse.set(d,new Set());reverse.get(d).add(k)}}catch(_){asts.set(k,{type:'error',value:ERR.PARSE});deps.set(k,new Set())}}
function downstream(seeds){const seen=new Set(seeds),q=[...seeds];for(let i=0;i<q.length;i++){for(const x of reverse.get(q[i])||[])if(!seen.has(x)){seen.add(x);q.push(x)}}return seen}
function numberValue(v){if(typeof v==='number')return v;if(typeof v==='string'&&v.trim()!==''&&Number.isFinite(Number(v)))return Number(v);return 0}
function isNum(v){return typeof v==='number'&&Number.isFinite(v)}
function evaluateNode(n,resolve){if(n.type==='num')return n.value;if(n.type==='error')return n.value;if(n.type==='ref'){if(!n.p||n.p.r<0||n.p.c<0||n.p.r>=ROWS||n.p.c>=COLS)return ERR.REF;return resolve(key(n.p.r,n.p.c))}if(n.type==='range')return{range:n};if(n.type==='unary'){const v=evaluateNode(n.arg,resolve);if(typeof v==='string'&&v.startsWith('#'))return v;return n.op==='-'?-numberValue(v):numberValue(v)}if(n.type==='bin'){const a=evaluateNode(n.left,resolve),b=evaluateNode(n.right,resolve);if(typeof a==='string'&&a.startsWith('#'))return a;if(typeof b==='string'&&b.startsWith('#'))return b;const x=numberValue(a),y=numberValue(b);if(n.op==='+' )return x+y;if(n.op==='-')return x-y;if(n.op==='*')return x*y;if(y===0)return ERR.DIV;return x/y}if(n.type==='call'){const vals=[];for(const arg of n.args){if(arg.type==='range'){const rg=arg;if(!rg.a||!rg.b)return ERR.REF;const r0=Math.min(rg.a.r,rg.b.r),r1=Math.max(rg.a.r,rg.b.r),c0=Math.min(rg.a.c,rg.b.c),c1=Math.max(rg.a.c,rg.b.c);if(r0<0||c0<0||r1>=ROWS||c1>=COLS)return ERR.REF;for(let r=r0;r<=r1;r++)for(let c=c0;c<=c1;c++)vals.push({v:resolve(key(r,c)),ref:true})}else if(arg.type==='ref'){if(!arg.p||arg.p.r<0||arg.p.c<0||arg.p.r>=ROWS||arg.p.c>=COLS)return ERR.REF;vals.push({v:resolve(key(arg.p.r,arg.p.c)),ref:true})}else{const v=evaluateNode(arg,resolve);if(typeof v==='string'&&v.startsWith('#'))return v;vals.push({v,ref:false})}}
 const nums=[];for(const x of vals){if(typeof x.v==='string'&&x.v.startsWith('#'))return x.v;if(isNum(x.v))nums.push(x.v);else if(!x.ref&&typeof x.v==='number')nums.push(x.v)}if(n.name==='COUNT')return nums.length;if(n.name==='SUM')return nums.reduce((a,b)=>a+b,0);if(n.name==='AVG'||n.name==='AVERAGE')return nums.length?nums.reduce((a,b)=>a+b,0)/nums.length:0;if(n.name==='MIN')return nums.length?Math.min(...nums):0;if(n.name==='MAX')return nums.length?Math.max(...nums):0}return ERR.PARSE}

// Recalculate only the changed cell and its transitive dependents; DFS is topological and cycle-safe
function recalculate(seeds){const affected=downstream(seeds),state=new Map(),stack=[],cyclic=new Set();function calc(k){if(!affected.has(k)){const c=cells.get(k);return c?c.value:''}if(state.get(k)===2){const c=cells.get(k);return c?c.value:''}if(state.get(k)===1){const at=stack.indexOf(k);for(let i=Math.max(0,at);i<stack.length;i++)cyclic.add(stack[i]);cyclic.add(k);return ERR.CYCLE}state.set(k,1);stack.push(k);let result='';const c=cells.get(k);if(c&&c.raw.startsWith('=')){const ast=asts.get(k);result=ast?evaluateNode(ast,calc):ERR.PARSE}else if(c){const raw=c.raw.trim();if(raw!==''&&Number.isFinite(Number(raw)))result=Number(raw);else result=c.raw}stack.pop();state.set(k,2);if(c){c.value=result;c.error=typeof result==='string'&&result.startsWith('#')?result:''}return result}
 for(const k of affected)calc(k);if(cyclic.size){for(const k of cyclic){const c=ensureCell(...k.split(',').map(Number));c.value=ERR.CYCLE;c.error=ERR.CYCLE;state.set(k,2)}}// refresh any formulas evaluated before cycle membership was known
 for(const k of affected){const c=cells.get(k);if(c&&c.raw.startsWith('=')&&!cyclic.has(k)&&!state.get(k)){calc(k)}}
 renderCells(affected);scheduleSave()}
function writeRaw(r,c,raw,record=true){const k=key(r,c),before=getCell(r,c).raw;if(before===raw)return;if(record){history.push({r,c,before,after:raw});if(history.length>150)history.shift();future=[]}const cell=ensureCell(r,c);cell.raw=raw;parseCell(k);recalculate([k]);updateFormulaBar()}

// Build and update the visible spreadsheet grid
const headRow=document.getElementById('headRow'),tbody=document.getElementById('body'),cellEls=new Map();for(let c=0;c<COLS;c++){const th=document.createElement('th');th.textContent=colName(c);th.dataset.col=c;headRow.append(th)}for(let r=0;r<ROWS;r++){const tr=document.createElement('tr'),th=document.createElement('th');th.className='row-head';th.textContent=r+1;th.dataset.row=r;tr.append(th);for(let c=0;c<COLS;c++){const td=document.createElement('td');td.className='cell';td.dataset.r=r;td.dataset.c=c;const span=document.createElement('span');span.className='cell-text';td.append(span);tr.append(td);cellEls.set(key(r,c),td)}tbody.append(tr)}
function showValue(v){if(v===null||v===undefined)return'';if(typeof v==='number'){if(!Number.isFinite(v))return String(v);return Number(v.toPrecision(12)).toString()}return String(v)}
function renderOne(k){const td=cellEls.get(k);if(!td)return;const [r,c]=k.split(',').map(Number),cell=getCell(r,c),span=td.firstElementChild;td.classList.toggle('numeric',typeof cell.value==='number');td.classList.toggle('error',!!cell.error);span.textContent=showValue(cell.value);td.title=cell.error||''}
function renderCells(set){for(const k of set)renderOne(k)}
function updateSelection(){document.querySelectorAll('.cell.selected').forEach(x=>x.classList.remove('selected'));const td=cellEls.get(key(selected.r,selected.c));if(td)td.classList.add('selected');document.getElementById('nameBox').textContent=address(selected.r,selected.c);if(!editInput)document.getElementById('formulaInput').value=getCell(selected.r,selected.c).raw;td?.scrollIntoView({block:'nearest',inline:'nearest'})}
function updateFormulaBar(){document.getElementById('formulaInput').value=editInput?editInput.value:getCell(selected.r,selected.c).raw}
function setSelected(r,c){selected={r:Math.max(0,Math.min(ROWS-1,r)),c:Math.max(0,Math.min(COLS-1,c))};updateSelection()}
function commitEdit(move){if(editInput){const input=editInput,raw=input.value;editInput=null;const td=cellEls.get(key(selected.r,selected.c));td.classList.remove('editing');input.remove();writeRaw(selected.r,selected.c,raw);document.getElementById('formulaInput').value=raw}if(move){if(move==='down')setSelected(selected.r+1,selected.c);else if(move==='right')setSelected(selected.r,selected.c+1)}}
function beginEdit(initial){if(editInput)return;const td=cellEls.get(key(selected.r,selected.c));const input=document.createElement('input');input.className='cell-editor';input.value=initial===undefined?getCell(selected.r,selected.c).raw:initial;td.classList.add('editing');td.append(input);editInput=input;const bar=document.getElementById('formulaInput');bar.value=input.value;input.addEventListener('input',()=>{bar.value=input.value});input.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();commitEdit('down')}else if(e.key==='Tab'){e.preventDefault();commitEdit('right')}else if(e.key==='Escape'){e.preventDefault();const td2=cellEls.get(key(selected.r,selected.c));editInput.remove();editInput=null;td2.classList.remove('editing');updateFormulaBar()} });input.focus();input.setSelectionRange(input.value.length,input.value.length)}

// Grid interactions, formula bar editing, navigation
const formulaInput=document.getElementById('formulaInput');formulaInput.addEventListener('input',()=>{if(editInput){editInput.value=formulaInput.value;return}writeRaw(selected.r,selected.c,formulaInput.value)});formulaInput.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();writeRaw(selected.r,selected.c,formulaInput.value);setSelected(selected.r+1,selected.c);document.querySelector('.grid-wrap').focus()}else if(e.key==='Tab'){e.preventDefault();writeRaw(selected.r,selected.c,formulaInput.value);setSelected(selected.r,selected.c+1)}else if(e.key==='Escape'){formulaInput.value=getCell(selected.r,selected.c).raw;formulaInput.blur()}});
document.getElementById('sheet').addEventListener('mousedown',e=>{const td=e.target.closest('td.cell');if(td){commitEdit();setSelected(+td.dataset.r,+td.dataset.c);document.getElementById('formulaInput').blur()}});document.getElementById('sheet').addEventListener('dblclick',e=>{const td=e.target.closest('td.cell');if(td){setSelected(+td.dataset.r,+td.dataset.c);beginEdit()}});
document.getElementById('sheet').addEventListener('contextmenu',e=>{const th=e.target.closest('th');if(!th)return;e.preventDefault();if(th.dataset.row!==undefined)openMenu(e.clientX,e.clientY,'row',+th.dataset.row);else if(th.dataset.col!==undefined)openMenu(e.clientX,e.clientY,'col',+th.dataset.col)});
document.addEventListener('keydown',e=>{if(e.target===formulaInput||e.target===editInput)return;if(e.ctrlKey&&e.key.toLowerCase()==='z'){e.preventDefault();undo()}else if(e.ctrlKey&&e.key.toLowerCase()==='y'){e.preventDefault();redo()}else if(e.key==='Delete'||e.key==='Backspace'){e.preventDefault();writeRaw(selected.r,selected.c,'')}else if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)){e.preventDefault();commitEdit();setSelected(selected.r+(e.key==='ArrowDown'?1:e.key==='ArrowUp'?-1:0),selected.c+(e.key==='ArrowRight'?1:e.key==='ArrowLeft'?-1:0))}else if(e.key==='Enter'){e.preventDefault();setSelected(selected.r+1,selected.c)}else if(e.key==='Tab'){e.preventDefault();setSelected(selected.r,selected.c+1)}else if(e.key.length===1&&!e.ctrlKey&&!e.metaKey){e.preventDefault();beginEdit(e.key)}});

// Formula-aware structural operations; rewrite refs before moving stored cells
function rewriteFormula(raw,axis,index,mode){if(!raw.startsWith('='))return raw;let out='',i=0;while(i<raw.length){const ch=raw[i];if(/[A-Za-z]/.test(ch)&&!(i>0&&/[A-Za-z0-9_]/.test(raw[i-1]))){const m=/^[A-Za-z]+[1-9]\d*/.exec(raw.slice(i));if(m){const p=parseAddress(m[0]);if(p){let coord=axis==='row'?p.r:p.c;const prev=raw.slice(0,i).trimEnd(),nextText=raw.slice(i+m[0].length).trimStart(),rangeEndpoint=prev.endsWith(':')||nextText.startsWith(':');if(mode==='insert'){if(coord>=index)coord++}else if(coord===index){if(!rangeEndpoint){out+='#REF!';i+=m[0].length;continue}else coord=Math.max(0,index-1)}else if(coord>index)coord--;if(axis==='row')p.r=coord;else p.c=coord;if(p.r<0||p.c<0||p.r>=ROWS||p.c>=COLS)out+='#REF!';else out+=address(p.r,p.c);i+=m[0].length;continue}}}out+=ch;i++}return out}
function structural(axis,index,mode){commitEdit();const newCells=new Map();for(const [k,v] of cells){let [r,c]=k.split(',').map(Number),coord=axis==='row'?r:c;if(mode==='insert'){if(coord>=index)coord++;if(coord>=(axis==='row'?ROWS:COLS))continue}else{if(coord===index)continue;if(coord>index)coord--}if(axis==='row')r=coord;else c=coord;newCells.set(key(r,c),{raw:v.raw,value:'',error:''})}cells.clear();for(const [k,v] of newCells)cells.set(k,v);for(const cell of cells.values())cell.raw=rewriteFormula(cell.raw,axis,index,mode);rebuildGraph();renderAll();updateSelection();scheduleSave()}
function rebuildGraph(){asts.clear();deps.clear();reverse.clear();for(const [k,c] of cells)if(c.raw.startsWith('='))parseCell(k);const all=new Set(cells.keys());for(const k of all){const c=cells.get(k);if(!c.raw.startsWith('=')){c.value=c.raw!==''&&Number.isFinite(Number(c.raw))?Number(c.raw):c.raw;c.error=''}}recalculate(new Set([...cells.keys(),...reverse.keys()]))}
function renderAll(){for(const k of cellEls.keys())renderOne(k)}

document.getElementById('insertRow').onclick=()=>structural('row',selected.r,'insert');document.getElementById('deleteRow').onclick=()=>structural('row',selected.r,'delete');document.getElementById('insertCol').onclick=()=>structural('col',selected.c,'insert');document.getElementById('deleteCol').onclick=()=>structural('col',selected.c,'delete');
const menu=document.getElementById('contextMenu');function openMenu(x,y,axis,index){menu.innerHTML=`<button data-op="insert">Insert ${axis==='row'?'row above':'column left'}</button><button data-op="delete">Delete ${axis}</button>`;menu.style.left=x+'px';menu.style.top=y+'px';menu.style.display='block';menu.onclick=e=>{const op=e.target.dataset.op;if(op){structural(axis,index,op);menu.style.display='none'}}}document.addEventListener('click',e=>{if(!menu.contains(e.target))menu.style.display='none'});

// Lightweight undo/redo and localStorage persistence of raw cell contents
function undo(){const x=history.pop();if(!x)return;future.push(x);writeRaw(x.r,x.c,x.before,false);updateUndo()}
function redo(){const x=future.pop();if(!x)return;history.push(x);writeRaw(x.r,x.c,x.after,false);updateUndo()}
function updateUndo(){document.getElementById('undoBtn').disabled=!history.length;document.getElementById('redoBtn').disabled=!future.length}document.getElementById('undoBtn').onclick=undo;document.getElementById('redoBtn').onclick=redo;
function scheduleSave(){document.getElementById('saveStatus').textContent='Saving…';clearTimeout(dirtyTimer);dirtyTimer=setTimeout(()=>{try{const obj={version:1,cells:[...cells].filter(([,v])=>v.raw!=='').map(([k,v])=>[k,v.raw])};localStorage.setItem(STORAGE_KEY,JSON.stringify(obj));document.getElementById('saveStatus').textContent='All changes saved'}catch(_){document.getElementById('saveStatus').textContent='Could not save'}} ,180)}
function loadSaved(){try{const obj=JSON.parse(localStorage.getItem(STORAGE_KEY)||'null');if(obj&&Array.isArray(obj.cells))for(const [k,raw] of obj.cells){if(/^\d+,\d+$/.test(k)){const [r,c]=k.split(',').map(Number);if(r<ROWS&&c<COLS)ensureCell(r,c).raw=String(raw)}}}catch(_){}}

// Startup: restore raw inputs, build dependencies, compute all formulas
loadSaved();rebuildGraph();setSelected(0,0);updateUndo();
</script>
</body>
</html>
<!-- agent-meta {"model":"openai/gpt-6-luna","provider":"openrouter","persona":"full","sessionId":"d0f8bf89-d5e3-4a22-8584-adf744dd2986","tokensIn":333348,"tokensOut":18654,"tokensTotal":352002,"cost":0.014875530000000001,"turns":21,"toolCalls":20,"failedToolCalls":1,"timestamp":"2026-09-23T00:01:51.066Z"} -->