Spreadsheet
GLM 5.3 Flash · full

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"} -->