← Spreadsheet results

Spreadsheet

Qwen 3.8 27B GGUF · 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 60,829 bytes · SHA-256 42d63c38eee8
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GridSheet — Spreadsheet</title>
<style>
/* ── css: sheet ── */
/* ===== Base & layout ===== */
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 13px;
  color: #202124;
  background: #f8f9fa;
  overflow: hidden;
  user-select: none;
  -webkit-user-select: none;
}
#app {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

/* ===== Toolbar ===== */
#toolbar {
  display: flex;
  align-items: center;
  gap: 14px;
  height: 38px;
  padding: 0 12px;
  background: #ffffff;
  border-bottom: 1px solid #e0e0e0;
  flex: none;
}
#logo {
  font-weight: 700;
  font-size: 14px;
  color: #1a73e8;
  letter-spacing: .2px;
}
#tb-group { display: flex; gap: 4px; }
#tb-group button {
  font: inherit;
  font-size: 12px;
  padding: 4px 10px;
  border: 1px solid #dadce0;
  border-radius: 4px;
  background: #fff;
  color: #3c4043;
  cursor: pointer;
}
#tb-group button:hover:not(:disabled) { background: #f1f5fe; border-color: #1a73e8; color: #1a73e8; }
#tb-group button:disabled { opacity: .45; cursor: default; }
#save-ind {
  margin-left: auto;
  font-size: 12px;
  color: #9aa0a6;
  transition: opacity .5s;
}

/* ===== Formula bar ===== */
#fbar {
  display: flex;
  align-items: center;
  gap: 0;
  height: 30px;
  padding: 0 8px;
  background: #ffffff;
  border-bottom: 1px solid #e0e0e0;
  flex: none;
}
#namebox {
  width: 64px;
  text-align: center;
  font-weight: 600;
  font-size: 12px;
  color: #3c4043;
  border-right: 1px solid #e8eaed;
  padding-right: 8px;
}
#fx {
  font-style: italic;
  font-family: Georgia, "Times New Roman", serif;
  color: #80868b;
  font-size: 13px;
  padding: 0 10px 0 12px;
  border-right: 1px solid #e8eaed;
}
#barinput {
  flex: 1;
  margin-left: 8px;
  border: 1px solid transparent;
  border-radius: 3px;
  padding: 3px 8px;
  font: inherit;
  font-size: 13px;
  background: transparent;
  color: #202124;
  outline: none;
  user-select: text;
  -webkit-user-select: text;
}
#barinput:hover { background: #f8f9fa; }
#barinput:focus { background: #fff; border-color: #1a73e8; }

/* ===== Grid ===== */
#scroll {
  flex: 1;
  overflow: auto;
  background: #fff;
  position: relative;
}
table.grid {
  border-collapse: separate;
  border-spacing: 0;
  table-layout: fixed;
  margin: 0;
}
table.grid th, table.grid td {
  border-right: 1px solid #e6e8ea;
  border-bottom: 1px solid #e6e8ea;
  white-space: nowrap;
}
table.grid thead th, table.grid tbody th {
  background: #f8f9fa;
  color: #5f6368;
  font-weight: 400;
  font-size: 11px;
  text-align: center;
}
table.grid thead th { height: 24px; position: sticky; top: 0; z-index: 3; box-shadow: inset 0 -1px 0 #dadce0; }
table.grid tbody th {
  width: 44px;
  position: sticky;
  left: 0;
  z-index: 2;
  box-shadow: inset -1px 0 0 #dadce0;
}
table.grid thead th.corner {
  left: 0;
  z-index: 4;
  width: 44px;
}
table.grid td {
  font-size: 13px;
  padding: 0 6px;
  color: #202124;
  cursor: default;
}
table.grid td.num { text-align: right; font-variant-numeric: tabular-nums; }
table.grid td.neg { color: #d93025; }
table.grid td.error { color: #d93025; font-weight: 600; }

/* selection */
td.sel {
  outline: 2px solid #1a73e8;
  outline-offset: -1px;
}
td.editing {
  position: relative;
  overflow: visible;
  border-color: #1a73e8 !important;
}
td.editing::after {
  content: "";
  position: absolute;
  left: -1px; right: -1px; top: -1px; bottom: -1px;
  border: 2px solid #1a73e8;
  pointer-events: none;
  z-index: 1;
}
td.editing input {
  position: absolute;
  left: 0; top: 0;
  width: 100%; height: 100%;
  margin: 0;
  border: none;
  padding: 0 6px;
  font: inherit;
  font-size: 13px;
  outline: none;
  background: #fff;
  color: #202124;
  z-index: 2;
  user-select: text;
  -webkit-user-select: text;
}
td.editing input.num { text-align: right; }

/* ===== Context menu ===== */
#ctxmenu {
  position: fixed;
  z-index: 100;
  min-width: 190px;
  background: #fff;
  border: 1px solid #dadce0;
  border-radius: 6px;
  box-shadow: 0 4px 14px rgba(60, 64, 67, .25);
  padding: 6px 0;
  display: none;
}
#ctxmenu.open { display: block; }
#ctxmenu .mi {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  padding: 6px 16px;
  font-size: 13px;
  color: #3c4043;
  cursor: default;
}
#ctxmenu .mi:hover { background: #f1f5fe; color: #1a73e8; }
#ctxmenu .mi kbd { color: #9aa0a6; font-family: inherit; font-size: 11px; }
#ctxmenu .sep { height: 1px; background: #ececec; margin: 5px 0; }
</style>
</head>
<body>
<div id="app">
  <div id="toolbar">
    <span id="logo">▦ GridSheet</span>
    <div id="tb-group">
      <button type="button" data-act="ins-row" title="Insert a row above the selected row">+ Row</button>
      <button type="button" data-act="del-row" title="Delete the selected row">− Row</button>
      <button type="button" data-act="ins-col" title="Insert a column to the left of the selected column">+ Col</button>
      <button type="button" data-act="del-col" title="Delete the selected column">− Col</button>
    </div>
    <span id="save-ind"></span>
  </div>
  <div id="fbar">
    <div id="namebox">A1</div>
    <div id="fx">fx</div>
    <input id="barinput" type="text" spellcheck="false" autocomplete="off" autocapitalize="off">
  </div>
  <div id="scroll" aria-label="spreadsheet grid"></div>
</div>
<div id="ctxmenu" role="menu"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./store":"src/store.ts","./ui":"src/ui.ts"},"src/eval.ts":{"./formula":"src/formula.ts"},"src/store.ts":{"./config":"src/config.ts","./formula":"src/formula.ts","./eval":"src/eval.ts"},"src/ui.ts":{"./config":"src/config.ts","./formula":"src/formula.ts","./store":"src/store.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";
Object.defineProperty(exports, "__esModule", { value: true });
/* ============================================================
 * MAIN — wire the store and UI together.
 * ============================================================ */
const store_1 = require("./store");
const ui_1 = require("./ui");
function main() {
    const sheet = new store_1.Sheet();
    const ui = new ui_1.UI(sheet);
    sheet.onSave = () => ui.flashSave();
    // expose a dev handle for probing
    window.__sheet = sheet;
    window.__ui = ui;
    window.addEventListener('beforeunload', () => {
        try {
            const data = { v: 1, cols: sheet.cols, rows: sheet.rows, raw: sheet.raw };
            localStorage.setItem('gridsheet.v1', JSON.stringify(data));
        }
        catch { /* ignore */ }
    });
    // initial save indicator state
    const ind = document.getElementById('save-ind');
    ind.style.opacity = '0.55';
}
main();
};

// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.C = exports.CELL_H = exports.HEAD_W = exports.COL_WIDTH = exports.MAX_ROWS = exports.MAX_COLS = exports.ROWS = exports.COLS = void 0;
/* ============================================================
 * TUNABLE CONSTANTS — grid size, colors, cell dimensions
 * ============================================================ */
exports.COLS = 26; // A–Z
exports.ROWS = 100; // 1–100
exports.MAX_COLS = 30; // cap for insert operations
exports.MAX_ROWS = 105;
exports.COL_WIDTH = 88; // px, data columns
exports.HEAD_W = 44; // px, row header column
exports.CELL_H = 24; // px, row height
exports.C = {
    sel: '#1a73e8',
    line: '#e6e8ea',
    lineStrong: '#dadce0',
    headBg: '#f8f9fa',
    headFg: '#5f6368',
    error: '#d93025',
};
};

// ── module: src/formula.ts ──
__mods["src/formula.ts"] = function (exports, require, module) {
"use strict";
/* ============================================================
 * FORMULA ENGINE — tokenizer + recursive-descent parser,
 * then interpreter over an AST. No eval(), no Function().
 * ============================================================ */
Object.defineProperty(exports, "__esModule", { value: true });
exports.RANGE_FN = exports.err = exports.txt = exports.num = exports.ERR_VALUE = exports.ERR_CYCLE = exports.ERR_PARSE = exports.ERR_DIV = exports.ERR_REF = void 0;
exports.isErr = isErr;
exports.isNum = isNum;
exports.toCoercible = toCoercible;
exports.tokenize = tokenize;
exports.parseFormula = parseFormula;
exports.ERR_REF = '#REF!';
exports.ERR_DIV = '#DIV/0!';
exports.ERR_PARSE = '#ERR!';
exports.ERR_CYCLE = '#CYCLE!';
exports.ERR_VALUE = '#VALUE!';
const num = (v) => ({ t: 'num', v });
exports.num = num;
const txt = (s) => ({ t: 'text', s });
exports.txt = txt;
const err = (e) => ({ t: 'err', e });
exports.err = err;
function isErr(v) { return v.t === 'err'; }
function isNum(v) { return v.t === 'num'; }
/** Coerce for arithmetic: text that parses as a number becomes a number;
 *  blank text becomes 0; other text stays text (becomes #VALUE! in ops). */
function toCoercible(v) {
    if (v.t !== 'text')
        return v;
    if (v.s === '')
        return (0, exports.num)(0);
    const n = Number(v.s);
    return Number.isFinite(n) ? (0, exports.num)(n) : v;
}
exports.RANGE_FN = ['SUM', 'AVG', 'AVERAGE', 'MIN', 'MAX', 'COUNT'];
function colOfLetters(s) {
    let c = 0;
    for (let i = 0; i < s.length; i++)
        c = c * 26 + (s.charCodeAt(i) - 64);
    return c - 1;
}
function tokenize(src) {
    const toks = [];
    const n = src.length;
    let i = 0;
    while (i < n) {
        const ch = src[i];
        if (ch === ' ' || ch === '\t') {
            i++;
            continue;
        }
        if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < n && src[i + 1] >= '0' && src[i + 1] <= '9')) {
            let j = i + 1;
            while (j < n && (src[j] >= '0' && src[j] <= '9' || src[j] === '.'))
                j++;
            const raw = src.slice(i, j);
            if ((raw.match(/\./g) || []).length > 1)
                throw new Error('bad number');
            toks.push({ t: 'num', v: parseFloat(raw) });
            i = j;
            continue;
        }
        if (ch === '"' || ch === "'") {
            const q = ch;
            let j = i + 1, s = '', closed = false;
            while (j < n) {
                if (src[j] === q) {
                    if (src[j + 1] === q) {
                        s += q;
                        j += 2;
                        continue;
                    } // escaped quote
                    j++;
                    closed = true;
                    break;
                }
                s += src[j];
                j++;
            }
            if (!closed)
                throw new Error('unterminated string');
            toks.push({ t: 'str', s });
            i = j;
            continue;
        }
        if (ch === '#') {
            // error literal: #REF! #DIV/0! #N/A #VALUE! #NAME? …
            let j = i + 1;
            while (j < n && /[A-Za-z0-9/?!]/.test(src[j]))
                j++;
            if (j <= i + 1)
                throw new Error('bad error literal');
            toks.push({ t: 'errlit', e: src.slice(i, j) });
            i = j;
            continue;
        }
        if (ch === '$') {
            i++;
            continue;
        } // absolute-marker: tolerated, ignored
        if (ch === '+' || ch === '-' || ch === '*' || ch === '/' || ch === ':') {
            toks.push({ t: 'op', v: ch });
            i++;
            continue;
        }
        if (ch === '(' || ch === ')' || ch === ',') {
            toks.push({ t: 'p', v: ch });
            i++;
            continue;
        }
        if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) {
            let j = i + 1;
            while (j < n && /[A-Za-z0-9._]/.test(src[j]))
                j++;
            const w = src.slice(i, j);
            if (/^[A-Z]+[0-9]+$/i.test(w)) {
                const m = w.toUpperCase().match(/^([A-Z]+)([0-9]+)$/);
                if (m[1].length < 3) {
                    toks.push({ t: 'ref', ref: { c: colOfLetters(m[1]), r: parseInt(m[2], 10) - 1 } });
                }
                else {
                    toks.push({ t: 'name', s: w.toUpperCase() }); // e.g. AAB1 — not a function; parser will reject
                }
            }
            else if (/^[A-Z][A-Z0-9._]*$/i.test(w)) {
                toks.push({ t: 'name', s: w.toUpperCase() });
            }
            else {
                throw new Error('bad identifier');
            }
            i = j;
            continue;
        }
        throw new Error('unexpected character');
    }
    toks.push({ t: 'eof' });
    return toks;
}
/* ---------- Parser (recursive descent) ----------
 * expr    := term (('+'|'-') term)*
 * term    := factor (('*'|'/') factor)*
 * factor  := ('+'|'-') factor | primary
 * primary := NUM | STR | REF (':' REF)? | NAME '(' args? ')' | '(' expr ')'
 * ------------------------------------------------------------ */
class Parser {
    constructor(toks) {
        this.toks = toks;
        this.pos = 0;
    }
    peek() { return this.toks[this.pos]; }
    next() { return this.toks[this.pos++]; }
    parse() {
        const e = this.expr();
        if (this.peek().t !== 'eof')
            throw new Error('trailing tokens');
        return e;
    }
    expr() {
        let a = this.term();
        for (;;) {
            const t = this.peek();
            if (t.t === 'op' && (t.v === '+' || t.v === '-')) {
                this.next();
                a = { k: 'bin', op: t.v, a, b: this.term() };
            }
            else
                return a;
        }
    }
    term() {
        let a = this.factor();
        for (;;) {
            const t = this.peek();
            if (t.t === 'op' && (t.v === '*' || t.v === '/')) {
                this.next();
                a = { k: 'bin', op: t.v, a, b: this.factor() };
            }
            else
                return a;
        }
    }
    factor() {
        const t = this.peek();
        if (t.t === 'op' && (t.v === '+' || t.v === '-')) {
            this.next();
            const a = this.factor();
            return t.v === '-' ? { k: 'neg', a } : a;
        }
        return this.primary();
    }
    primary() {
        const t = this.next();
        if (t.t === 'num')
            return { k: 'lit', v: t.v };
        if (t.t === 'str')
            return { k: 'str', s: t.s };
        if (t.t === 'errlit')
            return { k: 'errlit', e: t.e };
        if (t.t === 'ref') {
            if (this.peek().t === 'op' && this.peek().v === ':') {
                this.next();
                const e = this.next();
                if (e.t !== 'ref')
                    throw new Error('range end must be a ref');
                const a = t.ref, b = e.ref;
                return { k: 'range', range: {
                        c1: Math.min(a.c, b.c), r1: Math.min(a.r, b.r),
                        c2: Math.max(a.c, b.c), r2: Math.max(a.r, b.r)
                    } };
            }
            return { k: 'ref', ref: t.ref };
        }
        if (t.t === 'name') {
            if (this.peek().t === 'p' && this.peek().v === '(') {
                this.next();
                const args = [];
                if (this.peek().t === 'p' && this.peek().v === ')') {
                    this.next();
                }
                else {
                    for (;;) {
                        args.push(this.expr());
                        const p = this.peek();
                        if (p.t === 'p' && p.v === ',') {
                            this.next();
                            continue;
                        }
                        if (p.t === 'p' && p.v === ')') {
                            this.next();
                            break;
                        }
                        throw new Error('expected , or )');
                    }
                }
                return { k: 'call', name: t.s, args };
            }
            if (t.s === 'TRUE')
                return { k: 'lit', v: 1 };
            if (t.s === 'FALSE')
                return { k: 'lit', v: 0 };
            throw new Error('unknown name ' + t.s);
        }
        if (t.t === 'p' && t.v === '(') {
            const e = this.expr();
            const c = this.next();
            if (c.t !== 'p' || c.v !== ')')
                throw new Error('missing )');
            return e;
        }
        throw new Error('unexpected token');
    }
}
/** Parse a formula body (no leading '='). Throws on syntax error. */
function parseFormula(src) {
    return new Parser(tokenize(src)).parse();
}
};

// ── module: src/eval.ts ──
__mods["src/eval.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.keyOf = void 0;
exports.evalNode = evalNode;
exports.collectDeps = collectDeps;
/* ============================================================
 * EVALUATOR — interprets an AST against a value getter.
 * Dependency extraction walks the same AST.
 * ============================================================ */
const formula_1 = require("./formula");
function evalNode(n, get) {
    switch (n.k) {
        case 'lit': return (0, formula_1.num)(n.v);
        case 'str': return (0, formula_1.txt)(n.s);
        case 'errlit': return (0, formula_1.err)(n.e);
        case 'ref': return get(n.ref);
        case 'range': return (0, formula_1.err)(formula_1.ERR_VALUE); // bare range outside a function
        case 'neg': {
            const v = evalNode(n.a, get);
            if (v.t === 'err')
                return v;
            const c = (0, formula_1.toCoercible)(v);
            return c.t === 'num' ? (0, formula_1.num)(-c.v) : (0, formula_1.err)(formula_1.ERR_VALUE);
        }
        case 'bin': {
            const a = evalNode(n.a, get), b = evalNode(n.b, get);
            if (a.t === 'err' || b.t === 'err')
                return a.t === 'err' ? a : b;
            return applyBin(n.op, a, b);
        }
        case 'call': return evalCall(n, get);
    }
}
const strOf = (v) => v.t === 'num' ? String(v.v) : v.t === 'text' ? v.s : v.e;
const numOf = (v) => v.t === 'num' ? v.v : v.t === 'text' ? Number(v.s) : NaN;
function applyBin(op, a, b) {
    // '+' with any text side → string concatenation (spreadsheet behavior)
    if (op === '+' && (a.t === 'text' || b.t === 'text')) {
        return (0, formula_1.txt)(strOf(a) + strOf(b));
    }
    const x = (0, formula_1.toCoercible)(a), y = (0, formula_1.toCoercible)(b);
    if (x.t === 'err' || y.t === 'err')
        return x.t === 'err' ? x : y;
    if (x.t !== 'num' || y.t !== 'num')
        return (0, formula_1.err)(formula_1.ERR_VALUE);
    let r;
    switch (op) {
        case '+':
            r = x.v + y.v;
            break;
        case '-':
            r = x.v - y.v;
            break;
        case '*':
            r = x.v * y.v;
            break;
        case '/':
            if (y.v === 0)
                return (0, formula_1.err)(formula_1.ERR_DIV);
            r = x.v / y.v;
            break;
    }
    return (0, formula_1.num)(r);
}
function evalCall(n, get) {
    const name = n.name;
    if (!formula_1.RANGE_FN.includes(name))
        return (0, formula_1.err)(formula_1.ERR_VALUE);
    const args = n.args.map(a => (a.k === 'range' ? evalRange(a.range, get) : evalNode(a, get)));
    const flat = [];
    for (const x of args)
        if (Array.isArray(x))
            flat.push(...x);
        else
            flat.push(x);
    // any error value propagates (spreadsheet behavior)
    for (const v of flat)
        if (v.t === 'err')
            return v;
    const isNumLike = (v) => (0, formula_1.isNum)(v) || (v.t === 'text' && v.s !== '' && Number.isFinite(Number(v.s)));
    switch (name) {
        case 'COUNT': {
            let c = 0;
            for (const v of flat)
                if (isNumLike(v))
                    c++;
            return (0, formula_1.num)(c);
        }
        case 'SUM':
        case 'AVG':
        case 'AVERAGE':
        case 'MIN':
        case 'MAX': {
            const vals = [];
            for (const v of flat)
                if (isNumLike(v))
                    vals.push(numOf(v));
            if (name === 'MIN' || name === 'MAX') {
                if (vals.length === 0)
                    return (0, formula_1.num)(0);
                return (0, formula_1.num)(name === 'MIN' ? Math.min(...vals) : Math.max(...vals));
            }
            if (vals.length === 0)
                return (0, formula_1.num)(0);
            if (name === 'AVG' || name === 'AVERAGE') {
                let s = 0;
                for (const v of vals)
                    s += v;
                return (0, formula_1.num)(s / vals.length);
            }
            let s = 0;
            for (const v of vals)
                s += v;
            return (0, formula_1.num)(s);
        }
    }
    return (0, formula_1.err)(formula_1.ERR_VALUE);
}
/** Materialize a range (flattened row-major). */
function evalRange(range, get) {
    const out = [];
    for (let r = range.r1; r <= range.r2; r++)
        for (let c = range.c1; c <= range.c2; c++)
            out.push(get({ c, r }));
    return out;
}
/* ---------- Dependency extraction ---------- */
function collectDeps(n, out) {
    switch (n.k) {
        case 'lit':
        case 'str':
        case 'errlit': return;
        case 'ref':
            out.add((0, exports.keyOf)(n.ref.c, n.ref.r));
            break;
        case 'range': {
            const R = n.range;
            for (let r = R.r1; r <= R.r2; r++)
                for (let c = R.c1; c <= R.c2; c++)
                    out.add((0, exports.keyOf)(c, r));
            break;
        }
        case 'neg':
            collectDeps(n.a, out);
            break;
        case 'bin':
            collectDeps(n.a, out);
            collectDeps(n.b, out);
            break;
        case 'call':
            n.args.forEach(a => collectDeps(a, out));
            break;
    }
}
const keyOf = (c, r) => r * 1000 + c;
exports.keyOf = keyOf;
};

// ── module: src/store.ts ──
__mods["src/store.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Sheet = exports.refName = void 0;
exports.colName = colName;
exports.parseRefName = parseRefName;
exports.sameVal = sameVal;
/* ============================================================
 * SHEET STORE — raw cell contents, dependency graph,
 * incremental topological recalculation, row/col ops with
 * formula rewriting, localStorage persistence.
 *
 * `raw` is the single source of truth (user input: "", number,
 * text, or "=..."). `vals` are computed results.
 * `deps`  : key(c,r) -> Set of keys this cell DEPENDS ON.
 * `rdeps` : key -> Set of keys that depend on it.
 *
 * Recalc = BFS of dependents from changed cells, then a Kahn
 * topo-sort over that subgraph, then recompute in order.
 * Cells that can never be ordered (in a cycle, or depending on
 * one) are set to #CYCLE!. Cycles therefore cannot hang or
 * overflow — everything is iterative.
 * ============================================================ */
const config_1 = require("./config");
const formula_1 = require("./formula");
const eval_1 = require("./eval");
const LS_KEY = 'gridsheet.v1';
const isA = (ch) => (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
const isD = (ch) => ch >= '0' && ch <= '9';
function colName(c) {
    let s = '';
    c += 1;
    while (c > 0) {
        const m = (c - 1) % 26;
        s = String.fromCharCode(65 + m) + s;
        c = Math.floor((c - 1) / 26);
    }
    return s;
}
const refName = (c, r) => colName(c) + (r + 1);
exports.refName = refName;
function parseRefName(name) {
    const m = name.trim().match(/^([A-Za-z]{1,3})([0-9]{1,5})$/);
    if (!m)
        return null;
    let c = 0;
    for (let i = 0; i < m[1].length; i++)
        c = c * 26 + (m[1].toUpperCase().charCodeAt(i) - 64);
    return { c: c - 1, r: parseInt(m[2], 10) - 1 };
}
const emptyVal = () => (0, formula_1.num)(0);
const idx = (c, r, cols) => r * cols + c;
class Sheet {
    constructor() {
        this.cols = config_1.COLS;
        this.rows = config_1.ROWS;
        this.raw = new Array(config_1.COLS * config_1.ROWS).fill('');
        this.vals = new Array(config_1.COLS * config_1.ROWS).fill(emptyVal());
        this.deps = new Map();
        this.rdeps = new Map();
        this.listeners = [];
        this.persistT = null;
        /* ---------- evaluation ---------- */
        this.getter = (ref) => {
            if (ref.c < 0 || ref.r < 0 || ref.c >= this.cols || ref.r >= this.rows)
                return (0, formula_1.err)(formula_1.ERR_REF);
            return this.vals[idx(ref.c, ref.r, this.cols)];
        };
        this.load();
        this.recalcAll();
    }
    /* ---------- accessors ---------- */
    getRaw(c, r) {
        if (c < 0 || r < 0 || c >= this.cols || r >= this.rows)
            return '';
        return this.raw[idx(c, r, this.cols)];
    }
    valOf(c, r) {
        if (c < 0 || r < 0 || c >= this.cols || r >= this.rows)
            return (0, formula_1.err)(formula_1.ERR_REF);
        return this.vals[idx(c, r, this.cols)];
    }
    inBounds(c, r) {
        return c >= 0 && r >= 0 && c < this.cols && r < this.rows;
    }
    /* ---------- dependency graph maintenance ---------- */
    reparseCell(c, r) {
        const raw = this.raw[idx(c, r, this.cols)];
        if (!raw.startsWith('='))
            return null;
        try {
            return (0, formula_1.parseFormula)(raw.slice(1));
        }
        catch {
            return null;
        }
    }
    replaceDeps(c, r, ast) {
        const k = (0, eval_1.keyOf)(c, r);
        const old = this.deps.get(k);
        if (old) {
            for (const d of old) {
                const s = this.rdeps.get(d);
                if (s) {
                    s.delete(k);
                    if (s.size === 0)
                        this.rdeps.delete(d);
                }
            }
            this.deps.delete(k);
        }
        if (ast) {
            const ds = new Set();
            (0, eval_1.collectDeps)(ast, ds);
            this.deps.set(k, ds); // self-refs stay: they make the cell a self-cycle
            for (const d of ds) {
                const dc = d % 1000, dr = Math.floor(d / 1000);
                if (!this.inBounds(dc, dr))
                    continue; // out-of-grid ref: getter yields #REF!
                let s = this.rdeps.get(d);
                if (!s) {
                    s = new Set();
                    this.rdeps.set(d, s);
                }
                s.add(k);
            }
        }
    }
    computeCell(c, r) {
        const raw = this.raw[idx(c, r, this.cols)];
        if (!raw.startsWith('=')) {
            if (raw === '')
                return (0, formula_1.num)(0);
            const t = raw.trim();
            if (t !== '' && /^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/.test(t))
                return (0, formula_1.num)(Number(t));
            return (0, formula_1.txt)(raw);
        }
        let ast;
        try {
            ast = (0, formula_1.parseFormula)(raw.slice(1));
        }
        catch {
            return (0, formula_1.err)(formula_1.ERR_PARSE);
        }
        try {
            return (0, eval_1.evalNode)(ast, this.getter);
        }
        catch {
            return (0, formula_1.err)(formula_1.ERR_PARSE);
        }
    }
    /** Full rebuild of graph + values (used on load and after structure ops).
     *  Kahn topo-order over the whole graph so references in any direction
     *  read fresh values; leftover cells (in cycles) become #CYCLE!. */
    recalcAll() {
        this.deps = new Map();
        this.rdeps = new Map();
        const keys = [];
        for (let r = 0; r < this.rows; r++)
            for (let c = 0; c < this.cols; c++) {
                this.replaceDeps(c, r, this.reparseCell(c, r));
                keys.push((0, eval_1.keyOf)(c, r));
            }
        const indeg = new Map();
        for (const k of keys) {
            let deg = 0;
            const ds = this.deps.get(k);
            if (ds)
                for (const d of ds) {
                    const dc = d % 1000, dr = Math.floor(d / 1000);
                    if (this.inBounds(dc, dr))
                        deg++;
                }
            indeg.set(k, deg);
        }
        const q = [];
        for (const k of keys)
            if (indeg.get(k) === 0)
                q.push(k);
        const order = [];
        while (q.length) {
            const k = q.pop();
            order.push(k);
            const rs = this.rdeps.get(k);
            if (rs)
                for (const d of rs) {
                    const nd = (indeg.get(d) || 1) - 1;
                    indeg.set(d, nd);
                    if (nd === 0)
                        q.push(d);
                }
        }
        const done = new Set(order);
        for (const k of order) {
            const r = Math.floor(k / 1000), c = k % 1000;
            this.vals[idx(c, r, this.cols)] = this.computeCell(c, r);
        }
        for (const k of keys) {
            if (done.has(k))
                continue;
            const r = Math.floor(k / 1000), c = k % 1000;
            this.vals[idx(c, r, this.cols)] = (0, formula_1.err)(formula_1.ERR_CYCLE);
        }
    }
    /** Incremental: recompute only the dependents of `keys`, topo order. */
    recalcDirty(keys) {
        const changed = [];
        // 1) collect all downstream dependents
        const affected = new Set(keys);
        const queue = [...keys];
        while (queue.length) {
            const k = queue.pop();
            const rs = this.rdeps.get(k);
            if (rs)
                for (const d of rs)
                    if (!affected.has(d)) {
                        affected.add(d);
                        queue.push(d);
                    }
        }
        // 2) Kahn topo sort over the induced subgraph
        const indeg = new Map();
        for (const k of affected) {
            let deg = 0;
            const ds = this.deps.get(k);
            if (ds)
                for (const d of ds)
                    if (affected.has(d))
                        deg++;
            indeg.set(k, deg);
        }
        const order = [];
        const q2 = [];
        for (const k of affected)
            if (indeg.get(k) === 0)
                q2.push(k);
        while (q2.length) {
            const k = q2.pop();
            order.push(k);
            const rs = this.rdeps.get(k);
            if (rs)
                for (const d of rs) {
                    if (!affected.has(d))
                        continue;
                    const nd = (indeg.get(d) || 1) - 1;
                    indeg.set(d, nd);
                    if (nd === 0)
                        q2.push(d);
                }
        }
        // 3) recompute in topo order
        const done = new Set(order);
        for (const k of order) {
            const r = Math.floor(k / 1000), c = k % 1000;
            const i = idx(c, r, this.cols);
            const v = this.computeCell(c, r);
            if (!sameVal(this.vals[i], v)) {
                this.vals[i] = v;
                changed.push({ c, r });
            }
        }
        // 4) leftovers are in a cycle / depend on one → #CYCLE!
        for (const k of affected) {
            if (done.has(k))
                continue;
            const r = Math.floor(k / 1000), c = k % 1000;
            const i = idx(c, r, this.cols);
            if (!sameVal(this.vals[i], (0, formula_1.err)(formula_1.ERR_CYCLE))) {
                this.vals[i] = (0, formula_1.err)(formula_1.ERR_CYCLE);
                changed.push({ c, r });
            }
        }
        return { changed, structural: null };
    }
    /* ---------- user edits ---------- */
    setRaw(c, r, raw, opts) {
        if (!this.inBounds(c, r))
            return { changed: [], structural: null };
        const k = (0, eval_1.keyOf)(c, r);
        const i = idx(c, r, this.cols);
        if (raw === this.raw[i])
            return { changed: [], structural: null };
        this.raw[i] = raw;
        this.replaceDeps(c, r, this.reparseCell(c, r));
        const v = this.computeCell(c, r);
        let changed = [];
        if (!sameVal(this.vals[i], v)) {
            this.vals[i] = v;
            changed.push({ c, r });
        }
        // Always propagate: a formula may now create a cycle even when its
        // own value is unchanged, and dependents must re-read it.
        const sub = this.recalcDirty([k]);
        for (const x of sub.changed)
            changed.push(x);
        if (!opts?.silent)
            this.persist();
        this.emit({ changed, structural: null });
        return { changed, structural: null };
    }
    /* ---------- formula rewriting for structural ops ---------- */
    /** Transform one ref for an insert/delete of a row or col.
     *  Deleted target → (-1,-1) which renders as #REF!. */
    static shiftRef(ref, op) {
        let c = ref.c, r = ref.r;
        if (op.kind === 'col') {
            if (op.dir === 1) {
                if (c >= op.index)
                    c++;
            }
            else if (c > op.index)
                c--;
            else if (c === op.index)
                return { c: -1, r: -1 };
        }
        else {
            if (op.dir === 1) {
                if (r >= op.index)
                    r++;
            }
            else if (r > op.index)
                r--;
            else if (r === op.index)
                return { c: -1, r: -1 };
        }
        return { c, r };
    }
    /** Read a cell ref (letters+digits) starting at pos, if any. */
    static readRef(src, pos) {
        const n = src.length;
        let j = pos;
        while (j < n && isA(src[j]))
            j++;
        if (j === pos || j - pos > 3)
            return null;
        let k = j;
        while (k < n && isD(src[k]))
            k++;
        if (k === j)
            return null;
        let c = 0;
        for (let x = pos; x < j; x++)
            c = c * 26 + (src[x].toUpperCase().charCodeAt(0) - 64);
        return { ref: { c: c - 1, r: parseInt(src.slice(j, k), 10) - 1 }, end: k };
    }
    /** Rewrite a raw formula string, skipping quoted string regions.
     *  Ranges (REF:REF) are rewritten as a unit: if either endpoint is
     *  deleted, the whole range becomes #REF!. */
    static rewriteFormula(src, op) {
        if (!src.startsWith('='))
            return src;
        let out = '=';
        let i = 1;
        const n = src.length;
        while (i < n) {
            const ch = src[i];
            // quoted strings are copied verbatim
            if (ch === '"' || ch === "'") {
                const q = ch;
                out += ch;
                i++;
                while (i < n) {
                    out += src[i];
                    i++;
                    if (src[i - 1] === q) {
                        if (i < n && src[i] === q) {
                            out += q;
                            i++;
                            continue;
                        } // escaped quote
                        break;
                    }
                }
                continue;
            }
            if (isA(ch)) {
                const a = Sheet.readRef(src, i);
                if (a) {
                    // range? ref ':' ref
                    let colon = a.end;
                    if (colon < n && src[colon] === '$')
                        colon++;
                    const b = (colon < n && src[colon] === ':' ? Sheet.readRef(src, colon + 1) : null);
                    if (b) {
                        const na = Sheet.shiftRef(a.ref, op);
                        const nb = Sheet.shiftRef(b.ref, op);
                        if (na.c < 0 || na.r < 0 || nb.c < 0 || nb.r < 0)
                            out += '#REF!';
                        else
                            out += (0, exports.refName)(na.c, na.r) + ':' + (0, exports.refName)(nb.c, nb.r);
                        i = b.end;
                        continue;
                    }
                    const nr = Sheet.shiftRef(a.ref, op);
                    out += (nr.c < 0 || nr.r < 0) ? '#REF!' : (0, exports.refName)(nr.c, nr.r);
                    i = a.end;
                    continue;
                }
                out += ch;
                i++;
            }
            else {
                out += ch;
                i++;
            }
        }
        return out;
    }
    shiftRows(index, dir) {
        const oldC = this.cols, oldR = this.rows;
        const newR = oldR + dir;
        const next = new Array(oldC * newR).fill('');
        for (let r = 0; r < oldR; r++) {
            if (dir === -1 && r === index)
                continue;
            const nr = dir === 1 && r >= index ? r + 1 : r;
            const shift = dir === -1 && r > index ? -1 : 0;
            const dst = nr + shift;
            for (let c = 0; c < oldC; c++)
                next[dst * oldC + c] = this.raw[r * oldC + c];
        }
        this.raw = next;
        this.rows = newR;
    }
    shiftCols(index, dir) {
        const oldC = this.cols, oldR = this.rows;
        const newC = oldC + dir;
        const next = new Array(newC * oldR).fill('');
        for (let r = 0; r < oldR; r++) {
            for (let c = 0; c < oldC; c++) {
                if (dir === -1 && c === index)
                    continue;
                let nc = c;
                if (dir === 1 && c >= index)
                    nc = c + 1;
                else if (dir === -1 && c > index)
                    nc = c - 1;
                next[r * newC + nc] = this.raw[r * oldC + c];
            }
        }
        this.raw = next;
        this.cols = newC;
    }
    structural(op) {
        // 1) move content
        if (op.kind === 'row')
            this.shiftRows(op.index, op.op === 'ins' ? 1 : -1);
        else
            this.shiftCols(op.index, op.op === 'ins' ? 1 : -1);
        // 2) rewrite formulas in the surviving grid
        for (let r = 0; r < this.rows; r++)
            for (let c = 0; c < this.cols; c++) {
                const i = r * this.cols + c;
                if (this.raw[i].startsWith('=')) {
                    const dir = op.op === 'ins' ? 1 : -1;
                    this.raw[i] = Sheet.rewriteFormula(this.raw[i], { kind: op.kind, dir, index: op.index });
                }
            }
        // 3) fresh values array + full rebuild
        this.vals = new Array(this.cols * this.rows).fill(emptyVal());
        this.recalcAll();
        this.persist();
        this.emit({ changed: [], structural: op });
    }
    insertRow(index) {
        if (index < 0 || index >= this.rows || this.rows >= config_1.MAX_ROWS)
            return { changed: [], structural: null };
        this.structural({ kind: 'row', op: 'ins', index });
        return { changed: [], structural: { kind: 'row', op: 'ins', index } };
    }
    deleteRow(index) {
        if (index < 0 || index >= this.rows || this.rows <= 1)
            return { changed: [], structural: null };
        this.structural({ kind: 'row', op: 'del', index });
        return { changed: [], structural: { kind: 'row', op: 'del', index } };
    }
    insertCol(index) {
        if (index < 0 || index >= this.cols || this.cols >= config_1.MAX_COLS)
            return { changed: [], structural: null };
        this.structural({ kind: 'col', op: 'ins', index });
        return { changed: [], structural: { kind: 'col', op: 'ins', index } };
    }
    deleteCol(index) {
        if (index < 0 || index >= this.cols || this.cols <= 1)
            return { changed: [], structural: null };
        this.structural({ kind: 'col', op: 'del', index });
        return { changed: [], structural: { kind: 'col', op: 'del', index } };
    }
    /* ---------- persistence (raw formulas, not values) ---------- */
    persist() {
        if (this.persistT !== null)
            window.clearTimeout(this.persistT);
        this.persistT = window.setTimeout(() => {
            try {
                const data = { v: 1, cols: this.cols, rows: this.rows, raw: this.raw };
                localStorage.setItem(LS_KEY, JSON.stringify(data));
                this.onSave?.();
            }
            catch { /* storage unavailable: ignore */ }
        }, 250);
    }
    load() {
        try {
            const s = localStorage.getItem(LS_KEY);
            if (!s)
                return;
            const data = JSON.parse(s);
            if (!data || data.v !== 1 || !Array.isArray(data.raw))
                return;
            this.cols = Math.min(Math.max(1, data.cols | 0), config_1.MAX_COLS);
            this.rows = Math.min(Math.max(1, data.rows | 0), config_1.MAX_ROWS);
            this.raw = new Array(this.cols * this.rows).fill('');
            for (let i = 0; i < this.cols * this.rows; i++) {
                const x = data.raw[i];
                this.raw[i] = typeof x === 'string' ? x : '';
            }
        }
        catch { /* corrupt save: start fresh */ }
    }
    /* ---------- events ---------- */
    on(fn) { this.listeners.push(fn); }
    emit(ch) { for (const fn of this.listeners)
        fn(ch); }
}
exports.Sheet = Sheet;
function sameVal(a, b) {
    if (a.t !== b.t)
        return false;
    if (a.t === 'err')
        return b.t === 'err' && a.e === b.e;
    if (a.t === 'num')
        return b.t === 'num' && a.v === b.v;
    return b.t === 'text' && a.s === b.s;
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ERR_CYCLE = exports.ERR_REF = exports.UI = void 0;
exports.fmtNum = fmtNum;
exports.isNumLike = isNumLike;
/* ============================================================
 * UI — grid rendering (sticky headers), cell selection/editing,
 * formula bar sync, keyboard handling, context menus, toolbar.
 * ============================================================ */
const config_1 = require("./config");
const formula_1 = require("./formula");
Object.defineProperty(exports, "ERR_REF", { enumerable: true, get: function () { return formula_1.ERR_REF; } });
Object.defineProperty(exports, "ERR_CYCLE", { enumerable: true, get: function () { return formula_1.ERR_CYCLE; } });
const store_1 = require("./store");
class UI {
    constructor(sheet) {
        this.sheet = sheet;
        this.sel = { c: 0, r: 0 };
        this.editing = false;
        this.editStartSel = { c: 0, r: 0 };
        this.editVal = '';
        this.table = null;
        this.scroll = document.getElementById('scroll');
        this.namebox = document.getElementById('namebox');
        this.bar = document.getElementById('barinput');
        this.menu = document.getElementById('ctxmenu');
        this.saveInd = document.getElementById('save-ind');
        this.buildTable();
        this.bindGlobal();
        sheet.on(ch => this.onChanged(ch));
        this.refreshSelUI();
    }
    /* ---------- table construction ---------- */
    buildTable() {
        if (this.table)
            this.table.remove();
        const t = document.createElement('table');
        t.className = 'grid';
        const thead = document.createElement('thead');
        const hr = document.createElement('tr');
        const corner = document.createElement('th');
        corner.className = 'corner';
        corner.style.width = config_1.HEAD_W + 'px';
        corner.style.left = '0';
        hr.appendChild(corner);
        for (let c = 0; c < this.sheet.cols; c++) {
            const th = document.createElement('th');
            th.dataset.c = String(c);
            th.style.width = config_1.COL_WIDTH + 'px';
            th.style.left = (config_1.HEAD_W + c * config_1.COL_WIDTH) + 'px';
            th.textContent = (0, store_1.refName)(c, 0).replace(/[0-9]/g, '');
            hr.appendChild(th);
        }
        thead.appendChild(hr);
        t.appendChild(thead);
        const tbody = document.createElement('tbody');
        for (let r = 0; r < this.sheet.rows; r++) {
            const tr = document.createElement('tr');
            tr.style.height = config_1.CELL_H + 'px';
            const th = document.createElement('th');
            th.dataset.r = String(r);
            th.textContent = String(r + 1);
            tr.appendChild(th);
            for (let c = 0; c < this.sheet.cols; c++) {
                const td = document.createElement('td');
                td.dataset.c = String(c);
                td.dataset.r = String(r);
                this.paintCell(td, this.sheet.valOf(c, r));
                tr.appendChild(td);
            }
            tbody.appendChild(tr);
        }
        t.appendChild(tbody);
        this.table = t;
        this.scroll.appendChild(t);
        this.bindGrid(t);
    }
    td(c, r) {
        const tr = this.table.tBodies[0].rows[r];
        return tr.cells[c + 1];
    }
    headC(c) {
        return this.table.tHead.rows[0].cells[c + 1];
    }
    headR(r) {
        return this.table.tBodies[0].rows[r].cells[0];
    }
    /* ---------- painting ---------- */
    paintCell(td, v) {
        td.classList.remove('num', 'neg', 'error');
        if (v.t === 'num') {
            // empty cells (raw "") display blank even though they compute to 0
            const raw = this.sheet.getRaw(+td.dataset.c, +td.dataset.r);
            if (raw === '' && v.v === 0) {
                td.textContent = '';
                return;
            }
            td.textContent = fmtNum(v.v);
            td.classList.add('num');
            if (v.v < 0)
                td.classList.add('neg');
        }
        else if (v.t === 'text') {
            td.textContent = v.s;
        }
        else {
            td.textContent = v.e;
            td.classList.add('error');
        }
    }
    renderCell(c, r) {
        if (!this.sheet.inBounds(c, r))
            return;
        this.paintCell(this.td(c, r), this.sheet.valOf(c, r));
    }
    renderAll() {
        for (let r = 0; r < this.sheet.rows; r++)
            for (let c = 0; c < this.sheet.cols; c++)
                this.paintCell(this.td(c, r), this.sheet.valOf(c, r));
    }
    /* ---------- selection ---------- */
    selectCell(c, r) {
        if (this.editing)
            this.commitEdit(true);
        const old = this.sel;
        this.sel = { c, r };
        if (old.c >= 0) {
            const ot = this.td(old.c, old.r);
            ot.classList.remove('sel');
        }
        const td = this.td(c, r);
        td.classList.add('sel');
        this.namebox.textContent = (0, store_1.refName)(c, r);
        this.bar.value = this.sheet.getRaw(c, r);
        this.scrollIntoView(c, r);
    }
    refreshSelUI() {
        const td = this.td(this.sel.c, this.sel.r);
        td.classList.add('sel');
        this.namebox.textContent = (0, store_1.refName)(this.sel.c, this.sel.r);
        this.bar.value = this.sheet.getRaw(this.sel.c, this.sel.r);
        this.scrollIntoView(this.sel.c, this.sel.r);
    }
    scrollIntoView(c, r) {
        const el = this.td(c, r);
        const s = this.scroll;
        const rTop = r * config_1.CELL_H, rBot = (r + 1) * config_1.CELL_H;
        if (rTop < s.scrollTop)
            s.scrollTop = rTop;
        else if (rBot > s.scrollTop + s.clientHeight - 24)
            s.scrollTop = rBot - s.clientHeight + 24;
        const lLeft = c * config_1.COL_WIDTH, lRight = (c + 1) * config_1.COL_WIDTH;
        if (lLeft < s.scrollLeft)
            s.scrollLeft = lLeft;
        else if (lRight > s.scrollLeft + s.clientWidth - config_1.COL_WIDTH)
            s.scrollLeft = lRight - s.clientWidth + config_1.COL_WIDTH;
    }
    /* ---------- editing ---------- */
    startEdit(initial) {
        const { c, r } = this.sel;
        if (!this.sheet.inBounds(c, r))
            return;
        this.editStartSel = { ...this.sel };
        const td = this.td(c, r);
        if (this.editing)
            return;
        this.editing = true;
        const v = this.sheet.valOf(c, r);
        const init = initial !== undefined ? initial : (v.t === 'text' ? v.s : v.t === 'num' ? String(v.v) : '');
        const input = document.createElement('input');
        input.value = init;
        input.className = 'num';
        td.classList.add('editing');
        td.appendChild(input);
        input.focus();
        if (v.t === 'text')
            input.setSelectionRange(0, init.length);
        else
            input.select();
        input.addEventListener('input', () => { this.bar.value = input.value; });
        input.addEventListener('keydown', ev => this.onEditKey(ev, input));
        input.addEventListener('blur', () => { if (this.editing)
            this.commitEdit(true); });
    }
    onEditKey(ev, input) {
        if (ev.key === 'Enter') {
            ev.preventDefault();
            this.commitEdit(true);
            this.moveSel(1, 0);
        }
        else if (ev.key === 'Tab') {
            ev.preventDefault();
            this.commitEdit(true);
            this.moveSel(0, ev.shiftKey ? -1 : 1);
        }
        else if (ev.key === 'Escape') {
            ev.preventDefault();
            this.cancelEdit();
        }
        else {
            ev.stopPropagation(); // don't let arrows/delete hit the sheet handler
        }
    }
    commitEdit(keepFocus) {
        if (!this.editing)
            return;
        const { c, r } = this.editStartSel;
        const td = this.td(c, r);
        const input = td.querySelector('input');
        const val = input ? input.value : '';
        this.editing = false;
        td.classList.remove('editing');
        if (input)
            input.remove();
        const cur = this.sheet.getRaw(c, r);
        if (val !== cur)
            this.sheet.setRaw(c, r, val);
        this.namebox.textContent = (0, store_1.refName)(c, r);
        this.bar.value = this.sheet.getRaw(c, r);
        if (!keepFocus)
            this.bar.focus();
    }
    cancelEdit() {
        if (!this.editing)
            return;
        const { c, r } = this.editStartSel;
        const td = this.td(c, r);
        const input = td.querySelector('input');
        this.editing = false;
        td.classList.remove('editing');
        if (input)
            input.remove();
        this.bar.value = this.sheet.getRaw(c, r);
    }
    /* ---------- grid events ---------- */
    bindGrid(t) {
        t.addEventListener('click', ev => {
            const target = ev.target;
            const td = target.closest('td');
            if (td && td.dataset.c !== undefined && td.dataset.r !== undefined) {
                this.selectCell(+td.dataset.c, +td.dataset.r);
                this.bar.focus();
                return;
            }
            const th = target.closest('th');
            if (th) {
                if (th.dataset.c !== undefined)
                    this.selectCell(+th.dataset.c, Math.min(this.sel.r, this.sheet.rows - 1));
                else if (th.dataset.r !== undefined)
                    this.selectCell(Math.min(this.sel.c, this.sheet.cols - 1), +th.dataset.r);
            }
        });
        t.addEventListener('dblclick', ev => {
            const target = ev.target;
            const td = target.closest('td');
            if (td && td.dataset.c !== undefined && td.dataset.r !== undefined) {
                this.selectCell(+td.dataset.c, +td.dataset.r);
                this.startEdit();
            }
        });
        t.addEventListener('contextmenu', ev => {
            ev.preventDefault();
            const target = ev.target;
            const td = target.closest('td');
            const th = target.closest('th');
            if (td && td.dataset.c !== undefined && td.dataset.r !== undefined) {
                this.selectCell(+td.dataset.c, +td.dataset.r);
                this.openMenu(ev.clientX, ev.clientY, 'cell');
            }
            else if (th && th.dataset.c !== undefined) {
                this.openMenu(ev.clientX, ev.clientY, 'col', +th.dataset.c);
            }
            else if (th && th.dataset.r !== undefined) {
                this.openMenu(ev.clientX, ev.clientY, 'row', +th.dataset.r);
            }
        });
    }
    /* ---------- context menu ---------- */
    openMenu(x, y, kind, index = -1) {
        const m = this.menu;
        m.innerHTML = '';
        const item = (label, fn) => {
            const d = document.createElement('div');
            d.className = 'mi';
            d.textContent = label;
            d.addEventListener('click', () => { this.closeMenu(); fn(); });
            m.appendChild(d);
        };
        if (kind === 'cell') {
            item('Clear contents', () => { this.sheet.setRaw(this.sel.c, this.sel.r, ''); });
            const sep = document.createElement('div');
            sep.className = 'sep';
            m.appendChild(sep);
            item('Insert row above', () => this.sheet.insertRow(this.sel.r));
            item('Insert column left', () => this.sheet.insertCol(this.sel.c));
            m.appendChild(sepClone());
            item('Delete this row', () => this.sheet.deleteRow(this.sel.r));
            item('Delete this column', () => this.sheet.deleteCol(this.sel.c));
        }
        else if (kind === 'row') {
            item('Insert row above', () => this.sheet.insertRow(index));
            m.appendChild(sepClone());
            item('Delete this row', () => this.sheet.deleteRow(index));
        }
        else {
            item('Insert column left', () => this.sheet.insertCol(index));
            m.appendChild(sepClone());
            item('Delete this column', () => this.sheet.deleteCol(index));
        }
        m.classList.add('open');
        const mw = m.offsetWidth, mh = m.offsetHeight;
        m.style.left = Math.min(x, window.innerWidth - mw - 6) + 'px';
        m.style.top = Math.min(y, window.innerHeight - mh - 6) + 'px';
    }
    closeMenu() { this.menu.classList.remove('open'); }
    /* ---------- global (sheet-level) keys ---------- */
    bindGlobal() {
        document.addEventListener('keydown', ev => this.onDocKey(ev));
        document.addEventListener('click', ev => {
            if (!this.menu.contains(ev.target))
                this.closeMenu();
        });
        window.addEventListener('blur', () => this.closeMenu());
        this.bar.addEventListener('focus', () => {
            if (!this.editing)
                this.bar.select();
        });
        this.bar.addEventListener('input', () => { });
        this.bar.addEventListener('keydown', ev => {
            if (ev.key === 'Enter') {
                ev.preventDefault();
                this.sheet.setRaw(this.sel.c, this.sel.r, this.bar.value);
                this.moveSel(1, 0);
                this.bar.focus();
            }
            else if (ev.key === 'Tab') {
                ev.preventDefault();
                this.sheet.setRaw(this.sel.c, this.sel.r, this.bar.value);
                this.moveSel(0, ev.shiftKey ? -1 : 1);
                this.bar.focus();
            }
            else if (ev.key === 'Escape') {
                ev.preventDefault();
                this.bar.value = this.sheet.getRaw(this.sel.c, this.sel.r);
            }
        });
        // toolbar
        const tb = document.getElementById('toolbar');
        if (tb)
            tb.addEventListener('click', ev => {
                const b = ev.target.closest('button');
                if (!b)
                    return;
                const act = b.dataset.act;
                if (act === 'ins-row')
                    this.sheet.insertRow(this.sel.r);
                else if (act === 'del-row')
                    this.sheet.deleteRow(this.sel.r);
                else if (act === 'ins-col')
                    this.sheet.insertCol(this.sel.c);
                else if (act === 'del-col')
                    this.sheet.deleteCol(this.sel.c);
            });
    }
    onDocKey(ev) {
        const tag = ev.target.tagName;
        if (this.editing)
            return;
        if (tag === 'INPUT') {
            // typing in the name box or formula bar
            if (ev.target.id === 'namebox') {
                if (ev.key === 'Enter') {
                    ev.preventDefault();
                    const p = (0, store_1.parseRefName)(ev.target.value);
                    if (p && this.sheet.inBounds(p.c, p.r))
                        this.selectCell(p.c, p.r);
                }
            }
            return; // formula bar handles its own keys
        }
        if (ev.key === 'ArrowUp') {
            ev.preventDefault();
            this.moveSel(-1, 0);
        }
        else if (ev.key === 'ArrowDown') {
            ev.preventDefault();
            this.moveSel(1, 0);
        }
        else if (ev.key === 'ArrowLeft') {
            ev.preventDefault();
            this.moveSel(0, -1);
        }
        else if (ev.key === 'ArrowRight') {
            ev.preventDefault();
            this.moveSel(0, 1);
        }
        else if (ev.key === 'Tab') {
            ev.preventDefault();
            this.moveSel(0, ev.shiftKey ? -1 : 1);
        }
        else if (ev.key === 'Enter') {
            ev.preventDefault();
            this.startEdit();
        }
        else if (ev.key === 'Delete' || ev.key === 'Backspace') {
            ev.preventDefault();
            this.sheet.setRaw(this.sel.c, this.sel.r, '');
        }
        else if (ev.key.length === 1 && !ev.ctrlKey && !ev.metaKey && !ev.altKey) {
            // start typing → replace cell content
            ev.preventDefault();
            this.startEdit(ev.key);
        }
    }
    moveSel(dr, dc) {
        const c = Math.max(0, Math.min(this.sheet.cols - 1, this.sel.c + dc));
        const r = Math.max(0, Math.min(this.sheet.rows - 1, this.sel.r + dr));
        this.selectCell(c, r);
    }
    /* ---------- reacting to store changes ---------- */
    onChanged(ch) {
        if (ch.structural) {
            this.buildTable();
            this.renderAll();
            const s = this.sel;
            if (s.c >= this.sheet.cols || s.r >= this.sheet.rows)
                this.sel = { c: 0, r: 0 };
            this.refreshSelUI();
            return;
        }
        for (const x of ch.changed)
            this.renderCell(x.c, x.r);
        // keep formula bar in sync if the selected cell changed
        if (ch.changed.some(x => x.c === this.sel.c && x.r === this.sel.r) && !this.editing) {
            this.bar.value = this.sheet.getRaw(this.sel.c, this.sel.r);
        }
    }
    /* ---------- save indicator ---------- */
    flashSave() {
        this.saveInd.textContent = 'Saved';
        this.saveInd.style.opacity = '1';
        window.setTimeout(() => { this.saveInd.style.opacity = '0.55'; }, 1200);
    }
}
exports.UI = UI;
function sepClone() {
    const d = document.createElement('div');
    d.className = 'sep';
    return d;
}
/* ---------- number formatting ---------- */
function fmtNum(v) {
    if (!isFinite(v))
        return '#NUM!';
    if (v === 0)
        return '0';
    const a = Math.abs(v);
    if (a !== 0 && (a < 1e-9 || a >= 1e15))
        return v.toExponential(8).replace(/\.?0+e/, 'e');
    const s = String(v);
    // JS may produce a long float for repeating binary fractions; round to 10 sig digits
    if (s.length > 12) {
        const r = parseFloat(v.toPrecision(10));
        return String(r);
    }
    return s;
}
function isNumLike(v) {
    return v.t === 'num' || (v.t === 'text' && v.s !== '' && Number.isFinite(Number(v.s)));
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.8-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"5a9f568a-b959-4443-890d-39c7f41f0244","tokensIn":5640692,"tokensOut":203665,"tokensTotal":5844357,"cost":0,"turns":86,"toolCalls":109,"failedToolCalls":3,"timestamp":"2026-08-15T11:07:12.993Z"} -->