← Spreadsheet results

Spreadsheet

Deepseek v4.1 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 38,725 bytes · SHA-256 84016e11533d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sheet — Spreadsheet</title>
<style>
/* ==========================================================================
   THEME + LAYOUT
   ========================================================================== */
:root{
  --gridline:#e8eaed;
  --hdr-bg:#f8f9fa;
  --hdr-fg:#5f6368;
  --hdr-border:#dadce0;
  --sel:#1a73e8;
  --sel-tint:#e8f0fe;
  --err:#d93025;
  --fg:#202124;
  --col-w:96px;
  --row-h:26px;
  --hdr-w:48px;
  --hdr-h:26px;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0}
body{
  font:13px/1.25 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  color:var(--fg);background:#fff;overflow:hidden;
  -webkit-font-smoothing:antialiased;
}
#app{display:flex;flex-direction:column;height:100vh;min-height:0}

/* ---------- toolbar ---------- */
.toolbar{
  flex:0 0 auto;display:flex;align-items:center;gap:6px;
  padding:9px 12px;border-bottom:1px solid var(--hdr-border);background:#fff;
}
.brand{display:flex;align-items:center;gap:8px;font-weight:600;font-size:14px;margin-right:12px;letter-spacing:.1px}
.logo{width:16px;height:16px;border-radius:3px;background:linear-gradient(135deg,#1a73e8,#34a853);box-shadow:inset 0 0 0 1px rgba(0,0,0,.06)}
.sep{width:1px;height:20px;background:var(--hdr-border);margin:0 6px}
button.tool{
  font:12px/1 inherit;color:#3c4043;background:#fff;border:1px solid var(--hdr-border);
  border-radius:4px;padding:6px 10px;cursor:pointer;white-space:nowrap;
}
button.tool:hover{background:#f1f3f4}
button.tool:active{background:#e8eaed}
button.tool.danger:hover{background:#fce8e6;border-color:#f5c6c2;color:#c5221f}
.spacer{flex:1}
.hint{font-size:11px;color:#80868b}

/* ---------- formula bar ---------- */
.fbar{
  flex:0 0 auto;display:flex;align-items:stretch;height:32px;
  border-bottom:1px solid var(--hdr-border);background:#fff;
}
#cellRef{
  width:74px;flex:0 0 74px;display:flex;align-items:center;justify-content:center;
  border-right:1px solid var(--hdr-border);font-size:12px;font-weight:600;color:#3c4043;
  letter-spacing:.3px;background:#fff;
}
.fx{
  display:flex;align-items:center;padding:0 11px;color:#80868b;font-style:italic;
  font-family:Georgia,"Times New Roman",serif;font-size:14px;border-right:1px solid var(--hdr-border);
  user-select:none;
}
#finput{
  flex:1;border:0;outline:0;padding:0 12px;background:#fff;color:var(--fg);
  font:13px/1 inherit;
}
#finput:focus{background:#fbfcfe}

/* ---------- grid ---------- */
.grid-wrap{
  flex:1 1 auto;min-height:0;overflow:auto;background:#fff;position:relative;
  border-bottom:1px solid var(--hdr-border);
}
table#grid{border-collapse:separate;border-spacing:0;table-layout:fixed}
table#grid th,table#grid td{
  padding:0 6px;margin:0;height:var(--row-h);line-height:calc(var(--row-h) - 1px);
  font-weight:400;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:clip;
  border-right:1px solid var(--gridline);border-bottom:1px solid var(--gridline);
  background:#fff;text-align:left;vertical-align:middle;
}
thead th{
  position:sticky;top:0;z-index:3;height:var(--hdr-h);line-height:calc(var(--hdr-h) - 1px);
  background:var(--hdr-bg);color:var(--hdr-fg);font-size:11px;font-weight:500;
  text-align:center;text-transform:uppercase;letter-spacing:.4px;
  border-bottom:1px solid var(--hdr-border);user-select:none;cursor:default;
  border-right:1px solid var(--hdr-border);
}
tbody th{
  position:sticky;left:0;z-index:2;width:var(--hdr-w);min-width:var(--hdr-w);max-width:var(--hdr-w);
  background:var(--hdr-bg);color:var(--hdr-fg);font-size:11px;font-weight:500;text-align:center;
  border-right:1px solid var(--hdr-border);user-select:none;
}
thead th.corner{left:0;z-index:4;width:var(--hdr-w);min-width:var(--hdr-w)}
thead th.colh{width:var(--col-w);min-width:var(--col-w)}
thead th.hl{background:var(--sel-tint);color:#174ea6}
tbody th.hl{background:var(--sel-tint);color:#174ea6}
td.cell{cursor:cell;user-select:none}
td.cell.num{text-align:right;font-variant-numeric:tabular-nums}
td.cell.err{color:var(--err);text-align:right;font-weight:500}
td.cell.sel{box-shadow:inset 0 0 0 2px var(--sel);background:#fff}
td.cell.editing{color:transparent}

/* corner handle on active cell */
td.cell.sel::after{
  content:"";position:absolute;right:0;bottom:0;width:2px;height:2px;background:var(--sel);
}
td.cell{position:relative}

/* ---------- in-cell editor ---------- */
#editor{
  position:fixed;display:none;z-index:60;margin:0;
  border:2px solid var(--sel);outline:0;background:#fff;color:var(--fg);
  font:13px/1.25 inherit;padding:0 7px;box-shadow:0 1px 3px rgba(60,64,67,.28);
  border-radius:1px;
}

/* ---------- context menu ---------- */
.ctx{
  position:fixed;z-index:80;display:none;min-width:186px;background:#fff;
  border:1px solid #dadce0;border-radius:6px;box-shadow:0 4px 14px rgba(60,64,67,.22);
  padding:5px;font-size:13px;
}
.ctx button{
  display:block;width:100%;text-align:left;background:none;border:0;font:13px/1 inherit;
  color:#3c4043;padding:8px 10px;border-radius:4px;cursor:pointer;
}
.ctx button:hover{background:#f1f3f4}
.ctx hr{border:0;border-top:1px solid var(--hdr-border);margin:4px 2px}

/* ---------- status ---------- */
.status{
  flex:0 0 auto;display:flex;align-items:center;gap:14px;height:24px;
  padding:0 12px;background:var(--hdr-bg);font-size:11px;color:#80868b;
}
.status b{color:#5f6368;font-weight:600}
</style>
</head>
<body>
<div id="app">
  <header class="toolbar">
    <div class="brand"><span class="logo"></span>Sheet</div>
    <button class="tool" data-act="insRow">Insert row</button>
    <button class="tool" data-act="delRow">Delete row</button>
    <span class="sep"></span>
    <button class="tool" data-act="insCol">Insert column</button>
    <button class="tool" data-act="delCol">Delete column</button>
    <span class="sep"></span>
    <button class="tool danger" data-act="clear">Clear cell</button>
    <span class="spacer"></span>
    <span class="hint">Right-click a row / column header for more</span>
  </header>

  <div class="fbar">
    <span id="cellRef">A1</span>
    <span class="fx">fx</span>
    <input id="finput" spellcheck="false" autocomplete="off" autocapitalize="off">
  </div>

  <div class="grid-wrap" id="gridWrap" tabindex="-1">
    <table id="grid"></table>
  </div>

  <div class="status">
    <span id="statSel">A1</span>
    <span id="statVal"></span>
    <span class="spacer"></span>
    <span>26 × 100 · formulas: <b>=SUM(A1:A5)</b> · <b>=B1*2+1</b></span>
  </div>
</div>

<input id="editor" spellcheck="false" autocomplete="off">
<div class="ctx" id="ctx"></div>

<script>
/* ==========================================================================
   1. CONFIG — tunable constants
   ========================================================================== */
const CONFIG = {
  ROWS: 100,
  COLS: 26,
  COL_WIDTH: 96,
  ROW_HEIGHT: 26,
  HEADER_WIDTH: 48,
  HEADER_HEIGHT: 26,
  STORAGE_KEY: 'spreadsheet.artifact.v1',
  NUM_PRECISION: 12,      // significant digits kept for display
  SAVE_DEBOUNCE: 250      // ms
};
const ROWS = CONFIG.ROWS, COLS = CONFIG.COLS;

/* ==========================================================================
   2. SMALL UTILITIES / ERROR VALUES
   ========================================================================== */
const ERR_REF   = { err: '#REF!' };
const ERR_DIV   = { err: '#DIV/0!' };
const ERR_PARSE = { err: '#ERR!' };
const ERR_CYCLE = { err: '#CYCLE!' };
const ERRS = { '#REF!': ERR_REF, '#DIV/0!': ERR_DIV, '#ERR!': ERR_PARSE, '#CYCLE!': ERR_CYCLE };
const BLANK = '';
const isErr = v => !!v && typeof v === 'object' && typeof v.err === 'string';

/** stable numeric key for a (row, col) pair */
const K = (r, c) => r * 10000 + c;
const KR = k => Math.floor(k / 10000);
const KC = k => k % 10000;

function colName(c) {
  let s = '', n = c + 1;
  while (n > 0) { const m = (n - 1) % 26; s = String.fromCharCode(65 + m) + s; n = Math.floor((n - 1) / 26); }
  return s;
}
const REF_RE = /^(\$?)([A-Za-z]+)(\$?)(\d+)$/;
/** "A1" -> {col,row} (0-based). Returns null when not a reference. */
function parseRef(str) {
  const m = REF_RE.exec(str);
  if (!m) return null;
  const L = m[2].toUpperCase();
  let col = 0;
  for (let i = 0; i < L.length; i++) {
    const code = L.charCodeAt(i);
    if (code < 65 || code > 90) return null;
    col = col * 26 + (code - 64);
  }
  const row = parseInt(m[4], 10) - 1;
  if (!isFinite(col) || !isFinite(row)) return null;
  return { col: col - 1, row: row, absC: !!m[1], absR: !!m[3] };
}
/** rebuild reference text keeping $ anchors: "$A$1" style */
function refText(orig, col, row) {
  const m = REF_RE.exec(orig);
  return (m ? m[1] : '') + colName(col) + (m ? m[3] : '') + (row + 1);
}
class FormulaError extends Error {
  constructor(code) { super(code); this.code = code || '#ERR!'; }
}

/* ==========================================================================
   3. FORMULA ENGINE — tokenizer
   ========================================================================== */
const OPS = '+-*/^(),:%<>=&';
/** tokenize(source without leading '=') -> [{t,v,raw}] */
function tokenize(src) {
  const tokens = [];
  let i = 0;
  while (i < src.length) {
    const ch = src[i];
    const start = i;
    if (ch === ' ' || ch === '\t' || ch === '\n') { i++; tokens.push({ t: 'ws', v: ch, raw: ch }); continue; }
    if (ch >= '0' && ch <= '9' || (ch === '.' && /[0-9]/.test(src[i + 1] || ''))) {
      let j = i;
      while (j < src.length && /[0-9.]/.test(src[j])) j++;
      if (/[eE][+-]?[0-9]/.test(src.slice(j, j + 3))) { j += 1; if (src[j] === '+' || src[j] === '-') j++; while (j < src.length && /[0-9]/.test(src[j])) j++; }
      const raw = src.slice(i, j);
      const v = parseFloat(raw);
      if (!isFinite(v)) throw new FormulaError('#ERR!');
      tokens.push({ t: 'num', v: v, raw: raw });
      i = j; continue;
    }
    if (/[A-Za-z_$]/.test(ch)) {
      let j = i;
      while (j < src.length && /[A-Za-z0-9_$.]/.test(src[j])) j++;
      tokens.push({ t: 'id', v: src.slice(i, j), raw: src.slice(i, j) });
      i = j; continue;
    }
    if (ch === '"') {
      let j = i + 1, s = '';
      while (j < src.length && src[j] !== '"') { s += src[j]; j++; }
      if (j >= src.length) throw new FormulaError('#ERR!');
      tokens.push({ t: 'str', v: s, raw: src.slice(i, j + 1) });
      i = j + 1; continue;
    }
    if (OPS.indexOf(ch) >= 0) {
      tokens.push({ t: 'op', v: ch, raw: ch });
      i++; continue;
    }
    throw new FormulaError('#ERR!');
  }
  return tokens;
}

/* ==========================================================================
   4. FORMULA ENGINE — recursive-descent parser -> AST
   Ast nodes: {k:'num'|'str'|'ref'|'range'|'un'|'bin'|'fn'}
   ========================================================================== */
const FUNCS = { SUM: 1, AVG: 1, AVERAGE: 1, MIN: 1, MAX: 1, COUNT: 1, ABS: 1, ROUND: 1, SQRT: 1 };

function parseFormula(src) {
  /* a rewritten reference that died is carried as literal #REF! */
  if (src.indexOf('#REF!') >= 0) throw new FormulaError('#REF!');
  const tokens = tokenize(src).filter(t => t.t !== 'ws');
  let pos = 0;
  const peek = () => tokens[pos];
  const next = () => tokens[pos++];
  const isOp = tok => tok && tok.t === 'op';

  function parseExpr() {
    let left = parseTerm();
    while (isOp(peek()) && (peek().v === '+' || peek().v === '-')) {
      const op = next().v;
      left = { k: 'bin', op: op, l: left, r: parseTerm() };
    }
    return left;
  }
  function parseTerm() {
    let left = parseFactor();
    while (isOp(peek()) && (peek().v === '*' || peek().v === '/' || peek().v === '^')) {
      const op = next().v;
      left = { k: 'bin', op: op, l: left, r: parseFactor() };
    }
    return left;
  }
  function parseFactor() {
    if (isOp(peek()) && (peek().v === '-' || peek().v === '+')) {
      const op = next().v;
      return { k: 'un', op: op, x: parseFactor() };
    }
    return parsePrimary();
  }
  function parsePrimary() {
    const tok = next();
    if (!tok) throw new FormulaError('#ERR!');
    if (tok.t === 'num') return { k: 'num', v: tok.v };
    if (tok.t === 'str') return { k: 'str', v: tok.v };
    if (isOp(tok) && tok.v === '(') {
      const e = parseExpr();
      const close = next();
      if (!isOp(close) || close.v !== ')') throw new FormulaError('#ERR!');
      return e;
    }
    if (tok.t === 'id') {
      const up = tok.v.toUpperCase();
      if (FUNCS[up] && isOp(peek()) && peek().v === '(') {
        next(); // eat '('
        const args = [];
        if (!(isOp(peek()) && peek().v === ')')) {
          args.push(parseExpr());
          while (isOp(peek()) && peek().v === ',') { next(); args.push(parseExpr()); }
        }
        const close = next();
        if (!isOp(close) || close.v !== ')') throw new FormulaError('#ERR!');
        return { k: 'fn', name: up, args: args };
      }
      const ref = parseRef(tok.v);
      if (!ref) throw new FormulaError('#ERR!');
      if (isOp(peek()) && peek().v === ':') {
        next();
        const t2 = next();
        if (!t2 || t2.t !== 'id') throw new FormulaError('#ERR!');
        const ref2 = parseRef(t2.v);
        if (!ref2) throw new FormulaError('#ERR!');
        return { k: 'range', a: ref, b: ref2 };
      }
      return { k: 'ref', ref: ref };
    }
    throw new FormulaError('#ERR!');
  }

  const ast = parseExpr();
  if (pos !== tokens.length) throw new FormulaError('#ERR!');
  return ast;
}

/** statically collect referenced cell keys from an AST */
function collectRefs(node, out) {
  if (!node) return;
  switch (node.k) {
    case 'ref': {
      const r = node.ref;
      if (r.row >= 0 && r.row < ROWS && r.col >= 0 && r.col < COLS) out.add(K(r.row, r.col));
      break;
    }
    case 'range': {
      const r1 = Math.min(node.a.row, node.b.row), r2 = Math.max(node.a.row, node.b.row);
      const c1 = Math.min(node.a.col, node.b.col), c2 = Math.max(node.a.col, node.b.col);
      for (let r = Math.max(0, r1); r <= Math.min(ROWS - 1, r2); r++)
        for (let c = Math.max(0, c1); c <= Math.min(COLS - 1, c2); c++) out.add(K(r, c));
      break;
    }
    case 'bin': collectRefs(node.l, out); collectRefs(node.r, out); break;
    case 'un': collectRefs(node.x, out); break;
    case 'fn': node.args.forEach(a => collectRefs(a, out)); break;
  }
}

/* ==========================================================================
   5. FORMULA ENGINE — evaluator
   ========================================================================== */
/** coerce a value to a number ('' -> 0). Returns null when impossible. */
function toNum(v) {
  if (isErr(v)) return null;
  if (typeof v === 'number') return v;
  if (v === BLANK || v === undefined || v === null) return 0;
  const s = String(v).trim();
  if (s === '') return 0;
  if (!/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(s)) return null;
  const n = parseFloat(s);
  return isFinite(n) ? n : null;
}

function evalNode(node, getVal) {
  switch (node.k) {
    case 'num': return node.v;
    case 'str': return node.v;
    case 'ref': {
      const r = node.ref;
      if (r.row < 0 || r.row >= ROWS || r.col < 0 || r.col >= COLS) return ERR_REF;
      return getVal(K(r.row, r.col));
    }
    case 'range': return ERR_PARSE;                       // ranges only valid as function args
    case 'un': {
      const v = evalNode(node.x, getVal);
      if (isErr(v)) return v;
      const n = toNum(v);
      if (n === null) return ERR_PARSE;
      return node.op === '-' ? -n : n;
    }
    case 'bin': {
      const l = evalNode(node.l, getVal);
      if (isErr(l)) return l;
      const r = evalNode(node.r, getVal);
      if (isErr(r)) return r;
      const a = toNum(l), b = toNum(r);
      if (a === null || b === null) return ERR_PARSE;
      switch (node.op) {
        case '+': return a + b;
        case '-': return a - b;
        case '*': return a * b;
        case '/': return b === 0 ? ERR_DIV : a / b;
        case '^': return Math.pow(a, b);
      }
      return ERR_PARSE;
    }
    case 'fn': return evalFn(node, getVal);
  }
  return ERR_PARSE;
}

function evalFn(node, getVal) {
  const nums = [];
  let err = null;
  const push = v => {
    if (isErr(v)) { if (!err) err = v; return; }
    if (typeof v === 'number') { nums.push(v); return; }
    if (typeof v === 'string' && v.trim() !== '') {
      const n = Number(v);
      if (!isNaN(n) && isFinite(n)) nums.push(n);
    }
  };
  for (const arg of node.args) {
    if (arg.k === 'range') {
      const r1 = Math.min(arg.a.row, arg.b.row), r2 = Math.max(arg.a.row, arg.b.row);
      const c1 = Math.min(arg.a.col, arg.b.col), c2 = Math.max(arg.a.col, arg.b.col);
      if (r1 < 0 || c1 < 0 || r2 >= ROWS || c2 >= COLS) { if (!err) err = ERR_REF; continue; }
      for (let r = r1; r <= r2; r++) for (let c = c1; c <= c2; c++) push(getVal(K(r, c)));
    } else {
      push(evalNode(arg, getVal));
    }
  }
  if (err) return err;
  const sum = nums.reduce((s, x) => s + x, 0);
  switch (node.name) {
    case 'SUM': return sum;
    case 'AVG': case 'AVERAGE': return nums.length ? sum / nums.length : ERR_DIV;
    case 'MIN': return nums.length ? nums.reduce((m, x) => x < m ? x : m, nums[0]) : 0;
    case 'MAX': return nums.length ? nums.reduce((m, x) => x > m ? x : m, nums[0]) : 0;
    case 'COUNT': return nums.length;
    case 'ABS': return nums.length ? Math.abs(nums[0]) : ERR_PARSE;
    case 'SQRT': return nums.length ? (nums[0] < 0 ? { err: '#NUM!' } : Math.sqrt(nums[0])) : ERR_PARSE;
    case 'ROUND': return nums.length ? Number(nums[0].toFixed(Math.max(0, Math.min(15, nums[1] | 0)))) : ERR_PARSE;
  }
  return ERR_PARSE;
}

/** value of a non-formula cell */
function literal(raw) {
  if (raw === undefined || raw === null || raw === '') return BLANK;
  if (raw.charAt(0) === "'") return raw.slice(1);       // forced text
  const s = raw.trim();
  if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(s)) {
    const n = parseFloat(s);
    if (isFinite(n)) return n;
  }
  return raw;
}

function formatNumber(n) {
  if (!isFinite(n)) return '#NUM!';
  if (Number.isInteger(n) && Math.abs(n) < 1e15) return String(n);
  const r = Number(n.toPrecision(CONFIG.NUM_PRECISION));
  if (r !== 0 && (Math.abs(r) >= 1e15 || Math.abs(r) < 1e-9)) return r.toExponential(6).replace(/e([+-])(\d)$/, 'e$10$2');
  return String(r);
}

/* ==========================================================================
   6. DEPENDENCY GRAPH + RECALCULATION
   ========================================================================== */
let data = [];                 // data[r][c] = raw string
let values = new Map();        // key -> computed value
let precedents = new Map();    // key -> Set(key)   what a cell reads
let dependents = new Map();    // key -> Set(key)   who reads a cell
let parseCache = new Map();    // key -> {ast|err}

const emptyGrid = () => Array.from({ length: ROWS }, () => new Array(COLS).fill(''));
const rawOf = k => data[KR(k)][KC(k)] || '';

function compile(raw) {
  try { return { ast: parseFormula(raw.slice(1)) }; }
  catch (e) { return { err: (e instanceof FormulaError ? e.code : '#ERR!') }; }
}

/** re-register the precedents/dependents of one cell */
function updateDeps(k, raw) {
  const old = precedents.get(k);
  if (old) for (const p of old) { const s = dependents.get(p); if (s) s.delete(k); }
  const set = new Set();
  if (raw && raw.charAt(0) === '=') {
    const comp = compile(raw);
    parseCache.set(k, comp);
    if (comp.ast) collectRefs(comp.ast, set);
  } else {
    parseCache.delete(k);
  }
  precedents.set(k, set);
  for (const p of set) {
    let s = dependents.get(p);
    if (!s) { s = new Set(); dependents.set(p, s); }
    s.add(k);
  }
}

/**
 * Recompute the transitive dependent-closure of `changed`, in topological order
 * via memoised DFS. Grey-node hit => circular reference (#CYCLE!).
 */
function recompute(changed) {
  const closure = new Set();
  const stack = changed.slice();
  while (stack.length) {
    const k = stack.pop();
    if (closure.has(k)) continue;
    closure.add(k);
    const d = dependents.get(k);
    if (d) for (const x of d) if (!closure.has(x)) stack.push(x);
  }
  const memo = new Map(), state = new Map();   // state: 1 = in-progress

  function evaluate(k) {
    if (memo.has(k)) return memo.get(k);
    if (!closure.has(k)) {
      const cached = values.get(k);
      return cached === undefined ? BLANK : cached;
    }
    if (state.get(k) === 1) return ERR_CYCLE;
    state.set(k, 1);
    let v;
    try {
      const raw = rawOf(k);
      if (raw.charAt(0) === '=') {
        let comp = parseCache.get(k);
        if (!comp) { comp = compile(raw); parseCache.set(k, comp); }
        v = comp.err ? (ERRS[comp.err] || ERR_PARSE) : evalNode(comp.ast, evaluate);
      } else {
        v = literal(raw);
      }
    } catch (e) { v = ERR_PARSE; }
    state.set(k, 2);
    if (v === undefined) v = BLANK;
    memo.set(k, v);
    return v;
  }

  for (const k of closure) evaluate(k);
  for (const k of closure) {
    const v = memo.get(k);
    values.set(k, v === undefined ? BLANK : v);
    paintKey(k);
  }
}

/** full rebuild of graph + values (used after structural edits and on load) */
function rebuildAll() {
  precedents.clear(); dependents.clear(); parseCache.clear(); values.clear();
  for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
    const raw = data[r][c];
    if (raw) updateDeps(K(r, c), raw);
  }
  const all = [];
  for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) all.push(K(r, c));
  recompute(all);
}

/** public write path */
function setCell(r, c, raw) {
  if (raw === undefined || raw === null) raw = '';
  const k = K(r, c);
  if (data[r][c] === raw) return;
  data[r][c] = raw;
  updateDeps(k, raw);
  recompute([k]);
  refreshStatus();
  scheduleSave();
}

/* ==========================================================================
   7. STRUCTURAL EDITS — insert / delete rows & columns with ref rewriting
   ========================================================================== */
/** re-serialize a formula, mapping each reference through rowT/colT (-1 => #REF!) */
function rewriteFormula(raw, rowT, colT) {
  if (!raw || raw.charAt(0) !== '=') return raw;
  if (raw.indexOf('#REF!') >= 0) return raw;
  let toks;
  try { toks = tokenize(raw.slice(1)); } catch (e) { return raw; }
  let out = '=';
  for (let i = 0; i < toks.length; i++) {
    const tk = toks[i];
    if (tk.t === 'id') {
      const up = tk.v.toUpperCase();
      const nx = toks[i + 1];
      const isFunc = FUNCS[up] && nx && nx.t === 'op' && nx.v === '(';
      if (!isFunc) {
        const ref = parseRef(tk.v);
        if (ref) {
          const nr = rowT(ref.row), nc = colT(ref.col);
          if (nr < 0 || nc < 0) { out += '#REF!'; continue; }
          out += refText(tk.raw, nc, nr);
          continue;
        }
      }
    }
    out += tk.raw;
  }
  return out;
}

function mapAllFormulas(rowT, colT) {
  for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
    const raw = data[r][c];
    if (raw && raw.charAt(0) === '=') data[r][c] = rewriteFormula(raw, rowT, colT);
  }
}

function insertRow(at) {
  if (at < 0) at = 0; if (at > ROWS) at = ROWS;
  mapAllFormulas(row => row >= at ? row + 1 : row, col => col);
  for (let r = ROWS - 1; r > at; r--) data[r] = data[r - 1].slice();
  data[at] = new Array(COLS).fill('');
  data.length = ROWS;
  rebuildAll(); scheduleSave(); refreshStatus();
}
function deleteRow(at) {
  if (at < 0 || at >= ROWS) return;
  mapAllFormulas(row => row === at ? -1 : (row > at ? row - 1 : row), col => col);
  data.splice(at, 1);
  data.push(new Array(COLS).fill(''));
  rebuildAll(); scheduleSave(); refreshStatus();
}
function insertCol(at) {
  if (at < 0) at = 0; if (at > COLS) at = COLS;
  mapAllFormulas(row => row, col => col >= at ? col + 1 : col);
  for (let r = 0; r < ROWS; r++) {
    const row = data[r];
    for (let c = COLS - 1; c > at; c--) row[c] = row[c - 1];
    row[at] = '';
    row.length = COLS;
  }
  rebuildAll(); scheduleSave(); refreshStatus();
}
function deleteCol(at) {
  if (at < 0 || at >= COLS) return;
  mapAllFormulas(row => row, col => col === at ? -1 : (col > at ? col - 1 : col));
  for (let r = 0; r < ROWS; r++) {
    const row = data[r];
    row.splice(at, 1);
    row.push('');
  }
  rebuildAll(); scheduleSave(); refreshStatus();
}
function clearCell(r, c) { if (editing) endEdit(true); setCell(r, c, ''); }

/* ==========================================================================
   8. PERSISTENCE
   ========================================================================== */
let saveTimer = null;
function scheduleSave() {
  if (saveTimer) clearTimeout(saveTimer);
  saveTimer = setTimeout(saveNow, CONFIG.SAVE_DEBOUNCE);
}
function saveNow() {
  saveTimer = null;
  try {
    const cells = [];
    for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++)
      if (data[r][c]) cells.push([r, c, data[r][c]]);
    localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify({ v: 1, rows: ROWS, cols: COLS, cells: cells }));
  } catch (e) { /* storage unavailable — ignore */ }
}
function loadSaved() {
  try {
    const txt = localStorage.getItem(CONFIG.STORAGE_KEY);
    if (!txt) return false;
    const obj = JSON.parse(txt);
    if (!obj || !Array.isArray(obj.cells)) return false;
    for (const entry of obj.cells) {
      const r = entry[0], c = entry[1], v = entry[2];
      if (r >= 0 && r < ROWS && c >= 0 && c < COLS) data[r][c] = String(v);
    }
    return true;
  } catch (e) { return false; }
}

/* ==========================================================================
   9. DOM — grid construction & painting
   ========================================================================== */
const gridWrap = document.getElementById('gridWrap');
const table = document.getElementById('grid');
const editor = document.getElementById('editor');
const finput = document.getElementById('finput');
const cellRefEl = document.getElementById('cellRef');
const statSel = document.getElementById('statSel');
const statVal = document.getElementById('statVal');
const ctxMenu = document.getElementById('ctx');
let tds = [];                      // tds[r][c]
let colHeads = [], rowHeads = [];

function buildGrid() {
  let html = '<thead><tr><th class="corner"></th>';
  for (let c = 0; c < COLS; c++) html += '<th class="colh" data-c="' + c + '">' + colName(c) + '</th>';
  html += '</tr></thead><tbody>';
  for (let r = 0; r < ROWS; r++) {
    html += '<tr><th class="rowh" data-r="' + r + '">' + (r + 1) + '</th>';
    for (let c = 0; c < COLS; c++) html += '<td class="cell" data-r="' + r + '" data-c="' + c + '"></td>';
    html += '</tr>';
  }
  html += '</tbody>';
  table.innerHTML = html;
  tds = [];
  for (let r = 0; r < ROWS; r++) {
    const tr = table.tBodies[0].rows[r];
    tds.push([]);
    for (let c = 0; c < COLS; c++) tds[r].push(tr.cells[c + 1]);
  }
  colHeads = Array.from(table.tHead.rows[0].cells).slice(1);
  rowHeads = Array.from(table.tBodies[0].rows).map(tr => tr.cells[0]);
}

function paintKey(k) { paint(KR(k), KC(k)); }
function paint(r, c) {
  const td = tds[r][c];
  if (!td) return;
  const v = values.get(K(r, c));
  let text = '', cls = 'cell';
  if (isErr(v)) { text = v.err; cls += ' err'; }
  else if (typeof v === 'number') { text = formatNumber(v); cls += ' num'; }
  else if (v === BLANK || v === undefined) { text = ''; }
  else { text = String(v); }
  if (td.textContent !== text) td.textContent = text;
  let extra = '';
  if (r === sel.r && c === sel.c) { extra += ' sel'; if (editing) extra += ' editing'; }
  if (td.className !== cls + extra) td.className = cls + extra;
}
function paintAll() {
  for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) paint(r, c);
}

/* ==========================================================================
   10. SELECTION
   ========================================================================== */
let sel = { r: 0, c: 0 };
let prevSel = { r: 0, c: 0 };
let editing = false, editBackup = '';

function clampSel() {
  if (sel.r < 0) sel.r = 0; if (sel.r > ROWS - 1) sel.r = ROWS - 1;
  if (sel.c < 0) sel.c = 0; if (sel.c > COLS - 1) sel.c = COLS - 1;
}
function select(r, c, opts) {
  if (editing && !(opts && opts.keepEdit)) endEdit();
  prevSel = { r: sel.r, c: sel.c };
  sel.r = r; sel.c = c;
  clampSel();
  refreshSelUI();
  if (!opts || !opts.noScroll) ensureVisible();
}
function refreshSelUI() {
  for (let c = 0; c < COLS; c++) colHeads[c].classList.toggle('hl', c === sel.c);
  for (let r = 0; r < ROWS; r++) rowHeads[r].classList.toggle('hl', r === sel.r);
  cellRefEl.textContent = colName(sel.c) + (sel.r + 1);
  finput.value = data[sel.r][sel.c] || '';
  paint(prevSel.r, prevSel.c);
  paint(sel.r, sel.c);
  refreshStatus();
  if (document.activeElement === finput) finput.blur();
}
function refreshStatus() {
  statSel.textContent = colName(sel.c) + (sel.r + 1);
  const raw = data[sel.r][sel.c];
  const v = values.get(K(sel.r, sel.c));
  if (raw && raw.charAt(0) === '=') {
    statVal.textContent = isErr(v) ? v.err : '= ' + (typeof v === 'number' ? formatNumber(v) : v);
  } else {
    statVal.textContent = '';
  }
}
function ensureVisible() {
  const td = tds[sel.r][sel.c];
  const r = td.getBoundingClientRect(), w = gridWrap.getBoundingClientRect();
  const top = w.top + CONFIG.HEADER_HEIGHT, left = w.left + CONFIG.HEADER_WIDTH;
  if (r.top < top) gridWrap.scrollTop -= (top - r.top);
  else if (r.bottom > w.bottom) gridWrap.scrollTop += (r.bottom - w.bottom);
  if (r.left < left) gridWrap.scrollLeft -= (left - r.left);
  else if (r.right > w.right) gridWrap.scrollLeft += (r.right - w.right);
}
function move(dr, dc) {
  select(sel.r + dr, sel.c + dc);
}

/* ==========================================================================
   11. IN-CELL EDITING  (floating input over the active cell)
   ========================================================================== */
function positionEditor() {
  const td = tds[sel.r][sel.c];
  if (!td) return;
  const r = td.getBoundingClientRect();
  editor.style.left = r.left + 'px';
  editor.style.top = r.top + 'px';
  editor.style.width = Math.max(24, r.width) + 'px';
  editor.style.height = Math.max(18, r.height) + 'px';
  editor.style.fontSize = getComputedStyle(td).fontSize;
}
function startEdit(initial) {
  if (editing) return;
  editing = true;
  editBackup = data[sel.r][sel.c] || '';
  editor.value = initial === undefined ? editBackup : initial;
  editor.style.display = 'block';
  positionEditor();
  editor.focus();
  if (initial === undefined) editor.select();
  else { editor.setSelectionRange(editor.value.length, editor.value.length); setCell(sel.r, sel.c, editor.value); }
  finput.value = editor.value;
  paint(sel.r, sel.c);
}
function endEdit(cancel) {
  if (!editing) return;
  if (cancel) { setCell(sel.r, sel.c, editBackup); }
  else { setCell(sel.r, sel.c, editor.value); }
  editing = false;
  editor.style.display = 'none';
  editor.value = '';
  try { editor.blur(); } catch (e) { }
  if (document.activeElement === editor && document.activeElement.blur) document.activeElement.blur();
  try { gridWrap.focus({ preventScroll: true }); } catch (e) { }
  finput.value = data[sel.r][sel.c] || '';
  paint(sel.r, sel.c);
  refreshStatus();
}
function cancelEdit() { endEdit(true); }

editor.addEventListener('input', () => {
  setCell(sel.r, sel.c, editor.value);
  finput.value = editor.value;
});
editor.addEventListener('keydown', e => {
  if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); endEdit(); move(1, 0); }
  else if (e.key === 'Tab') { e.preventDefault(); e.stopPropagation(); endEdit(); move(0, e.shiftKey ? -1 : 1); }
  else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); endEdit(true); }
});
gridWrap.addEventListener('scroll', () => { if (editing) positionEditor(); });
window.addEventListener('resize', () => { if (editing) positionEditor(); });

/* ==========================================================================
   12. FORMULA BAR
   ========================================================================== */
let fbarBackup = '';
finput.addEventListener('focus', () => { fbarBackup = data[sel.r][sel.c] || ''; });
finput.addEventListener('input', () => {
  if (editing) {
    editor.value = finput.value;
    setCell(sel.r, sel.c, finput.value);
  } else {
    setCell(sel.r, sel.c, finput.value);
    paint(sel.r, sel.c);
  }
  refreshStatus();
});
finput.addEventListener('keydown', e => {
  if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); endEdit(); move(1, 0); }
  else if (e.key === 'Tab') { e.preventDefault(); e.stopPropagation(); endEdit(); move(0, e.shiftKey ? -1 : 1); }
  else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); endEdit(); setCell(sel.r, sel.c, fbarBackup); finput.value = fbarBackup; finput.blur(); }
});

/* ==========================================================================
   13. MOUSE — selection, double-click edit, header right-click menu
   ========================================================================== */
function cellFromEvent(e) {
  const td = e.target.closest ? e.target.closest('td.cell') : null;
  if (!td) return null;
  return { r: +td.dataset.r, c: +td.dataset.c };
}
gridWrap.addEventListener('mousedown', e => {
  if (e.target.closest('th')) return;
  const pos = cellFromEvent(e);
  if (!pos) return;
  if (editing && (pos.r !== sel.r || pos.c !== sel.c)) endEdit();
  if (pos.r === sel.r && pos.c === sel.c) { if (!editing) select(pos.r, pos.c, { keepEdit: true }); return; }
  select(pos.r, pos.c, { keepEdit: true });
});
gridWrap.addEventListener('dblclick', e => {
  const pos = cellFromEvent(e);
  if (!pos) return;
  select(pos.r, pos.c);
  startEdit();
});
/* click on a row/column header selects the whole first cell of it + opens context */
gridWrap.addEventListener('contextmenu', e => {
  const th = e.target.closest ? e.target.closest('th') : null;
  if (!th) return;
  e.preventDefault();
  if (th.classList.contains('colh')) {
    const c = +th.dataset.c;
    select(sel.r, c);
    openMenu(e.clientX, e.clientY, [
      { label: 'Insert column left', fn: () => { insertCol(c); select(sel.r, Math.min(c, COLS - 1)); } },
      { label: 'Insert column right', fn: () => { insertCol(c + 1); select(sel.r, Math.min(c + 1, COLS - 1)); } },
      null,
      { label: 'Delete column ' + colName(c), fn: () => { deleteCol(c); select(sel.r, Math.min(c, COLS - 1)); } }
    ]);
  } else if (th.classList.contains('rowh')) {
    const r = +th.dataset.r;
    select(r, sel.c);
    openMenu(e.clientX, e.clientY, [
      { label: 'Insert row above', fn: () => { insertRow(r); select(Math.min(r, ROWS - 1), sel.c); } },
      { label: 'Insert row below', fn: () => { insertRow(r + 1); select(Math.min(r + 1, ROWS - 1), sel.c); } },
      null,
      { label: 'Delete row ' + (r + 1), fn: () => { deleteRow(r); select(Math.min(r, ROWS - 1), sel.c); } }
    ]);
  }
});
function openMenu(x, y, items) {
  ctxMenu.innerHTML = '';
  for (const it of items) {
    if (!it) { ctxMenu.appendChild(document.createElement('hr')); continue; }
    const b = document.createElement('button');
    b.textContent = it.label;
    b.addEventListener('click', () => { closeMenu(); it.fn(); });
    ctxMenu.appendChild(b);
  }
  ctxMenu.style.display = 'block';
  ctxMenu.style.left = Math.min(x, window.innerWidth - 200) + 'px';
  ctxMenu.style.top = Math.min(y, window.innerHeight - 20 - items.length * 34) + 'px';
}
function closeMenu() { ctxMenu.style.display = 'none'; }
document.addEventListener('mousedown', e => { if (!e.target.closest('#ctx')) closeMenu(); }, true);

/* ==========================================================================
   14. KEYBOARD NAVIGATION
   ========================================================================== */
/* capture phase at window: fires before any focus-related quirks can swallow grid keys */
window.addEventListener('keydown', e => {
  const t = e.target;
  if (editing || t === finput) return;
  if (e.key === 'Escape') { closeMenu(); return; }
  const nav = { ArrowUp: [-1, 0], ArrowDown: [1, 0], ArrowLeft: [0, -1], ArrowRight: [0, 1] };
  if (nav[e.key]) {
    e.preventDefault();
    if (e.metaKey || e.ctrlKey) move(e.key === 'ArrowDown' ? ROWS : e.key === 'ArrowUp' ? -ROWS : 0,
      e.key === 'ArrowRight' ? COLS : e.key === 'ArrowLeft' ? -COLS : 0);
    else move(nav[e.key][0], nav[e.key][1]);
    return;
  }
  if (e.key === 'Enter') {
    e.preventDefault();
    if (e.shiftKey) move(-1, 0); else move(1, 0);
    return;
  }
  if (e.key === 'Tab') { e.preventDefault(); move(0, e.shiftKey ? -1 : 1); return; }
  if (e.key === 'F2') { e.preventDefault(); startEdit(); return; }
  if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); setCell(sel.r, sel.c, ''); return; }
  if ((e.ctrlKey || e.metaKey) && (e.key === 'c' || e.key === 'v' || e.key === 'x')) return;
  if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
    e.preventDefault();
    startEdit(e.key);
  }
}, true);

/* ==========================================================================
   15. TOOLBAR
   ========================================================================== */
function doAction(act) {
  switch (act) {
    case 'insRow': insertRow(sel.r); select(Math.min(sel.r, ROWS - 1), sel.c); break;
    case 'delRow': deleteRow(sel.r); select(Math.min(sel.r, ROWS - 1), sel.c); break;
    case 'insCol': insertCol(sel.c); select(sel.r, Math.min(sel.c, COLS - 1)); break;
    case 'delCol': deleteCol(sel.c); select(sel.r, Math.min(sel.c, COLS - 1)); break;
    case 'clear': setCell(sel.r, sel.c, ''); break;
  }
}
document.querySelectorAll('[data-act]').forEach(b => {
  b.addEventListener('click', () => { doAction(b.dataset.act); b.blur(); });
});

/* ==========================================================================
   16. BOOT
   ========================================================================== */
function init() {
  data = emptyGrid();
  buildGrid();
  loadSaved();
  rebuildAll();
  select(0, 0, { noScroll: true });
  paintAll();
  window.addEventListener('beforeunload', saveNow);
}
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4.1-flash","provider":"openrouter","persona":"full","sessionId":"b8c0bca5-571d-4e98-833b-f0758dfd1a69","tokensIn":4024705,"tokensOut":69860,"tokensTotal":4094565,"cost":0.06215640600000001,"turns":54,"toolCalls":53,"failedToolCalls":0,"timestamp":"2026-09-10T17:16:45.559Z"} -->