← Spreadsheet results

Spreadsheet

Qwen 3.8 · 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 79,224 bytes · SHA-256 81890936b2e2
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sheet — Vanilla Spreadsheet</title>
<style>
/* ── css: base ── */
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
  font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  font-size: 13px;
  color: #202124;
  background: #fff;
  overflow: hidden;
}
#app { display: flex; flex-direction: column; height: 100vh; }
/* ── css: chrome ── */
/* ---------- Toolbar ---------- */
.toolbar {
  display: flex; align-items: center; gap: 14px;
  padding: 6px 12px;
  background: #f8f9fa;
  border-bottom: 1px solid #e0e2e5;
  flex: none;
}
.brand {
  font-size: 15px; font-weight: 600; color: #444;
  letter-spacing: .3px; margin-right: 6px;
  display: flex; align-items: center; gap: 7px;
}
.brand::before {
  content: ""; width: 16px; height: 16px; display: inline-block;
  border: 2px solid #1a73e8; border-radius: 3px;
  background:
    linear-gradient(#1a73e8, #1a73e8) 50% 3px / 10px 2px no-repeat,
    #e8f0fe;
}
.tb-group { display: flex; gap: 4px; }
.tb-group + .tb-group { margin-left: 8px; padding-left: 8px; border-left: 1px solid #dadce0; }
button {
  font: inherit; font-size: 12.5px;
  padding: 4px 10px;
  border: 1px solid #dadce0; border-radius: 4px;
  background: #fff; color: #3c4043; cursor: pointer;
}
button:hover { background: #f1f3f4; border-color: #bdc1c6; }
button:active { background: #e8eaed; }
button:disabled { opacity: .45; cursor: default; }
.tb-spacer { flex: 1; }
.save-note { font-size: 11.5px; color: #9aa0a6; transition: opacity .4s; }

/* ---------- Formula bar ---------- */
.formulabar {
  display: flex; align-items: stretch; flex: none;
  border-bottom: 1px solid #e0e2e5;
  background: #fff;
  height: 34px;
}
.namebox {
  width: 64px; flex: none;
  display: flex; align-items: center; justify-content: center;
  font-weight: 600; font-size: 12.5px;
  border-right: 1px solid #e0e2e5;
  background: #f8f9fa; color: #1a73e8;
  user-select: none;
}
.fx { display: flex; flex: 1; align-items: center; padding: 0 10px; gap: 10px; }
.fx-label {
  font-family: Georgia, "Times New Roman", serif; font-style: italic;
  font-size: 13px; color: #5f6368; user-select: none;
}
#fx-input {
  flex: 1; height: 26px;
  font: inherit; font-size: 13px;
  border: none; outline: none; background: transparent;
  border-bottom: 1px solid transparent;
  color: #202124;
}
#fx-input:focus { border-bottom-color: #1a73e8; }
#fx-input::placeholder { color: #bdc1c6; }
/* ── css: grid ── */
/* ---------- Grid ---------- */
.gridwrap {
  position: relative; flex: 1; min-height: 0;
  overflow: auto; background: #fff;
}
table.sheet {
  border-collapse: separate; border-spacing: 0;
  table-layout: fixed;
  /* HEAD_WIDTH + COLS*COL_WIDTH = 46 + 26*88 — keep in sync with constants */
  width: 2334px;
  min-width: 100%;
  background: #fff;
}
table.sheet th, table.sheet td {
  border-right: 1px solid #e8eaed;
  border-bottom: 1px solid #e8eaed;
  padding: 0;
  height: 28px;
  font-size: 13px;
  line-height: 27px;
}
table.sheet thead th {
  position: sticky; top: 0; z-index: 3;
  background: #f8f9fa;
  color: #5f6368;
  font-weight: 500;
  text-align: center;
  user-select: none;
}
table.sheet thead th.rh {
  left: 0; z-index: 5;
  width: 46px; min-width: 46px; max-width: 46px;
}
table.sheet thead th.ch { width: 88px; }
table.sheet thead th.ch.hl { background: #e8f0fe; color: #1a73e8; }
table.sheet tbody th {
  position: sticky; left: 0; z-index: 2;
  background: #f8f9fa;
  color: #5f6368;
  font-weight: 500;
  text-align: center;
  width: 46px; min-width: 46px; max-width: 46px;
  user-select: none;
}
table.sheet tbody th.rh.hl { background: #e8f0fe; color: #1a73e8; }
table.sheet tbody th:hover { background: #eef1f4; }
table.sheet thead th.ch:hover { background: #eef1f4; }
table.sheet td {
  position: relative;
  width: 88px;
  padding: 0 6px;
  white-space: pre-wrap;
  word-break: break-word;
  overflow: hidden;
  cursor: default;
}
table.sheet td.align-right { text-align: right; }
table.sheet td.align-left { text-align: left; }
table.sheet td.align-err { text-align: center; color: #d93025; font-weight: 600; }

/* Selection: bold blue ring on the active cell */
table.sheet td.sel {
  outline: 2px solid #1a73e8;
  outline-offset: -2px;
  z-index: 1;
}

/* In-cell editor */
.editing {
  position: absolute; z-index: 10;
  display: none;
}
.editing.on { display: block; }
#cell-input {
  width: 100%; height: 100%;
  border: 2px solid #1a73e8;
  border-radius: 0;
  padding: 0 5px;
  font: inherit; font-size: 13px;
  line-height: 24px;
  outline: none;
  background: #fff; color: #202124;
  box-shadow: 0 1px 4px rgba(60,64,67,.3);
}
#cell-input.align-right { text-align: right; }

/* ---------- Right-click menu ---------- */
.menu {
  position: fixed; z-index: 100;
  min-width: 190px;
  background: #fff;
  border: 1px solid #dadce0;
  border-radius: 6px;
  box-shadow: 0 3px 10px rgba(0,0,0,.18);
  padding: 5px 0;
  display: none;
}
.menu.on { display: block; }
.menu button {
  display: block; width: 100%;
  border: none; border-radius: 0;
  text-align: left;
  padding: 7px 16px;
  font-size: 13px; color: #3c4043;
  background: transparent;
}
.menu button:hover { background: #f1f3f4; }
.menu .sep { height: 1px; background: #e8eaed; margin: 4px 0; }

/* ---------- Hint bar ---------- */
.hint {
  flex: none;
  padding: 4px 12px;
  font-size: 11.5px; color: #9aa0a6;
  background: #f8f9fa;
  border-top: 1px solid #e0e2e5;
  user-select: none;
}
</style>
</head>
<body>
<div id="app">
  <div class="toolbar">
    <div class="brand">Sheet</div>
    <div class="tb-group">
      <button id="btn-insert-row" title="Insert row below selection">+ Row</button>
      <button id="btn-delete-row" title="Delete selected row">− Row</button>
    </div>
    <div class="tb-group">
      <button id="btn-insert-col" title="Insert column right of selection">+ Col</button>
      <button id="btn-delete-col" title="Delete selected column">− Col</button>
    </div>
    <div class="tb-spacer"></div>
    <div class="save-note" id="save-note"></div>
  </div>
  <div class="formulabar">
    <div class="namebox" id="namebox">A1</div>
    <div class="fx"><span class="fx-label">fx</span><input id="fx-input" type="text" autocomplete="off" spellcheck="false" placeholder="Enter a value or a formula like =SUM(A1:A3)"></div>
  </div>
  <div class="gridwrap" id="gridwrap">
    <table class="sheet" id="sheet"></table>
    <div class="editing" id="editing"><input id="cell-input" type="text" autocomplete="off" spellcheck="false"></div>
  </div>
  <div class="menu" id="menu"></div>
  <div class="hint">Click a cell to select · type to edit · Enter ↓ / Tab → · arrows move · double-click to edit · right-click headers for row/column ops · Delete clears</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./controller":"src/controller.ts"},"src/tokenizer.ts":{"./constants":"src/constants.ts"},"src/parser.ts":{"./tokenizer":"src/tokenizer.ts"},"src/evaluator.ts":{"./constants":"src/constants.ts","./tokenizer":"src/tokenizer.ts","./parser":"src/parser.ts"},"src/sheet.ts":{"./constants":"src/constants.ts","./parser":"src/parser.ts","./evaluator":"src/evaluator.ts"},"src/controller.ts":{"./constants":"src/constants.ts","./sheet":"src/sheet.ts","./evaluator":"src/evaluator.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";
// =====================================================================
// ENTRY — boots the spreadsheet.
// =====================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const controller_1 = require("./controller");
function start() {
    (0, controller_1.init)();
    // Test/debug hook (harmless in production): exposes the live model so
    // automated checks can drive it without simulating full UI interaction.
    window.__sheet = controller_1.state;
}
start();
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.STORAGE_KEY = exports.C = exports.HEAD_WIDTH = exports.ROW_HEIGHT = exports.COL_WIDTH = exports.COL_LETTERS = exports.ROWS = exports.COLS = void 0;
// =====================================================================
// TUNABLE CONSTANTS — grid size, colors, cell dimensions
// =====================================================================
exports.COLS = 26; // A–Z
exports.ROWS = 100;
exports.COL_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
exports.COL_WIDTH = 88; // px (must match CSS `td { width }`)
exports.ROW_HEIGHT = 28; // px (must match CSS row height)
exports.HEAD_WIDTH = 46; // px row-header width (must match CSS)
exports.C = {
    select: "#1a73e8",
    grid: "#e8eaed",
    headerBg: "#f8f9fa",
    headerFg: "#5f6368",
    hl: "#e8f0fe",
    errFg: "#d93025",
    text: "#202124",
};
exports.STORAGE_KEY = "vanilla-sheet-v1";
};

// ── module: src/tokenizer.ts ──
__mods["src/tokenizer.ts"] = function (exports, require, module) {
"use strict";
// =====================================================================
// TOKENIZER — converts a formula string into a token stream.
// No eval(), no regex-on-whole-string hacks: a single-pass scanner.
// =====================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParseError = void 0;
exports.validCol = validCol;
exports.validRow = validRow;
exports.parseCell = parseCell;
exports.tokenize = tokenize;
const constants_1 = require("./constants");
class ParseError extends Error {
    constructor(msg) { super(msg); }
}
exports.ParseError = ParseError;
// Is `s` a syntactically valid column reference (1..COLS)?
function validCol(s) {
    if (!s || s.length > 2)
        return false;
    let c = 0;
    for (const ch of s) {
        if (ch < "A" || ch > "Z")
            return false;
        c = c * 26 + (ch.charCodeAt(0) - 64);
    }
    return c <= constants_1.COLS;
}
// Is `s` a syntactically valid row number (1..ROWS)?
function validRow(s) {
    if (!s || s[0] === "0")
        return false;
    if (!/^[1-9][0-9]*$/.test(s))
        return false;
    const n = parseInt(s, 10);
    return n >= 1 && n <= constants_1.ROWS;
}
// Parse one cell reference "B7" -> {c:1, r:6} (0-based), or null.
function parseCell(s) {
    let i = 0;
    let c = 0;
    while (i < s.length && s[i] >= "A" && s[i] <= "Z") {
        c = c * 26 + (s.charCodeAt(i) - 64);
        i++;
    }
    const rest = s.slice(i);
    if (!rest || !validRow(rest) || c < 1 || c > constants_1.COLS)
        return null;
    return { c: c - 1, r: parseInt(rest, 10) - 1 };
}
function tokenize(src) {
    const toks = [];
    let i = 0;
    const n = src.length;
    while (i < n) {
        const ch = src[i];
        // ---- whitespace: skip --------------------------------------------
        if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
            i++;
            continue;
        }
        // ---- number (with optional fractional part) ----------------------
        if (ch >= "0" && ch <= "9" || (ch === "." && i + 1 < n && src[i + 1] >= "0" && src[i + 1] <= "9")) {
            const start = i;
            while (i < n && ((src[i] >= "0" && src[i] <= "9") || src[i] === "."))
                i++;
            const raw = src.slice(start, i);
            const num = Number(raw);
            if (!isFinite(num))
                throw new ParseError("Number too large at position " + (start + 1));
            toks.push({ type: "num", value: raw, n: num, p: start + 1 });
            continue;
        }
        // ---- cell ref / range ---------------------------------------------
        if (ch >= "A" && ch <= "Z") {
            // gather maximal "letters digits" sequence: e.g. A1, AB2, A1A2 (invalid), A1:A10
            const start = i;
            while (i < n && src[i] >= "A" && src[i] <= "Z")
                i++;
            while (i < n && src[i] >= "0" && src[i] <= "9")
                i++;
            const chunk = src.slice(start, i);
            const isRef = parseCell(chunk) !== null;
            // reference-shaped but out of grid (e.g. "AA1", "A101") -> badref
            const refShaped = /^[A-Z]{1,2}[0-9]+$/.test(chunk);
            // range? "A1:A10" — the separator ":" must be directly adjacent
            if ((isRef || refShaped) && i < n && src[i] === ":") {
                i++;
                const rStart = i;
                while (i < n && src[i] >= "A" && src[i] <= "Z")
                    i++;
                while (i < n && src[i] >= "0" && src[i] <= "9")
                    i++;
                const rEnd = src.slice(rStart, i);
                const a = parseCell(chunk);
                const b = parseCell(rEnd);
                if (!a || !b) {
                    // reference-shaped but broken (out of grid) -> #REF! token
                    toks.push({ type: "badref", value: chunk + ":" + rEnd, p: start + 1 });
                    continue;
                }
                // normalize so a <= b
                const from = a.r <= b.r && a.c <= b.c ? a : b;
                const to = from === a ? b : a;
                toks.push({ type: "range", value: chunk + ":" + rEnd, r: { a: from, b: to }, p: start + 1 });
                continue;
            }
            if (isRef) {
                const cell = parseCell(chunk);
                toks.push({ type: "ref", value: chunk, a: cell, p: start + 1 });
                continue;
            }
            if (refShaped) {
                toks.push({ type: "badref", value: chunk, p: start + 1 });
                continue;
            }
            // letters but not a valid reference -> identifier (function name)
            toks.push({ type: "ident", value: chunk, p: start + 1 });
            continue;
        }
        // ---- string literal ("...", with "" for an escaped quote) -----------
        if (ch === '"') {
            const start = i;
            i++;
            let s = "";
            let closed = false;
            while (i < n) {
                if (src[i] === '"') {
                    if (src[i + 1] === '"') {
                        s += '"';
                        i += 2;
                    }
                    else {
                        i++;
                        closed = true;
                        break;
                    }
                }
                else {
                    s += src[i++];
                }
            }
            if (!closed)
                throw new ParseError("Unterminated string literal at position " + (start + 1));
            toks.push({ type: "str", value: s, p: start + 1 });
            continue;
        }
        // ---- comma (arg separator) -----------------------------------------
        if (ch === ",") {
            toks.push({ type: "op", value: ",", p: i + 1 });
            i++;
            continue;
        }
        // ---- colon handled inside ref logic; bare colon is an error --------
        if (ch === ":")
            throw new ParseError("Unexpected ':' at position " + (i + 1));
        // ---- error literal ("#REF!", "#DIV/0!", ...) -------------------------
        if (ch === "#") {
            const end = src.indexOf("!", i);
            if (end === -1 || end - i > 12)
                throw new ParseError("Bad error literal at position " + (i + 1));
            toks.push({ type: "err", value: src.slice(i, end + 1), p: i + 1 });
            i = end + 1;
            continue;
        }
        // ---- operators -------------------------------------------------------
        // two-character comparison operators first
        if ((ch === ">" || ch === "<" || ch === "!" || ch === "=") && i + 1 < n) {
            const two = src.slice(i, i + 2);
            if (two === ">=" || two === "<=" || two === "==" || two === "!=" || two === "<>") {
                toks.push({ type: "op", value: two, p: i + 1 });
                i += 2;
                continue;
            }
        }
        if (ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "(" || ch === ")" || ch === "&" ||
            ch === ">" || ch === "<" || ch === "=" || ch === "!") {
            toks.push({ type: "op", value: ch, p: i + 1 });
            i++;
            continue;
        }
        // ---- percentage (suffix, e.g. 10%) — accepted, stored as 0.1 --------
        if (ch === "%") {
            const last = toks[toks.length - 1];
            if (last && last.type === "num") {
                last.n = (last.n ?? 0) / 100;
                last.value += "%";
            }
            i++;
            continue;
        }
        throw new ParseError("Unexpected character '" + ch + "' at position " + (i + 1));
    }
    return toks;
}
};

// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
// =====================================================================
// PARSER — recursive descent over the token stream.
//
//   expr    := cond ( "?" expr ":" expr )?
//   cond    := add ( ("=="|"="|"!="|"<>"|"<"|">"|"<="|">=") add )*
//   add     := term ( ("+"|"-") term )*        // + also does string concat
//   term    := factor ( ("*"|"/") factor )*
//   factor  := ("+"|"-") factor | primary
//   primary := NUMBER | TRUE | FALSE | NULL | REF | RANGE
//             | IDENT "(" args ")" | "(" expr ")"
//   args    := expr ( "," expr )* | %empty
//
// Produces an AST (plain objects). Also extracts every cell/range
// reference into a flat list for the dependency graph. No eval().
// =====================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.RefError = void 0;
exports.parseFormula = parseFormula;
const tokenizer_1 = require("./tokenizer");
// Thrown at eval time for out-of-bounds / deleted references.
class RefError extends Error {
    constructor() { super("#REF!"); }
}
exports.RefError = RefError;
// ---------------- the parser ------------------------------------------
class Parser {
    constructor(src) {
        this.pos = 0;
        this.toks = (0, tokenizer_1.tokenize)(src);
    }
    static parse(src) {
        const p = new Parser(src);
        if (p.toks.length === 0)
            throw new tokenizer_1.ParseError("Empty formula");
        const node = p.expr();
        if (p.pos < p.toks.length) {
            const t = p.toks[p.pos];
            throw new tokenizer_1.ParseError("Unexpected '" + t.value + "' at position " + t.p);
        }
        return node;
    }
    static collectRefs(node, out) {
        const walk = (n) => {
            switch (n.k) {
                case "ref":
                    out.push({ c: n.c, r: n.r });
                    break;
                case "range":
                    for (let c = n.a.c; c <= n.b.c; c++)
                        for (let r = n.a.r; r <= n.b.r; r++)
                            out.push({ c, r });
                    break;
                case "un":
                    walk(n.x);
                    break;
                case "bin":
                    walk(n.x);
                    walk(n.y);
                    break;
                case "tern":
                    walk(n.x);
                    walk(n.y);
                    walk(n.z);
                    break;
                case "fn":
                    n.args.forEach(walk);
                    break;
                case "err": break;
                default: break;
            }
        };
        walk(node);
    }
    // ---------- token helpers -------------------------------------------
    peek() { return this.toks[this.pos]; }
    next() {
        const t = this.toks[this.pos];
        if (!t)
            throw new tokenizer_1.ParseError("Unexpected end of formula");
        this.pos++;
        return t;
    }
    isOp(v) {
        const t = this.peek();
        return !!t && t.type === "op" && t.value === v;
    }
    eatOp(v) {
        if (this.isOp(v)) {
            this.pos++;
            return true;
        }
        return false;
    }
    // ---------- grammar ----------------------------------------------------
    expr() {
        const cond = this.cond();
        if (this.eatOp("?")) {
            const a = this.expr();
            if (!this.eatOp(":"))
                throw new tokenizer_1.ParseError("Missing ':' in conditional");
            const b = this.expr();
            return { k: "tern", x: cond, y: a, z: b };
        }
        return cond;
    }
    cond() {
        let l = this.additive();
        for (;;) {
            let op = "";
            if (this.eatOp("==") || this.eatOp("="))
                op = "==";
            else if (this.eatOp("<>") || this.eatOp("!="))
                op = "!=";
            else if (this.eatOp("<="))
                op = "<=";
            else if (this.eatOp(">="))
                op = ">=";
            else if (this.eatOp("<"))
                op = "<";
            else if (this.eatOp(">"))
                op = ">";
            if (!op)
                break;
            const r = this.additive();
            l = { k: "bin", op, x: l, y: r };
        }
        return l;
    }
    additive() {
        let l = this.term();
        for (;;) {
            const t = this.peek();
            if (t && t.type === "op" && (t.value === "+" || t.value === "-" || t.value === "&")) {
                this.pos++;
                const r = this.term();
                l = { k: "bin", op: t.value, x: l, y: r };
            }
            else
                break;
        }
        return l;
    }
    term() {
        let l = this.factor();
        for (;;) {
            const t = this.peek();
            if (t && t.type === "op" && (t.value === "*" || t.value === "/")) {
                this.pos++;
                const r = this.factor();
                l = { k: "bin", op: t.value, x: l, y: r };
            }
            else
                break;
        }
        return l;
    }
    factor() {
        const t = this.peek();
        if (t && t.type === "op" && (t.value === "+" || t.value === "-")) {
            this.pos++;
            const x = this.factor();
            return t.value === "-" ? { k: "un", op: "neg", x } : x;
        }
        return this.primary();
    }
    primary() {
        const t = this.peek();
        if (!t)
            throw new tokenizer_1.ParseError("Unexpected end of formula");
        if (t.type === "num") {
            this.pos++;
            return { k: "num", v: t.n };
        }
        if (t.type === "str") {
            this.pos++;
            return { k: "str", v: t.value };
        }
        if (t.type === "ref") {
            this.pos++;
            return { k: "ref", c: t.a.c, r: t.a.r };
        }
        if (t.type === "range") {
            this.pos++;
            return { k: "range", a: t.r.a, b: t.r.b };
        }
        if (t.type === "ident") {
            this.pos++;
            const up = t.value.toUpperCase();
            if (up === "TRUE")
                return { k: "bool", v: true };
            if (up === "FALSE")
                return { k: "bool", v: false };
            if (up === "NULL" || up === "NIL")
                return { k: "null" };
            // function call?
            if (this.eatOp("(")) {
                const args = [];
                if (!this.isOp(")")) {
                    args.push(this.expr());
                    while (this.eatOp(","))
                        args.push(this.expr());
                }
                if (!this.eatOp(")"))
                    throw new tokenizer_1.ParseError("Missing ')' for " + up + "()");
                return { k: "fn", name: up, args };
            }
            // bare identifier -> treat as text
            return { k: "str", v: t.value };
        }
        if (t.type === "op" && t.value === "(") {
            this.pos++;
            const x = this.expr();
            if (!this.eatOp(")"))
                throw new tokenizer_1.ParseError("Missing ')'");
            return x;
        }
        if (t.type === "err") {
            this.pos++;
            return { k: "err", e: t.value };
        }
        if (t.type === "badref") {
            this.pos++;
            return { k: "err", e: "#REF!" };
        }
        throw new tokenizer_1.ParseError("Unexpected '" + t.value + "' at position " + t.p);
    }
}
// Convenience: parse a formula body and list all the references it uses.
function parseFormula(src) {
    const ast = Parser.parse(src);
    const refs = [];
    Parser.collectRefs(ast, refs);
    return { ast, refs };
}
};

// ── module: src/evaluator.ts ──
__mods["src/evaluator.ts"] = function (exports, require, module) {
"use strict";
// =====================================================================
// EVALUATOR — walks the AST and computes a typed value.
//
// Values: number | string | boolean | null.
// Errors are first-class: { e: "#DIV/0!" | "#REF!" | "#VALUE!" | "#NAME?" }
// so that errors propagate through arithmetic (like real spreadsheets)
// without ever throwing past a cell boundary.
// =====================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.E_NAME = exports.E_VALUE = exports.E_REF = exports.E_DIV0 = void 0;
exports.isErr = isErr;
exports.evalNode = evalNode;
exports.formatVal = formatVal;
exports.computeRaw = computeRaw;
const constants_1 = require("./constants");
const tokenizer_1 = require("./tokenizer");
const parser_1 = require("./parser");
exports.E_DIV0 = "#DIV/0!";
exports.E_REF = "#REF!";
exports.E_VALUE = "#VALUE!";
exports.E_NAME = "#NAME?";
function isErr(v) {
    return typeof v === "object" && v !== null && v.e !== undefined;
}
// -----------------------------------------------------------------------
// Argument flattening: ranges expand to their (computed) values; other
// nodes are evaluated directly.
// -----------------------------------------------------------------------
function flattenArgs(args, getVal) {
    const out = [];
    const take = (n) => {
        if (n.k === "range") {
            for (let c = n.a.c; c <= n.b.c; c++)
                for (let r = n.a.r; r <= n.b.r; r++)
                    out.push(getVal(c, r));
        }
        else {
            out.push(evalNode(n, getVal));
        }
    };
    args.forEach(take);
    return out;
}
// Numeric coercion: numbers pass, booleans become 1/0, null/"" is 0,
// numeric text is parsed; everything else is #VALUE!.
function toNum(v, forArith) {
    if (typeof v === "number")
        return v;
    if (typeof v === "boolean")
        return v ? 1 : 0;
    if (v === null)
        return 0;
    if (typeof v === "string") {
        if (v.trim() === "")
            return 0;
        const n = Number(v);
        if (isFinite(n))
            return n;
        return NaN;
    }
    // error: propagate in arithmetic; comparisons handled by caller
    if (forArith)
        throw new Error("ERR:" + v.e);
    return NaN;
}
function evalNode(node, getVal) {
    switch (node.k) {
        case "num": return node.v;
        case "str": return node.v;
        case "bool": return node.v;
        case "null": return null;
        case "err": return { e: node.e };
        case "ref": {
            if (node.c < 0 || node.c >= constants_1.COLS || node.r < 0 || node.r >= constants_1.ROWS)
                return { e: exports.E_REF };
            return getVal(node.c, node.r);
        }
        case "range": {
            // A bare range is not a scalar: in argument position it flattens;
            // if evaluated directly (e.g. =A1:A3) it's a type error.
            throw new Error("ERR:" + exports.E_VALUE);
        }
        case "un": {
            if (node.op === "neg") {
                const v = evalNode(node.x, getVal);
                if (isErr(v))
                    return v;
                const n = toNum(v, true);
                return -n;
            }
            break;
        }
        case "tern": {
            const c = evalNode(node.x, getVal);
            if (isErr(c))
                return c;
            return truthy(c) ? evalNode(node.y, getVal) : evalNode(node.z, getVal);
        }
        case "bin": {
            const x = evalNode(node.x, getVal);
            const y = evalNode(node.y, getVal);
            if (isErr(x) || isErr(y))
                return x; // propagate the error
            // string concatenation: & always concatenates; + concatenates only when
            // a string operand is not numeric text (numeric text coerces like Excel)
            const isNumStr = (v) => typeof v === "string" && v.trim() !== "" && isFinite(Number(v));
            if (node.op === "&")
                return disp(x) + disp(y);
            if (node.op === "+" &&
                ((typeof x === "string" && !isNumStr(x)) || (typeof y === "string" && !isNumStr(y)))) {
                return disp(x) + disp(y);
            }
            let nx, ny;
            try {
                nx = toNum(x, true);
                ny = toNum(y, true);
            }
            catch (err) {
                const m = err.message;
                return { e: m.slice(4) };
            }
            switch (node.op) {
                case "+": return nx + ny;
                case "-": return nx - ny;
                case "*": return nx * ny;
                case "/": {
                    if (ny === 0)
                        return { e: exports.E_DIV0 };
                    return nx / ny;
                }
                case "==": return cmp(x, y) === 0;
                case "!=": return cmp(x, y) !== 0;
                case "<": {
                    const c = cmp(x, y);
                    if (c === null)
                        return { e: exports.E_VALUE };
                    return c < 0;
                }
                case ">": {
                    const c = cmp(x, y);
                    if (c === null)
                        return { e: exports.E_VALUE };
                    return c > 0;
                }
                case "<=": {
                    const c = cmp(x, y);
                    if (c === null)
                        return { e: exports.E_VALUE };
                    return c <= 0;
                }
                case ">=": {
                    const c = cmp(x, y);
                    if (c === null)
                        return { e: exports.E_VALUE };
                    return c >= 0;
                }
                default: break;
            }
            break;
        }
        case "fn": {
            const vals = flattenArgs(node.args, getVal);
            return callFn(node.name, vals);
        }
        default: break;
    }
    return { e: exports.E_VALUE };
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
function disp(v) {
    if (v === null)
        return "";
    if (typeof v === "boolean")
        return v ? "TRUE" : "FALSE";
    return String(v);
}
function truthy(v) {
    if (isErr(v))
        return false;
    if (typeof v === "number")
        return v !== 0;
    if (typeof v === "boolean")
        return v;
    if (typeof v === "string")
        return v.length > 0;
    return false;
}
// Cross-type comparison: number vs number, string vs string (case-
// insensitive, natural), booleans as 1/0, null as 0.
function cmp(a, b) {
    if (typeof a === "number" && typeof b === "number")
        return a - b;
    if (typeof a === "boolean" && typeof b === "boolean")
        return (a ? 1 : 0) - (b ? 1 : 0);
    const as = disp(a);
    const bs = disp(b);
    // both parseable as numbers?
    const an = Number(as), bn = Number(bs);
    if (as.trim() !== "" && bs.trim() !== "" && isFinite(an) && isFinite(bn))
        return an - bn;
    return as.localeCompare(bs, undefined, { numeric: true, sensitivity: "base" });
}
// -----------------------------------------------------------------------
// Functions
// -----------------------------------------------------------------------
function numOr(v) {
    return typeof v === "boolean" ? (v ? 1 : 0) : v;
}
function callFn(name, args) {
    // numbers and booleans (booleans count as 1/0, like Excel arguments)
    const nums = args.filter((v) => typeof v === "number" || typeof v === "boolean");
    // error propagation: the first error argument wins (Excel behavior)
    for (const a of args)
        if (isErr(a))
            return a;
    switch (name) {
        case "SUM": {
            let t = 0;
            for (const v of nums)
                t += numOr(v);
            return t;
        }
        case "AVG":
        case "AVERAGE": {
            if (nums.length === 0)
                return { e: exports.E_DIV0 };
            let t = 0;
            for (const v of nums)
                t += numOr(v);
            return t / nums.length;
        }
        case "MIN": {
            if (nums.length === 0)
                return 0;
            let t = Infinity;
            for (const v of nums)
                t = Math.min(t, numOr(v));
            return t;
        }
        case "MAX": {
            if (nums.length === 0)
                return 0;
            let t = -Infinity;
            for (const v of nums)
                t = Math.max(t, numOr(v));
            return t;
        }
        case "COUNT":
            return nums.length;
        case "PRODUCT": {
            let t = 1;
            for (const v of nums)
                t *= numOr(v);
            return t;
        }
        case "ABS":
            return Math.abs(toNum(args[0], true));
        case "ROUND": {
            const d = args.length > 1 ? toNum(args[1], true) : 0;
            const f = Math.pow(10, d);
            return Math.round(toNum(args[0], true) * f) / f;
        }
        case "IF":
            return truthy(args[0]) ? args[1] : (args.length > 2 ? args[2] : false);
        case "CONCATENATE":
        case "CONCAT":
            return args.map(disp).join("");
        case "TEXT": {
            const d = args.length > 1 ? disp(args[1]).replace(/"/g, "") : "0";
            const n = toNum(args[0], true);
            const dec = (d.match(/0\.\d*/g) || [])[0];
            const digits = dec ? dec.length - 1 : 0;
            return n.toFixed(digits);
        }
        default:
            return { e: exports.E_NAME };
    }
}
// -----------------------------------------------------------------------
// Format a computed value for display.
// -----------------------------------------------------------------------
function formatVal(v) {
    if (v === null)
        return "";
    if (isErr(v))
        return v.e;
    if (typeof v === "boolean")
        return v ? "TRUE" : "FALSE";
    if (typeof v === "string")
        return v;
    if (!isFinite(v))
        return "#NUM!";
    // trim float noise: 0.30000000000000004 -> 0.3
    let s = String(Math.round(v * 1e10) / 1e10);
    if (s.length > 15)
        s = v.toPrecision(12).replace(/(\.\d*?)0+/, "$1").replace(/\.$/, "");
    return s;
}
// -----------------------------------------------------------------------
// Top-level entry: raw cell content -> computed value.
//   ""        -> null
//   "5", "hi" -> literal
//   "=..."    -> parse + evaluate (parse failures become #ERR!)
// -----------------------------------------------------------------------
function computeRaw(raw, getVal) {
    if (raw === null || raw === "")
        return null;
    if (raw[0] !== "=") {
        // plain text; if it's a plain number the cell stays text? No —
        // spreadsheet convention: "5" entered without '=' is a number.
        const t = raw.trim();
        if (t !== "" && isFinite(Number(t)))
            return Number(t);
        return raw;
    }
    try {
        const { ast } = (0, parser_1.parseFormula)(raw.slice(1));
        return evalNode(ast, getVal);
    }
    catch (err) {
        if (err instanceof tokenizer_1.ParseError)
            return { e: "#ERR!" };
        if (err instanceof parser_1.RefError)
            return { e: exports.E_REF };
        const m = err.message;
        if (m.startsWith("ERR:"))
            return { e: m.slice(4) };
        return { e: "#ERR!" };
    }
}
};

// ── module: src/sheet.ts ──
__mods["src/sheet.ts"] = function (exports, require, module) {
"use strict";
// =====================================================================
// SHEET MODEL — raw cell contents, the dependency graph, and the
// incremental (topological) recalculation engine.
//
// Model:
//   raw  : number[][]  — the user's exact input (formulas keep "=")
//   ast  : (Node|null)[][] — cached parse tree per formula cell
//   deps : Set<number>[] — for each cell, the flat index set of cells it
//                          reads (from A/B-refs and ranges)
//   dependents : Set<number>[] — reverse edges (who reads me)
//   value: Val[][] — last computed value (cached, drives the UI)
//   dirty: boolean[][] — cells whose value may be stale
//
// Recalculation:
//   setCell() marks the cell and BFS its transitive dependents dirty.
//   recalc() does an iterative DFS with explicit colors (white/gray/black)
//   over only the dirty subgraph, computing in topological order. A gray
//   node reached again is a cycle: every node in the back-edge's path
//   becomes #CYCLE!. No recursion anywhere, so arbitrarily deep chains
//   and cycles cannot overflow the stack or hang.
// =====================================================================
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Sheet = exports.rowOf = exports.colOf = exports.idx = void 0;
const constants_1 = require("./constants");
const parser_1 = require("./parser");
const evaluator_1 = require("./evaluator");
const idx = (c, r) => r * constants_1.COLS + c;
exports.idx = idx;
const colOf = (i) => i % constants_1.COLS;
exports.colOf = colOf;
const rowOf = (i) => Math.floor(i / constants_1.COLS);
exports.rowOf = rowOf;
const WHITE = 0, GRAY = 1, BLACK = 2;
class Sheet {
    constructor() {
        this.raw = [];
        this.ast = [];
        this.deps = [];
        this.dependents = [];
        this.value = [];
        this.dirty = [];
        this.hooks = {};
        for (let r = 0; r < constants_1.ROWS; r++) {
            this.raw.push(new Array(constants_1.COLS).fill(null));
            this.ast.push(new Array(constants_1.COLS).fill(null));
            this.value.push(new Array(constants_1.COLS).fill(null));
            this.dirty.push(new Array(constants_1.COLS).fill(false));
        }
        for (let i = 0; i < constants_1.COLS * constants_1.ROWS; i++) {
            this.deps.push(new Set());
            this.dependents.push(new Set());
        }
    }
    // ---------------- raw access ------------------------------------------
    getRaw(c, r) {
        if (c < 0 || c >= constants_1.COLS || r < 0 || r >= constants_1.ROWS)
            return null;
        return this.raw[r][c];
    }
    getVal(c, r) {
        if (c < 0 || c >= constants_1.COLS || r < 0 || r >= constants_1.ROWS)
            return { e: evaluator_1.E_REF };
        return this.value[r][c];
    }
    // Re-parse a formula's dependencies and rebuild its graph edges.
    // `c,r` must be the cell's current position (after any shift).
    rebuildDeps(c, r) {
        const i = (0, exports.idx)(c, r);
        // drop old outgoing + incoming edges
        for (const d of this.deps[i])
            this.dependents[d].delete(i);
        this.deps[i].clear();
        const raw = this.raw[r][c];
        this.ast[r][c] = null;
        if (!raw || raw[0] !== "=")
            return;
        try {
            const { ast, refs } = (0, parser_1.parseFormula)(raw.slice(1));
            this.ast[r][c] = ast;
            for (const { c: rc, r: rr } of refs) {
                if (rc < 0 || rc >= constants_1.COLS || rr < 0 || rr >= constants_1.ROWS) {
                    // reference outside the grid: the cell will show #REF!
                    continue;
                }
                const d = (0, exports.idx)(rc, rr);
                if (d === i)
                    continue; // self-ref -> handled as cycle by recalc
                this.deps[i].add(d);
                this.dependents[d].add(i);
            }
        }
        catch {
            // parse failure: no deps, value will be #ERR!
            this.ast[r][c] = null;
        }
    }
    // ---------------- writes ---------------------------------------------
    setCell(c, r, raw) {
        if (c < 0 || c >= constants_1.COLS || r < 0 || r >= constants_1.ROWS)
            return;
        this.raw[r][c] = raw;
        this.dirty[r][c] = true;
        this.rebuildDeps(c, r);
        // mark transitive dependents dirty (BFS over reverse edges)
        const stack = [(0, exports.idx)(c, r)];
        while (stack.length) {
            const i = stack.pop();
            for (const d of this.dependents[i]) {
                if (!this.dirty[(0, exports.rowOf)(d)][(0, exports.colOf)(d)]) {
                    this.dirty[(0, exports.rowOf)(d)][(0, exports.colOf)(d)] = true;
                    stack.push(d);
                }
            }
        }
        this.recalc();
    }
    // ---------------- recalculation ---------------------------------------
    // Iterative DFS with explicit colors over the dirty subgraph.
    // Computes values in dependency order; reports #CYCLE! on cycles.
    recalc() {
        const n = constants_1.COLS * constants_1.ROWS;
        // Dirty cells are unvisited (WHITE); clean cells are already done (BLACK).
        const color = new Array(n);
        for (let i = 0; i < n; i++)
            color[i] = this.dirty[(0, exports.rowOf)(i)][(0, exports.colOf)(i)] ? WHITE : BLACK;
        const changed = [];
        // worklist: dirty cells, processed as DFS roots
        const roots = [];
        for (let r = 0; r < constants_1.ROWS; r++)
            for (let c = 0; c < constants_1.COLS; c++)
                if (this.dirty[r][c])
                    roots.push((0, exports.idx)(c, r));
        for (const root of roots) {
            if (!this.dirty[(0, exports.rowOf)(root)][(0, exports.colOf)(root)])
                continue;
            const stack = [{ i: root, it: null }];
            color[root] = GRAY;
            while (stack.length) {
                const frame = stack[stack.length - 1];
                const i = frame.i;
                if (frame.it === null)
                    frame.it = this.deps[i].values();
                let depDone = false;
                let d = frame.it.next();
                while (!d.done) {
                    const di = d.value;
                    const dcol = color[di];
                    if (dcol === WHITE) {
                        // not yet visited in this pass; must be dirty (it's a dep of
                        // a dirty cell, so it was marked) — push it
                        this.dirty[(0, exports.rowOf)(di)][(0, exports.colOf)(di)] = true;
                        color[di] = GRAY;
                        stack.push({ i: di, it: null });
                        depDone = true; // resume this frame after child returns
                        break;
                    }
                    else if (dcol === GRAY) {
                        // back edge -> cycle: i ... di. Mark everything on the path
                        // from di (inclusive) to i (inclusive) as #CYCLE!.
                        this.markCycle(stack, di, i);
                        depDone = true;
                        break;
                    }
                    // BLACK: already computed, skip
                    d = frame.it.next();
                }
                if (depDone)
                    continue;
                // all deps processed (or none) -> compute this node
                stack.pop();
                color[i] = BLACK;
                const c = (0, exports.colOf)(i), r = (0, exports.rowOf)(i);
                const old = this.value[r][c];
                const val = this.computeCell(c, r);
                this.value[r][c] = val;
                this.dirty[r][c] = false;
                if (!sameVal(old, val))
                    changed.push(i);
            }
        }
        // surface changes to the UI (batched)
        for (const i of changed)
            this.hooks.onCellChanged?.((0, exports.colOf)(i), (0, exports.rowOf)(i));
    }
    // Every cell on the gray stack path from `from` to `to` (both ends
    // inclusive) is part of the cycle: force #CYCLE! on them.
    markCycle(stack, from, to) {
        let start = -1;
        for (let s = 0; s < stack.length; s++) {
            if (stack[s].i === from) {
                start = s;
                break;
            }
        }
        const members = new Set();
        if (start >= 0) {
            for (let s = start; s < stack.length; s++)
                members.add(stack[s].i);
        }
        else {
            members.add(from);
            members.add(to);
        }
        for (const m of members) {
            const c = (0, exports.colOf)(m), r = (0, exports.rowOf)(m);
            this.value[r][c] = { e: "#CYCLE!" };
            this.dirty[r][c] = false;
            // the value may be "unchanged" per sameVal, but the UI has never
            // rendered it — force a UI update for every cycle member
            this.hooks.onCellChanged?.(c, r);
        }
    }
    // Compute one cell's value from raw content + current dep values.
    computeCell(c, r) {
        const raw = this.raw[r][c];
        if (!raw)
            return null;
        if (raw[0] !== "=")
            return (0, evaluator_1.computeRaw)(raw, (cc, rr) => this.getVal(cc, rr));
        const ast = this.ast[r][c];
        if (!ast)
            return { e: "#ERR!" }; // unparseable formula
        // fast path: cached AST; recompute by walking it with live getVal
        return this.evalAst(c, r, ast);
    }
    // Walk the cached AST for cell (c,r) with live values; cycle guard:
    // a dependency reading back into an ancestor would be #CYCLE! already
    // (markCycle set it), and getVal returns it — but for the cell's own
    // self-reference we must also guard here.
    evalAst(c, r, ast) {
        const self = (0, exports.idx)(c, r);
        const getVal = (cc, rr) => {
            const i = (0, exports.idx)(cc, rr);
            if (i === self)
                return { e: "#CYCLE!" }; // direct self-reference
            if (cc < 0 || cc >= constants_1.COLS || rr < 0 || rr >= constants_1.ROWS)
                return { e: evaluator_1.E_REF };
            return this.value[rr][cc];
        };
        const { evalNode } = evalLazy();
        try {
            return evalNode(ast, getVal);
        }
        catch (err) {
            const m = err.message;
            if (m.startsWith("ERR:"))
                return { e: m.slice(4) };
            return { e: "#ERR!" };
        }
    }
    // ---------------- structural operations ---------------------------------
    // Insert/delete rows & columns. Formulas are rewritten so references
    // follow the logical cell they pointed at; references that fall outside
    // the grid (or point into the deleted band) become #REF! naturally.
    forEachFormulaCell(fn) {
        for (let r = 0; r < constants_1.ROWS; r++)
            for (let c = 0; c < constants_1.COLS; c++) {
                const raw = this.raw[r][c];
                if (raw && raw[0] === "=") {
                    const out = fn(c, r, raw);
                    if (out !== raw)
                        this.raw[r][c] = out;
                }
            }
    }
    // Rewrite one formula string under a row/col transform.
    // `rowAt(oldRow)` -> new row index, `colAt(oldCol)` -> new col index.
    // Out-of-range results are flagged so the caller can emit #REF!.
    static rewriteFormula(raw, rowAt, colAt) {
        let out = "";
        let i = 0;
        const n = raw.length;
        const isDigit = (s, k) => s[k] >= "0" && s[k] <= "9";
        const isUpper = (s, k) => s[k] >= "A" && s[k] <= "Z";
        while (i < n) {
            const ch = raw[i];
            // string literals pass through untouched
            if (ch === '"') {
                out += ch;
                i++;
                while (i < n) {
                    if (raw[i] === '"') {
                        out += raw[i];
                        if (raw[i + 1] === '"') {
                            out += raw[i + 1];
                            i += 2;
                        }
                        else {
                            i++;
                            break;
                        }
                    }
                    else {
                        out += raw[i++];
                    }
                }
                continue;
            }
            if (isUpper(raw, i)) {
                // collect letters+digits chunk
                const start = i;
                while (i < n && isUpper(raw, i))
                    i++;
                while (i < n && isDigit(raw, i))
                    i++;
                const chunk = raw.slice(start, i);
                // validate as cell ref
                let cl = "", rl = "";
                for (const cc of chunk)
                    (cc <= "9" ? (rl += cc) : (cl += cc));
                const cc0 = cl.length ? (cl.length === 1 ? cl.charCodeAt(0) - 64 : (cl.charCodeAt(0) - 64) * 26 + (cl.charCodeAt(1) - 64)) : 0;
                const rr0 = rl ? parseInt(rl, 10) : 0;
                const validRef = cc0 >= 1 && cc0 <= constants_1.COLS && rr0 >= 1 && rr0 <= constants_1.ROWS;
                if (validRef) {
                    // range?
                    if (i < n && raw[i] === ":") {
                        i++;
                        const s2 = i;
                        while (i < n && isUpper(raw, i))
                            i++;
                        while (i < n && isDigit(raw, i))
                            i++;
                        const chunk2 = raw.slice(s2, i);
                        let cl2 = "", rl2 = "";
                        for (const cc of chunk2)
                            (cc <= "9" ? (rl2 += cc) : (cl2 += cc));
                        const cc1 = cl2.length ? (cl2.length === 1 ? cl2.charCodeAt(0) - 64 : (cl2.charCodeAt(0) - 64) * 26 + (cl2.charCodeAt(1) - 64)) : 0;
                        const rr1 = rl2 ? parseInt(rl2, 10) : 0;
                        const validRef2 = cc1 >= 1 && cc1 <= constants_1.COLS && rr1 >= 1 && rr1 <= constants_1.ROWS;
                        const aC = colAt(cc0 - 1);
                        const aR = rowAt(rr0 - 1);
                        const bC = validRef2 ? colAt(cc1 - 1) : null;
                        const bR = validRef2 ? rowAt(rr1 - 1) : null;
                        if (aC === null || aR === null || bC === null || bR === null) {
                            out += "#REF!"; // the range is broken; whole range becomes #REF!
                        }
                        else {
                            const c1 = Math.min(aC, bC), c2 = Math.max(aC, bC);
                            const r1 = Math.min(aR, bR), r2 = Math.max(aR, bR);
                            out += Sheet.colLetter(c1 + 1) + (r1 + 1) + ":" + Sheet.colLetter(c2 + 1) + (r2 + 1);
                        }
                    }
                    else {
                        const aC = colAt(cc0 - 1);
                        const aR = rowAt(rr0 - 1);
                        if (aC === null || aR === null)
                            out += "#REF!";
                        else
                            out += Sheet.colLetter(aC + 1) + (aR + 1);
                    }
                    continue;
                }
                // not a valid ref: identifier (function name) — copy verbatim
                out += chunk;
                continue;
            }
            out += ch;
            i++;
        }
        return out;
    }
    static colLetter(c1) {
        let s = "";
        let c = c1;
        while (c > 0) {
            const m = (c - 1) % 26;
            s = String.fromCharCode(65 + m) + s;
            c = Math.floor((c - 1) / 26);
        }
        return s;
    }
    // ---- row ops -----------------------------------------------------------
    // Insert a new EMPTY row at index after+1; old rows after+1 and below
    // shift down by one. No-op at the last row (nothing below to displace).
    insertRow(after) {
        if (after < 0 || after >= constants_1.ROWS - 1)
            return;
        for (let r = constants_1.ROWS - 1; r > after + 1; r--) {
            for (let c = 0; c < constants_1.COLS; c++)
                this.raw[r][c] = this.raw[r - 1][c];
        }
        for (let c = 0; c < constants_1.COLS; c++)
            this.raw[after + 1][c] = null;
        // rewrite formulas: old row R -> R+1 if R > after
        this.forEachFormulaCell((_c, r, raw) => Sheet.rewriteFormula(raw, (oldR) => (oldR > after ? oldR + 1 : oldR), (oldC) => oldC));
        this.markAllDirty();
        this.hooks.onCellChanged?.(0, after + 1); // UI re-render signal
    }
    deleteRow(at) {
        if (at === 0) {
            for (let r = 0; r < constants_1.ROWS - 1; r++)
                for (let c = 0; c < constants_1.COLS; c++)
                    this.raw[r][c] = this.raw[r + 1][c];
            for (let c = 0; c < constants_1.COLS; c++)
                this.raw[constants_1.ROWS - 1][c] = null;
        }
        else {
            for (let r = at; r < constants_1.ROWS - 1; r++)
                for (let c = 0; c < constants_1.COLS; c++)
                    this.raw[r][c] = this.raw[r + 1][c];
            for (let c = 0; c < constants_1.COLS; c++)
                this.raw[constants_1.ROWS - 1][c] = null;
        }
        // old row R -> R-1 if R > at ; old R == at -> gone
        this.forEachFormulaCell((_c, r, raw) => Sheet.rewriteFormula(raw, (oldR) => (oldR === at ? null : oldR > at ? oldR - 1 : oldR), (oldC) => oldC));
        this.markAllDirty();
        this.hooks.onCellChanged?.(0, at);
    }
    // ---- column ops ----------------------------------------------------------
    // Insert a new EMPTY column at index after+1; old columns after+1 and to
    // the right shift right by one. No-op at the last column.
    insertCol(after) {
        if (after < 0 || after >= constants_1.COLS - 1)
            return;
        for (let c = constants_1.COLS - 1; c > after + 1; c--)
            for (let r = 0; r < constants_1.ROWS; r++)
                this.raw[r][c] = this.raw[r][c - 1];
        for (let r = 0; r < constants_1.ROWS; r++)
            this.raw[r][after + 1] = null;
        this.forEachFormulaCell((_c, _r, raw) => Sheet.rewriteFormula(raw, (oldR) => oldR, (oldC) => (oldC > after ? oldC + 1 : oldC)));
        this.markAllDirty();
        this.hooks.onCellChanged?.(after + 1, 0);
    }
    deleteCol(at) {
        if (at === 0) {
            for (let c = 0; c < constants_1.COLS - 1; c++)
                for (let r = 0; r < constants_1.ROWS; r++)
                    this.raw[r][c] = this.raw[r][c + 1];
            for (let r = 0; r < constants_1.ROWS; r++)
                this.raw[r][constants_1.COLS - 1] = null;
        }
        else {
            for (let c = at; c < constants_1.COLS - 1; c++)
                for (let r = 0; r < constants_1.ROWS; r++)
                    this.raw[r][c] = this.raw[r][c + 1];
            for (let r = 0; r < constants_1.ROWS; r++)
                this.raw[r][constants_1.COLS - 1] = null;
        }
        this.forEachFormulaCell((_c, _r, raw) => Sheet.rewriteFormula(raw, (oldR) => oldR, (oldC) => (oldC === at ? null : oldC > at ? oldC - 1 : oldC)));
        this.markAllDirty();
        this.hooks.onCellChanged?.(at, 0);
    }
    markAllDirty() {
        for (let r = 0; r < constants_1.ROWS; r++)
            for (let c = 0; c < constants_1.COLS; c++)
                this.dirty[r][c] = true;
    }
    // Full re-parse + recalc (used after structural ops and load).
    refreshAll() {
        for (let r = 0; r < constants_1.ROWS; r++)
            for (let c = 0; c < constants_1.COLS; c++) {
                this.dirty[r][c] = true;
                this.rebuildDeps(c, r);
            }
        this.recalc();
    }
    // ---------------- serialization -------------------------------------------
    // Serializes RAW content (formulas, not values) to a compact string:
    // "A1=5\nB1==A1*2\n..." — only non-empty cells.
    serialize() {
        const parts = [];
        for (let r = 0; r < constants_1.ROWS; r++)
            for (let c = 0; c < constants_1.COLS; c++) {
                const v = this.raw[r][c];
                if (v !== null && v !== "")
                    parts.push(Sheet.colLetter(c + 1) + (r + 1) + "\u0001" + v);
            }
        return parts.join("\u0002");
    }
    load(text) {
        if (!text)
            return;
        for (let r = 0; r < constants_1.ROWS; r++)
            for (let c = 0; c < constants_1.COLS; c++)
                this.raw[r][c] = null;
        const cells = text.split("\u0002");
        for (const cell of cells) {
            if (!cell)
                continue;
            const sep = cell.indexOf("\u0001");
            const addr = cell.slice(0, sep);
            const val = cell.slice(sep + 1);
            const m = addr.match(/^([A-Z]+)([1-9][0-9]*)$/);
            if (!m)
                continue;
            const c = colLetterToNum(m[1]) - 1;
            const r = parseInt(m[2], 10) - 1;
            if (c < 0 || c >= constants_1.COLS || r < 0 || r >= constants_1.ROWS)
                continue;
            this.raw[r][c] = val;
        }
    }
}
exports.Sheet = Sheet;
// ---------------- small helpers ------------------------------------------
function sameVal(a, b) {
    if (a === b)
        return true;
    if ((0, evaluator_1.isErr)(a) && (0, evaluator_1.isErr)(b))
        return a.e === b.e;
    if ((0, evaluator_1.isErr)(a) || (0, evaluator_1.isErr)(b))
        return false;
    return a === b;
}
function colLetterToNum(s) {
    let n = 0;
    for (const ch of s)
        n = n * 26 + (ch.charCodeAt(0) - 64);
    return n;
}
// Lazy accessor avoids a circular import at module init time.
const evalMod = __importStar(require("./evaluator"));
function evalLazy() {
    return evalMod;
}
};

// ── module: src/controller.ts ──
__mods["src/controller.ts"] = function (exports, require, module) {
"use strict";
// =====================================================================
// CONTROLLER — DOM construction, selection, editing, keyboard,
// right-click menus, structural row/col ops, and localStorage persistence.
// =====================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.state = void 0;
exports.init = init;
const constants_1 = require("./constants");
const sheet_1 = require("./sheet");
const evaluator_1 = require("./evaluator");
// ---------------------------------------------------------------------
// Elements
// ---------------------------------------------------------------------
const $ = (id) => document.getElementById(id);
const wrap = $("gridwrap");
const table = $("sheet");
const namebox = $("namebox");
const fxInput = $("fx-input");
const cellInput = $("cell-input");
const editingDiv = $("editing");
const menu = $("menu");
const saveNote = $("save-note");
let ui;
let saveTimer = null;
// ---------------------------------------------------------------------
// DOM construction
// ---------------------------------------------------------------------
function buildDOM() {
    const thead = document.createElement("thead");
    const hr = document.createElement("tr");
    const corner = document.createElement("th");
    corner.className = "rh";
    hr.appendChild(corner);
    const colHeads = [];
    for (let c = 0; c < constants_1.COLS; c++) {
        const th = document.createElement("th");
        th.className = "ch";
        th.dataset.c = String(c);
        th.textContent = constants_1.COL_LETTERS[c];
        th.title = "Column " + constants_1.COL_LETTERS[c];
        colHeads.push(th);
        hr.appendChild(th);
    }
    thead.appendChild(hr);
    table.appendChild(thead);
    const tbody = document.createElement("tbody");
    const cells = [];
    const rowHeads = [];
    for (let r = 0; r < constants_1.ROWS; r++) {
        const tr = document.createElement("tr");
        const th = document.createElement("th");
        th.className = "rh";
        th.dataset.r = String(r);
        th.textContent = String(r + 1);
        th.title = "Row " + (r + 1);
        rowHeads.push(th);
        tr.appendChild(th);
        const rowCells = [];
        for (let c = 0; c < constants_1.COLS; c++) {
            const td = document.createElement("td");
            td.dataset.c = String(c);
            td.dataset.r = String(r);
            rowCells.push(td);
            tr.appendChild(td);
        }
        cells.push(rowCells);
        tbody.appendChild(tr);
    }
    table.appendChild(tbody);
    ui = {
        sheet,
        sel: { c: 0, r: 0 },
        editing: null,
        cells,
        rowHeads,
        colHeads,
    };
}
const sheet = new sheet_1.Sheet();
// ---------------------------------------------------------------------
// Cell rendering
// ---------------------------------------------------------------------
function cellText(c, r) {
    const v = sheet.getVal(c, r);
    const td = ui.cells[r][c];
    if ((0, evaluator_1.isErr)(v)) {
        td.textContent = v.e;
        td.className = "align-err" + selCls(c, r);
    }
    else if (v === null) {
        td.textContent = "";
        td.className = selCls(c, r);
    }
    else if (typeof v === "number" || typeof v === "boolean") {
        td.textContent = (0, evaluator_1.formatVal)(v);
        td.className = "align-right" + selCls(c, r);
    }
    else {
        td.textContent = v;
        td.className = "align-left" + selCls(c, r);
    }
}
function selCls(c, r) {
    return ui.sel.c === c && ui.sel.r === r ? " sel" : "";
}
function fullRedraw() {
    for (let r = 0; r < constants_1.ROWS; r++)
        for (let c = 0; c < constants_1.COLS; c++)
            cellText(c, r);
    syncSelectionUI();
    syncFormulaBar();
}
function syncSelectionUI() {
    const { c, r } = ui.sel;
    namebox.textContent = constants_1.COL_LETTERS[c] + (r + 1);
    for (let i = 0; i < constants_1.COLS; i++)
        ui.colHeads[i].classList.toggle("hl", i === c);
    for (let i = 0; i < constants_1.ROWS; i++)
        ui.rowHeads[i].classList.toggle("hl", i === r);
    const td = ui.cells[r][c];
    td.scrollIntoView({ block: "nearest", inline: "nearest" });
}
function syncFormulaBar() {
    const raw = sheet.getRaw(ui.sel.c, ui.sel.r);
    if (ui.editing?.mode === "cell") {
        // while editing in-cell, show the raw text (live-ish) in the bar
        fxInput.value = raw ?? "";
        fxInput.disabled = true;
    }
    else {
        fxInput.value = raw ?? "";
        fxInput.disabled = false;
    }
}
// ---------------------------------------------------------------------
// Selection
// ---------------------------------------------------------------------
function setSelection(c, r) {
    if (c < 0 || c >= constants_1.COLS || r < 0 || r >= constants_1.ROWS)
        return;
    const oldC = ui.sel.c, oldR = ui.sel.r;
    const moved = oldC !== c || oldR !== r;
    ui.sel = { c, r };
    if (moved) {
        // rewrite old cell (drops the "sel" class) and the new one (gains it)
        cellText(oldC, oldR);
        cellText(c, r);
    }
    syncSelectionUI();
    syncFormulaBar();
}
function moveSelection(dc, dr) {
    setSelection(Math.max(0, Math.min(constants_1.COLS - 1, ui.sel.c + dc)), Math.max(0, Math.min(constants_1.ROWS - 1, ui.sel.r + dr)));
}
// ---------------------------------------------------------------------
// Editing
// ---------------------------------------------------------------------
function startEdit(mode, initial) {
    if (ui.editing)
        commitEdit("blur", null, false);
    const { c, r } = ui.sel;
    const init = initial !== undefined ? initial : (sheet.getRaw(c, r) ?? "");
    ui.editing = { c, r, mode };
    if (mode === "cell") {
        const td = ui.cells[r][c];
        const wr = wrap.getBoundingClientRect();
        const tr = td.getBoundingClientRect();
        editingDiv.style.left = tr.left - wr.left + wrap.scrollLeft + "px";
        editingDiv.style.top = tr.top - wr.top + wrap.scrollTop + "px";
        editingDiv.style.width = tr.width + "px";
        editingDiv.style.height = tr.height + "px";
        editingDiv.classList.add("on");
        cellInput.value = init;
        cellInput.className = typeof Number(init) === "number" && init.trim() !== "" ? "align-right" : "";
        cellInput.focus();
        cellInput.select();
        fxInput.disabled = true;
        fxInput.value = init;
    }
    else {
        fxInput.disabled = false;
        fxInput.value = init;
        fxInput.focus();
        fxInput.select();
        cellInput.disabled = true;
    }
    syncSelectionUI();
}
function commitEdit(source, moveDir, moveAfter) {
    if (!ui.editing)
        return;
    const { c, r, mode } = ui.editing;
    const input = mode === "cell" ? cellInput : fxInput;
    const text = input.value;
    ui.editing = null;
    cellInput.disabled = false;
    editingDiv.classList.remove("on");
    const wasFormulaCell = ui.sel.c === c && ui.sel.r === r;
    if (wasFormulaCell && text !== (sheet.getRaw(c, r) ?? "")) {
        sheet.setCell(c, r, text);
    }
    else if (text !== (sheet.getRaw(c, r) ?? "")) {
        // committed from fx editing a different cell — shouldn't happen, guard
        sheet.setCell(c, r, text);
    }
    syncFormulaBar();
    if (moveAfter && moveDir) {
        setSelection(c + moveDir.dc, r + moveDir.dr);
    }
    void source;
}
function cancelEdit() {
    if (!ui.editing)
        return;
    const { c, r, mode } = ui.editing;
    ui.editing = null;
    cellInput.disabled = false;
    editingDiv.classList.remove("on");
    syncFormulaBar();
    if (mode === "fx")
        fxInput.value = sheet.getRaw(c, r) ?? "";
}
// ---------------------------------------------------------------------
// Menus (right-click on headers)
// ---------------------------------------------------------------------
function closeMenu() {
    menu.classList.remove("on");
    menu.innerHTML = "";
}
function showMenu(x, y, items) {
    closeMenu();
    for (const item of items) {
        const b = document.createElement("button");
        b.textContent = item.label;
        if (item.disabled)
            b.disabled = true;
        else
            b.onclick = () => { closeMenu(); item.run(); };
        menu.appendChild(b);
    }
    menu.classList.add("on");
    menu.style.left = Math.min(x, window.innerWidth - 210) + "px";
    menu.style.top = Math.min(y, window.innerHeight - menu.offsetHeight - 8) + "px";
}
// ---------------------------------------------------------------------
// Structural operations
// ---------------------------------------------------------------------
function afterStructuralOp() {
    sheet.refreshAll();
    fullRedraw();
    saveSoon();
}
function insertRowAt(after) {
    const a = Math.max(0, Math.min(constants_1.ROWS - 2, after));
    if (ui.sel.r > a)
        ui.sel.r++;
    sheet.insertRow(a);
    afterStructuralOp();
}
function deleteRowAt(at) {
    if (at < 0 || at >= constants_1.ROWS)
        return;
    if (ui.sel.r > at)
        ui.sel.r--;
    sheet.deleteRow(at);
    afterStructuralOp();
}
function insertColAt(after) {
    const a = Math.max(0, Math.min(constants_1.COLS - 2, after));
    if (ui.sel.c > a)
        ui.sel.c++;
    sheet.insertCol(a);
    afterStructuralOp();
}
function deleteColAt(at) {
    if (at < 0 || at >= constants_1.COLS)
        return;
    if (ui.sel.c > at)
        ui.sel.c--;
    sheet.deleteCol(at);
    afterStructuralOp();
}
// ---------------------------------------------------------------------
// Persistence
// ---------------------------------------------------------------------
function saveNow() {
    try {
        localStorage.setItem(constants_1.STORAGE_KEY, JSON.stringify({ v: 1, data: sheet.serialize() }));
        saveNote.textContent = "Saved";
        saveNote.style.opacity = "1";
        window.setTimeout(() => { saveNote.style.opacity = "0"; }, 1200);
    }
    catch {
        saveNote.textContent = "Save failed";
    }
}
function saveSoon() {
    if (saveTimer !== null)
        window.clearTimeout(saveTimer);
    saveTimer = window.setTimeout(saveNow, 500);
}
function loadState() {
    try {
        const text = localStorage.getItem(constants_1.STORAGE_KEY);
        if (!text)
            return false;
        const obj = JSON.parse(text);
        if (obj.v !== 1 || typeof obj.data !== "string")
            return false;
        sheet.load(obj.data);
        return true;
    }
    catch {
        return false;
    }
}
function seedSample() {
    sheet.setCell(0, 0, "5");
    sheet.setCell(1, 0, "=A1*2");
    sheet.setCell(2, 0, "=SUM(A1:B1)");
    sheet.setCell(0, 1, "hello");
    sheet.setCell(1, 1, "=COUNT(A1:B1)&\" numbers above\"");
    saveNow();
}
// ---------------------------------------------------------------------
// Wiring
// ---------------------------------------------------------------------
function onGridClick(e) {
    const t = e.target;
    if (t.tagName === "TD") {
        const c = parseInt(t.dataset.c, 10);
        const r = parseInt(t.dataset.r, 10);
        if (ui.editing)
            commitEdit("click", null, false);
        setSelection(c, r);
        return;
    }
    const th = t;
    if (th.tagName === "TH") {
        if (th.dataset.c !== undefined && th.dataset.r === undefined)
            setSelection(parseInt(th.dataset.c, 10), ui.sel.r);
        else if (th.dataset.r !== undefined)
            setSelection(ui.sel.c, parseInt(th.dataset.r, 10));
    }
}
function onDblClick(e) {
    const t = e.target;
    if (t.tagName === "TD") {
        const c = parseInt(t.dataset.c, 10);
        const r = parseInt(t.dataset.r, 10);
        setSelection(c, r);
        startEdit("cell");
    }
}
function onContextMenu(e) {
    const t = e.target;
    if (t.tagName !== "TH")
        return;
    e.preventDefault();
    if (t.dataset.c !== undefined && t.dataset.r === undefined) {
        const c = parseInt(t.dataset.c, 10);
        showMenu(e.clientX, e.clientY, [
            { label: "Insert column to the left", disabled: c === 0, run: () => insertColAt(c - 1) },
            { label: "Insert column to the right", disabled: c === constants_1.COLS - 1, run: () => insertColAt(c) },
            { label: "Delete column " + constants_1.COL_LETTERS[c], run: () => deleteColAt(c) },
        ]);
    }
    else if (t.dataset.r !== undefined) {
        const r = parseInt(t.dataset.r, 10);
        showMenu(e.clientX, e.clientY, [
            { label: "Insert row above", disabled: r === 0, run: () => insertRowAt(r - 1) },
            { label: "Insert row below", disabled: r === constants_1.ROWS - 1, run: () => insertRowAt(r) },
            { label: "Delete row " + (r + 1), run: () => deleteRowAt(r) },
        ]);
    }
}
function onKeyDown(e) {
    const target = e.target;
    // ---- while editing in the cell input --------------------------------
    if (ui.editing?.mode === "cell" && target === cellInput) {
        if (e.key === "Enter") {
            e.preventDefault();
            commitEdit("enter", { dc: 0, dr: 1 }, true);
        }
        else if (e.key === "Tab") {
            e.preventDefault();
            commitEdit("tab", { dc: 1, dr: 0 }, true);
        }
        else if (e.key === "Escape") {
            e.preventDefault();
            cancelEdit();
        }
        return;
    }
    // ---- while editing in the formula bar --------------------------------
    if (ui.editing && ui.editing.mode === "fx") {
        if (e.key === "Enter") {
            e.preventDefault();
            commitEdit("enter", null, false);
        }
        else if (e.key === "Escape") {
            e.preventDefault();
            cancelEdit();
        }
        return;
    }
    // ---- not editing -------------------------------------------------------
    if (target === fxInput || target === cellInput)
        return;
    switch (e.key) {
        case "ArrowLeft":
            e.preventDefault();
            moveSelection(-1, 0);
            return;
        case "ArrowRight":
            e.preventDefault();
            moveSelection(1, 0);
            return;
        case "ArrowUp":
            e.preventDefault();
            moveSelection(0, -1);
            return;
        case "ArrowDown":
            e.preventDefault();
            moveSelection(0, 1);
            return;
        case "Enter":
            e.preventDefault();
            startEdit("cell");
            return;
        case "F2":
            e.preventDefault();
            startEdit("cell");
            return;
        case "Tab":
            e.preventDefault();
            moveSelection(e.shiftKey ? -1 : 1, 0);
            return;
        case "Delete":
        case "Backspace":
            e.preventDefault();
            sheet.setCell(ui.sel.c, ui.sel.r, null);
            saveSoon();
            return;
        default:
            if (e.ctrlKey || e.metaKey || e.altKey)
                return;
            if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
                // start a fresh edit with the typed character
                e.preventDefault();
                startEdit("cell", e.key);
                cellInput.setSelectionRange(cellInput.value.length, cellInput.value.length);
            }
    }
}
function wireEvents() {
    table.addEventListener("click", onGridClick);
    table.addEventListener("dblclick", onDblClick);
    table.addEventListener("contextmenu", onContextMenu);
    document.addEventListener("keydown", onKeyDown);
    document.addEventListener("click", (e) => {
        if (!menu.contains(e.target) && e.target.tagName !== "TH")
            closeMenu();
    });
    window.addEventListener("scroll", () => {
        if (ui.editing?.mode === "cell")
            cancelEdit();
    }, true);
    cellInput.addEventListener("blur", () => {
        // defer so a click on the grid (which commits first) wins
        window.setTimeout(() => {
            if (ui.editing?.mode === "cell")
                commitEdit("blur", null, false);
        }, 0);
    });
    fxInput.addEventListener("focus", () => {
        // focusing the bar puts us in fx-edit mode for the selected cell
        if (!ui.editing && document.activeElement === fxInput) {
            // don't trap: editing in bar without explicit edit only on Enter/typing
        }
    });
    fxInput.addEventListener("blur", () => {
        window.setTimeout(() => {
            if (ui.editing?.mode === "fx")
                commitEdit("blur", null, false);
        }, 0);
    });
    // typing directly in the formula bar starts an fx edit
    fxInput.addEventListener("input", () => {
        if (!ui.editing)
            ui.editing = { c: ui.sel.c, r: ui.sel.r, mode: "fx" };
    });
    // while editing a cell, mirror every keystroke into the formula bar
    cellInput.addEventListener("input", () => {
        if (ui.editing?.mode === "cell")
            fxInput.value = cellInput.value;
    });
    $("btn-insert-row").addEventListener("click", () => insertRowAt(ui.sel.r));
    $("btn-delete-row").addEventListener("click", () => deleteRowAt(ui.sel.r));
    $("btn-insert-col").addEventListener("click", () => insertColAt(ui.sel.c));
    $("btn-delete-col").addEventListener("click", () => deleteColAt(ui.sel.c));
    window.addEventListener("beforeunload", () => saveNow());
}
// ---------------------------------------------------------------------
// Start
// ---------------------------------------------------------------------
function init() {
    sheet.hooks.onCellChanged = (c, r) => {
        cellText(c, r);
    };
    buildDOM();
    wireEvents();
    const loaded = loadState();
    if (!loaded)
        seedSample();
    sheet.refreshAll();
    fullRedraw();
    setSelection(0, 0);
}
exports.state = {
    get sheet() { return sheet; },
    get sel() { return ui ? ui.sel : { c: 0, r: 0 }; },
    is: (c, r) => !!ui && ui.cells[r][c].textContent === (0, evaluator_1.formatVal)(sheet.getVal(c, r)) && (0, sheet_1.idx)(c, r) >= 0,
    // force the debounced localStorage write (also used by tests)
    flush: () => saveNow(),
};
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.8","provider":"brigid","persona":"typescript","sessionId":"3f9cf177-505c-459d-b715-0851632397a4","tokensIn":5961237,"tokensOut":213253,"tokensTotal":6174490,"cost":0,"turns":93,"toolCalls":122,"failedToolCalls":3,"timestamp":"2026-09-09T05:15:26.329Z"} -->