← Spreadsheet results

Spreadsheet

Qwen 3.6 27B MTP 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 55,080 bytes · SHA-256 17690696e7ef
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spreadsheet</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  font-family: 'Segoe UI', Roboto, Arial, sans-serif;
  font-size: 13px;
  color: #202124;
  background: #fff;
  overflow: hidden;
}

#app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  width: 100vw;
}

/* Toolbar */
#toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: #f8f9fa;
  border-bottom: 1px solid #dadce0;
  min-height: 44px;
  flex-shrink: 0;
}

#toolbar .logo { font-size: 20px; }
#toolbar .title { font-size: 14px; font-weight: 500; color: #202124; }
#toolbar .hint { margin-left: auto; color: #5f6368; font-size: 11px; }

/* Formula Bar */
#fbar-row {
  display: flex;
  align-items: center;
  padding: 4px 8px;
  background: #fff;
  border-bottom: 1px solid #dadce0;
  flex-shrink: 0;
}

#fbar-ref {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 28px;
  font-size: 12px;
  font-weight: 600;
  color: #5f6368;
  background: #f1f3f4;
  border: 1px solid #dadce0;
  border-radius: 4px 0 0 4px;
  text-align: center;
  flex-shrink: 0;
  user-select: none;
}

#fbar-input {
  flex: 1;
  height: 28px;
  border: 1px solid #dadce0;
  border-left: none;
  border-radius: 0 4px 4px 0;
  padding: 0 10px;
  font-size: 13px;
  font-family: 'Roboto Mono', 'Courier New', monospace;
  outline: none;
  background: #fff;
}

#fbar-input:focus { border-color: #1a73e8; }

/* Grid Container */
#grid-container {
  flex: 1;
  overflow: auto;
  background: #fff;
  position: relative;
}

/* Grid table */
.spreadsheet-grid { border-collapse: collapse; }

/* Column headers: sticky top */
.col-header {
  position: sticky;
  top: 0;
  z-index: 5;
  user-select: none;
}

/* Row headers: sticky left */
.row-header {
  position: sticky;
  left: 0;
  z-index: 5;
  user-select: none;
}

/* Corner cell: sticky both */
.corner {
  position: sticky;
  top: 0;
  left: 0;
  z-index: 10;
  user-select: none;
}
</style>
</head>
<body>
<div id="toolbar"></div>
<div id="fbar-row">
  <span id="fbar-ref"></span>
  <span style="color:#bbb;padding:0 4px;">❯</span>
  <input id="fbar-input" type="text" placeholder="Formula">
</div>
<div id="grid-container"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./model":"src/model.ts","./engine":"src/engine.ts","./view":"src/view.ts"},"src/types.ts":{"./config":"src/config.ts"},"src/model.ts":{"./config":"src/config.ts","./types":"src/types.ts"},"src/parser.ts":{"./config":"src/config.ts","./types":"src/types.ts"},"src/engine.ts":{"./config":"src/config.ts","./model":"src/model.ts","./parser":"src/parser.ts","./types":"src/types.ts"},"src/view.ts":{"./config":"src/config.ts","./model":"src/model.ts","./engine":"src/engine.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 Point ──────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
const model_1 = require("./model");
const engine_1 = require("./engine");
const view_1 = require("./view");
function start() {
    const model = (0, model_1.loadModel)();
    const graph = (0, engine_1.rebuildGraph)(model);
    (0, engine_1.recalcAll)(model, graph);
    const gridEl = document.getElementById('grid-container');
    const fbarInput = document.getElementById('fbar-input');
    const fbarRef = document.getElementById('fbar-ref');
    const app = {
        model,
        selCol: 0,
        selRow: 0,
        editing: false,
        editValue: '',
        gridEl,
        tableEl: document.createElement('table'),
        fbarRef,
        fbarInput,
        cells: new Map(),
        rowHeaders: new Map(),
        colHeaders: new Map(),
    };
    // Expose for context menu
    // Expose engine functions for testing/debugging
    window.__spreadsheetApp = app;
    window.rebuildGraph = engine_1.rebuildGraph;
    window.recalcAll = engine_1.recalcAll;
    window.recalcFrom = engine_1.recalcFrom;
    window.commitEdit = () => (0, view_1.commitEdit)(app);
    window.renderAll = () => (0, view_1.renderAll)(app);
    window.selectCell = (c, r) => (0, view_1.selectCell)(app, c, r);
    (0, view_1.buildGrid)(app);
    (0, view_1.renderAll)(app);
    (0, view_1.selectCell)(app, 0, 0);
    (0, view_1.setupKeyboard)(app);
    (0, view_1.setupFormulaBar)(app);
    // Toolbar
    const toolbar = document.getElementById('toolbar');
    if (toolbar) {
        toolbar.innerHTML = `
      <span class="logo">📊</span>
      <span class="title">Spreadsheet</span>
      <span style="margin-left:auto;color:#5f6368;font-size:12px;">Right-click headers to insert/delete rows &amp; columns</span>
    `;
    }
}
start();
};

// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ERR_PARSE = exports.ERR_REF = exports.ERR_DIV0 = exports.ERR_CYCLE = exports.COLOR_TOOLBAR_BG = exports.COLOR_MENU_HOVER = exports.COLOR_MENU_BG = exports.COLOR_FBAR_REF = exports.COLOR_FBAR_BORDER = exports.COLOR_FBAR_BG = exports.COLOR_NUM = exports.COLOR_TEXT = exports.COLOR_ERROR = exports.COLOR_SELECTION = exports.COLOR_HEADER_TEXT = exports.COLOR_HEADER_BG = exports.COLOR_GRIDLINE = exports.COLOR_BG = exports.CELL_H = exports.CELL_W = exports.HEADER_H = exports.HEADER_W = exports.ROWS = exports.COLS = void 0;
// ─── Tunable Constants ────────────────────────────────────────────────
exports.COLS = 26; // A–Z
exports.ROWS = 100;
exports.HEADER_W = 42; // row header column width
exports.HEADER_H = 24; // column header row height
exports.CELL_W = 96; // default cell width
exports.CELL_H = 26; // default cell height
// ─── Colors ───────────────────────────────────────────────────────────
exports.COLOR_BG = '#ffffff';
exports.COLOR_GRIDLINE = '#e0e0e0';
exports.COLOR_HEADER_BG = '#f8f9fa';
exports.COLOR_HEADER_TEXT = '#333';
exports.COLOR_SELECTION = '#1a73e8';
exports.COLOR_ERROR = '#d32f2f';
exports.COLOR_TEXT = '#202124';
exports.COLOR_NUM = '#202124';
exports.COLOR_FBAR_BG = '#fff';
exports.COLOR_FBAR_BORDER = '#dadce0';
exports.COLOR_FBAR_REF = '#5f6368';
exports.COLOR_MENU_BG = '#fff';
exports.COLOR_MENU_HOVER = '#f1f3f4';
exports.COLOR_TOOLBAR_BG = '#f1f3f4';
// ─── Special Values ──────────────────────────────────────────────────
exports.ERR_CYCLE = '#CYCLE!';
exports.ERR_DIV0 = '#DIV/0!';
exports.ERR_REF = '#REF!';
exports.ERR_PARSE = '#ERR!';
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ─── Type Definitions ─────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.isError = isError;
const config_1 = require("./config");
function isError(tag) {
    return tag === config_1.ERR_CYCLE || tag === config_1.ERR_DIV0 || tag === config_1.ERR_REF || tag === config_1.ERR_PARSE;
}
};

// ── module: src/model.ts ──
__mods["src/model.ts"] = function (exports, require, module) {
"use strict";
// ─── Model: Grid data, serialization, cell address utilities ───────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.key = key;
exports.parseKey = parseKey;
exports.colToLetter = colToLetter;
exports.letterToCol = letterToCol;
exports.addrToLabel = addrToLabel;
exports.createModel = createModel;
exports.getCell = getCell;
exports.setCell = setCell;
exports.clearCell = clearCell;
exports.hasCell = hasCell;
exports.insertRow = insertRow;
exports.deleteRow = deleteRow;
exports.insertCol = insertCol;
exports.deleteCol = deleteCol;
exports.saveModel = saveModel;
exports.loadModel = loadModel;
const config_1 = require("./config");
function key(col, row) {
    return `${col},${row}`;
}
function parseKey(k) {
    const idx = k.indexOf(',');
    return { col: parseInt(k.slice(0, idx), 10), row: parseInt(k.slice(idx + 1), 10) };
}
function colToLetter(col) {
    return String.fromCharCode(65 + col);
}
function letterToCol(letter) {
    return letter.toUpperCase().charCodeAt(0) - 65;
}
function addrToLabel(addr) {
    return `${colToLetter(addr.col)}${addr.row + 1}`;
}
function createModel() {
    return { cells: new Map(), cols: config_1.COLS, rows: config_1.ROWS };
}
function getCell(model, col, row) {
    return model.cells.get(key(col, row));
}
function setCell(model, col, row, raw) {
    model.cells.set(key(col, row), { raw });
}
function clearCell(model, col, row) {
    model.cells.delete(key(col, row));
}
function hasCell(model, col, row) {
    return model.cells.has(key(col, row));
}
// Insert row at given index, shift rows down, rewrite all cell references
function insertRow(model, row) {
    model.rows++;
    // Shift cells below the inserted row
    const newCells = new Map();
    for (const [k, data] of model.cells) {
        const { col, row: r2 } = parseKey(k);
        if (r2 >= row) {
            const newKey = key(col, r2 + 1);
            // Rewrite raw formula to point to shifted references
            newCells.set(newKey, { raw: shiftRowRefs(data.raw, row, 1) });
        }
        else {
            newCells.set(k, data);
        }
    }
    model.cells = newCells;
}
// Delete row at given index, shift rows up
function deleteRow(model, row) {
    if (model.rows <= 1)
        return;
    model.rows--;
    const newCells = new Map();
    for (const [k, data] of model.cells) {
        const { col, row: r } = parseKey(k);
        if (r === row) {
            continue;
        }
        else if (r > row) {
            const newKey = key(col, r - 1);
            newCells.set(newKey, { raw: shiftRowRefs(data.raw, row, -1) });
        }
        else {
            newCells.set(k, data);
        }
    }
    model.cells = newCells;
    for (const [k] of newCells) {
        const { row: rr } = parseKey(k);
        if (rr >= model.rows)
            newCells.delete(k);
    }
}
// Insert column at given index
function insertCol(model, col) {
    model.cols++;
    const newCells = new Map();
    for (const [k, data] of model.cells) {
        const { col: c2, row: r } = parseKey(k);
        if (c2 >= col) {
            const newKey = key(c2 + 1, r);
            newCells.set(newKey, { raw: shiftColRefs(data.raw, col, 1) });
        }
        else {
            newCells.set(k, data);
        }
    }
    model.cells = newCells;
}
// Delete column
function deleteCol(model, col) {
    if (model.cols <= 1)
        return;
    model.cols--;
    const newCells = new Map();
    for (const [k, data] of model.cells) {
        const { col: c2, row: r } = parseKey(k);
        if (c2 === col) {
            continue;
        }
        else if (c2 > col) {
            const newKey = key(c2 - 1, r);
            newCells.set(newKey, { raw: shiftColRefs(data.raw, col, -1) });
        }
        else {
            newCells.set(k, data);
        }
    }
    model.cells = newCells;
    for (const [k] of newCells) {
        const { col: c2 } = parseKey(k);
        if (c2 >= model.cols)
            newCells.delete(k);
    }
}
// Rewrite row references in a formula string
function shiftRowRefs(raw, pivotRow, delta) {
    return raw.replace(/([A-Z])(\d+)/g, (_, letter, rowNum) => {
        const row = parseInt(rowNum, 10) - 1; // 0-based
        if (row >= pivotRow) {
            return letter + (row + delta + 1);
        }
        return letter + rowNum;
    });
}
// Rewrite column references in a formula string
function shiftColRefs(raw, pivotCol, delta) {
    return raw.replace(/([A-Z])(\d+)/g, (_, letter, rowNum) => {
        const col = letter.charCodeAt(0) - 65;
        if (col >= pivotCol) {
            return String.fromCharCode(65 + col + delta) + rowNum;
        }
        return letter + rowNum;
    });
}
// ─── Serialization ────────────────────────────────────────────────────
const STORAGE_KEY = 'spreadsheet_data_v2';
function saveModel(model) {
    const data = {};
    data.cols = model.cols;
    data.rows = model.rows;
    for (const [k, cell] of model.cells) {
        data[k] = cell.raw;
    }
    try {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
    }
    catch { }
}
function loadModel() {
    try {
        const raw = localStorage.getItem(STORAGE_KEY);
        if (!raw)
            return createModel();
        const data = JSON.parse(raw);
        const model = createModel();
        model.cols = data.cols ?? config_1.COLS;
        model.rows = data.rows ?? config_1.ROWS;
        for (const [k, rawVal] of Object.entries(data)) {
            if (k === 'cols' || k === 'rows')
                continue;
            if (typeof rawVal === 'string') {
                model.cells.set(k, { raw: rawVal });
            }
        }
        return model;
    }
    catch {
        return createModel();
    }
}
};

// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
// ─── Formula Parser: Tokenizer + Recursive-Descent ─────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.tokenize = tokenize;
exports.parse = parse;
const config_1 = require("./config");
// ─── Tokenizer ────────────────────────────────────────────────────────
function tokenize(input) {
    const tokens = [];
    let i = 0;
    const src = input.trim();
    while (i < src.length) {
        // Skip whitespace
        if (src[i] === ' ') {
            i++;
            continue;
        }
        // Number (possibly with decimal point)
        if (src[i] >= '0' && src[i] <= '9') {
            let start = i;
            while (i < src.length && ((src[i] >= '0' && src[i] <= '9') || src[i] === '.'))
                i++;
            tokens.push({ type: 'number', value: parseFloat(src.slice(start, i)) });
            continue;
        }
        // Letter → name (cell ref or function)
        if ((src[i] >= 'A' && src[i] <= 'Z') || (src[i] >= 'a' && src[i] <= 'z')) {
            let start = i;
            while (i < src.length && ((src[i] >= 'A' && src[i] <= 'Z') || (src[i] >= 'a' && src[i] <= 'z')))
                i++;
            tokens.push({ type: 'name', value: src.slice(start, i) });
            continue;
        }
        // Operators and punctuation
        switch (src[i]) {
            case '+':
                tokens.push({ type: 'op', value: '+' });
                i++;
                break;
            case '-':
                tokens.push({ type: 'op', value: '-' });
                i++;
                break;
            case '*':
                tokens.push({ type: 'op', value: '*' });
                i++;
                break;
            case '/':
                tokens.push({ type: 'op', value: '/' });
                i++;
                break;
            case ',':
                tokens.push({ type: 'comma' });
                i++;
                break;
            case ':':
                tokens.push({ type: 'colon' });
                i++;
                break;
            case '(':
                tokens.push({ type: 'lparen' });
                i++;
                break;
            case ')':
                tokens.push({ type: 'rparen' });
                i++;
                break;
            default:
                i++;
                break; // skip unknown chars
        }
    }
    tokens.push({ type: 'eof' });
    return tokens;
}
function parse(input) {
    const tokens = tokenize(input);
    const p = { tokens, pos: 0, error: false };
    const node = parseExpr(p);
    if (p.error || p.pos >= p.tokens.length - 1) {
        if (p.error)
            return { kind: 'error', value: config_1.ERR_PARSE };
        // Not all tokens consumed
        const remaining = p.tokens[p.pos];
        if (remaining && remaining.type !== 'eof') {
            return { kind: 'error', value: config_1.ERR_PARSE };
        }
    }
    return node;
}
function parseExpr(p) {
    return parseAddExpr(p);
}
function isOpToken(tok, op) {
    return tok.type === 'op' && tok.value === op;
}
function parseAddExpr(p) {
    let left = parseMulExpr(p);
    if (left.kind === 'error')
        return left;
    while (p.pos < p.tokens.length && (isOpToken(p.tokens[p.pos], '+') || isOpToken(p.tokens[p.pos], '-'))) {
        const tok = p.tokens[p.pos];
        const op = tok.type === 'op' ? tok.value : '+';
        p.pos++;
        const right = parseMulExpr(p);
        if (right.kind === 'error')
            return right;
        left = { kind: 'binop', op, left, right };
    }
    return left;
}
function parseMulExpr(p) {
    let left = parseUnary(p);
    if (left.kind === 'error')
        return left;
    while (p.pos < p.tokens.length && (isOpToken(p.tokens[p.pos], '*') || isOpToken(p.tokens[p.pos], '/'))) {
        const tok = p.tokens[p.pos];
        const op = tok.type === 'op' ? tok.value : '*';
        p.pos++;
        const right = parseUnary(p);
        if (right.kind === 'error')
            return right;
        left = { kind: 'binop', op, left, right };
    }
    return left;
}
function parseUnary(p) {
    if (p.pos < p.tokens.length && isOpToken(p.tokens[p.pos], '-')) {
        p.pos++;
        const operand = parseUnary(p);
        if (operand.kind === 'error')
            return operand;
        return { kind: 'unop', op: '-', operand };
    }
    return parsePrimary(p);
}
function parsePrimary(p) {
    if (p.pos >= p.tokens.length) {
        return { kind: 'error', value: config_1.ERR_PARSE };
    }
    const tok = p.tokens[p.pos];
    // Number literal
    if (tok.type === 'number') {
        p.pos++;
        return { kind: 'number', value: tok.value };
    }
    // Parenthesized expression
    if (tok.type === 'lparen') {
        p.pos++; // consume '('
        const expr = parseExpr(p);
        if (expr.kind === 'error')
            return expr;
        if (p.pos < p.tokens.length && p.tokens[p.pos].type === 'rparen') {
            p.pos++;
        }
        else {
            return { kind: 'error', value: config_1.ERR_PARSE };
        }
        return expr;
    }
    // Name → could be cell ref, range, or function
    if (tok.type === 'name') {
        const name = tok.value;
        p.pos++;
        // Check if it looks like a cell reference (letter(s) followed by digits)
        // We need to look ahead for digits since we consumed the letter part
        // Actually, cell refs like A1 are tokenized as NAME since digits follow letters
        // Let's check: our tokenizer eats all letters, so "A1" → {type:'name', value:'A'}, then {type:'number', value:1}
        // So a cell ref is actually NAME followed by NUMBER. Let me handle that.
        // Check for function call: name followed by '('
        if (p.pos < p.tokens.length && p.tokens[p.pos].type === 'lparen') {
            // Function call
            p.pos++; // consume '('
            const args = [];
            // Parse first argument
            if (p.pos < p.tokens.length && p.tokens[p.pos].type !== 'rparen') {
                args.push(parseExpr(p));
            }
            while (p.pos < p.tokens.length && p.tokens[p.pos].type === 'comma') {
                p.pos++;
                args.push(parseExpr(p));
            }
            if (p.pos < p.tokens.length && p.tokens[p.pos].type === 'rparen') {
                p.pos++;
            }
            else {
                return { kind: 'error', value: config_1.ERR_PARSE };
            }
            return { kind: 'function', name: name.toUpperCase(), args };
        }
        // Check if next token is a number → cell reference
        if (p.pos < p.tokens.length && p.tokens[p.pos].type === 'number') {
            const numTok = p.tokens[p.pos];
            const rowVal = numTok.value;
            p.pos++;
            const col = parseColumnName(name);
            if (col < 0)
                return { kind: 'error', value: config_1.ERR_REF };
            const row = Math.floor(rowVal) - 1; // 1-based to 0-based
            if (row < 0)
                return { kind: 'error', value: config_1.ERR_REF };
            // Check for range: cellRef ':' cellRef
            if (p.pos < p.tokens.length && p.tokens[p.pos].type === 'colon') {
                p.pos++; // consume ':'
                const rangeNode = parseCellRefOrRangeEnd(p);
                if (rangeNode.kind === 'error')
                    return rangeNode;
                if (rangeNode.kind === 'cell') {
                    return {
                        kind: 'range',
                        colStart: col, rowStart: row,
                        colEnd: rangeNode.col, rowEnd: rangeNode.row
                    };
                }
                return { kind: 'error', value: config_1.ERR_PARSE };
            }
            return { kind: 'cell', col, row };
        }
        // Just a name that's not a cell ref or function → treat as string
        return { kind: 'string', value: name };
    }
    return { kind: 'error', value: config_1.ERR_PARSE };
}
function parseCellRefOrRangeEnd(p) {
    if (p.pos >= p.tokens.length) {
        return { kind: 'error', value: config_1.ERR_PARSE };
    }
    const tok = p.tokens[p.pos];
    if (tok.type === 'name') {
        const name = tok.value;
        p.pos++;
        if (p.pos < p.tokens.length && p.tokens[p.pos].type === 'number') {
            const numTok = p.tokens[p.pos];
            const rowVal = numTok.value;
            p.pos++;
            const col = parseColumnName(name);
            if (col < 0)
                return { kind: 'error', value: config_1.ERR_REF };
            const row = Math.floor(rowVal) - 1;
            return { kind: 'cell', col, row };
        }
    }
    return { kind: 'error', value: config_1.ERR_PARSE };
}
function parseColumnName(name) {
    // Support A-Z (single letter for now; could extend to AA, AB, etc.)
    if (name.length === 1) {
        return name.charCodeAt(0) - 65;
    }
    // Multi-letter column names (AA, AB, ..., AZ, BA, ...)
    let col = 0;
    for (let i = 0; i < name.length; i++) {
        col = col * 26 + (name.charCodeAt(i) - 64);
    }
    return col - 1; // 0-based
}
};

// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
// ─── Formula Evaluator & Dependency Graph ─────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.createDepGraph = createDepGraph;
exports.extractRefs = extractRefs;
exports.rebuildGraph = rebuildGraph;
exports.detectCycles = detectCycles;
exports.evaluateNode = evaluateNode;
exports.recalcFrom = recalcFrom;
exports.recalcAll = recalcAll;
const config_1 = require("./config");
const model_1 = require("./model");
const parser_1 = require("./parser");
function createDepGraph() {
    return { deps: new Map(), reverseDeps: new Map() };
}
function ensureSet(map, k) {
    if (!map.has(k))
        map.set(k, new Set());
    return map.get(k);
}
function extractRefs(raw) {
    const refs = new Set();
    if (!raw.startsWith('='))
        return [];
    const formula = raw.slice(1);
    const node = (0, parser_1.parse)(formula);
    if (node.kind === 'error')
        return [];
    collectRefs(node, refs);
    return [...refs];
}
function collectRefs(node, refs) {
    switch (node.kind) {
        case 'cell':
            refs.add((0, model_1.key)(node.col, node.row));
            break;
        case 'range': {
            for (let r = Math.min(node.rowStart, node.rowEnd); r <= Math.max(node.rowStart, node.rowEnd); r++) {
                for (let c = Math.min(node.colStart, node.colEnd); c <= Math.max(node.colStart, node.colEnd); c++) {
                    refs.add((0, model_1.key)(c, r));
                }
            }
            break;
        }
        case 'binop':
            collectRefs(node.left, refs);
            collectRefs(node.right, refs);
            break;
        case 'unop':
            collectRefs(node.operand, refs);
            break;
        case 'function':
            for (const arg of node.args)
                collectRefs(arg, refs);
            break;
    }
}
function rebuildGraph(model) {
    const graph = createDepGraph();
    for (const [cellKey, cell] of model.cells) {
        if (!cell.raw.startsWith('='))
            continue;
        const refs = extractRefs(cell.raw).filter(r => r !== cellKey);
        if (refs.length > 0) {
            graph.deps.set(cellKey, new Set(refs));
            for (const ref of refs) {
                ensureSet(graph.reverseDeps, ref).add(cellKey);
            }
        }
    }
    return graph;
}
// ─── Cycle Detection ──────────────────────────────────────────────────
function detectCycles(model) {
    const cycleCells = new Set();
    // For each formula cell, do a DFS through deps looking for cycles
    for (const [cellKey, cell] of model.cells) {
        if (!cell.raw.startsWith('='))
            continue;
        // DFS from this cell through its deps
        const path = new Set();
        const allVisited = new Set();
        if (findCycle(cellKey, model, path, allVisited, cycleCells)) {
            // All cells remaining on the path are part of the cycle
            for (const s of path)
                cycleCells.add(s);
        }
    }
    return cycleCells;
}
function findCycle(cellKey, model, path, allVisited, cycleCells) {
    if (path.has(cellKey))
        return true;
    if (allVisited.has(cellKey))
        return false;
    allVisited.add(cellKey);
    path.add(cellKey);
    const cell = model.cells.get(cellKey);
    if (cell?.raw.startsWith('=')) {
        const refs = extractRefs(cell.raw).filter(r => r !== cellKey);
        for (const ref of refs) {
            if (findCycle(ref, model, path, allVisited, cycleCells)) {
                cycleCells.add(cellKey);
                path.delete(cellKey);
                return true;
            }
        }
    }
    path.delete(cellKey);
    return false;
}
function evaluateNode(node, model, evalStack) {
    switch (node.kind) {
        case 'number': return { value: node.value };
        case 'string': return { value: node.value };
        case 'error': return { error: node.value };
        case 'cell': {
            const cellKey = (0, model_1.key)(node.col, node.row);
            if (node.col < 0 || node.col >= model.cols || node.row < 0 || node.row >= model.rows)
                return { error: config_1.ERR_REF };
            if (evalStack.has(cellKey))
                return { error: config_1.ERR_CYCLE };
            const cell = model.cells.get(cellKey);
            if (!cell)
                return { value: 0 };
            if (cell.error)
                return { error: cell.error };
            const val = cell.value;
            if (val === undefined || val === null || val === '')
                return { value: 0 };
            return { value: val };
        }
        case 'range': {
            const values = [];
            const cs = Math.min(node.colStart, node.colEnd);
            const ce = Math.max(node.colStart, node.colEnd);
            const rs = Math.min(node.rowStart, node.rowEnd);
            const re = Math.max(node.rowStart, node.rowEnd);
            for (let r = rs; r <= re; r++) {
                for (let c = cs; c <= ce; c++) {
                    const cell = model.cells.get((0, model_1.key)(c, r));
                    if (cell) {
                        if (cell.error)
                            return { error: cell.error };
                        const val = cell.value;
                        if (typeof val === 'number')
                            values.push(val);
                        else if (typeof val === 'string' && val !== '') {
                            const parsed = parseFloat(val);
                            if (!isNaN(parsed))
                                values.push(parsed);
                        }
                    }
                }
            }
            return { value: values };
        }
        case 'unop': {
            const result = evaluateNode(node.operand, model, evalStack);
            if (result.error)
                return result;
            if (typeof result.value === 'number')
                return { value: -result.value };
            return { error: config_1.ERR_PARSE };
        }
        case 'binop': {
            const left = evaluateNode(node.left, model, evalStack);
            if (left.error)
                return left;
            if (Array.isArray(left.value))
                return { error: config_1.ERR_PARSE };
            const right = evaluateNode(node.right, model, evalStack);
            if (right.error)
                return right;
            if (Array.isArray(right.value))
                return { error: config_1.ERR_PARSE };
            const lv = typeof left.value === 'number' ? left.value : parseNum(left.value);
            const rv = typeof right.value === 'number' ? right.value : parseNum(right.value);
            if (isNaN(lv) || isNaN(rv))
                return { error: config_1.ERR_PARSE };
            switch (node.op) {
                case '+': return { value: lv + rv };
                case '-': return { value: lv - rv };
                case '*': return { value: lv * rv };
                case '/': return rv === 0 ? { error: config_1.ERR_DIV0 } : { value: lv / rv };
                default: return { error: config_1.ERR_PARSE };
            }
        }
        case 'function': return evaluateFunction(node.name, node.args, model, evalStack);
        default: return { error: config_1.ERR_PARSE };
    }
}
function parseNum(v) {
    if (v === undefined || v === null || v === '')
        return 0;
    if (typeof v === 'number')
        return v;
    if (typeof v === 'string') {
        const n = parseFloat(v);
        return isNaN(n) ? 0 : n;
    }
    return 0;
}
function evaluateFunction(name, args, model, evalStack) {
    const numbers = [];
    for (const arg of args) {
        const result = evaluateNode(arg, model, evalStack);
        if (result.error)
            return result;
        if (Array.isArray(result.value)) {
            for (const n of result.value)
                numbers.push(n);
        }
        else if (typeof result.value === 'number')
            numbers.push(result.value);
        else if (typeof result.value === 'string') {
            const n = parseFloat(result.value);
            if (!isNaN(n))
                numbers.push(n);
        }
    }
    switch (name) {
        case 'SUM': return { value: numbers.reduce((a, b) => a + b, 0) };
        case 'AVG':
        case 'AVERAGE':
            return numbers.length === 0 ? { value: 0 } : { value: numbers.reduce((a, b) => a + b, 0) / numbers.length };
        case 'MIN': return numbers.length > 0 ? { value: Math.min(...numbers) } : { value: 0 };
        case 'MAX': return numbers.length > 0 ? { value: Math.max(...numbers) } : { value: 0 };
        case 'COUNT': return { value: numbers.length };
        default: return { error: config_1.ERR_PARSE };
    }
}
// ─── Recalculation ────────────────────────────────────────────────────
function computeCell(model, cellKey) {
    const cell = model.cells.get(cellKey);
    if (!cell)
        return { raw: '' };
    if (!cell.raw || !cell.raw.startsWith('=')) {
        const n = parseFloat(cell.raw);
        return { raw: cell.raw, value: (isNaN(n) || cell.raw === '') ? cell.raw : n };
    }
    const node = (0, parser_1.parse)(cell.raw.slice(1));
    if (node.kind === 'error')
        return { raw: cell.raw, error: node.value };
    const evalStack = new Set();
    evalStack.add(cellKey);
    const result = evaluateNode(node, model, evalStack);
    evalStack.delete(cellKey);
    if (result.error)
        return { raw: cell.raw, error: result.error };
    const val = result.value;
    let displayVal;
    if (Array.isArray(val))
        displayVal = val.join(',');
    else if (typeof val === 'number')
        displayVal = Math.round(val * 1e10) / 1e10;
    else
        displayVal = String(val ?? '');
    return { raw: cell.raw, value: displayVal };
}
// Recompute starting from a changed cell, cascading to dependents
function recalcFrom(model, graph, cellKey) {
    const cycleCells = detectCycles(model);
    // Collect all cells that need recalculation using BFS through reverse deps
    const toRecalc = new Set();
    const queue = [cellKey];
    while (queue.length > 0) {
        const current = queue.shift();
        if (toRecalc.has(current))
            continue;
        toRecalc.add(current);
        const reverse = graph.reverseDeps.get(current);
        if (reverse) {
            for (const dep of reverse) {
                if (!toRecalc.has(dep))
                    queue.push(dep);
            }
        }
    }
    // Sort in dependency order: cells with no deps in toRecalc first
    const sorted = topoSortSet(graph, toRecalc, cycleCells);
    // Mark cycle cells
    for (const ck of cycleCells) {
        const cell = model.cells.get(ck);
        if (cell)
            model.cells.set(ck, { raw: cell.raw, value: '#CYCLE!', error: config_1.ERR_CYCLE });
    }
    // Evaluate in order
    for (const ck of sorted) {
        if (cycleCells.has(ck))
            continue;
        model.cells.set(ck, computeCell(model, ck));
    }
}
// Recompute ALL formula cells in the correct dependency order
function recalcAll(model, graph) {
    const cycleCells = detectCycles(model);
    // Step 1: Set values for non-formula cells first (so formulas can read them)
    for (const [cellKey, cell] of model.cells) {
        if (!cell.raw || !cell.raw.startsWith('=')) {
            const n = parseFloat(cell.raw);
            model.cells.set(cellKey, { raw: cell.raw, value: (isNaN(n) || cell.raw === '') ? cell.raw : n });
        }
    }
    // Step 2: Collect all formula cell keys
    const formulaKeys = new Set();
    for (const [cellKey, cell] of model.cells) {
        if (cell.raw.startsWith('='))
            formulaKeys.add(cellKey);
    }
    const sorted = topoSortSet(graph, formulaKeys, cycleCells);
    // Step 3: Mark cycle cells
    for (const ck of cycleCells) {
        const cell = model.cells.get(ck);
        if (cell)
            model.cells.set(ck, { raw: cell.raw, value: '#CYCLE!', error: config_1.ERR_CYCLE });
    }
    // Step 4: Evaluate formula cells in topological order
    for (const ck of sorted) {
        if (cycleCells.has(ck))
            continue;
        model.cells.set(ck, computeCell(model, ck));
    }
}
// Topological sort of a set of cells in dependency order
// Returns cells in order: dependencies before dependents
function topoSortSet(graph, cellSet, cycleCells) {
    const sorted = [];
    const visited = new Set();
    const visiting = new Set(); // for cycle detection during topo sort
    for (const ck of cellSet) {
        if (!visited.has(ck)) {
            topoVisit(ck, graph, cellSet, visited, visiting, sorted, cycleCells);
        }
    }
    return sorted;
}
function topoVisit(cellKey, graph, cellSet, visited, visiting, sorted, cycleCells) {
    if (visited.has(cellKey))
        return;
    if (visiting.has(cellKey))
        return; // already being processed
    if (cycleCells.has(cellKey)) {
        visited.add(cellKey);
        return;
    }
    visiting.add(cellKey);
    // Visit dependencies first
    const deps = graph.deps.get(cellKey);
    if (deps) {
        for (const dep of deps) {
            // Visit even if not in cellSet (they might be external deps)
            if (!visited.has(dep) && !cycleCells.has(dep)) {
                topoVisit(dep, graph, cellSet, visited, visiting, sorted, cycleCells);
            }
        }
    }
    visiting.delete(cellKey);
    visited.add(cellKey);
    if (cellSet.has(cellKey) && !cycleCells.has(cellKey)) {
        sorted.push(cellKey);
    }
}
};

// ── module: src/view.ts ──
__mods["src/view.ts"] = function (exports, require, module) {
"use strict";
// ─── View: Grid rendering, input handling, context menus ──────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildGrid = buildGrid;
exports.renderCellDisplay = renderCellDisplay;
exports.renderAll = renderAll;
exports.selectCell = selectCell;
exports.startEdit = startEdit;
exports.commitEdit = commitEdit;
exports.cancelEdit = cancelEdit;
exports.setupKeyboard = setupKeyboard;
exports.setupFormulaBar = setupFormulaBar;
const config_1 = require("./config");
const model_1 = require("./model");
const engine_1 = require("./engine");
// ─── Build Grid DOM ───────────────────────────────────────────────────
function buildGrid(app) {
    const { model, gridEl } = app;
    gridEl.innerHTML = '';
    const table = document.createElement('table');
    table.style.borderCollapse = 'collapse';
    table.style.width = `${config_1.HEADER_W + model.cols * config_1.CELL_W}px`;
    table.style.tableLayout = 'fixed';
    app.tableEl = table;
    app.cells = new Map();
    app.rowHeaders = new Map();
    app.colHeaders = new Map();
    // Header row
    const thead = document.createElement('thead');
    const headerRow = document.createElement('tr');
    const corner = document.createElement('th');
    corner.className = 'corner';
    corner.style.cssText = `width:${config_1.HEADER_W}px;height:${config_1.HEADER_H}px;min-width:${config_1.HEADER_W}px;background:${config_1.COLOR_MENU_BG};border-bottom:2px solid ${config_1.COLOR_GRIDLINE};border-right:1px solid ${config_1.COLOR_GRIDLINE};`;
    headerRow.appendChild(corner);
    for (let c = 0; c < model.cols; c++) {
        const th = document.createElement('th');
        th.textContent = (0, model_1.colToLetter)(c);
        th.className = 'col-header';
        th.style.cssText = `width:${config_1.CELL_W}px;min-width:${config_1.CELL_W}px;height:${config_1.HEADER_H}px;background:${config_1.COLOR_MENU_BG};text-align:center;font-weight:600;color:#333;border-bottom:2px solid ${config_1.COLOR_GRIDLINE};border-right:1px solid ${config_1.COLOR_GRIDLINE};`;
        th.dataset.col = String(c);
        app.colHeaders.set(c, th);
        th.addEventListener('contextmenu', (e) => { e.preventDefault(); showHeaderMenu(e, c, 'col'); });
        headerRow.appendChild(th);
    }
    thead.appendChild(headerRow);
    table.appendChild(thead);
    // Body rows
    const tbody = document.createElement('tbody');
    for (let r = 0; r < model.rows; r++) {
        const tr = document.createElement('tr');
        const rh = document.createElement('th');
        rh.textContent = String(r + 1);
        rh.className = 'row-header';
        rh.style.cssText = `width:${config_1.HEADER_W}px;min-width:${config_1.HEADER_W}px;height:${config_1.CELL_H}px;background:${config_1.COLOR_MENU_BG};text-align:center;font-weight:600;color:#333;border-bottom:1px solid ${config_1.COLOR_GRIDLINE};border-right:2px solid ${config_1.COLOR_GRIDLINE};`;
        rh.dataset.row = String(r);
        app.rowHeaders.set(r, rh);
        rh.addEventListener('contextmenu', (e) => { e.preventDefault(); showHeaderMenu(e, r, 'row'); });
        tr.appendChild(rh);
        for (let c = 0; c < model.cols; c++) {
            const td = document.createElement('td');
            td.style.cssText = `width:${config_1.CELL_W}px;height:${config_1.CELL_H}px;border-left:1px solid ${config_1.COLOR_GRIDLINE};border-top:1px solid ${config_1.COLOR_GRIDLINE};padding:2px 4px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;cursor:cell;box-sizing:border-box;background:#fff;`;
            td.dataset.col = String(c);
            td.dataset.row = String(r);
            const cellKey = (0, model_1.key)(c, r);
            app.cells.set(cellKey, td);
            td.addEventListener('mousedown', (e) => {
                if (e.button === 0) {
                    if (app.editing)
                        commitEdit(app);
                    selectCell(app, c, r);
                }
            });
            td.addEventListener('dblclick', () => { if (!app.editing)
                startEdit(app); });
            tr.appendChild(td);
        }
        tbody.appendChild(tr);
    }
    table.appendChild(tbody);
    gridEl.appendChild(table);
}
// ─── Render ───────────────────────────────────────────────────────────
function renderCellDisplay(app, col, row) {
    const td = app.cells.get((0, model_1.key)(col, row));
    if (!td)
        return;
    const cell = app.model.cells.get((0, model_1.key)(col, row));
    if (!cell || cell.raw === '') {
        td.textContent = '';
        td.style.color = '';
        td.style.textAlign = '';
        return;
    }
    if (cell.error) {
        td.textContent = cell.error;
        td.style.color = config_1.COLOR_ERROR;
        td.style.textAlign = 'left';
        return;
    }
    const val = cell.value;
    td.textContent = val === undefined || val === null ? '' : String(val);
    td.style.color = config_1.COLOR_TEXT;
    td.style.textAlign = typeof val === 'number' ? 'right' : 'left';
}
function renderAll(app) {
    const { model } = app;
    for (const cellKey of model.cells.keys()) {
        const parts = cellKey.split(',');
        renderCellDisplay(app, parseInt(parts[0], 10), parseInt(parts[1], 10));
    }
    for (const [cellKey, td] of app.cells) {
        if (!model.cells.has(cellKey)) {
            td.textContent = '';
            td.style.color = '';
            td.style.textAlign = '';
        }
    }
}
// ─── Selection ────────────────────────────────────────────────────────
function selectCell(app, col, row) {
    const prevTd = app.cells.get((0, model_1.key)(app.selCol, app.selRow));
    if (prevTd) {
        prevTd.style.outline = '';
    }
    app.selCol = col;
    app.selRow = row;
    const curTd = app.cells.get((0, model_1.key)(col, row));
    if (curTd) {
        curTd.style.outline = `2px solid ${config_1.COLOR_SELECTION}`;
        curTd.style.outlineOffset = '-2px';
        curTd.scrollIntoView({ block: 'nearest', inline: 'nearest' });
    }
    const cell = app.model.cells.get((0, model_1.key)(col, row));
    app.fbarInput.value = cell ? cell.raw : '';
    app.fbarRef.textContent = (0, model_1.addrToLabel)({ col, row });
    for (const [r, rh] of app.rowHeaders)
        rh.style.background = r === row ? config_1.COLOR_SELECTION : '';
    for (const [c, ch] of app.colHeaders)
        ch.style.background = c === col ? config_1.COLOR_SELECTION : '';
    // Selection headers: text white when selected
    const selRh = app.rowHeaders.get(row);
    if (selRh)
        selRh.style.color = '#fff';
    const selCh = app.colHeaders.get(col);
    if (selCh)
        selCh.style.color = '#fff';
    for (const [r, rh] of app.rowHeaders)
        if (r !== row)
            rh.style.color = '#333';
    for (const [c, ch] of app.colHeaders)
        if (c !== col)
            ch.style.color = '#333';
}
// ─── Editing ──────────────────────────────────────────────────────────
function startEdit(app) {
    const { selCol, selRow } = app;
    const cellKey = (0, model_1.key)(selCol, selRow);
    const cell = app.model.cells.get(cellKey);
    const td = app.cells.get(cellKey);
    if (!td)
        return;
    app.editing = true;
    app.editValue = cell ? cell.raw : '';
    app.fbarInput.value = app.editValue;
    td.innerHTML = '';
    const input = document.createElement('input');
    input.type = 'text';
    input.value = app.editValue;
    input.style.cssText = `width:100%;height:100%;border:none;outline:none;padding:2px 4px;font:inherit;font-family:monospace;font-size:13px;background:#fff;box-sizing:border-box;`;
    td.appendChild(input);
    input.focus();
    input.select();
    input.addEventListener('input', () => { app.fbarInput.value = input.value; app.editValue = input.value; });
    input.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
            e.preventDefault();
            commitEdit(app);
            selectCell(app, selCol, Math.min(selRow + 1, app.model.rows - 1));
        }
        else if (e.key === 'Tab') {
            e.preventDefault();
            commitEdit(app);
            selectCell(app, Math.min(selCol + 1, app.model.cols - 1), selRow);
        }
        else if (e.key === 'Escape') {
            cancelEdit(app);
        }
    });
}
function commitEdit(app) {
    if (!app.editing)
        return;
    const { selCol, selRow } = app;
    const raw = app.editValue;
    const cellKey = (0, model_1.key)(selCol, selRow);
    const td = app.cells.get(cellKey);
    if (td) {
        const inp = td.querySelector('input');
        if (inp)
            inp.remove();
    }
    app.editing = false;
    if (raw === '')
        (0, model_1.clearCell)(app.model, selCol, selRow);
    else
        (0, model_1.setCell)(app.model, selCol, selRow, raw);
    const graph = (0, engine_1.rebuildGraph)(app.model);
    (0, engine_1.recalcFrom)(app.model, graph, cellKey);
    renderAll(app);
    (0, model_1.saveModel)(app.model);
    const cell = app.model.cells.get(cellKey);
    app.fbarInput.value = cell ? cell.raw : '';
}
function cancelEdit(app) {
    if (!app.editing)
        return;
    const { selCol, selRow } = app;
    const td = app.cells.get((0, model_1.key)(selCol, selRow));
    if (td) {
        const inp = td.querySelector('input');
        if (inp)
            inp.remove();
    }
    app.editing = false;
    renderCellDisplay(app, selCol, selRow);
    const cell = app.model.cells.get((0, model_1.key)(selCol, selRow));
    app.fbarInput.value = cell ? cell.raw : '';
}
// ─── Context Menu ─────────────────────────────────────────────────────
let menuTimer = null;
let activeMenu = null;
function showHeaderMenu(e, index, type) {
    removeMenu();
    const app = window.__spreadsheetApp;
    const menu = document.createElement('div');
    menu.style.cssText = `position:fixed;left:${e.clientX}px;top:${e.clientY}px;background:${config_1.COLOR_MENU_BG};border:1px solid #dadce0;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.2);z-index:1000;padding:4px 0;min-width:180px;`;
    const labels = type === 'row'
        ? [`Insert row above ${index + 1}`, `Delete row ${index + 1}`]
        : [`Insert col left of ${(0, model_1.colToLetter)(index)}`, `Delete col ${(0, model_1.colToLetter)(index)}`];
    for (const label of labels) {
        const item = document.createElement('div');
        item.textContent = label;
        item.style.cssText = `padding:6px 16px;cursor:pointer;font-size:13px;color:${config_1.COLOR_TEXT};font-family:inherit;`;
        item.addEventListener('mouseenter', () => item.style.background = config_1.COLOR_MENU_HOVER);
        item.addEventListener('mouseleave', () => item.style.background = '');
        item.addEventListener('click', () => {
            if (label.startsWith('Insert')) {
                if (type === 'row')
                    (0, model_1.insertRow)(app.model, index);
                else
                    (0, model_1.insertCol)(app.model, index);
            }
            else {
                if (type === 'row')
                    (0, model_1.deleteRow)(app.model, index);
                else
                    (0, model_1.deleteCol)(app.model, index);
            }
            buildGrid(app);
            const graph = (0, engine_1.rebuildGraph)(app.model);
            (0, engine_1.recalcAll)(app.model, graph);
            renderAll(app);
            selectCell(app, app.selCol, app.selRow);
            (0, model_1.saveModel)(app.model);
            removeMenu();
        });
        menu.appendChild(item);
    }
    document.body.appendChild(menu);
    activeMenu = menu;
    menuTimer = setTimeout(() => removeMenu(), 3000);
}
function removeMenu() {
    if (menuTimer) {
        clearTimeout(menuTimer);
        menuTimer = null;
    }
    if (activeMenu) {
        activeMenu.remove();
        activeMenu = null;
    }
}
// Click outside to close menu
document.addEventListener('click', () => removeMenu());
// ─── Keyboard Handler ─────────────────────────────────────────────────
function setupKeyboard(app) {
    document.addEventListener('keydown', (e) => {
        if (app.editing)
            return;
        const { selCol, selRow } = app;
        switch (e.key) {
            case 'ArrowUp':
                e.preventDefault();
                selectCell(app, selCol, Math.max(0, selRow - 1));
                break;
            case 'ArrowDown':
                e.preventDefault();
                selectCell(app, selCol, Math.min(app.model.rows - 1, selRow + 1));
                break;
            case 'ArrowLeft':
                e.preventDefault();
                selectCell(app, Math.max(0, selCol - 1), selRow);
                break;
            case 'ArrowRight':
                e.preventDefault();
                selectCell(app, Math.min(app.model.cols - 1, selCol + 1), selRow);
                break;
            case 'Tab':
                e.preventDefault();
                selectCell(app, Math.min(app.model.cols - 1, selCol + 1), selRow);
                break;
            case 'Enter': {
                e.preventDefault();
                const cell = app.model.cells.get((0, model_1.key)(app.selCol, app.selRow));
                if (cell && cell.raw !== '') {
                    commitEdit(app);
                    selectCell(app, selCol, Math.min(selRow + 1, app.model.rows - 1));
                }
                else {
                    startEdit(app);
                }
                break;
            }
            case 'Delete':
            case 'Backspace':
                e.preventDefault();
                (0, model_1.clearCell)(app.model, selCol, selRow);
                const g1 = (0, engine_1.rebuildGraph)(app.model);
                (0, engine_1.recalcFrom)(app.model, g1, (0, model_1.key)(selCol, selRow));
                renderAll(app);
                app.fbarInput.value = '';
                (0, model_1.saveModel)(app.model);
                break;
            default:
                if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
                    app.editValue = e.key;
                    startEdit(app);
                    const td = app.cells.get((0, model_1.key)(selCol, selRow));
                    if (td) {
                        const inp = td.querySelector('input');
                        if (inp) {
                            inp.value = e.key;
                            app.fbarInput.value = e.key;
                        }
                    }
                }
                break;
        }
    });
}
// ─── Formula Bar ──────────────────────────────────────────────────────
function setupFormulaBar(app) {
    const input = app.fbarInput;
    input.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
            e.preventDefault();
            app.editValue = input.value;
            const { selCol, selRow } = app;
            const cellKey = (0, model_1.key)(selCol, selRow);
            if (input.value === '')
                (0, model_1.clearCell)(app.model, selCol, selRow);
            else
                (0, model_1.setCell)(app.model, selCol, selRow, input.value);
            app.editing = false;
            const graph = (0, engine_1.rebuildGraph)(app.model);
            (0, engine_1.recalcFrom)(app.model, graph, cellKey);
            renderAll(app);
            (0, model_1.saveModel)(app.model);
        }
        else if (e.key === 'Escape') {
            const cell = app.model.cells.get((0, model_1.key)(app.selCol, app.selRow));
            input.value = cell ? cell.raw : '';
        }
    });
    input.addEventListener('focus', () => {
        if (!app.editing) {
            const cell = app.model.cells.get((0, model_1.key)(app.selCol, app.selRow));
            input.value = cell ? cell.raw : '';
        }
    });
    input.addEventListener('input', () => {
        app.editValue = input.value;
        if (app.editing) {
            const td = app.cells.get((0, model_1.key)(app.selCol, app.selRow));
            if (td) {
                const ci = td.querySelector('input');
                if (ci)
                    ci.value = input.value;
            }
        }
    });
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-MTP-GGUF","provider":"brigid","persona":"typescript","sessionId":"60f28adf-ced3-497f-a00b-5e36c3daba0c","tokensIn":5402740,"tokensOut":75106,"tokensTotal":5477846,"cost":0,"turns":95,"toolCalls":99,"failedToolCalls":1,"timestamp":"2026-08-02T04:31:45.162Z"} -->