← Spreadsheet results

Spreadsheet

Claude Opus 5 · typescript

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 74,942 bytes · SHA-256 e0c9c1235d7c
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sheets — Spreadsheet</title>
<style>
/* ── css: base ── */
:root {
  --gridline: #e1e3e6;
  --gridline-strong: #c8ccd0;
  --header-bg: #f8f9fa;
  --header-fg: #5f6368;
  --header-active-bg: #d3e3fd;
  --sel: #1a73e8;
  --err: #d93025;
  --fg: #202124;
  --chrome: #ffffff;
  --chrome-alt: #f1f3f4;
  --cell-w: 96px;
  --cell-h: 25px;
  --rowhead-w: 46px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--chrome);
  color: var(--fg);
  font: 13px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

#app {
  display: flex;
  flex-direction: column;
  height: 100%;
}

button { font: inherit; color: inherit; }
/* ── css: chrome ── */
#toolbar {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--gridline);
  background: var(--chrome);
  flex: 0 0 auto;
}

.brand {
  font-size: 15px;
  font-weight: 500;
  color: #3c4043;
  letter-spacing: .2px;
  display: flex;
  align-items: center;
  gap: 7px;
  padding-right: 6px;
}
.brand .logo { color: #0f9d58; font-size: 17px; }

.tbgroup { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }

.tbtn {
  border: 1px solid transparent;
  background: var(--chrome-alt);
  border-radius: 6px;
  padding: 5px 11px;
  cursor: pointer;
  color: #3c4043;
  transition: background .12s, box-shadow .12s;
  white-space: nowrap;
}
.tbtn:hover { background: #e4e7ea; }
.tbtn:active { background: #dadce0; }
.tbtn:focus-visible { outline: 2px solid var(--sel); outline-offset: 1px; }
.tbtn.danger { color: #b3261e; }
.tbtn.danger:hover { background: #fce8e6; }

.tsep { width: 1px; height: 20px; background: var(--gridline); margin: 0 5px; }

.status {
  margin-left: auto;
  font-size: 12px;
  color: #80868b;
  min-width: 130px;
  text-align: right;
  white-space: nowrap;
}

/* ---- formula bar ---- */
#fbar {
  display: flex;
  align-items: stretch;
  gap: 0;
  background: var(--chrome);
  border-bottom: 1px solid var(--gridline-strong);
  padding: 5px 12px 6px;
  flex: 0 0 auto;
}

.cellref {
  min-width: 62px;
  padding: 0 8px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 12.5px;
  color: #3c4043;
  background: var(--chrome-alt);
  border: 1px solid var(--gridline);
  border-radius: 4px;
}

.fx {
  display: flex;
  align-items: center;
  padding: 0 12px;
  color: #80868b;
  font-style: italic;
  font-family: Georgia, serif;
  font-size: 14px;
  border-right: 1px solid var(--gridline);
  margin-right: 8px;
}

.finput {
  flex: 1;
  height: 26px;
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 0 8px;
  font: 13px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: #fff;
  color: var(--fg);
}
.finput:focus { outline: none; border-color: var(--sel); box-shadow: 0 0 0 1px var(--sel) inset; }
/* ── css: grid ── */
#gridwrap {
  flex: 1 1 auto;
  overflow: auto;
  background: #fff;
  position: relative;
  outline: none;
}

#grid {
  border-collapse: separate;
  border-spacing: 0;
  table-layout: fixed;
  width: max-content;
  font-variant-numeric: tabular-nums;
}

#grid th, #grid td {
  padding: 0;
  margin: 0;
  height: var(--cell-h);
  border-right: 1px solid var(--gridline);
  border-bottom: 1px solid var(--gridline);
  overflow: hidden;
  white-space: nowrap;
}

/* column headers */
#grid thead th {
  position: sticky;
  top: 0;
  z-index: 3;
  width: var(--cell-w);
  background: var(--header-bg);
  color: var(--header-fg);
  font-weight: 500;
  font-size: 11.5px;
  text-align: center;
  cursor: pointer;
  user-select: none;
  border-bottom: 1px solid var(--gridline-strong);
}
#grid thead th.corner {
  left: 0;
  z-index: 5;
  width: var(--rowhead-w);
  min-width: var(--rowhead-w);
  border-right: 1px solid var(--gridline-strong);
  cursor: default;
}
#grid thead th.hl { background: var(--header-active-bg); color: #174ea6; }

/* row headers */
#grid tbody th {
  position: sticky;
  left: 0;
  z-index: 2;
  width: var(--rowhead-w);
  min-width: var(--rowhead-w);
  background: var(--header-bg);
  color: var(--header-fg);
  font-weight: 400;
  font-size: 11.5px;
  text-align: center;
  cursor: pointer;
  user-select: none;
  border-right: 1px solid var(--gridline-strong);
}
#grid tbody th.hl { background: var(--header-active-bg); color: #174ea6; }

/* data cells */
#grid td {
  width: var(--cell-w);
  min-width: var(--cell-w);
  max-width: var(--cell-w);
  padding: 0 4px;
  text-align: left;
  color: var(--fg);
  cursor: cell;
  position: relative;
  text-overflow: clip;
  line-height: calc(var(--cell-h) - 1px);
}
#grid td.num { text-align: right; }
#grid td.err { color: var(--err); text-align: center; font-weight: 500; }

/* selection */
#grid td.sel {
  outline: 2px solid var(--sel);
  outline-offset: -2px;
  z-index: 1;
  box-shadow: 0 0 0 1px rgba(26,115,232,.18);
}
#grid td.sel::after {
  content: "";
  position: absolute;
  right: -3px;
  bottom: -3px;
  width: 6px;
  height: 6px;
  background: var(--sel);
  border: 1px solid #fff;
  z-index: 4;
}

/* in-cell editor */
#grid td.editing { outline: 2px solid var(--sel); outline-offset: -2px; padding: 0; overflow: visible; z-index: 6; }
#grid td.editing::after { display: none; }
.celledit {
  position: absolute;
  left: 0;
  top: 0;
  min-width: 100%;
  width: 100%;
  height: 100%;
  border: none;
  padding: 0 3px;
  margin: 0;
  font: 13px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: #fff;
  color: var(--fg);
  outline: none;
  box-shadow: 0 1px 6px rgba(60,64,67,.28);
  z-index: 7;
}

.ctxmenu {
  position: fixed;
  z-index: 50;
  background: #fff;
  border: 1px solid var(--gridline);
  border-radius: 6px;
  box-shadow: 0 4px 14px rgba(60,64,67,.24);
  padding: 5px 0;
  min-width: 190px;
}
.ctxmenu button {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 7px 16px;
  cursor: pointer;
  font-size: 13px;
  white-space: nowrap;
}
.ctxmenu button:hover { background: var(--chrome-alt); }
</style>
</head>
<body>
<div id="app">
  <header id="toolbar">
    <div class="brand"><span class="logo">▦</span> Sheets</div>
    <div class="tbgroup">
      <button id="btn-ins-row" class="tbtn" title="Insert a row above the selection">Insert row</button>
      <button id="btn-del-row" class="tbtn" title="Delete the selected row">Delete row</button>
      <span class="tsep"></span>
      <button id="btn-ins-col" class="tbtn" title="Insert a column left of the selection">Insert col</button>
      <button id="btn-del-col" class="tbtn" title="Delete the selected column">Delete col</button>
      <span class="tsep"></span>
      <button id="btn-clear" class="tbtn" title="Clear the selected cell (Delete)">Clear</button>
      <button id="btn-reset" class="tbtn danger" title="Erase the whole sheet">Reset sheet</button>
    </div>
    <div id="status" class="status"></div>
  </header>

  <div id="fbar">
    <div id="cellref" class="cellref">A1</div>
    <div class="fx">fx</div>
    <input id="finput" class="finput" type="text" spellcheck="false" autocomplete="off" aria-label="Formula bar">
  </div>

  <div id="gridwrap">
    <table id="grid"><thead></thead><tbody></tbody></table>
  </div>

  <div id="ctxmenu" class="ctxmenu" hidden=""></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./app":"src/app.ts"},"src/refs.ts":{"./config":"src/config.ts"},"src/lexer.ts":{"./ast":"src/ast.ts"},"src/parser.ts":{"./ast":"src/ast.ts","./lexer":"src/lexer.ts","./refs":"src/refs.ts"},"src/printer.ts":{"./ast":"src/ast.ts","./refs":"src/refs.ts"},"src/value.ts":{"./ast":"src/ast.ts","./config":"src/config.ts"},"src/eval.ts":{"./ast":"src/ast.ts","./value":"src/value.ts","./refs":"src/refs.ts"},"src/rewrite.ts":{"./ast":"src/ast.ts"},"src/sheet.ts":{"./config":"src/config.ts","./ast":"src/ast.ts","./parser":"src/parser.ts","./printer":"src/printer.ts","./rewrite":"src/rewrite.ts","./eval":"src/eval.ts","./value":"src/value.ts","./refs":"src/refs.ts"},"src/view.ts":{"./config":"src/config.ts","./refs":"src/refs.ts","./value":"src/value.ts","./sheet":"src/sheet.ts"},"src/app.ts":{"./config":"src/config.ts","./refs":"src/refs.ts","./sheet":"src/sheet.ts","./view":"src/view.ts","./rewrite":"src/rewrite.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * MAIN — bootstrap
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const app_1 = require("./app");
const DEMO = {
    A1: 'Item', B1: 'Qty', C1: 'Price', D1: 'Total',
    A2: 'Widget', B2: '4', C2: '2.5', D2: '=B2*C2',
    A3: 'Gadget', B3: '10', C3: '1.75', D3: '=B3*C3',
    A4: 'Doohickey', B4: '3', C4: '9.99', D4: '=B4*C4',
    A5: 'Sprocket', B5: '7', C5: '4', D5: '=B5*C5',
    A7: 'Subtotal', D7: '=SUM(D2:D5)',
    A8: 'Tax (8%)', D8: '=D7*0.08',
    A9: 'Grand total', D9: '=D7+D8',
    F1: 'Stats', F2: 'Count', G2: '=COUNT(D2:D5)',
    F3: 'Average', G3: '=AVG(D2:D5)',
    F4: 'Min', G4: '=MIN(D2:D5)',
    F5: 'Max', G5: '=MAX(D2:D5)',
    F7: 'Chain', G7: '5', H7: '=G7*2', I7: '=H7+1',
};
function start() {
    const app = new app_1.App();
    if (app.sheet.allKeys().length === 0) {
        app.sheet.loadJSON(DEMO);
        app.view.paintAll();
        app.select(0, 0);
        app.sheet.save();
        app.setStatus('Loaded sample sheet');
    }
    window.app = app;
}
start();
};

// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * CONFIG — tunable constants
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.COLORS = exports.CONFIG = void 0;
exports.CONFIG = {
    COLS: 26, // number of columns (A..Z)
    ROWS: 100, // number of rows
    CELL_W: 96, // px, data column width
    CELL_H: 25, // px, row height
    ROW_HEAD_W: 46, // px, width of the row-number gutter
    STORAGE_KEY: 'vanilla-sheets:v1',
    SAVE_DEBOUNCE_MS: 200,
    MAX_DIGITS: 12, // significant digits used when formatting numbers
};
exports.COLORS = {
    gridline: '#e1e3e6',
    headerBg: '#f8f9fa',
    headerFg: '#5f6368',
    headerActiveBg: '#d3e3fd',
    selection: '#1a73e8',
    error: '#d93025',
    text: '#202124',
};
};

// ── module: src/refs.ts ──
__mods["src/refs.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * REFS — cell coordinate <-> A1 name helpers
 * col and row are ZERO-BASED internally.
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.colName = colName;
exports.colIndex = colIndex;
exports.keyOf = keyOf;
exports.parseKey = parseKey;
exports.inBounds = inBounds;
exports.clampCol = clampCol;
exports.clampRow = clampRow;
const config_1 = require("./config");
function colName(col) {
    let n = col, s = '';
    do {
        s = String.fromCharCode(65 + (n % 26)) + s;
        n = Math.floor(n / 26) - 1;
    } while (n >= 0);
    return s;
}
function colIndex(name) {
    let n = 0;
    const up = name.toUpperCase();
    for (let i = 0; i < up.length; i++)
        n = n * 26 + (up.charCodeAt(i) - 64);
    return n - 1;
}
/** "A1" for col 0, row 0 */
function keyOf(col, row) {
    return colName(col) + (row + 1);
}
function parseKey(key) {
    const m = /^([A-Za-z]+)([0-9]+)$/.exec(key.trim());
    if (!m)
        return null;
    const col = colIndex(m[1]);
    const row = parseInt(m[2], 10) - 1;
    if (row < 0)
        return null;
    return { col, row };
}
function inBounds(col, row) {
    return col >= 0 && row >= 0 && col < config_1.CONFIG.COLS && row < config_1.CONFIG.ROWS;
}
function clampCol(c) {
    return Math.max(0, Math.min(config_1.CONFIG.COLS - 1, c));
}
function clampRow(r) {
    return Math.max(0, Math.min(config_1.CONFIG.ROWS - 1, r));
}
};

// ── module: src/ast.ts ──
__mods["src/ast.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * AST — node types for parsed formulas
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
};

// ── module: src/lexer.ts ──
__mods["src/lexer.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * LEXER — tokenizer for the formula language
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.LexError = void 0;
exports.tokenize = tokenize;
const ERRORS = ['#REF!', '#DIV/0!', '#CYCLE!', '#VALUE!', '#NAME?', '#ERR!'];
class LexError extends Error {
}
exports.LexError = LexError;
function tokenize(src) {
    const out = [];
    let i = 0;
    const n = src.length;
    while (i < n) {
        const c = src[i];
        // whitespace
        if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
            i++;
            continue;
        }
        // number: 12, 12.5, .5, 1e-3
        if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(src[i + 1] ?? ''))) {
            const start = i;
            while (i < n && /[0-9]/.test(src[i]))
                i++;
            if (src[i] === '.') {
                i++;
                while (i < n && /[0-9]/.test(src[i]))
                    i++;
            }
            if (src[i] === 'e' || src[i] === 'E') {
                const save = i;
                i++;
                if (src[i] === '+' || src[i] === '-')
                    i++;
                if (/[0-9]/.test(src[i] ?? '')) {
                    while (i < n && /[0-9]/.test(src[i]))
                        i++;
                }
                else
                    i = save;
            }
            const s = src.slice(start, i);
            out.push({ t: 'num', s, n: parseFloat(s), pos: start });
            continue;
        }
        // quoted string
        if (c === '"') {
            const start = i;
            i++;
            let val = '';
            let closed = false;
            while (i < n) {
                if (src[i] === '"') {
                    if (src[i + 1] === '"') {
                        val += '"';
                        i += 2;
                        continue;
                    }
                    i++;
                    closed = true;
                    break;
                }
                val += src[i++];
            }
            if (!closed)
                throw new LexError('unterminated string');
            out.push({ t: 'str', s: val, pos: start });
            continue;
        }
        // error literal
        if (c === '#') {
            const rest = src.slice(i).toUpperCase();
            const hit = ERRORS.find((e) => rest.startsWith(e));
            if (!hit)
                throw new LexError('bad error literal');
            out.push({ t: 'err', s: hit, pos: i });
            i += hit.length;
            continue;
        }
        // reference or identifier
        if (/[A-Za-z_$]/.test(c)) {
            const start = i;
            // try absolute/relative reference: $?A$?1
            const m = /^(\$?)([A-Za-z]{1,3})(\$?)([0-9]{1,7})(?![A-Za-z0-9_.])/.exec(src.slice(i));
            if (m) {
                out.push({ t: 'ref', s: m[0], pos: start });
                i += m[0].length;
                continue;
            }
            while (i < n && /[A-Za-z0-9_.]/.test(src[i]))
                i++;
            out.push({ t: 'ident', s: src.slice(start, i), pos: start });
            continue;
        }
        if (c === '$') {
            const m = /^(\$)([A-Za-z]{1,3})(\$?)([0-9]{1,7})(?![A-Za-z0-9_.])/.exec(src.slice(i));
            if (m) {
                out.push({ t: 'ref', s: m[0], pos: i });
                i += m[0].length;
                continue;
            }
            throw new LexError('unexpected $');
        }
        if (c === '(') {
            out.push({ t: 'lparen', s: c, pos: i++ });
            continue;
        }
        if (c === ')') {
            out.push({ t: 'rparen', s: c, pos: i++ });
            continue;
        }
        if (c === ',' || c === ';') {
            out.push({ t: 'comma', s: ',', pos: i++ });
            continue;
        }
        if (c === ':') {
            out.push({ t: 'colon', s: c, pos: i++ });
            continue;
        }
        // operators
        const two = src.substr(i, 2);
        if (two === '<=' || two === '>=' || two === '<>') {
            out.push({ t: 'op', s: two, pos: i });
            i += 2;
            continue;
        }
        if ('+-*/^=<>&'.includes(c)) {
            out.push({ t: 'op', s: c, pos: i++ });
            continue;
        }
        throw new LexError('unexpected character "' + c + '"');
    }
    out.push({ t: 'eof', s: '', pos: n });
    return out;
}
};

// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * PARSER — recursive descent, precedence climbing
 *
 *   expr    := compare
 *   compare := concat (( = | <> | < | > | <= | >= ) concat)*
 *   concat  := additive ( & additive )*
 *   additive:= term (( + | - ) term)*
 *   term    := power (( * | / ) power)*
 *   power   := unary ( ^ unary )*
 *   unary   := (+|-) unary | postfix
 *   primary := num | str | err | ref [ : ref ] | ident '(' args ')' | '(' expr ')'
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParseError = void 0;
exports.parseFormula = parseFormula;
exports.collectRefs = collectRefs;
const lexer_1 = require("./lexer");
const refs_1 = require("./refs");
class ParseError extends Error {
}
exports.ParseError = ParseError;
class Parser {
    constructor(toks) {
        this.i = 0;
        this.toks = toks;
    }
    peek() { return this.toks[this.i]; }
    next() { return this.toks[this.i++]; }
    isOp(...ops) {
        const t = this.peek();
        return t.t === 'op' && ops.includes(t.s);
    }
    expect(t, what) {
        const tok = this.peek();
        if (tok.t !== t)
            throw new ParseError('expected ' + what);
        return this.next();
    }
    parseProgram() {
        const n = this.parseExpr();
        if (this.peek().t !== 'eof')
            throw new ParseError('unexpected trailing input');
        return n;
    }
    parseExpr() { return this.parseCompare(); }
    parseCompare() {
        let a = this.parseConcat();
        while (this.isOp('=', '<>', '<', '>', '<=', '>=')) {
            const op = this.next().s;
            a = { k: 'binary', op, a, b: this.parseConcat() };
        }
        return a;
    }
    parseConcat() {
        let a = this.parseAdditive();
        while (this.isOp('&')) {
            this.next();
            a = { k: 'binary', op: '&', a, b: this.parseAdditive() };
        }
        return a;
    }
    parseAdditive() {
        let a = this.parseTerm();
        while (this.isOp('+', '-')) {
            const op = this.next().s;
            a = { k: 'binary', op, a, b: this.parseTerm() };
        }
        return a;
    }
    parseTerm() {
        let a = this.parsePower();
        while (this.isOp('*', '/')) {
            const op = this.next().s;
            a = { k: 'binary', op, a, b: this.parsePower() };
        }
        return a;
    }
    parsePower() {
        // left-associative, matching spreadsheet convention (2^3^2 = 64)
        let a = this.parseUnary();
        while (this.isOp('^')) {
            this.next();
            a = { k: 'binary', op: '^', a, b: this.parseUnary() };
        }
        return a;
    }
    parseUnary() {
        if (this.isOp('-', '+')) {
            const op = this.next().s;
            return { k: 'unary', op, a: this.parseUnary() };
        }
        return this.parsePrimary();
    }
    parsePrimary() {
        const t = this.peek();
        switch (t.t) {
            case 'num':
                this.next();
                return { k: 'num', v: t.n };
            case 'str':
                this.next();
                return { k: 'str', v: t.s };
            case 'err':
                this.next();
                return { k: 'err', v: t.s };
            case 'ref': {
                this.next();
                const a = refFromToken(t.s);
                if (this.peek().t === 'colon') {
                    this.next();
                    const bt = this.peek();
                    if (bt.t !== 'ref')
                        throw new ParseError('expected reference after ":"');
                    this.next();
                    return { k: 'range', a, b: refFromToken(bt.s) };
                }
                return a;
            }
            case 'ident': {
                this.next();
                const name = t.s.toUpperCase();
                if (this.peek().t === 'lparen') {
                    this.next();
                    const args = [];
                    if (this.peek().t !== 'rparen') {
                        args.push(this.parseExpr());
                        while (this.peek().t === 'comma') {
                            this.next();
                            args.push(this.parseExpr());
                        }
                    }
                    this.expect('rparen', '")"');
                    return { k: 'call', name, args };
                }
                if (name === 'TRUE')
                    return { k: 'num', v: 1 };
                if (name === 'FALSE')
                    return { k: 'num', v: 0 };
                throw new ParseError('unknown name ' + name);
            }
            case 'lparen': {
                this.next();
                const inner = this.parseExpr();
                this.expect('rparen', '")"');
                return { k: 'paren', a: inner };
            }
            default:
                throw new ParseError('unexpected ' + (t.t === 'eof' ? 'end of formula' : '"' + t.s + '"'));
        }
    }
}
function refFromToken(s) {
    const m = /^(\$?)([A-Za-z]{1,3})(\$?)([0-9]{1,7})$/.exec(s);
    if (!m)
        throw new ParseError('bad reference ' + s);
    return {
        k: 'ref',
        col: (0, refs_1.colIndex)(m[2]),
        row: parseInt(m[4], 10) - 1,
        absCol: m[1] === '$',
        absRow: m[3] === '$',
    };
}
/** Parse the body of a formula (text AFTER the leading "="). Throws ParseError. */
function parseFormula(body) {
    let toks;
    try {
        toks = (0, lexer_1.tokenize)(body);
    }
    catch (e) {
        if (e instanceof lexer_1.LexError)
            throw new ParseError(e.message);
        throw e;
    }
    if (toks.length === 1)
        throw new ParseError('empty formula');
    return new Parser(toks).parseProgram();
}
/** Collect every ref/range in an AST. */
function collectRefs(n, out = []) {
    switch (n.k) {
        case 'ref':
        case 'range':
            out.push(n);
            break;
        case 'unary':
            collectRefs(n.a, out);
            break;
        case 'paren':
            collectRefs(n.a, out);
            break;
        case 'binary':
            collectRefs(n.a, out);
            collectRefs(n.b, out);
            break;
        case 'call':
            for (const a of n.args)
                collectRefs(a, out);
            break;
        default: break;
    }
    return out;
}
};

// ── module: src/printer.ts ──
__mods["src/printer.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * PRINTER — AST back to source text (used when formulas are
 * rewritten after row/column insert & delete)
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.printRef = printRef;
exports.printNode = printNode;
const refs_1 = require("./refs");
function printRef(r) {
    if (r.col < 0 || r.row < 0)
        return '#REF!';
    return (r.absCol ? '$' : '') + (0, refs_1.colName)(r.col) + (r.absRow ? '$' : '') + String(r.row + 1);
}
function printNode(n) {
    switch (n.k) {
        case 'num': return String(n.v);
        case 'str': return '"' + n.v.replace(/"/g, '""') + '"';
        case 'err': return n.v;
        case 'ref': return printRef(n);
        case 'range': {
            const a = printRef(n.a), b = printRef(n.b);
            if (a === '#REF!' || b === '#REF!')
                return '#REF!';
            return a + ':' + b;
        }
        case 'unary': return n.op + printNode(n.a);
        case 'paren': return '(' + printNode(n.a) + ')';
        case 'binary': return printNode(n.a) + n.op + printNode(n.b);
        case 'call': return n.name + '(' + n.args.map(printNode).join(', ') + ')';
    }
}
};

// ── module: src/value.ts ──
__mods["src/value.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * VALUE — the runtime value model + coercion + formatting
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.err = exports.str = exports.num = exports.BLANK = void 0;
exports.isErr = isErr;
exports.toNum = toNum;
exports.toStr = toStr;
exports.formatNumber = formatNumber;
exports.display = display;
exports.literalOf = literalOf;
const config_1 = require("./config");
exports.BLANK = { t: 'blank' };
const num = (v) => Number.isFinite(v) ? { t: 'num', v } : { t: 'err', v: '#VALUE!' };
exports.num = num;
const str = (v) => ({ t: 'str', v });
exports.str = str;
const err = (v) => ({ t: 'err', v });
exports.err = err;
function isErr(v) { return v.t === 'err'; }
/** Coerce to number for arithmetic. Returns an error Value on failure. */
function toNum(v) {
    switch (v.t) {
        case 'num': return { ok: true, n: v.v };
        case 'bool': return { ok: true, n: v.v ? 1 : 0 };
        case 'blank': return { ok: true, n: 0 };
        case 'err': return { ok: false, e: v };
        case 'str': {
            const s = v.v.trim();
            if (s === '')
                return { ok: true, n: 0 };
            const n = Number(s);
            if (Number.isFinite(n))
                return { ok: true, n };
            return { ok: false, e: (0, exports.err)('#VALUE!') };
        }
    }
}
function toStr(v) {
    switch (v.t) {
        case 'num': return formatNumber(v.v);
        case 'str': return v.v;
        case 'bool': return v.v ? 'TRUE' : 'FALSE';
        case 'err': return v.v;
        case 'blank': return '';
    }
}
/** Compact, spreadsheet-ish number formatting. */
function formatNumber(n) {
    if (!Number.isFinite(n))
        return n > 0 ? '#DIV/0!' : '#DIV/0!';
    if (Number.isInteger(n) && Math.abs(n) < 1e15)
        return String(n);
    const abs = Math.abs(n);
    if (abs !== 0 && (abs < 1e-9 || abs >= 1e12))
        return n.toExponential(6).replace(/e([+-])(\d)$/, 'e$10$2');
    let s = n.toPrecision(config_1.CONFIG.MAX_DIGITS);
    if (s.includes('.'))
        s = s.replace(/0+$/, '').replace(/\.$/, '');
    return s;
}
/** How a value renders in a cell. */
function display(v) {
    switch (v.t) {
        case 'num': return { text: formatNumber(v.v), cls: 'num' };
        case 'bool': return { text: v.v ? 'TRUE' : 'FALSE', cls: 'text' };
        case 'err': return { text: v.v, cls: 'err' };
        case 'str': return { text: v.v, cls: 'text' };
        case 'blank': return { text: '', cls: 'blank' };
    }
}
/** Parse raw user input that is NOT a formula. */
function literalOf(raw) {
    if (raw === '')
        return exports.BLANK;
    const s = raw.trim();
    if (s !== '' && /^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/.test(s)) {
        const n = Number(s);
        if (Number.isFinite(n))
            return { t: 'num', v: n };
    }
    if (/^-?\d+(\.\d+)?%$/.test(s)) {
        return { t: 'num', v: Number(s.slice(0, -1)) / 100 };
    }
    return { t: 'str', v: raw };
}
};

// ── module: src/eval.ts ──
__mods["src/eval.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * EVAL — AST evaluation against a value provider
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.evalNode = evalNode;
exports.refKeys = refKeys;
const value_1 = require("./value");
const refs_1 = require("./refs");
/** Flatten one argument node into a list of values (ranges expand). */
function argValues(n, src) {
    if (n.k === 'range')
        return rangeValues(n.a, n.b, src);
    return [evalNode(n, src)];
}
function rangeValues(a, b, src) {
    if (a.col < 0 || a.row < 0 || b.col < 0 || b.row < 0)
        return [(0, value_1.err)('#REF!')];
    if (!(0, refs_1.inBounds)(a.col, a.row) || !(0, refs_1.inBounds)(b.col, b.row))
        return [(0, value_1.err)('#REF!')];
    const c0 = Math.min(a.col, b.col), c1 = Math.max(a.col, b.col);
    const r0 = Math.min(a.row, b.row), r1 = Math.max(a.row, b.row);
    const out = [];
    for (let r = r0; r <= r1; r++)
        for (let c = c0; c <= c1; c++)
            out.push(src.valueAt(c, r));
    return out;
}
/** Numbers only, skipping blanks/text — the SUM/AVG convention. */
function numericArgs(args, src) {
    const ns = [];
    for (const a of args) {
        const vals = argValues(a, src);
        const fromRange = a.k === 'range';
        for (const v of vals) {
            if (v.t === 'err')
                return { ok: false, e: v };
            if (v.t === 'blank')
                continue;
            if (v.t === 'str') {
                if (fromRange)
                    continue; // text in a range is ignored
                const c = (0, value_1.toNum)(v);
                if (!c.ok)
                    return { ok: false, e: c.e };
                ns.push(c.n);
                continue;
            }
            const c = (0, value_1.toNum)(v);
            if (!c.ok)
                return { ok: false, e: c.e };
            ns.push(c.n);
        }
    }
    return { ok: true, ns };
}
/* ---------- functions ---------- */
function callFn(name, args, src) {
    switch (name) {
        case 'SUM': {
            const r = numericArgs(args, src);
            if (!r.ok)
                return r.e;
            return (0, value_1.num)(r.ns.reduce((a, b) => a + b, 0));
        }
        case 'AVG':
        case 'AVERAGE': {
            const r = numericArgs(args, src);
            if (!r.ok)
                return r.e;
            if (r.ns.length === 0)
                return (0, value_1.err)('#DIV/0!');
            return (0, value_1.num)(r.ns.reduce((a, b) => a + b, 0) / r.ns.length);
        }
        case 'MIN': {
            const r = numericArgs(args, src);
            if (!r.ok)
                return r.e;
            if (r.ns.length === 0)
                return (0, value_1.num)(0);
            return (0, value_1.num)(Math.min(...r.ns));
        }
        case 'MAX': {
            const r = numericArgs(args, src);
            if (!r.ok)
                return r.e;
            if (r.ns.length === 0)
                return (0, value_1.num)(0);
            return (0, value_1.num)(Math.max(...r.ns));
        }
        case 'COUNT': {
            const r = numericArgs(args, src);
            if (!r.ok)
                return r.e;
            return (0, value_1.num)(r.ns.length);
        }
        case 'COUNTA': {
            let n = 0;
            for (const a of args)
                for (const v of argValues(a, src))
                    if (v.t !== 'blank')
                        n++;
            return (0, value_1.num)(n);
        }
        case 'ABS':
        case 'ROUND':
        case 'SQRT':
        case 'INT':
        case 'POWER':
        case 'MOD': {
            const r = numericArgs(args, src);
            if (!r.ok)
                return r.e;
            const [x, y] = r.ns;
            if (x === undefined)
                return (0, value_1.err)('#ERR!');
            if (name === 'ABS')
                return (0, value_1.num)(Math.abs(x));
            if (name === 'SQRT')
                return x < 0 ? (0, value_1.err)('#VALUE!') : (0, value_1.num)(Math.sqrt(x));
            if (name === 'INT')
                return (0, value_1.num)(Math.floor(x));
            if (name === 'ROUND') {
                const d = y === undefined ? 0 : Math.trunc(y);
                const f = Math.pow(10, d);
                return (0, value_1.num)(Math.round(x * f) / f);
            }
            if (name === 'POWER')
                return y === undefined ? (0, value_1.err)('#ERR!') : (0, value_1.num)(Math.pow(x, y));
            /* MOD */
            if (y === undefined)
                return (0, value_1.err)('#ERR!');
            if (y === 0)
                return (0, value_1.err)('#DIV/0!');
            return (0, value_1.num)(x - y * Math.floor(x / y));
        }
        case 'IF': {
            if (args.length < 2)
                return (0, value_1.err)('#ERR!');
            const c = evalNode(args[0], src);
            if (c.t === 'err')
                return c;
            const cn = (0, value_1.toNum)(c);
            const truthy = c.t === 'str' ? c.v !== '' : (cn.ok ? cn.n !== 0 : true);
            if (truthy)
                return evalNode(args[1], src);
            return args.length > 2 ? evalNode(args[2], src) : { t: 'bool', v: false };
        }
        case 'CONCAT':
        case 'CONCATENATE': {
            let s = '';
            for (const a of args)
                for (const v of argValues(a, src)) {
                    if (v.t === 'err')
                        return v;
                    s += (0, value_1.toStr)(v);
                }
            return (0, value_1.str)(s);
        }
        case 'LEN': {
            if (args.length !== 1)
                return (0, value_1.err)('#ERR!');
            const v = evalNode(args[0], src);
            if (v.t === 'err')
                return v;
            return (0, value_1.num)((0, value_1.toStr)(v).length);
        }
        default:
            return (0, value_1.err)('#NAME?');
    }
}
/* ---------- core ---------- */
function evalNode(n, src) {
    switch (n.k) {
        case 'num': return { t: 'num', v: n.v };
        case 'str': return { t: 'str', v: n.v };
        case 'err': return { t: 'err', v: n.v };
        case 'paren': return evalNode(n.a, src);
        case 'ref': {
            if (n.col < 0 || n.row < 0 || !(0, refs_1.inBounds)(n.col, n.row))
                return (0, value_1.err)('#REF!');
            return src.valueAt(n.col, n.row);
        }
        case 'range': {
            // A bare range outside a function collapses to its first cell's value.
            const vs = rangeValues(n.a, n.b, src);
            return vs.length ? vs[0] : value_1.BLANK;
        }
        case 'unary': {
            const v = evalNode(n.a, src);
            if (v.t === 'err')
                return v;
            const c = (0, value_1.toNum)(v);
            if (!c.ok)
                return c.e;
            return (0, value_1.num)(n.op === '-' ? -c.n : c.n);
        }
        case 'call': return callFn(n.name, n.args, src);
        case 'binary': {
            const a = evalNode(n.a, src);
            if (a.t === 'err')
                return a;
            const b = evalNode(n.b, src);
            if (b.t === 'err')
                return b;
            if (n.op === '&')
                return (0, value_1.str)((0, value_1.toStr)(a) + (0, value_1.toStr)(b));
            if (n.op === '=' || n.op === '<>' || n.op === '<' || n.op === '>' || n.op === '<=' || n.op === '>=') {
                return compare(n.op, a, b);
            }
            const ca = (0, value_1.toNum)(a);
            if (!ca.ok)
                return ca.e;
            const cb = (0, value_1.toNum)(b);
            if (!cb.ok)
                return cb.e;
            switch (n.op) {
                case '+': return (0, value_1.num)(ca.n + cb.n);
                case '-': return (0, value_1.num)(ca.n - cb.n);
                case '*': return (0, value_1.num)(ca.n * cb.n);
                case '/': return cb.n === 0 ? (0, value_1.err)('#DIV/0!') : (0, value_1.num)(ca.n / cb.n);
                case '^': return (0, value_1.num)(Math.pow(ca.n, cb.n));
            }
            return (0, value_1.err)('#ERR!');
        }
    }
}
function compare(op, a, b) {
    let r;
    if (a.t === 'str' || b.t === 'str') {
        const sa = (0, value_1.toStr)(a).toLowerCase(), sb = (0, value_1.toStr)(b).toLowerCase();
        r = sa < sb ? -1 : sa > sb ? 1 : 0;
    }
    else {
        const ca = (0, value_1.toNum)(a);
        if (!ca.ok)
            return ca.e;
        const cb = (0, value_1.toNum)(b);
        if (!cb.ok)
            return cb.e;
        r = ca.n < cb.n ? -1 : ca.n > cb.n ? 1 : 0;
    }
    const res = op === '=' ? r === 0 :
        op === '<>' ? r !== 0 :
            op === '<' ? r < 0 :
                op === '>' ? r > 0 :
                    op === '<=' ? r <= 0 : r >= 0;
    return { t: 'bool', v: res };
}
/** Used by the dependency graph: every single cell key an AST reads. */
function refKeys(n, out) {
    switch (n.k) {
        case 'ref':
            if (n.col >= 0 && n.row >= 0 && (0, refs_1.inBounds)(n.col, n.row))
                out.add((0, refs_1.keyOf)(n.col, n.row));
            break;
        case 'range': {
            const { a, b } = n;
            if (a.col < 0 || a.row < 0 || b.col < 0 || b.row < 0)
                break;
            const c0 = Math.max(0, Math.min(a.col, b.col)), c1 = Math.min(999, Math.max(a.col, b.col));
            const r0 = Math.max(0, Math.min(a.row, b.row)), r1 = Math.max(a.row, b.row);
            for (let r = r0; r <= r1; r++)
                for (let c = c0; c <= c1; c++) {
                    if ((0, refs_1.inBounds)(c, r))
                        out.add((0, refs_1.keyOf)(c, r));
                }
            break;
        }
        case 'unary':
        case 'paren':
            refKeys(n.a, out);
            break;
        case 'binary':
            refKeys(n.a, out);
            refKeys(n.b, out);
            break;
        case 'call':
            for (const a of n.args)
                refKeys(a, out);
            break;
        default: break;
    }
}
};

// ── module: src/rewrite.ts ──
__mods["src/rewrite.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * REWRITE — structural edits (insert/delete row & column) applied
 * to a formula AST so it keeps pointing at the same logical cells.
 * A destroyed target becomes col/row = -1, which prints as #REF!.
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.rewriteNode = rewriteNode;
const DEAD = -1;
function shiftInsert(v, at) { return v >= at ? v + 1 : v; }
/** single reference under a deletion of index `at` */
function shiftDeleteSingle(v, at) {
    if (v === at)
        return DEAD;
    return v > at ? v - 1 : v;
}
function isDead(r) { return r.col === DEAD || r.row === DEAD; }
/**
 * Returns a rewritten copy, or null if the whole expression became #REF!
 * (callers still get a node — refs are marked dead and print as #REF!).
 */
function rewriteNode(n, kind, at) {
    switch (n.k) {
        case 'num':
        case 'str':
        case 'err': return n;
        case 'paren': return { k: 'paren', a: rewriteNode(n.a, kind, at) };
        case 'unary': return { k: 'unary', op: n.op, a: rewriteNode(n.a, kind, at) };
        case 'binary': return { k: 'binary', op: n.op, a: rewriteNode(n.a, kind, at), b: rewriteNode(n.b, kind, at) };
        case 'call': return { k: 'call', name: n.name, args: n.args.map((a) => rewriteNode(a, kind, at)) };
        case 'ref': return rewriteRef(n, kind, at);
        case 'range': return rewriteRange(n, kind, at);
    }
}
function rewriteRef(r, kind, at) {
    if (isDead(r))
        return r;
    const out = { ...r };
    switch (kind) {
        case 'insertRow':
            out.row = shiftInsert(r.row, at);
            break;
        case 'insertCol':
            out.col = shiftInsert(r.col, at);
            break;
        case 'deleteRow':
            out.row = shiftDeleteSingle(r.row, at);
            break;
        case 'deleteCol':
            out.col = shiftDeleteSingle(r.col, at);
            break;
    }
    return out;
}
function rewriteRange(n, kind, at) {
    const a = n.a, b = n.b;
    if (isDead(a) || isDead(b))
        return n;
    const axis = (kind === 'insertRow' || kind === 'deleteRow') ? 'row' : 'col';
    const lo = Math.min(a[axis], b[axis]);
    const hi = Math.max(a[axis], b[axis]);
    let nlo = lo, nhi = hi;
    if (kind === 'insertRow' || kind === 'insertCol') {
        nlo = lo >= at ? lo + 1 : lo;
        nhi = hi >= at ? hi + 1 : hi;
    }
    else {
        if (lo === at && hi === at) {
            return { k: 'range', a: { ...a, col: DEAD, row: DEAD }, b: { ...b, col: DEAD, row: DEAD } };
        }
        nlo = lo > at ? lo - 1 : lo; // lo === at: start slides onto the next survivor
        nhi = hi >= at ? hi - 1 : hi;
        if (nlo > nhi) {
            return { k: 'range', a: { ...a, col: DEAD, row: DEAD }, b: { ...b, col: DEAD, row: DEAD } };
        }
    }
    // preserve the original endpoint ordering
    const aFirst = a[axis] <= b[axis];
    const na = { ...a }, nb = { ...b };
    if (axis === 'row') {
        na.row = aFirst ? nlo : nhi;
        nb.row = aFirst ? nhi : nlo;
    }
    else {
        na.col = aFirst ? nlo : nhi;
        nb.col = aFirst ? nhi : nlo;
    }
    return { k: 'range', a: na, b: nb };
}
};

// ── module: src/sheet.ts ──
__mods["src/sheet.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * SHEET — cell store, dependency graph, topological recalc,
 * cycle detection, structural edits, serialization
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Sheet = void 0;
const config_1 = require("./config");
const parser_1 = require("./parser");
const printer_1 = require("./printer");
const rewrite_1 = require("./rewrite");
const eval_1 = require("./eval");
const value_1 = require("./value");
const refs_1 = require("./refs");
class Sheet {
    constructor() {
        this.cells = new Map();
        /** key -> cells it reads (precedents) */
        this.precedents = new Map();
        /** key -> cells that read it (dependents) */
        this.dependents = new Map();
        /** bumped whenever any value changes; the view uses it to know what to repaint */
        this.dirty = new Set();
    }
    /* ---------------- basic access ---------------- */
    getCell(key) { return this.cells.get(key); }
    raw(key) { return this.cells.get(key)?.raw ?? ''; }
    value(key) { return this.cells.get(key)?.value ?? value_1.BLANK; }
    valueAt(col, row) {
        if (!(0, refs_1.inBounds)(col, row))
            return (0, value_1.err)('#REF!');
        return this.cells.get((0, refs_1.keyOf)(col, row))?.value ?? value_1.BLANK;
    }
    allKeys() { return [...this.cells.keys()]; }
    /* ---------------- dependency graph ---------------- */
    unlink(key) {
        const pre = this.precedents.get(key);
        if (!pre)
            return;
        for (const p of pre) {
            const set = this.dependents.get(p);
            if (set) {
                set.delete(key);
                if (set.size === 0)
                    this.dependents.delete(p);
            }
        }
        this.precedents.delete(key);
    }
    link(key, ast) {
        if (!ast)
            return;
        const refs = new Set();
        (0, eval_1.refKeys)(ast, refs);
        // NB: a self-reference is kept, so Kahn's algorithm reports it as #CYCLE!
        if (refs.size === 0)
            return;
        this.precedents.set(key, refs);
        for (const p of refs) {
            let set = this.dependents.get(p);
            if (!set) {
                set = new Set();
                this.dependents.set(p, set);
            }
            set.add(key);
        }
    }
    /* ---------------- editing ---------------- */
    /** Write raw text into a cell and recalculate everything it affects. */
    setRaw(key, rawIn) {
        const raw = rawIn;
        if (raw === '') {
            this.unlink(key);
            this.cells.delete(key);
            this.dirty.add(key);
            this.recalcFrom([key]);
            return;
        }
        const cell = this.compile(raw);
        this.unlink(key);
        this.cells.set(key, cell);
        this.link(key, cell.ast);
        this.dirty.add(key);
        this.recalcFrom([key]);
    }
    compile(raw) {
        if (raw.startsWith('=')) {
            try {
                const ast = (0, parser_1.parseFormula)(raw.slice(1));
                return { raw, ast, parseFailed: false, value: value_1.BLANK };
            }
            catch (e) {
                if (e instanceof parser_1.ParseError)
                    return { raw, ast: null, parseFailed: true, value: (0, value_1.err)('#ERR!') };
                return { raw, ast: null, parseFailed: true, value: (0, value_1.err)('#ERR!') };
            }
        }
        return { raw, ast: null, parseFailed: false, value: (0, value_1.literalOf)(raw) };
    }
    clear(key) { this.setRaw(key, ''); }
    /* ---------------- recalculation ---------------- */
    /** Recompute the seed cells and everything downstream, in topological order. */
    recalcFrom(seeds) {
        // 1. transitive closure over dependents
        const affected = new Set();
        const stack = [...seeds];
        while (stack.length) {
            const k = stack.pop();
            if (affected.has(k))
                continue;
            affected.add(k);
            const ds = this.dependents.get(k);
            if (ds)
                for (const d of ds)
                    if (!affected.has(d))
                        stack.push(d);
        }
        // 2. in-degrees inside the affected subgraph
        const indeg = new Map();
        for (const k of affected) {
            let d = 0;
            const pre = this.precedents.get(k);
            if (pre)
                for (const p of pre)
                    if (affected.has(p))
                        d++;
            indeg.set(k, d);
        }
        // 3. Kahn
        const order = [];
        const queue = [];
        for (const [k, d] of indeg)
            if (d === 0)
                queue.push(k);
        while (queue.length) {
            const k = queue.shift();
            order.push(k);
            const ds = this.dependents.get(k);
            if (!ds)
                continue;
            for (const d of ds) {
                if (!affected.has(d))
                    continue;
                const nd = indeg.get(d) - 1;
                indeg.set(d, nd);
                if (nd === 0)
                    queue.push(d);
            }
        }
        // 4. leftovers are in (or downstream of) a cycle
        const cyclic = [];
        for (const [k, d] of indeg)
            if (d > 0)
                cyclic.push(k);
        for (const k of cyclic) {
            const c = this.cells.get(k);
            if (c) {
                c.value = (0, value_1.err)('#CYCLE!');
                this.dirty.add(k);
            }
        }
        // 5. evaluate in dependency order
        for (const k of order)
            this.evaluateCell(k);
    }
    evaluateCell(key) {
        const c = this.cells.get(key);
        if (!c) {
            this.dirty.add(key);
            return;
        }
        let next;
        if (c.parseFailed)
            next = (0, value_1.err)('#ERR!');
        else if (c.ast)
            next = (0, eval_1.evalNode)(c.ast, this);
        else
            next = (0, value_1.literalOf)(c.raw);
        const prev = c.value;
        c.value = next;
        if (prev.t !== next.t || prev.v !== next.v) {
            this.dirty.add(key);
        }
    }
    /** Full recompute of every cell (used after load and structural edits). */
    recalcAll() {
        this.precedents.clear();
        this.dependents.clear();
        for (const [key, c] of this.cells)
            this.link(key, c.ast);
        for (const key of this.cells.keys())
            this.dirty.add(key);
        this.recalcFrom([...this.cells.keys()]);
    }
    /* ---------------- structural edits ---------------- */
    structuralEdit(kind, at) {
        const old = [...this.cells.entries()];
        const next = new Map();
        for (const [key, cell] of old) {
            const pos = (0, refs_1.parseKey)(key);
            if (!pos)
                continue;
            let { col, row } = pos;
            if (kind === 'insertRow') {
                if (row >= at)
                    row++;
            }
            else if (kind === 'deleteRow') {
                if (row === at)
                    continue;
                if (row > at)
                    row--;
            }
            else if (kind === 'insertCol') {
                if (col >= at)
                    col++;
            }
            else if (kind === 'deleteCol') {
                if (col === at)
                    continue;
                if (col > at)
                    col--;
            }
            if (!(0, refs_1.inBounds)(col, row))
                continue; // pushed off the edge of the sheet
            let raw = cell.raw;
            let ast = cell.ast;
            if (ast) {
                ast = (0, rewrite_1.rewriteNode)(ast, kind, at);
                raw = '=' + (0, printer_1.printNode)(ast);
            }
            const recompiled = this.compile(raw);
            next.set((0, refs_1.keyOf)(col, row), recompiled);
        }
        this.cells = next;
        this.dirty = new Set();
        for (let c = 0; c < config_1.CONFIG.COLS; c++)
            for (let r = 0; r < config_1.CONFIG.ROWS; r++)
                this.dirty.add((0, refs_1.keyOf)(c, r));
        this.recalcAll();
    }
    /* ---------------- serialization ---------------- */
    toJSON() {
        const out = {};
        for (const [k, c] of this.cells)
            if (c.raw !== '')
                out[k] = c.raw;
        return out;
    }
    loadJSON(data) {
        this.cells.clear();
        this.precedents.clear();
        this.dependents.clear();
        for (const k of Object.keys(data)) {
            const pos = (0, refs_1.parseKey)(k);
            if (!pos || !(0, refs_1.inBounds)(pos.col, pos.row))
                continue;
            const raw = data[k];
            if (typeof raw !== 'string' || raw === '')
                continue;
            this.cells.set((0, refs_1.keyOf)(pos.col, pos.row), this.compile(raw));
        }
        this.recalcAll();
    }
    save() {
        try {
            localStorage.setItem(config_1.CONFIG.STORAGE_KEY, JSON.stringify(this.toJSON()));
        }
        catch { /* storage unavailable — keep working in memory */ }
    }
    load() {
        try {
            const s = localStorage.getItem(config_1.CONFIG.STORAGE_KEY);
            if (!s)
                return false;
            const data = JSON.parse(s);
            if (!data || typeof data !== 'object')
                return false;
            this.loadJSON(data);
            return true;
        }
        catch {
            return false;
        }
    }
}
exports.Sheet = Sheet;
};

// ── module: src/view.ts ──
__mods["src/view.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * VIEW — builds the table once, then paints cells on demand
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.View = void 0;
const config_1 = require("./config");
const refs_1 = require("./refs");
const value_1 = require("./value");
class View {
    constructor(table, sheet) {
        this.tds = []; // [row][col]
        this.colHeads = [];
        this.rowHeads = [];
        this.selCol = 0;
        this.selRow = 0;
        this.table = table;
        this.sheet = sheet;
        this.thead = table.tHead;
        this.tbody = table.tBodies[0];
        this.build();
    }
    /* ---------- construction ---------- */
    build() {
        const hrow = document.createElement('tr');
        const corner = document.createElement('th');
        corner.className = 'corner';
        hrow.appendChild(corner);
        for (let c = 0; c < config_1.CONFIG.COLS; c++) {
            const th = document.createElement('th');
            th.textContent = (0, refs_1.colName)(c);
            th.dataset.col = String(c);
            hrow.appendChild(th);
            this.colHeads.push(th);
        }
        this.thead.appendChild(hrow);
        const frag = document.createDocumentFragment();
        for (let r = 0; r < config_1.CONFIG.ROWS; r++) {
            const tr = document.createElement('tr');
            const th = document.createElement('th');
            th.textContent = String(r + 1);
            th.dataset.row = String(r);
            tr.appendChild(th);
            this.rowHeads.push(th);
            const rowCells = [];
            for (let c = 0; c < config_1.CONFIG.COLS; c++) {
                const td = document.createElement('td');
                td.dataset.col = String(c);
                td.dataset.row = String(r);
                tr.appendChild(td);
                rowCells.push(td);
            }
            this.tds.push(rowCells);
            frag.appendChild(tr);
        }
        this.tbody.appendChild(frag);
    }
    /* ---------- painting ---------- */
    tdAt(col, row) {
        const row2 = this.tds[row];
        return row2 ? (row2[col] ?? null) : null;
    }
    paintCell(col, row) {
        const td = this.tdAt(col, row);
        if (!td)
            return;
        const v = this.sheet.value((0, refs_1.keyOf)(col, row));
        const d = (0, value_1.display)(v);
        if (td.firstChild && td.firstChild.nodeType === 3) {
            if (td.firstChild.nodeValue !== d.text)
                td.firstChild.nodeValue = d.text;
        }
        else if (!td.querySelector('input')) {
            td.textContent = d.text;
        }
        const sel = td.classList.contains('sel');
        const editing = td.classList.contains('editing');
        let cls = '';
        if (d.cls === 'num')
            cls = 'num';
        else if (d.cls === 'err')
            cls = 'err';
        td.className = [cls, sel ? 'sel' : '', editing ? 'editing' : ''].filter(Boolean).join(' ');
        const raw = this.sheet.raw((0, refs_1.keyOf)(col, row));
        if (raw)
            td.title = raw.startsWith('=') ? raw + '  →  ' + d.text : '';
        else if (td.title)
            td.title = '';
    }
    /** Repaint every cell the sheet flagged as changed. */
    flush() {
        for (const key of this.sheet.dirty) {
            const m = /^([A-Z]+)(\d+)$/.exec(key);
            if (!m)
                continue;
            let col = 0;
            for (let i = 0; i < m[1].length; i++)
                col = col * 26 + (m[1].charCodeAt(i) - 64);
            this.paintCell(col - 1, parseInt(m[2], 10) - 1);
        }
        this.sheet.dirty.clear();
    }
    paintAll() {
        for (let r = 0; r < config_1.CONFIG.ROWS; r++)
            for (let c = 0; c < config_1.CONFIG.COLS; c++)
                this.paintCell(c, r);
        this.sheet.dirty.clear();
    }
    /* ---------- selection ---------- */
    setSelection(col, row) {
        const prev = this.tdAt(this.selCol, this.selRow);
        if (prev)
            prev.classList.remove('sel');
        this.colHeads[this.selCol]?.classList.remove('hl');
        this.rowHeads[this.selRow]?.classList.remove('hl');
        this.selCol = col;
        this.selRow = row;
        const td = this.tdAt(col, row);
        if (td)
            td.classList.add('sel');
        this.colHeads[col]?.classList.add('hl');
        this.rowHeads[row]?.classList.add('hl');
    }
    scrollIntoView(col, row) {
        const td = this.tdAt(col, row);
        const wrap = this.table.parentElement;
        if (!td || !wrap)
            return;
        const cw = config_1.CONFIG.CELL_W, ch = config_1.CONFIG.CELL_H;
        const x = config_1.CONFIG.ROW_HEAD_W + col * (cw + 1);
        const y = row * (ch + 1);
        const padL = config_1.CONFIG.ROW_HEAD_W, padT = ch + 1;
        if (x - padL < wrap.scrollLeft)
            wrap.scrollLeft = Math.max(0, x - padL);
        else if (x + cw > wrap.scrollLeft + wrap.clientWidth)
            wrap.scrollLeft = x + cw - wrap.clientWidth;
        if (y - padT < wrap.scrollTop)
            wrap.scrollTop = Math.max(0, y - padT);
        else if (y + ch > wrap.scrollTop + wrap.clientHeight)
            wrap.scrollTop = y + ch - wrap.clientHeight;
    }
}
exports.View = View;
};

// ── module: src/app.ts ──
__mods["src/app.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * APP — controller: selection, editing, formula bar, keyboard,
 * toolbar, context menu, autosave
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.App = void 0;
const config_1 = require("./config");
const refs_1 = require("./refs");
const sheet_1 = require("./sheet");
const view_1 = require("./view");
function el(id) {
    const e = document.getElementById(id);
    if (!e)
        throw new Error('missing element #' + id);
    return e;
}
class App {
    constructor() {
        this.sheet = new sheet_1.Sheet();
        this.wrap = el('gridwrap');
        this.fInput = el('finput');
        this.cellRef = el('cellref');
        this.statusEl = el('status');
        this.ctx = el('ctxmenu');
        this.col = 0;
        this.row = 0;
        this.editor = null;
        this.editKey = '';
        this.saveTimer = 0;
        this.fbarDirty = false;
        this.view = new view_1.View(el('grid'), this.sheet);
        const restored = this.sheet.load();
        this.view.paintAll();
        this.select(0, 0);
        this.setStatus(restored ? 'Restored from local storage' : 'Ready');
        this.bindGrid();
        this.bindFormulaBar();
        this.bindKeys();
        this.bindToolbar();
        this.bindContextMenu();
    }
    /* ---------------- selection ---------------- */
    get key() { return (0, refs_1.keyOf)(this.col, this.row); }
    select(col, row, scroll = false) {
        this.commitEditor(false);
        this.col = (0, refs_1.clampCol)(col);
        this.row = (0, refs_1.clampRow)(row);
        this.view.setSelection(this.col, this.row);
        this.cellRef.textContent = this.key;
        this.fInput.value = this.sheet.raw(this.key);
        this.fbarDirty = false;
        if (scroll)
            this.view.scrollIntoView(this.col, this.row);
    }
    move(dc, dr) {
        this.select(this.col + dc, this.row + dr, true);
    }
    /* ---------------- committing values ---------------- */
    setCell(key, raw) {
        this.sheet.setRaw(key, raw);
        this.view.flush();
        // selection styling can be wiped by a repaint of the selected cell
        this.view.setSelection(this.col, this.row);
        if (key === this.key) {
            this.fInput.value = this.sheet.raw(key);
            this.fbarDirty = false;
        }
        this.queueSave();
    }
    /* ---------------- in-cell editing ---------------- */
    beginEdit(seed) {
        if (this.editor)
            return;
        const td = this.view.tdAt(this.col, this.row);
        if (!td)
            return;
        this.editKey = this.key;
        const input = document.createElement('input');
        input.className = 'celledit';
        input.type = 'text';
        input.spellcheck = false;
        input.value = seed === null ? this.sheet.raw(this.editKey) : seed;
        td.textContent = '';
        td.classList.add('editing');
        td.appendChild(input);
        this.editor = input;
        input.focus();
        if (seed === null)
            input.select();
        else {
            input.setSelectionRange(input.value.length, input.value.length);
        }
        input.addEventListener('input', () => { this.fInput.value = input.value; });
        // blur (e.g. clicking the formula bar or a toolbar button) commits, but must
        // not steal focus back to the grid
        input.addEventListener('blur', () => {
            if (this.editor === input)
                this.commitEditor(false);
        });
        input.addEventListener('keydown', (e) => this.onEditorKey(e, input));
    }
    onEditorKey(e, input) {
        if (e.key === 'Enter') {
            e.preventDefault();
            const raw = input.value;
            this.discardEditor();
            this.setCell(this.editKey, raw);
            this.move(0, e.shiftKey ? -1 : 1);
        }
        else if (e.key === 'Tab') {
            e.preventDefault();
            const raw = input.value;
            this.discardEditor();
            this.setCell(this.editKey, raw);
            this.move(e.shiftKey ? -1 : 1, 0);
        }
        else if (e.key === 'Escape') {
            e.preventDefault();
            this.discardEditor();
            const c = this.col, r = this.row;
            this.view.paintCell(c, r);
            this.view.setSelection(c, r);
            this.fInput.value = this.sheet.raw(this.key);
            this.focusGrid();
        }
        else {
            e.stopPropagation();
        }
    }
    /** Remove the editor DOM without writing anything. */
    discardEditor() {
        const input = this.editor;
        if (!input)
            return;
        const td = input.parentElement;
        this.editor = null; // clear first: removal fires blur re-entrantly
        if (input.parentNode)
            input.parentNode.removeChild(input);
        if (td) {
            td.classList.remove('editing');
            const c = Number(td.dataset.col), r = Number(td.dataset.row);
            this.view.paintCell(c, r);
        }
    }
    /** Commit whatever is in the in-cell editor (if any). */
    commitEditor(refocus = true) {
        if (!this.editor)
            return;
        const raw = this.editor.value;
        const key = this.editKey;
        this.discardEditor();
        this.sheet.setRaw(key, raw);
        this.view.flush();
        this.view.setSelection(this.col, this.row);
        this.queueSave();
        if (refocus)
            this.focusGrid();
    }
    focusGrid() {
        this.wrap.focus({ preventScroll: true });
    }
    /* ---------------- grid events ---------------- */
    cellFromEvent(e) {
        const t = e.target;
        if (!t)
            return null;
        const td = t.closest('td');
        if (!td || td.dataset.col === undefined)
            return null;
        return { col: Number(td.dataset.col), row: Number(td.dataset.row) };
    }
    bindGrid() {
        this.wrap.tabIndex = 0;
        this.wrap.addEventListener('mousedown', (e) => {
            this.hideCtx();
            this.commitFormulaBarIfPending();
            const hit = this.cellFromEvent(e);
            if (hit) {
                if (this.editor && hit.col === this.col && hit.row === this.row)
                    return;
                e.preventDefault();
                this.select(hit.col, hit.row);
                this.focusGrid();
                return;
            }
            const t = e.target;
            const th = t.closest('th');
            if (th && th.dataset.col !== undefined) {
                e.preventDefault();
                this.select(Number(th.dataset.col), 0, true);
                this.focusGrid();
            }
            else if (th && th.dataset.row !== undefined) {
                e.preventDefault();
                this.select(0, Number(th.dataset.row), true);
                this.focusGrid();
            }
        });
        this.wrap.addEventListener('dblclick', (e) => {
            const hit = this.cellFromEvent(e);
            if (!hit)
                return;
            this.select(hit.col, hit.row);
            this.beginEdit(null);
        });
    }
    /* ---------------- formula bar ---------------- */
    bindFormulaBar() {
        this.fInput.addEventListener('input', () => {
            this.fbarDirty = true;
            if (this.editor)
                this.editor.value = this.fInput.value;
        });
        this.fInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                this.discardEditor();
                this.setCell(this.key, this.fInput.value);
                this.move(0, 1);
                this.focusGrid();
            }
            else if (e.key === 'Tab') {
                e.preventDefault();
                this.discardEditor();
                this.setCell(this.key, this.fInput.value);
                this.move(1, 0);
                this.focusGrid();
            }
            else if (e.key === 'Escape') {
                e.preventDefault();
                this.fInput.value = this.sheet.raw(this.key);
                this.focusGrid();
            }
            e.stopPropagation();
        });
        this.fInput.addEventListener('blur', () => this.commitFormulaBarIfPending());
    }
    /** If the formula bar holds uncommitted text for the selected cell, write it. */
    commitFormulaBarIfPending() {
        if (!this.fbarDirty)
            return;
        this.fbarDirty = false;
        if (this.editor)
            return;
        if (this.fInput.value === this.sheet.raw(this.key))
            return;
        this.setCell(this.key, this.fInput.value);
    }
    /* ---------------- keyboard on the grid ---------------- */
    bindKeys() {
        document.addEventListener('keydown', (e) => {
            if (this.editor)
                return;
            const target = e.target;
            if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA'))
                return;
            switch (e.key) {
                case 'ArrowUp':
                    e.preventDefault();
                    this.move(0, -1);
                    return;
                case 'ArrowDown':
                    e.preventDefault();
                    this.move(0, 1);
                    return;
                case 'ArrowLeft':
                    e.preventDefault();
                    this.move(-1, 0);
                    return;
                case 'ArrowRight':
                    e.preventDefault();
                    this.move(1, 0);
                    return;
                case 'Tab':
                    e.preventDefault();
                    this.move(e.shiftKey ? -1 : 1, 0);
                    return;
                case 'Enter':
                    e.preventDefault();
                    this.beginEdit(null);
                    return;
                case 'F2':
                    e.preventDefault();
                    this.beginEdit(null);
                    return;
                case 'Escape':
                    this.hideCtx();
                    return;
                case 'Home':
                    e.preventDefault();
                    if (e.ctrlKey || e.metaKey)
                        this.select(0, 0, true);
                    else
                        this.select(0, this.row, true);
                    return;
                case 'End':
                    e.preventDefault();
                    this.select(config_1.CONFIG.COLS - 1, e.ctrlKey || e.metaKey ? config_1.CONFIG.ROWS - 1 : this.row, true);
                    return;
                case 'PageDown':
                    e.preventDefault();
                    this.move(0, 20);
                    return;
                case 'PageUp':
                    e.preventDefault();
                    this.move(0, -20);
                    return;
                case 'Delete':
                case 'Backspace':
                    e.preventDefault();
                    this.setCell(this.key, '');
                    return;
            }
            if (e.ctrlKey || e.metaKey || e.altKey)
                return;
            if (e.key.length === 1) {
                e.preventDefault();
                this.beginEdit(e.key);
            }
        });
    }
    /* ---------------- structural edits + toolbar ---------------- */
    structural(kind, at) {
        this.discardEditor();
        this.sheet.structuralEdit(kind, at);
        this.view.paintAll();
        this.view.setSelection(this.col, this.row);
        this.fInput.value = this.sheet.raw(this.key);
        this.queueSave();
        const label = {
            insertRow: 'Inserted row ' + (at + 1),
            deleteRow: 'Deleted row ' + (at + 1),
            insertCol: 'Inserted column ' + (0, refs_1.colName)(at),
            deleteCol: 'Deleted column ' + (0, refs_1.colName)(at),
        };
        this.setStatus(label[kind]);
    }
    bindToolbar() {
        el('btn-ins-row').addEventListener('click', () => this.structural('insertRow', this.row));
        el('btn-del-row').addEventListener('click', () => this.structural('deleteRow', this.row));
        el('btn-ins-col').addEventListener('click', () => this.structural('insertCol', this.col));
        el('btn-del-col').addEventListener('click', () => this.structural('deleteCol', this.col));
        el('btn-clear').addEventListener('click', () => { this.setCell(this.key, ''); this.focusGrid(); });
        el('btn-reset').addEventListener('click', () => {
            this.discardEditor();
            this.sheet.loadJSON({});
            this.view.paintAll();
            this.select(0, 0);
            this.queueSave();
            this.setStatus('Sheet cleared');
        });
    }
    /* ---------------- header context menu ---------------- */
    bindContextMenu() {
        this.wrap.addEventListener('contextmenu', (e) => {
            const t = e.target;
            const th = t.closest('th');
            const td = t.closest('td');
            let items = [];
            if (th && th.dataset.col !== undefined) {
                const c = Number(th.dataset.col);
                this.select(c, this.row);
                items = [
                    ['Insert 1 column left', () => this.structural('insertCol', c)],
                    ['Insert 1 column right', () => this.structural('insertCol', c + 1)],
                    ['Delete column ' + (0, refs_1.colName)(c), () => this.structural('deleteCol', c)],
                ];
            }
            else if (th && th.dataset.row !== undefined) {
                const r = Number(th.dataset.row);
                this.select(this.col, r);
                items = [
                    ['Insert 1 row above', () => this.structural('insertRow', r)],
                    ['Insert 1 row below', () => this.structural('insertRow', r + 1)],
                    ['Delete row ' + (r + 1), () => this.structural('deleteRow', r)],
                ];
            }
            else if (td) {
                const c = Number(td.dataset.col), r = Number(td.dataset.row);
                this.select(c, r);
                items = [
                    ['Clear ' + (0, refs_1.keyOf)(c, r), () => this.setCell((0, refs_1.keyOf)(c, r), '')],
                    ['Insert 1 row above', () => this.structural('insertRow', r)],
                    ['Insert 1 column left', () => this.structural('insertCol', c)],
                    ['Delete row ' + (r + 1), () => this.structural('deleteRow', r)],
                    ['Delete column ' + (0, refs_1.colName)(c), () => this.structural('deleteCol', c)],
                ];
            }
            else
                return;
            e.preventDefault();
            this.showCtx(e.clientX, e.clientY, items);
        });
        document.addEventListener('mousedown', (e) => {
            if (!this.ctx.hidden && !this.ctx.contains(e.target))
                this.hideCtx();
        });
    }
    showCtx(x, y, items) {
        this.ctx.textContent = '';
        for (const [label, fn] of items) {
            const b = document.createElement('button');
            b.textContent = label;
            b.addEventListener('click', () => { this.hideCtx(); fn(); });
            this.ctx.appendChild(b);
        }
        this.ctx.hidden = false;
        const w = this.ctx.offsetWidth, h = this.ctx.offsetHeight;
        this.ctx.style.left = Math.min(x, window.innerWidth - w - 6) + 'px';
        this.ctx.style.top = Math.min(y, window.innerHeight - h - 6) + 'px';
    }
    hideCtx() { this.ctx.hidden = true; }
    /* ---------------- persistence & status ---------------- */
    queueSave() {
        if (this.saveTimer)
            clearTimeout(this.saveTimer);
        this.saveTimer = window.setTimeout(() => {
            this.saveTimer = 0;
            this.sheet.save();
            this.setStatus('Saved to local storage');
        }, config_1.CONFIG.SAVE_DEBOUNCE_MS);
    }
    setStatus(msg) { this.statusEl.textContent = msg; }
}
exports.App = App;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"anthropic/claude-opus-5","provider":"openrouter","persona":"typescript","sessionId":"4dc5bbb5-038b-45a5-928a-deb8fbe9284d","tokensIn":2395144,"tokensOut":65904,"tokensTotal":2461048,"cost":3.2487059999999994,"turns":51,"toolCalls":58,"failedToolCalls":0,"timestamp":"2026-07-31T21:40:36.361Z"} -->