← Spreadsheet results

Spreadsheet

Tiel Coder 35B A3B GGUF MTP · 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 35,414 bytes · SHA-256 a2d78d342589
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sheet — Formula Spreadsheet</title>
<script>
/* ============================================================
   TUNABLE CONSTANTS
   ============================================================ */
const CONFIG = {
  COLS: 26,          // A–Z
  ROWS: 100,
  CELL_WIDTH: 100,
  CELL_HEIGHT: 22,
  HEADER_WIDTH: 50,
  HEADER_HEIGHT: 22,
  STORAGE_KEY: 'sheet.app.v1',
  COLORS: {
    selection: '#1a73e8',
    headerBg: '#f1f3f4',
    headerText: '#202124',
    gridline: '#e0e0e0',
    cellBg: '#ffffff',
    toolbarBg: '#ffffff',
    error: '#c5221f',
    accent: '#1a73e8'
  }
};
</script>
</head>
<body>
<div id="app">
  <div id="toolbar">
    <button data-op="insertColL" title="Insert column to the left">+ Col ◀</button>
    <button data-op="insertColR" title="Insert column to the right">+ Col ▶</button>
    <button data-op="delCol" title="Delete selected column">✕ Col</button>
    <span class="sep"></span>
    <button data-op="insertRowU" title="Insert row above">+ Row ▲</button>
    <button data-op="insertRowD" title="Insert row below">+ Row ▼</button>
    <button data-op="delRow" title="Delete selected row">✕ Row</button>
    <span class="sep"></span>
    <button data-op="clear" title="Clear selected cell (Delete)">Clear</button>
  </div>
  <div id="formula-bar">
    <span id="cell-name">A1</span>
    <span class="fx">fx</span>
    <input id="formula-input" type="text" autocomplete="off" spellcheck="false" placeholder="value or formula (=...)">
  </div>
  <div id="grid-scroll">
    <table id="grid">
      <colgroup></colgroup>
      <thead></thead>
      <tbody></tbody>
    </table>
    <input id="edit-input" type="text" autocomplete="off" spellcheck="false">
  </div>
  <div id="context-menu"></div>
</div>

<script>
/* ============================================================
   1. CONFIG / HELPERS
   ============================================================ */
const COLS = CONFIG.COLS, ROWS = CONFIG.ROWS;
const C = CONFIG.COLORS;

function key(r, c) { return r + ',' + c; }
function splitKey(k) { const i = k.indexOf(','); return [+k.slice(0, i), +k.slice(i + 1)]; }
function inBounds(r, c) { return r >= 0 && r < ROWS && c >= 0 && c < COLS; }
function colToLetters(col) {
  let s = '';
  while (col >= 0) { const rem = col % 26; s = String.fromCharCode(65 + rem) + s; col = Math.floor(col / 26) - 1; }
  return s;
}
function lettersToCol(letters) {
  let c = 0;
  for (let i = 0; i < letters.length; i++) c = c * 26 + (letters.charCodeAt(i) - 65);
  return c;
}
function refName(r, c) { return colToLetters(c) + (r + 1); }

/* ============================================================
   2. STYLES (injected, driven by CONFIG.COLORS)
   ============================================================ */
function injectStyles() {
  const vars = {
    '--cell-width': CONFIG.CELL_WIDTH + 'px',
    '--cell-height': CONFIG.CELL_HEIGHT + 'px',
    '--header-width': CONFIG.HEADER_WIDTH + 'px',
    '--header-height': CONFIG.HEADER_HEIGHT + 'px',
    '--sel': C.selection,
    '--gridline': C.gridline,
    '--header-bg': C.headerBg,
    '--header-text': C.headerText,
    '--error': C.error,
    '--cell-bg': C.cellBg,
    '--toolbar-bg': C.toolbarBg,
    '--accent': C.accent
  };
  let css = ':root{';
  for (const k in vars) css += k + ':' + vars[k] + ';';
  css += '}' + `
  *{box-sizing:border-box}
  html,body{margin:0;height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;font-size:13px;color:#202124}
  #app{display:flex;flex-direction:column;height:100vh;position:relative}
  #toolbar{display:flex;gap:4px;padding:4px 6px;background:var(--toolbar-bg);border-bottom:1px solid var(--gridline);align-items:center;flex:0 0 auto}
  #toolbar button{font:inherit;font-size:12px;padding:4px 9px;border:1px solid #dadce0;background:#fff;border-radius:4px;cursor:pointer;color:#202124}
  #toolbar button:hover{background:#e8f0fe;border-color:#c9d7f2}
  #toolbar button:active{background:#d3e3fb}
  #toolbar .sep{width:1px;height:20px;background:#dadce0;margin:0 2px}
  #formula-bar{display:flex;align-items:center;gap:6px;padding:3px 6px;background:#fff;border-bottom:1px solid var(--gridline);flex:0 0 auto}
  #cell-name{font-weight:600;min-width:46px;text-align:center;padding:0 6px;border:1px solid #dadce0;border-radius:3px;background:#fafafa;color:#202124}
  #formula-bar .fx{color:#9aa0a6;font-style:italic;font-weight:600}
  #formula-input{flex:1;border:1px solid transparent;padding:4px 6px;font:inherit;border-radius:3px;background:transparent;color:#202124}
  #formula-input:focus{border-color:var(--accent);background:#fff;outline:none}
  #grid-scroll{position:relative;overflow:auto;flex:1 1 auto;background:#fff}
  table#grid{border-collapse:collapse;table-layout:fixed}
  th,td{border:1px solid var(--gridline);height:var(--cell-height);min-width:var(--cell-width);padding:0 4px;overflow:hidden;white-space:nowrap;position:relative;background:var(--cell-bg)}
  thead th{position:sticky;top:0;z-index:2;background:var(--header-bg);color:var(--header-text);font-weight:600;text-align:center;height:var(--header-height);user-select:none}
  thead th.corner{left:0;z-index:4;background:var(--header-bg)}
  tbody th{position:sticky;left:0;z-index:1;background:var(--header-bg);color:var(--header-text);font-weight:600;text-align:center;height:var(--cell-height);width:var(--header-width);min-width:var(--header-width);user-select:none}
  td{cursor:cell}
  td.cell-num{text-align:right}
  td.cell-text{text-align:left}
  td.cell-error{color:var(--error);font-style:normal}
  td.sel{box-shadow:inset 0 0 0 2px var(--sel)}
  #edit-input{position:absolute;display:none;border:2px solid var(--accent);outline:none;padding:0 4px;font:inherit;background:#fff;z-index:5;color:#202124;height:100%;width:100%;margin:0}
  #context-menu{position:absolute;display:none;background:#fff;border:1px solid #dadce0;box-shadow:0 2px 10px rgba(0,0,0,.18);z-index:20;min-width:170px;padding:4px;border-radius:4px}
  #context-menu button{display:block;width:100%;text-align:left;border:none;background:none;padding:7px 10px;cursor:pointer;font:inherit;font-size:13px;border-radius:3px;color:#202124}
  #context-menu button:hover{background:#e8f0fe}
  #context-menu .cm-sep{height:1px;background:#dadce0;margin:4px 2px}
  `;
  const s = document.createElement('style');
  s.textContent = css;
  document.head.appendChild(s);
}

/* ============================================================
   3. SHEET STATE
   ============================================================ */
let grid = [];          // grid[r][c] = raw string ('' if empty)
let computed = [];      // computed[r][c] = {t:'num'|'str'|'err'|'null', v}
let deps = [];          // deps[r][c] = Set('r,c') this cell depends on
let dependents = [];    // dependents[r][c] = Set('r,c') that depend on this cell

function initState() {
  grid = Array.from({length: ROWS}, () => Array(COLS).fill(''));
  computed = Array.from({length: ROWS}, () => Array.from({length: COLS}, () => ({t: 'null'})));
  deps = Array.from({length: ROWS}, () => Array.from({length: COLS}, () => new Set()));
  dependents = Array.from({length: ROWS}, () => Array.from({length: COLS}, () => new Set()));
}

/* ============================================================
   4. TOKENIZER
   ============================================================ */
function tokenize(str) {
  const toks = [];
  let i = 0, n = str.length;
  const isDigit = c => c >= '0' && c <= '9';
  const isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
  while (i < n) {
    const ch = str[i];
    if (ch === ' ' || ch === '\t') { i++; continue; }
    if (isDigit(ch)) {
      let j = i;
      while (j < n && isDigit(str[j])) j++;
      if (j < n && str[j] === '.') { j++; while (j < n && isDigit(str[j])) j++; }
      toks.push({type: 'num', value: parseFloat(str.slice(i, j))});
      i = j; continue;
    }
    if (ch === '"') {
      let j = i + 1, val = '';
      while (j < n && str[j] !== '"') { if (str[j] === '"' && str[j + 1] === '"') { val += '"'; j += 2; } else { val += str[j]; j++; } }
      if (j >= n) throw new Error('unterminated string');
      toks.push({type: 'str', value: val});
      i = j + 1; continue;
    }
    if (ch === '#') {
      let j = i;
      while (j < n && /[A-Za-z0-9/#!]/.test(str[j])) j++;
      toks.push({type: 'err', value: str.slice(i, j)});
      i = j; continue;
    }
    if (isLetter(ch)) {
      let j = i;
      while (j < n && isLetter(str[j])) j++;
      if (j < n && isDigit(str[j])) {
        let dj = j; while (dj < n && isDigit(str[dj])) dj++;
        const letters = str.slice(i, j);
        const row = parseInt(str.slice(j, dj), 10) - 1;
        toks.push({type: 'ref', col: lettersToCol(letters), row: row});
        i = dj;
      } else {
        toks.push({type: 'ident', value: str.slice(i, j)});
        i = j;
      }
      continue;
    }
    if ('+-*/(),:='.indexOf(ch) !== -1) { toks.push({type: 'op', value: ch}); i++; continue; }
    throw new Error('unexpected char: ' + ch);
  }
  toks.push({type: 'eof'});
  return toks;
}

/* ============================================================
   5. RECURSIVE-DESCENT PARSER -> AST
   ============================================================ */
function parseFormula(raw) {
  const toks = tokenize(raw);
  let pos = 0;
  const peek = () => toks[pos];
  const next = () => toks[pos++];
  const expectOp = v => {
    if (peek().type === 'op' && peek().value === v) { pos++; return; }
    throw new Error('expected ' + v);
  };
  const parseExpr = () => {
    let node = parseTerm();
    while (peek().type === 'op' && (peek().value === '+' || peek().value === '-')) {
      const op = next().value;
      node = {type: 'binop', op, left: node, right: parseTerm()};
    }
    return node;
  };
  const parseTerm = () => {
    let node = parseFactor();
    while (peek().type === 'op' && (peek().value === '*' || peek().value === '/')) {
      const op = next().value;
      node = {type: 'binop', op, left: node, right: parseFactor()};
    }
    return node;
  };
  const parseFactor = () => {
    if (peek().type === 'op' && (peek().value === '+' || peek().value === '-')) {
      const op = next().value;
      return {type: 'unary', op, operand: parseFactor()};
    }
    return parsePrimary();
  };
  const parsePrimary = () => {
    const t = peek();
    if (t.type === 'op' && t.value === '(') {
      next();
      const node = parseExpr();
      expectOp(')');
      return node;
    }
    if (t.type === 'num') { next(); return {type: 'num', v: t.value}; }
    if (t.type === 'str') { next(); return {type: 'str', v: t.value}; }
    if (t.type === 'err') { next(); return {type: 'err', v: t.value}; }
    if (t.type === 'ident') {
      const name = t.value.toUpperCase();
      next();
      expectOp('(');
      const args = [];
      if (!(peek().type === 'op' && peek().value === ')')) {
        args.push(parseExpr());
        while (peek().type === 'op' && peek().value === ',') { next(); args.push(parseExpr()); }
      }
      expectOp(')');
      return {type: 'func', name, args};
    }
    if (t.type === 'ref') {
      next();
      if (peek().type === 'op' && peek().value === ':') {
        next();
        const right = parsePrimary();
        if (right.type !== 'ref') throw new Error('range end must be a reference');
        const c1 = {row: Math.min(t.row, right.row), col: Math.min(t.col, right.col)};
        const c2 = {row: Math.max(t.row, right.row), col: Math.max(t.col, right.col)};
        return {type: 'range', c1, c2};
      }
      return {type: 'ref', row: t.row, col: t.col};
    }
    throw new Error('unexpected token');
  };

  if (peek().type !== 'op' || peek().value !== '=') throw new Error('formula must start with =');
  next(); // consume '='
  const node = parseExpr();
  if (peek().type !== 'eof') throw new Error('trailing content');
  return node;
}

/* ============================================================
   6. DEPENDENCY COLLECTION (AST walk)
   ============================================================ */
function walkRefs(node, out) {
  switch (node.type) {
    case 'ref':
      if (inBounds(node.row, node.col)) out.add(key(node.row, node.col));
      break;
    case 'range': {
      const {c1, c2} = node;
      for (let r = c1.row; r <= c2.row; r++)
        for (let col = c1.col; col <= c2.col; col++)
          if (inBounds(r, col)) out.add(key(r, col));
      break;
    }
    case 'binop': walkRefs(node.left, out); walkRefs(node.right, out); break;
    case 'unary': walkRefs(node.operand, out); break;
    case 'func': node.args.forEach(a => walkRefs(a, out)); break;
  }
}

function computeDepsFromRaw(raw) {
  const d = new Set();
  if (raw && raw[0] === '=') {
    try { walkRefs(parseFormula(raw), d); } catch (e) { /* parse error => no deps */ }
  }
  return d;
}

/* ============================================================
   7. DEPENDENCY GRAPH UPDATE
   ============================================================ */
function applyDeps(r, c, newDeps) {
  const oldDeps = deps[r][c];
  if (oldDeps) for (const k of oldDeps) { const [dr, dc] = splitKey(k); dependents[dr][dc].delete(key(r, c)); }
  const nd = new Set(newDeps);
  for (const k of nd) { const [dr, dc] = splitKey(k); dependents[dr][dc].add(key(r, c)); }
  deps[r][c] = nd;
}

/* ============================================================
   8. VALUE EVALUATION
   ============================================================ */
// CellVal: {t:'num'|'str'|'err'|'null'|'arr', v}
function toScalar(val) {
  switch (val.t) {
    case 'num': return val.v;
    case 'null': return 0;
    case 'str': { const n = Number(val.v); return isNaN(n) ? '#ERR!' : n; }
    case 'err': return val.v;
    case 'arr': {
      let sum = 0;
      for (const x of val.v) { const s = toScalar(x); if (typeof s === 'string') return s; sum += s; }
      return sum;
    }
  }
}

function collectNumbers(args) {
  const vals = [], err = {v: null};
  for (const val of args) {
    if (err.v) break;
    switch (val.t) {
      case 'err': err.v = val.v; break;
      case 'null': break;
      case 'num': vals.push(val.v); break;
      case 'str': { const n = Number(val.v); if (!isNaN(n)) vals.push(n); break; }
      case 'arr': {
        const sub = collectNumbers(val.v);
        if (sub.err.v) { err.v = sub.err.v; } else for (const x of sub.vals) vals.push(x);
        break;
      }
    }
  }
  return {vals, err};
}

function evalFunc(name, args) {
  const {vals, err} = collectNumbers(args);
  if (err.v) return {t: 'err', v: err.v};
  switch (name) {
    case 'SUM': return {t: 'num', v: vals.reduce((a, b) => a + b, 0)};
    case 'AVG':
    case 'AVERAGE':
      return vals.length ? {t: 'num', v: vals.reduce((a, b) => a + b, 0) / vals.length}
                         : {t: 'err', v: '#DIV/0!'};
    case 'MIN': return vals.length ? {t: 'num', v: Math.min(...vals)} : {t: 'num', v: 0};
    case 'MAX': return vals.length ? {t: 'num', v: Math.max(...vals)} : {t: 'num', v: 0};
    case 'COUNT': return {t: 'num', v: vals.length};
    default: return {t: 'err', v: '#ERR!'};
  }
}

function evalAST(node) {
  switch (node.type) {
    case 'num': return {t: 'num', v: node.v};
    case 'str': return {t: 'str', v: node.v};
    case 'err': return {t: 'err', v: node.v};
    case 'ref':
      if (!inBounds(node.row, node.col)) return {t: 'err', v: '#REF!'};
      return computed[node.row][node.col];
    case 'range': {
      const arr = [];
      const {c1, c2} = node;
      for (let r = c1.row; r <= c2.row; r++)
        for (let col = c1.col; col <= c2.col; col++)
          arr.push(inBounds(r, col) ? computed[r][col] : {t: 'err', v: '#REF!'});
      return {t: 'arr', v: arr};
    }
    case 'unary': {
      const s = toScalar(evalAST(node.operand));
      if (typeof s === 'string') return {t: 'err', v: s};
      return {t: 'num', v: node.op === '-' ? -s : s};
    }
    case 'binop': {
      const a = toScalar(evalAST(node.left));
      const b = toScalar(evalAST(node.right));
      if (typeof a === 'string') return {t: 'err', v: a};
      if (typeof b === 'string') return {t: 'err', v: b};
      switch (node.op) {
        case '+': return {t: 'num', v: a + b};
        case '-': return {t: 'num', v: a - b};
        case '*': return {t: 'num', v: a * b};
        case '/': return b === 0 ? {t: 'err', v: '#DIV/0!'} : {t: 'num', v: a / b};
      }
      return {t: 'err', v: '#ERR!'};
    }
    case 'func': return evalFunc(node.name, node.args.map(evalAST));
  }
  return {t: 'err', v: '#ERR!'};
}

function normalize(val) {
  switch (val.t) {
    case 'arr': {
      const s = toScalar(val);
      return typeof s === 'string' ? {t: 'err', v: s} : {t: 'num', v: s};
    }
    default: return val;
  }
}

function evalCell(r, c) {
  const raw = grid[r][c];
  if (!raw || raw.trim() === '') { applyDeps(r, c, new Set()); return {t: 'null'}; }
  if (raw[0] !== '=') {
    applyDeps(r, c, new Set());
    const t = raw.trim();
    if (t === '') return {t: 'null'};
    const n = Number(t);
    return !isNaN(n) && isFinite(n) ? {t: 'num', v: n} : {t: 'str', v: raw};
  }
  let ast;
  try { ast = parseFormula(raw); }
  catch (e) { applyDeps(r, c, new Set()); return {t: 'err', v: '#ERR!'}; }
  const d = new Set();
  walkRefs(ast, d);
  applyDeps(r, c, d);
  return normalize(evalAST(ast));
}

/* ============================================================
   9. RECALCULATION (topological, cycle-aware)
   ============================================================ */
function expand(startKeys) {
  const affected = new Set();
  const queue = [];
  for (const k of startKeys) if (!affected.has(k)) { affected.add(k); queue.push(k); }
  while (queue.length) {
    const k = queue.shift();
    const [r, c] = splitKey(k);
    for (const succ of dependents[r][c]) if (!affected.has(succ)) { affected.add(succ); queue.push(succ); }
  }
  return affected;
}

function topoSort(affected) {
  const indeg = new Map();
  for (const k of affected) indeg.set(k, 0);
  for (const k of affected) {
    const [r, c] = splitKey(k);
    let cnt = 0;
    for (const dep of deps[r][c]) if (affected.has(dep)) cnt++;
    indeg.set(k, cnt);
  }
  const queue = [];
  for (const k of affected) if (indeg.get(k) === 0) queue.push(k);
  const order = [];
  const processed = new Set();
  while (queue.length) {
    const k = queue.shift();
    order.push(k); processed.add(k);
    const [r, c] = splitKey(k);
    for (const succ of dependents[r][c]) {
      if (!affected.has(succ)) continue;
      const nv = indeg.get(succ) - 1;
      indeg.set(succ, nv);
      if (nv === 0) queue.push(succ);
    }
  }
  const cycle = [];
  for (const k of affected) if (!processed.has(k)) cycle.push(k);
  return {order, cycle};
}

function recompute(startKeys) {
  // Update dependency edges for the changed cells first, so the dependent BFS is correct.
  for (const k of startKeys) {
    const [r, c] = splitKey(k);
    const d = computeDepsFromRaw(grid[r][c]);
    applyDeps(r, c, d);
  }
  const affected = expand(startKeys);
  const {order, cycle} = topoSort(affected);
  for (const k of order) { const [r, c] = splitKey(k); computed[r][c] = evalCell(r, c); }
  for (const k of cycle) { const [r, c] = splitKey(k); computed[r][c] = {t: 'err', v: '#CYCLE!'}; }
  for (const k of affected) { const [r, c] = splitKey(k); updateCellDisplay(r, c); }
}

function evaluateAll() {
  for (let r = 0; r < ROWS; r++)
    for (let c = 0; c < COLS; c++)
      if (grid[r][c] && grid[r][c][0] === '=') {
        try { const d = computeDepsFromRaw(grid[r][c]); applyDeps(r, c, d); } catch (e) {}
      }
  const all = new Set();
  for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) all.add(key(r, c));
  const {order, cycle} = topoSort(all);
  for (const k of order) { const [r, c] = splitKey(k); computed[r][c] = evalCell(r, c); }
  for (const k of cycle) { const [r, c] = splitKey(k); computed[r][c] = {t: 'err', v: '#CYCLE!'}; }
  for (const k of all) { const [r, c] = splitKey(k); updateCellDisplay(r, c); }
}

/* ============================================================
   10. FORMULA REWRITING (insert/delete row & column)
   ============================================================ */
function rewriteFormula(raw, transform) {
  if (!raw || raw[0] !== '=') return raw;
  return raw.replace(/"([^"]*)"|([A-Za-z]+\d+)/g, (m, strLit, ref) => {
    if (ref === undefined) return m;
    const letters = ref.match(/[A-Za-z]+/)[0];
    const digits = ref.match(/\d+/)[0];
    const col = lettersToCol(letters);
    const row = parseInt(digits, 10) - 1;
    const res = transform(col, row);
    if (res == null) return '#REF!';
    return colToLetters(res.col) + (res.row + 1);
  });
}
const insertColT = idx => (col, row) => (col >= idx ? {col: col + 1, row} : {col, row});
const delColT = idx => (col, row) => (col === idx ? null : (col > idx ? {col: col - 1, row} : {col, row}));
const insertRowT = idx => (col, row) => (row >= idx ? {col, row: row + 1} : {col, row});
const delRowT = idx => (col, row) => (row === idx ? null : (row > idx ? {col, row: row - 1} : {col, row}));

function rewriteAll(transform) {
  for (let r = 0; r < ROWS; r++)
    for (let c = 0; c < COLS; c++) grid[r][c] = rewriteFormula(grid[r][c], transform);
}

function insertColAt(index) {
  for (let r = 0; r < ROWS; r++) { grid[r].splice(index, 0, ''); grid[r].pop(); }
  rewriteAll(insertColT(index));
  evaluateAll();
}
function deleteColAt(index) {
  for (let r = 0; r < ROWS; r++) { grid[r].splice(index, 1); grid[r].push(''); }
  rewriteAll(delColT(index));
  evaluateAll();
}
function insertRowAt(index) {
  grid.splice(index, 0, Array(COLS).fill(''));
  grid.pop();
  rewriteAll(insertRowT(index));
  evaluateAll();
}
function deleteRowAt(index) {
  grid.splice(index, 1);
  grid.push(Array(COLS).fill(''));
  rewriteAll(delRowT(index));
  evaluateAll();
}

/* ============================================================
   11. RENDERING
   ============================================================ */
let tdGrid = [];
let scroll, cellName, formulaInput, editInput, contextMenu, appEl;

function displayString(v) {
  if (v.t === 'null') return '';
  if (v.t === 'num') return formatNumber(v.v);
  return v.v;
}
function formatNumber(n) {
  if (Number.isInteger(n)) return String(n);
  return String(parseFloat(n.toPrecision(12)));
}

function updateCellDisplay(r, c) {
  const td = tdGrid[r][c];
  if (!td) return;
  const v = computed[r][c];
  td.textContent = displayString(v);
  td.classList.remove('cell-num', 'cell-text', 'cell-error');
  if (v.t === 'err') td.classList.add('cell-error');
  else if (v.t === 'num') td.classList.add('cell-num');
  else if (v.t === 'str') td.classList.add('cell-text');
}

function buildGridDOM() {
  const table = document.getElementById('grid');
  const cg = table.querySelector('colgroup');
  let colHtml = '<col style="width:' + CONFIG.HEADER_WIDTH + 'px">';
  for (let c = 0; c < COLS; c++) colHtml += '<col style="width:' + CONFIG.CELL_WIDTH + 'px">';
  cg.innerHTML = colHtml;

  const thead = table.querySelector('thead');
  let headTr = '<tr><th class="corner">&nbsp;</th>';
  for (let c = 0; c < COLS; c++) headTr += '<th data-col="' + c + '">' + colToLetters(c) + '</th>';
  headTr += '</tr>';
  thead.innerHTML = headTr;

  const tbody = table.querySelector('tbody');
  tdGrid = Array.from({length: ROWS}, () => Array(COLS));
  let rowsHtml = '';
  for (let r = 0; r < ROWS; r++) {
    rowsHtml += '<tr><th data-row="' + r + '">' + (r + 1) + '</th>';
    for (let c = 0; c < COLS; c++) rowsHtml += '<td data-r="' + r + '" data-c="' + c + '" data-key="' + key(r, c) + '">&nbsp;</td>';
    rowsHtml += '</tr>';
  }
  tbody.innerHTML = rowsHtml;

  // Populate tdGrid references
  const cells = table.querySelectorAll('td[data-r]');
  cells.forEach(td => { const [r, c] = [+td.dataset.r, +td.dataset.c]; tdGrid[r][c] = td; });
}

/* ============================================================
   12. SELECTION & FORMULA BAR
   ============================================================ */
let selR = 0, selC = 0;

function updateFormulaBar() {
  if (document.activeElement === formulaInput) return;
  formulaInput.value = grid[selR][selC] || '';
}
function updateCornerLabel() { cellName.textContent = refName(selR, selC); }

function selectCell(r, c) {
  if (selR != null && selC != null && tdGrid[selR] && tdGrid[selR][selC])
    tdGrid[selR][selC].classList.remove('sel');
  selR = r; selC = c;
  tdGrid[r][c].classList.add('sel');
  updateCornerLabel();
  updateFormulaBar();
  ensureVisible(r, c);
}

function ensureVisible(r, c) {
  const td = tdGrid[r][c];
  const rect = td.getBoundingClientRect();
  const sr = scroll.getBoundingClientRect();
  if (rect.left < sr.left) scroll.scrollLeft += rect.left - sr.left;
  else if (rect.right > sr.right) scroll.scrollLeft += rect.right - sr.right;
  if (rect.top < sr.top) scroll.scrollTop += rect.top - sr.top;
  else if (rect.bottom > sr.bottom) scroll.scrollTop += rect.bottom - sr.bottom;
}

/* ============================================================
   13. EDITING (grid overlay + formula bar)
   ============================================================ */
let editing = false;         // true when grid overlay is active
let editOriginal = null;

function setRaw(r, c, val) {
  grid[r][c] = val;
  recompute([key(r, c)]);
  scheduleSave();
}

function positionGridInput(r, c) {
  const td = tdGrid[r][c];
  const cr = td.getBoundingClientRect();
  const sr = scroll.getBoundingClientRect();
  editInput.style.left = (cr.left - sr.left) + 'px';
  editInput.style.top = (cr.top - sr.top) + 'px';
  editInput.style.width = cr.width + 'px';
  editInput.style.height = cr.height + 'px';
  editInput.style.display = 'flex';
}

function startEdit(mode, firstChar) {
  commitGrid();
  const r = selR, c = selC;
  editOriginal = grid[r][c];
  positionGridInput(r, c);
  if (mode === 'type') {
    editInput.value = firstChar || '';
    if (editInput.value) setRaw(r, c, editInput.value);
    editInput.focus();
    editInput.setSelectionRange(editInput.value.length, editInput.value.length);
  } else {
    editInput.value = grid[r][c];
    editInput.focus();
    editInput.setSelectionRange(editInput.value.length, editInput.value.length);
  }
  editing = true;
  formulaInput.value = editInput.value;
}

function commitGrid() {
  if (!editing) return;
  editing = false;
  editInput.style.display = 'none';
  setRaw(selR, selC, editInput.value);
}

function cancelEdit() {
  if (!editing) return;
  editing = false;
  editInput.style.display = 'none';
  grid[selR][selC] = editOriginal;
  recompute([key(selR, selC)]);
}

function clearCell() {
  setRaw(selR, selC, '');
}

/* ============================================================
   14. MOVEMENT
   ============================================================ */
function moveDown() { selectCell(Math.min(selR + 1, ROWS - 1), selC); }
function moveUp() { selectCell(Math.max(selR - 1, 0), selC); }
function moveRight() { selectCell(selR, Math.min(selC + 1, COLS - 1)); }
function moveLeft() { selectCell(selR, Math.max(selC - 1, 0)); }

/* ============================================================
   15. LOCALSTORAGE
   ============================================================ */
let saveTimer = null;
function scheduleSave() {
  if (saveTimer) clearTimeout(saveTimer);
  saveTimer = setTimeout(persist, 250);
}
function persist() {
  const obj = {};
  for (let r = 0; r < ROWS; r++)
    for (let c = 0; c < COLS; c++)
      if (grid[r][c] && grid[r][c] !== '') obj[key(r, c)] = grid[r][c];
  try { localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(obj)); } catch (e) {}
}
function loadFromStorage() {
  let s;
  try { s = localStorage.getItem(CONFIG.STORAGE_KEY); } catch (e) { s = null; }
  if (!s) return;
  let obj;
  try { obj = JSON.parse(s); } catch (e) { return; }
  for (const k in obj) {
    const [r, c] = splitKey(k);
    if (inBounds(r, c)) grid[r][c] = obj[k];
  }
}

/* ============================================================
   16. CONTEXT MENU
   ============================================================ */
function showContextMenu(x, y, items) {
  contextMenu.innerHTML = '';
  for (const it of items) {
    if (it === '-') { const s = document.createElement('div'); s.className = 'cm-sep'; contextMenu.appendChild(s); continue; }
    const b = document.createElement('button');
    b.textContent = it.label;
    b.onclick = () => { hideContextMenu(); it.action(); };
    contextMenu.appendChild(b);
  }
  const ar = appEl.getBoundingClientRect();
  contextMenu.style.left = (x - ar.left) + 'px';
  contextMenu.style.top = (y - ar.top) + 'px';
  contextMenu.style.display = 'block';
}
function hideContextMenu() { contextMenu.style.display = 'none'; }

function cellContextMenu(e, r, c) {
  showContextMenu(e.clientX, e.clientY, [
    {label: 'Insert Row Above', action: () => insertRowAt(r)},
    {label: 'Insert Row Below', action: () => insertRowAt(r + 1)},
    {label: 'Delete Row', action: () => deleteRowAt(r)},
    '-',
    {label: 'Insert Column Left', action: () => insertColAt(c)},
    {label: 'Insert Column Right', action: () => insertColAt(c + 1)},
    {label: 'Delete Column', action: () => deleteColAt(c)},
    '-',
    {label: 'Clear Cell', action: () => clearCell()}
  ]);
}
function columnContextMenu(e, col) {
  showContextMenu(e.clientX, e.clientY, [
    {label: 'Insert Column Left', action: () => insertColAt(col)},
    {label: 'Insert Column Right', action: () => insertColAt(col + 1)},
    {label: 'Delete Column', action: () => deleteColAt(col)}
  ]);
}
function rowContextMenu(e, row) {
  showContextMenu(e.clientX, e.clientY, [
    {label: 'Insert Row Above', action: () => insertRowAt(row)},
    {label: 'Insert Row Below', action: () => insertRowAt(row + 1)},
    {label: 'Delete Row', action: () => deleteRowAt(row)}
  ]);
}

/* ============================================================
   17. EVENT WIRING
   ============================================================ */
function wireEvents() {
  // Grid click -> select
  scroll.addEventListener('click', e => {
    const td = e.target.closest('td[data-r]');
    if (td) selectCell(+td.dataset.r, +td.dataset.c);
  });
  // Double-click -> edit
  scroll.addEventListener('dblclick', e => {
    const td = e.target.closest('td[data-r]');
    if (td) { e.preventDefault(); startEdit('dblclick'); }
  });
  // Right-click -> context menu
  scroll.addEventListener('contextmenu', e => {
    e.preventDefault();
    const td = e.target.closest('td[data-r]');
    if (td) { cellContextMenu(e, +td.dataset.r, +td.dataset.c); return; }
    const ch = e.target.closest('th[data-col]');
    if (ch) { columnContextMenu(e, +ch.dataset.col); return; }
    const rh = e.target.closest('th[data-row]');
    if (rh) { rowContextMenu(e, +rh.dataset.row); return; }
  });

  // Toolbar
  document.getElementById('toolbar').addEventListener('click', e => {
    const btn = e.target.closest('button[data-op]');
    if (!btn) return;
    const op = btn.dataset.op;
    switch (op) {
      case 'insertColL': insertColAt(selC); break;
      case 'insertColR': insertColAt(selC + 1); break;
      case 'delCol': deleteColAt(selC); break;
      case 'insertRowU': insertRowAt(selR); break;
      case 'insertRowD': insertRowAt(selR + 1); break;
      case 'delRow': deleteRowAt(selR); break;
      case 'clear': clearCell(); break;
    }
  });

  // Grid edit overlay
  editInput.addEventListener('input', () => {
    setRaw(selR, selC, editInput.value);
    formulaInput.value = editInput.value;
  });
  editInput.addEventListener('keydown', e => {
    if (e.key === 'Enter') { e.preventDefault(); commitGrid(); moveDown(); }
    else if (e.key === 'Tab') { e.preventDefault(); commitGrid(); e.shiftKey ? moveLeft() : moveRight(); }
    else if (e.key === 'Escape') { e.preventDefault(); cancelEdit(); }
  });
  editInput.addEventListener('blur', () => commitGrid());

  // Formula bar
  formulaInput.addEventListener('focus', () => { if (editing) commitGrid(); });
  formulaInput.addEventListener('input', () => {
    if (editing) editInput.value = formulaInput.value;
    setRaw(selR, selC, formulaInput.value);
  });
  formulaInput.addEventListener('keydown', e => {
    if (e.key === 'Enter') { e.preventDefault(); commitGrid(); moveDown(); }
    else if (e.key === 'Tab') { e.preventDefault(); commitGrid(); e.shiftKey ? moveLeft() : moveRight(); }
    else if (e.key === 'Escape') { e.preventDefault(); grid[selR][selC] = editOriginal; commitGrid(); }
  });

  // Global keyboard (when not editing in the overlay)
  window.addEventListener('keydown', e => {
    if (editing) return;
    const key = e.key;
    if (key === 'ArrowUp') { e.preventDefault(); moveUp(); }
    else if (key === 'ArrowDown') { e.preventDefault(); moveDown(); }
    else if (key === 'ArrowLeft') { e.preventDefault(); moveLeft(); }
    else if (key === 'ArrowRight') { e.preventDefault(); moveRight(); }
    else if (key === 'Enter') { e.preventDefault(); moveDown(); }
    else if (key === 'Tab') { e.preventDefault(); e.shiftKey ? moveLeft() : moveRight(); }
    else if (key === 'Delete') { clearCell(); }
    else if (key === 'F2') { e.preventDefault(); startEdit('dblclick'); }
    else if (key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
      e.preventDefault();
      startEdit('type', key);
    }
  });

  // Scroll: keep edit overlay on the active cell
  scroll.addEventListener('scroll', () => { if (editing) positionGridInput(selR, selC); });

  // Hide context menu on outside interaction
  document.addEventListener('click', e => { if (!contextMenu.contains(e.target)) hideContextMenu(); });
  window.addEventListener('beforeunload', () => { if (saveTimer) clearTimeout(saveTimer); persist(); });
}

/* ============================================================
   18. INIT
   ============================================================ */
function init() {
  injectStyles();
  scroll = document.getElementById('grid-scroll');
  cellName = document.getElementById('cell-name');
  formulaInput = document.getElementById('formula-input');
  editInput = document.getElementById('edit-input');
  contextMenu = document.getElementById('context-menu');
  appEl = document.getElementById('app');

  initState();
  buildGridDOM();
  wireEvents();
  loadFromStorage();
  evaluateAll();
  selectCell(0, 0);
  scheduleSave();
}

document.addEventListener('DOMContentLoaded', init);

/* Expose internals for programmatic verification */
window.__sheet = {
  recompute, evaluateAll, evalCell, setRaw, rewriteFormula,
  delRowT, delColT, insertRowT, insertColT, CONFIG,
  insertColAt, deleteColAt, insertRowAt, deleteRowAt,
  persist, loadFromStorage,
  get grid() { return grid; },
  get computed() { return computed; },
  get deps() { return deps; },
  get dependents() { return dependents; }
};
</script>
</body>
</html>
<!-- agent-meta {"model":"peculiar-ragdoll/Tiel-Coder-35B-A3B-GGUF-MTP","provider":"brigid","persona":"full","sessionId":"aaf0f4ad-6026-4ad4-852e-d09f29436f2f","tokensIn":3933387,"tokensOut":80691,"tokensTotal":4014078,"cost":0,"turns":83,"toolCalls":82,"failedToolCalls":0,"timestamp":"2026-09-04T19:42:27.653Z"} -->