← Spreadsheet results

Spreadsheet

GLM 5.3 Flash · 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 25,066 bytes · SHA-256 ba48eb5065cc
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sheet</title>
<style>
/* ============ STYLES ============ */
:root{
  --sel-border:#1a73e8;
  --header-bg:#f8f9fa;
  --header-text:#5f6368;
  --gridline:#e0e0e0;
  --err:#d93025;
}
*{box-sizing:border-box}
body{margin:0;font-family:'Segoe UI',Arial,sans-serif;font-size:13px;color:#202124;background:#fff;display:flex;flex-direction:column;height:100vh}

/* toolbar */
#toolbar{display:flex;gap:6px;padding:6px 10px;border-bottom:1px solid var(--gridline);background:var(--header-bg);align-items:center}
#toolbar button{font:inherit;padding:4px 10px;border:1px solid #dadce0;border-radius:4px;background:#fff;cursor:pointer;color:#202124}
#toolbar button:hover{background:#f1f3f4}
#toolbar .sep{width:1px;height:20px;background:#dadce0;margin:0 4px}
#toolbar span.lbl{color:var(--header-text);font-size:12px}

/* formula bar */
#fbarrow{display:flex;align-items:stretch;border-bottom:1px solid var(--gridline)}
#namebox{width:90px;padding:6px 10px;border-right:1px solid var(--gridline);color:#202124;font-weight:600;display:flex;align-items:center;background:var(--header-bg)}
#fbar{flex:1;border:none;outline:none;padding:6px 10px;font:13px/1.4 'Consolas','Menlo',monospace;color:#202124}

/* grid */
#gridwrap{flex:1;overflow:auto}
table{border-collapse:separate;border-spacing:0;table-layout:fixed}
th,td{width:var(--cellw);height:var(--cellh);padding:0 5px;border-right:1px solid var(--gridline);border-bottom:1px solid var(--gridline);white-space:nowrap;overflow:hidden;font-weight:400}
th{background:var(--header-bg);color:var(--header-text);text-align:center;font-size:11px;user-select:none;position:sticky;z-index:2}
thead th{top:0}
tbody th{left:0}
thead th:first-child{left:0;z-index:3}
tbody th.rowhdr{cursor:pointer}
td{background:#fff;cursor:cell;position:relative}
td.num{text-align:right}
td.err{color:var(--err);font-weight:600}
td.selected{outline:2px solid var(--sel-border);outline-offset:-2px}
th.hl{background:#e8f0fe;color:#1a73e8;font-weight:700}
#cellinput{position:absolute;inset:0;border:2px solid var(--sel-border);padding:0 4px;font:13px 'Segoe UI',Arial,sans-serif;outline:none;z-index:5;background:#fff}
#status{padding:3px 10px;color:var(--header-text);font-size:11px;border-top:1px solid var(--gridline)}
</style>
</head>
<body>

<div id="toolbar">
  <span class="lbl">Rows:</span>
  <button id="insRow" title="Insert row above selection">+ Insert</button>
  <button id="delRow" title="Delete selected row">− Delete</button>
  <span class="sep"></span>
  <span class="lbl">Cols:</span>
  <button id="insCol">+ Insert</button>
  <button id="delCol">− Delete</button>
  <span class="sep"></span>
  <button id="clearCell">Clear cell</button>
</div>

<div id="fbarrow">
  <div id="namebox">A1</div>
  <input id="fbar" spellcheck="false" autocomplete="off" placeholder="Enter a value or formula (=SUM(A1:A5))">
</div>

<div id="gridwrap"><table id="grid"></table></div>
<div id="status">Ready</div>

<script>
'use strict';
/* ================================================================
   CONFIG — tunable constants
   ================================================================ */
const CFG = {
  COLS: 26,          // A..Z
  ROWS: 100,
  CELL_W: 84,        // px
  CELL_H: 24,        // px
  STORE_KEY: 'sheet.v1',
  COLORS: { sel:'#1a73e8', err:'#d93025', headerBg:'#f8f9fa' }
};
document.documentElement.style.setProperty('--cellw', CFG.CELL_W+'px');
document.documentElement.style.setProperty('--cellh', CFG.CELL_H+'px');

/* ================================================================
   HELPERS — key / name conversion
   ================================================================ */
const K = (r,c)=> r+','+c;                       // 0-based internal key
function colName(c){ let s=''; c++; while(c>0){ s=String.fromCharCode(65+(c-1)%26)+s; c=Math.floor((c-1)/26);} return s; }
function colIndex(s){ let n=0; for(const ch of s) n=n*26+(ch.charCodeAt(0)-64); return n-1; }
const cellName=(r,c)=> colName(c)+(r+1);
function refName(r,c){ return colName(c)+(r+1); }
const ERRS = ['#REF!','#DIV/0!','#ERR!','#CYCLE!','#VALUE!','#NAME?'];
const isErr = v => typeof v==='string' && ERRS.includes(v);

/* ================================================================
   MODEL — raw cell contents
   ================================================================ */
const cells = new Map();      // "r,c" -> raw string
const astCache = new Map();   // "r,c" -> parsed AST (formulas only)
const values  = new Map();    // "r,c" -> number | string | error-string
let depsOf    = new Map();    // "r,c" -> Set of keys it depends on
let depsTo    = new Map();    // "r,c" -> Set of keys depending on it

function getRaw(k){ return cells.get(k); }

/* ================================================================
   FORMULA ENGINE — tokenizer
   ================================================================ */
function tokenize(src){
  const toks=[]; let i=0;
  const push=(t,v,s,e)=>toks.push({t,v,start:s,end:e});
  while(i<src.length){
    const c=src[i], start=i;
    if(c===' '||c==='\t'){ i++; continue; }
    if(/[0-9]/.test(c) || (c==='.' && /[0-9]/.test(src[i+1]||''))){
      const m=/^[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?/.exec(src.slice(i));
      push('num', parseFloat(m[0]), start, i+m[0].length); i+=m[0].length; continue;
    }
    if(c==='"'){
      let j=i+1, s='';
      while(j<src.length && src[j]!=='"'){ s+=src[j++]; }
      if(j>=src.length) throw {parse:true};
      push('str', s, start, j+1); i=j+1; continue;
    }
    if(/[A-Za-z_]/.test(c)){
      const m=/^[A-Za-z_]+[0-9]*/.exec(src.slice(i));
      push('ident', m[0].toUpperCase(), start, i+m[0].length); i+=m[0].length; continue;
    }
    if(src.startsWith('#REF!',i)){ push('errref',null,start,i+5); i+=5; continue; }
    if('+-*/(),:^&'.includes(c)){ push('op',c,start,i+1); i++; continue; }
    throw {parse:true};
  }
  return toks;
}

/* ================================================================
   FORMULA ENGINE — recursive-descent parser
   Grammar:  expr := term (('+'|'-') term)*
             term := pow (('*'|'/') pow)*
             pow  := unary ('^' unary)*        (right-assoc bonus)
             unary := ('-'|'+') unary | primary
             primary := num | str | ref/range | func(args) | '(' expr ')' | #REF!
   ================================================================ */
const REF_RE = /^([A-Z]{1,2})([0-9]{1,3})$/;
function parseRef(s){ // returns {r,c} or null if invalid/out of grid
  const m=REF_RE.exec(s); if(!m) return null;
  const c=colIndex(m[1]), r=parseInt(m[2],10)-1;
  if(c<0||c>=CFG.COLS||r<0||r>=CFG.ROWS) return null;
  return {r,c};
}
function parseFormula(src){
  const toks=tokenize(src.slice(1));
  let p=0;
  const peek=()=>toks[p], next=()=>toks[p++];
  function expect(op){ const t=next(); if(!t||t.t!=='op'||t.v!==op) throw {parse:true}; }
  function expr(){
    let n=term();
    while(peek()&&peek().t==='op'&&(peek().v==='+'||peek().v==='-'||peek().v==='&')){
      const op=next().v; n={t:'bin',op,l:n,r:term()};
    }
    return n;
  }
  function term(){
    let n=pow();
    while(peek()&&peek().t==='op'&&(peek().v==='*'||peek().v==='/')){
      const op=next().v; n={t:'bin',op,l:n,r:pow()};
    }
    return n;
  }
  function pow(){
    let n=unary();
    if(peek()&&peek().t==='op'&&peek().v==='^'){ next(); return {t:'bin',op:'^',l:n,r:pow()}; }
    return n;
  }
  function unary(){
    if(peek()&&peek().t==='op'&&(peek().v==='-'||peek().v==='+')){
      const op=next().v; const e=unary();
      return op==='-'? {t:'un',op:'-',e} : e;
    }
    return primary();
  }
  function primary(){
    const t=next();
    if(!t) throw {parse:true};
    if(t.t==='num') return {t:'num',v:t.v};
    if(t.t==='str') return {t:'str',v:t.v};
    if(t.t==='errref') return {t:'err',v:'#REF!'};
    if(t.t==='op'&&t.v==='('){ const e=expr(); expect(')'); return e; }
    if(t.t==='ident'){
      const ref=parseRef(t.v);
      if(ref){
        if(peek()&&peek().t==='op'&&peek().v===':'&&toks[p+1]&&toks[p+1].t==='ident'){
          next();
          const t2=next(); const ref2=parseRef(t2.v);
          if(!ref2) return {t:'err',v:'#REF!'};
          return {t:'range',r1:Math.min(ref.r,ref2.r),c1:Math.min(ref.c,ref2.c),
                        r2:Math.max(ref.r,ref2.r),c2:Math.max(ref.c,ref2.c)};
        }
        return {t:'ref',r:ref.r,c:ref.c};
      }
      if(peek()&&peek().t==='op'&&peek().v==='('){
        next();
        const args=[];
        if(!(peek()&&peek().t==='op'&&peek().v===')')){
          args.push(expr());
          while(peek()&&peek().t==='op'&&peek().v===','){ next(); args.push(expr()); }
        }
        expect(')');
        return {t:'fn',name:t.v,args};
      }
      return {t:'err',v:'#NAME?'};
    }
    throw {parse:true};
  }
  const ast=expr();
  if(p<toks.length) throw {parse:true};
  return ast;
}
function getAst(k){
  let a=astCache.get(k);
  if(a===undefined){
    const raw=cells.get(k);
    if(raw==null){ a=null; }
    else if(raw[0]==='='){
      try{ a=parseFormula(raw); }catch(e){ a={t:'err',v:'#ERR!'}; }
    } else a=null;
    astCache.set(k,a);
  }
  return a;
}

/* ================================================================
   DEPENDENCY GRAPH
   ================================================================ */
function computeDeps(k){
  const set=new Set();
  const ast=getAst(k);
  if(!ast) return set;
  (function walk(n){
    if(!n) return;
    if(n.t==='ref') set.add(K(n.r,n.c));
    else if(n.t==='range'){
      for(let r=n.r1;r<=n.r2;r++) for(let c=n.c1;c<=n.c2;c++) set.add(K(r,c));
    }
    else if(n.t==='bin'){ walk(n.l); walk(n.r); }
    else if(n.t==='un'){ walk(n.e); }
    else if(n.t==='fn'){ n.args.forEach(walk); }
  })(ast);
  return set; // self-reference kept -> flagged as #CYCLE! by Kahn
}
function rebuildDepsFor(k){
  const old=depsOf.get(k);
  if(old) for(const d of old){ const s=depsTo.get(d); if(s){ s.delete(k); if(!s.size) depsTo.delete(d);} }
  const deps=computeDeps(k);
  depsOf.set(k,deps);
  for(const d of deps){ let s=depsTo.get(d); if(!s){s=new Set();depsTo.set(d,s);} s.add(k); }
}

/* ================================================================
   EVALUATION
   ================================================================ */
function cellVal(r,c){ // value of a referenced cell; empty -> 0
  const v=values.get(K(r,c));
  if(v===undefined||v==='') return 0;
  if(isErr(v)) throw {err:v};
  return v;
}
function toNum(v){
  if(typeof v==='number') return v;
  if(typeof v==='string' && v.trim()!=='' && !isNaN(Number(v))) return Number(v);
  throw {err:'#ERR!'};
}
function evalAst(n){
  switch(n.t){
    case 'num': return n.v;
    case 'str': return n.v;
    case 'err': throw {err:n.v};
    case 'ref': return cellVal(n.r,n.c);
    case 'range':{ // array of raw values (errors thrown lazily on use)
      const out=[];
      for(let r=n.r1;r<=n.r2;r++) for(let c=n.c1;c<=n.c2;c++) out.push(cellVal(r,c));
      return out;
    }
    case 'un': return -toNum(evalAst(n.e));
    case 'bin':{
      let l=evalAst(n.l), r=evalAst(n.r);
      if(Array.isArray(l)) l=l.length? toNum(l[0]) : 0;
      if(Array.isArray(r)) r=r.length? toNum(r[0]) : 0;
      l=toNum(l); r=toNum(r);
      switch(n.op){
        case '+': return l+r;
        case '-': return l-r;
        case '*': return l*r;
        case '/': if(r===0) throw {err:'#DIV/0!'}; return l/r;
        case '^': return Math.pow(l,r);
        case '&': return String(l)+String(r);
      }
      throw {err:'#ERR!'};
    }
    case 'fn':{
      const name=n.name;
      const FNS={SUM:1,AVERAGE:1,AVG:1,MIN:1,MAX:1,COUNT:1};
      if(!FNS[name]) throw {err:'#NAME?'};
      // gather numbers
      const nums=[];
      const grab=(v)=>{
        if(Array.isArray(v)){ v.forEach(grab); return; }
        if(isErr(v)) throw {err:v};
        if(typeof v==='number') { if(!isFinite(v)) throw {err:'#DIV/0!'}; out.push(v); }
        // strings (text cells / string literals) are ignored by aggregates
      };
      const out=[];
      for(const a of n.args){
        const v=evalAst(a);
        if(typeof v==='string' && !Array.isArray(v)){
          // scalar string arg: numeric-looking strings count
          if(v.trim()!=='' && !isNaN(Number(v))) out.push(Number(v));
          continue;
        }
        grab(v);
      }
      if(name==='COUNT') return out.length;
      if(!out.length){ if(name==='SUM') return 0; throw {err:'#DIV/0!'}; }
      const sum=out.reduce((a,b)=>a+b,0);
      if(name==='SUM') return sum;
      if(name==='AVERAGE'||name==='AVG') return sum/out.length;
      if(name==='MIN') return Math.min(...out);
      if(name==='MAX') return Math.max(...out);
      throw {err:'#ERR!'};
    }
  }
  throw {err:'#ERR!'};
}

/* ================================================================
   RECALCULATION — affected set + Kahn topological order + cycle mark
   ================================================================ */
function evalCell(k){
  const raw=cells.get(k);
  if(raw==null||raw===''){ values.delete(k); return; }
  if(raw[0]!=='='){
    const n=Number(raw);
    values.set(k, raw.trim()!=='' && !isNaN(n) ? n : raw);
    return;
  }
  const ast=getAst(k);
  try{
    const v=evalAst(ast);
    values.set(k, typeof v==='number' ? (Math.round(v*1e10)/1e10) : v);
  }catch(e){
    values.set(k, e && e.err ? e.err : '#ERR!');
  }
}
function recompute(startKeys){
  // 1. transitive dependents
  const aff=new Set(), stack=[...startKeys];
  while(stack.length){
    const k=stack.pop();
    if(aff.has(k)) continue;
    aff.add(k);
    const d=depsTo.get(k);
    if(d) for(const x of d) stack.push(x);
  }
  // 2. Kahn's algorithm within affected set
  const indeg=new Map();
  for(const k of aff){
    let n=0;
    const d=depsOf.get(k);
    if(d) for(const x of d) if(aff.has(x)) n++;
    indeg.set(k,n);
  }
  const q=[]; for(const [k,n] of indeg) if(n===0) q.push(k);
  const order=[];
  while(q.length){
    const k=q.pop(); order.push(k);
    const d=depsTo.get(k);
    if(d) for(const x of d) if(aff.has(x)){
      const n=indeg.get(x)-1; indeg.set(x,n);
      if(n===0) q.push(x);
    }
  }
  // 3. evaluate in order; leftovers are in (or downstream of) a cycle
  for(const k of order) evalCell(k);
  const cycled=[];
  for(const k of aff){
    if(!order.includes(k)){ values.set(k,'#CYCLE!'); cycled.push(k); }
  }
  return {aff, cycled};
}
function fullRecalc(){
  depsOf=new Map(); depsTo=new Map();
  for(const k of cells.keys()) rebuildDepsFor(k);
  recompute([...cells.keys()]);
  renderAll();
}

/* ================================================================
   STRUCTURAL OPERATIONS
   ================================================================ */
function insertRow(at){
  const entries=[...cells.entries()];
  const next=new Map();
  for(const [k,raw] of entries){
    let [r,c]=k.split(',').map(Number);
    if(r>=at) r++;
    if(r>=CFG.ROWS) continue;
    next.set(K(r,c),raw);
  }
  applyStructure(next,(r,c)=> r>=at? {r:r+1,c} : (r+1>CFG.ROWS-1&&r>=at? null:{r,c}),
    (r1,c1,r2,c2)=>{
      const a=r1>=at?r1+1:r1, b=r2>=at?r2+1:r2;
      if(a>=CFG.ROWS) return null;
      return {r1:a,c1,r2:Math.min(b,CFG.ROWS-1),c2};
    });
}
function deleteRow(at){
  const entries=[...cells.entries()];
  const next=new Map();
  for(const [k,raw] of entries){
    let [r,c]=k.split(',').map(Number);
    if(r===at) continue;
    if(r>at) r--;
    next.set(K(r,c),raw);
  }
  applyStructure(next,(r,c)=> r===at? null : (r>at? {r:r-1,c}:{r,c}),
    (r1,c1,r2,c2)=>{
      if(r1===at&&r2===at) return null;
      const a=r1<at?r1:r1-1, b=r2<=at?r2:r2-1;
      if(a>b) return null;
      return {r1:a,c1,r2:b,c2};
    });
}
function insertCol(at){
  const entries=[...cells.entries()];
  const next=new Map();
  for(const [k,raw] of entries){
    let [r,c]=k.split(',').map(Number);
    if(c>=at) c++;
    if(c>=CFG.COLS) continue;
    next.set(K(r,c),raw);
  }
  applyStructure(next,(r,c)=> c>=at? {r,c:c+1}:{r,c},
    (r1,c1,r2,c2)=>{
      const a=c1>=at?c1+1:c1, b=c2>=at?c2+1:c2;
      if(a>=CFG.COLS) return null;
      return {r1,c1:a,r2,c2:Math.min(b,CFG.COLS-1)};
    });
}
function deleteCol(at){
  const entries=[...cells.entries()];
  const next=new Map();
  for(const [k,raw] of entries){
    let [r,c]=k.split(',').map(Number);
    if(c===at) continue;
    if(c>at) c--;
    next.set(K(r,c),raw);
  }
  applyStructure(next,(r,c)=> c===at? null : (c>at? {r,c:c-1}:{r,c}),
    (r1,c1,r2,c2)=>{
      if(c1===at&&c2===at) return null;
      const a=c1<at?c1:c1-1, b=c2<=at?c2:c2-1;
      if(a>b) return null;
      return {r1,c1:a,r2,c2:b};
    });
}
function applyStructure(nextMap, adjRef, adjRange){
  // rewrite formulas ('=' stripped before tokenize; token offsets +1 vs raw)
  for(const [k,raw] of nextMap){
    if(raw[0]!=='=') continue;
    let toks;
    try{ toks=tokenize(raw.slice(1)); }catch(e){ continue; }
    let out='=', pos=1, i=0;
    while(i<toks.length){
      const t=toks[i];
      if(t.t==='ident' && parseRef(t.v) &&
         toks[i+1]&&toks[i+1].t==='op'&&toks[i+1].v===':' &&
         toks[i+2]&&toks[i+2].t==='ident'&&parseRef(toks[i+2].v)){
        const a=parseRef(t.v), b=parseRef(toks[i+2].v);
        const r1=Math.min(a.r,b.r),c1=Math.min(a.c,b.c),r2=Math.max(a.r,b.r),c2=Math.max(a.c,b.c);
        const nr=adjRange(r1,c1,r2,c2);
        out+=raw.slice(pos,t.start+1);
        if(nr) out+=refName(nr.r1,nr.c1)+':'+refName(nr.r2,nr.c2);
        else out+='#REF!';
        pos=toks[i+2].end+1; i+=3; continue;
      }
      if(t.t==='ident' && parseRef(t.v)){
        const a=parseRef(t.v);
        const nr=adjRef(a.r,a.c);
        out+=raw.slice(pos,t.start+1);
        if(nr) out+=refName(nr.r,nr.c); else out+='#REF!';
        pos=t.end+1; i++; continue;
      }
      i++;
    }
    out+=raw.slice(pos);
    nextMap.set(k,out);
  }
  cells.clear();
  for(const [k,v] of nextMap) cells.set(k,v);
  astCache.clear();
  values.clear();
  fullRecalc();
  save();
}

/* ================================================================
   PERSISTENCE
   ================================================================ */
function save(){
  try{
    const arr=[...cells.entries()].filter(([k,v])=>v!=='');
    localStorage.setItem(CFG.STORE_KEY, JSON.stringify(arr));
  }catch(e){}
}
function load(){
  try{
    const s=localStorage.getItem(CFG.STORE_KEY);
    if(!s) return;
    for(const [k,v] of JSON.parse(s)) cells.set(k,v);
  }catch(e){}
}

/* ================================================================
   UI — grid rendering
   ================================================================ */
const grid=document.getElementById('grid');
const tds=[]; // tds[r][c]
function buildGrid(){
  let html='<thead><tr><th style="width:44px"></th>';
  for(let c=0;c<CFG.COLS;c++) html+=`<th id="ch-${c}">${colName(c)}</th>`;
  html+='</tr></thead><tbody>';
  for(let r=0;r<CFG.ROWS;r++){
    html+=`<tr><th class="rowhdr" id="rh-${r}">${r+1}</th>`;
    for(let c=0;c<CFG.COLS;c++) html+=`<td id="c-${r}-${c}"></td>`;
    html+='</tr>';
  }
  html+='</tbody>';
  grid.innerHTML=html;
  tds.length=0;
  for(let r=0;r<CFG.ROWS;r++){
    const row=[];
    for(let c=0;c<CFG.COLS;c++) row.push(document.getElementById(`c-${r}-${c}`));
    tds.push(row);
  }
}
function displayValue(k){
  const v=values.get(k);
  if(v===undefined||v==='') return '';
  return String(v);
}
function renderCell(r,c){
  const k=K(r,c), td=tds[r][c], v=values.get(k);
  td.textContent=displayValue(k);
  td.classList.toggle('err', isErr(v));
  td.classList.toggle('num', typeof v==='number');
}
function renderAll(){
  for(let r=0;r<CFG.ROWS;r++) for(let c=0;c<CFG.COLS;c++) renderCell(r,c);
}

/* ================================================================
   UI — selection & editing
   ================================================================ */
let sel={r:0,c:0};
let editing=false, cellInput=null;
const fbar=document.getElementById('fbar');
const namebox=document.getElementById('namebox');
const statusEl=document.getElementById('status');

function updateSelectionUI(){
  // clear old highlights
  document.querySelectorAll('td.selected').forEach(e=>e.classList.remove('selected'));
  document.querySelectorAll('th.hl').forEach(e=>e.classList.remove('hl'));
  tds[sel.r][sel.c].classList.add('selected');
  document.getElementById('ch-'+sel.c).classList.add('hl');
  document.getElementById('rh-'+sel.r).classList.add('hl');
  namebox.textContent=cellName(sel.r,sel.c);
  if(!editing){
    fbar.value=getRaw(K(sel.r,sel.c))||'';
  }
  tds[sel.r][sel.c].scrollIntoView({block:'nearest',inline:'nearest'});
}
function beginEdit(initial, keepFocus){
  if(editing) return;
  editing=true;
  const td=tds[sel.r][sel.c];
  cellInput=document.createElement('input');
  cellInput.id='cellinput';
  cellInput.value = initial!==undefined? initial : (getRaw(K(sel.r,sel.c))||'');
  td.appendChild(cellInput);
  fbar.value=cellInput.value;
  if(!keepFocus){
    cellInput.focus();
    if(initial===undefined) cellInput.select();
  }
  cellInput.addEventListener('input',()=>{ fbar.value=cellInput.value; });
  cellInput.addEventListener('keydown',e=>{
    if(e.key==='Enter'){ e.preventDefault(); commitEdit(1,0); }
    else if(e.key==='Tab'){ e.preventDefault(); commitEdit(0,1); }
    else if(e.key==='Escape'){ e.preventDefault(); cancelEdit(); }
  });
}
function cancelEdit(){
  if(!editing) return;
  editing=false;
  if(cellInput) cellInput.remove();
  cellInput=null;
  fbar.value=getRaw(K(sel.r,sel.c))||'';
  fbar.blur();
  updateSelectionUI();
}
function commitEdit(dr,dc){
  if(!editing) return;
  const k=K(sel.r,sel.c);
  const raw=cellInput.value;
  editing=false;
  cellInput.remove(); cellInput=null;
  setCell(k,raw);
  fbar.blur();
  moveSel(dr,dc);
}
function setCell(k,raw){
  if(raw==='') cells.delete(k); else cells.set(k,raw);
  astCache.delete(k);
  rebuildDepsFor(k);
  const {aff}=recompute([k]);
  for(const kk of aff){
    const [r,c]=kk.split(',').map(Number);
    if(r<CFG.ROWS&&c<CFG.COLS) renderCell(r,c);
  }
  fbar.value=getRaw(K(sel.r,sel.c))||'';
  save();
}
function moveSel(dr,dc){
  sel.r=Math.max(0,Math.min(CFG.ROWS-1,sel.r+dr));
  sel.c=Math.max(0,Math.min(CFG.COLS-1,sel.c+dc));
  updateSelectionUI();
}

/* grid events */
grid.addEventListener('mousedown',e=>{
  const td=e.target.closest('td');
  if(!td) return;
  if(editing){ commitEdit(0,0); }
  const m=td.id.match(/^c-(\d+)-(\d+)$/);
  if(m){ sel={r:+m[1],c:+m[2]}; updateSelectionUI(); }
});
grid.addEventListener('dblclick',e=>{
  const td=e.target.closest('td');
  if(!td) return;
  beginEdit();
});
document.addEventListener('keydown',e=>{
  const inFbar=document.activeElement===fbar;
  if(editing && document.activeElement===cellInput) return; // cell input has own handler
  if(inFbar){
    if(e.key==='Enter'){ e.preventDefault(); commitFromFbar(1,0); }
    else if(e.key==='Tab'){ e.preventDefault(); commitFromFbar(0,1); }
    else if(e.key==='Escape'){ e.preventDefault(); cancelEdit(); }
    return;
  }
  if(e.key==='ArrowUp'){ e.preventDefault(); moveSel(-1,0); }
  else if(e.key==='ArrowDown'||e.key==='Enter'){ e.preventDefault(); moveSel(1,0); }
  else if(e.key==='ArrowLeft'){ e.preventDefault(); moveSel(0,-1); }
  else if(e.key==='ArrowRight'||e.key==='Tab'){ e.preventDefault(); moveSel(0,1); }
  else if(e.key==='Delete'||e.key==='Backspace'){ e.preventDefault(); setCell(K(sel.r,sel.c),''); }
  else if(e.key==='F2'){ e.preventDefault(); beginEdit(); }
  else if(e.key.length===1 && !e.ctrlKey && !e.metaKey && !e.altKey){
    e.preventDefault(); beginEdit(e.key);
  }
});
function commitFromFbar(dr,dc){
  if(!editing) beginEdit(fbar.value); // pick up formula-bar edits even without cell edit
  commitEdit(dr,dc);
}
fbar.addEventListener('focus',()=>{
  if(!editing) beginEdit(fbar.value, true);
});
fbar.addEventListener('input',()=>{
  if(editing&&cellInput) cellInput.value=fbar.value;
});

/* toolbar */
document.getElementById('insRow').onclick=()=>{ if(!editing) insertRow(sel.r); };
document.getElementById('delRow').onclick=()=>{ if(!editing) deleteRow(sel.r); };
document.getElementById('insCol').onclick=()=>{ if(!editing) insertCol(sel.c); };
document.getElementById('delCol').onclick=()=>{ if(!editing) deleteCol(sel.c); };
document.getElementById('clearCell').onclick=()=>{ if(!editing) setCell(K(sel.r,sel.c),''); };

/* ================================================================
   BOOT
   ================================================================ */
buildGrid();
load();
fullRecalc();
updateSelectionUI();
statusEl.textContent=`${cells.size} cells · ${CFG.COLS}×${CFG.ROWS} grid · saved to localStorage`;
</script>
</body>
</html>
<!-- agent-meta {"model":"z-ai/glm-5.3-flash","provider":"openrouter","persona":"full","sessionId":"f634f190-6149-4e57-ae4f-de075f75aae5","tokensIn":1035960,"tokensOut":31566,"tokensTotal":1067526,"cost":0.037811515000000004,"turns":26,"toolCalls":28,"failedToolCalls":0,"timestamp":"2026-08-28T02:35:53.798Z"} -->