← Spreadsheet results

Spreadsheet

Qwen 3.7 Flash · typescript

Initial view of Spreadsheet

Browser check: Artifact runtime error. This is a technical check, not a quality review.

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,097 bytes · SHA-256 e1c13930ae5c
<!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 ── */
:root {
  --cell-width: 100px;
  --row-height: 24px;
  --header-bg: #f8f9fa;
  --header-border: #dee2e6;
  --grid-border: #dee2e6;
  --selection-color: #1a73e8;
  --bg: #ffffff;
  --text: #202124;
  --error-color: #d93025;
  --formula-bar-bg: #f8f9fa;
  --context-menu-bg: #ffffff;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: 'Google Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  font-size: 13px;
  color: var(--text);
  background: var(--bg);
  overflow: hidden;
  height: 100vh;
  display: flex;
  flex-direction: column;
}

#formula-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px;
  background: var(--formula-bar-bg);
  border-bottom: 1px solid var(--grid-border);
  min-height: 32px;
  flex-shrink: 0;
}

#address-label {
  font-weight: 600;
  min-width: 40px;
  text-align: center;
  color: #5f6368;
  font-size: 12px;
  user-select: none;
}

#formula-input {
  flex: 1;
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 2px 8px;
  font-family: inherit;
  font-size: 13px;
  outline: none;
  transition: border-color 0.15s;
}

#formula-input:focus {
  border-color: var(--selection-color);
}

#grid-container {
  flex: 1;
  overflow: auto;
  position: relative;
}

#grid-table {
  border-collapse: collapse;
  table-layout: fixed;
}

#grid-table th {
  background: var(--header-bg);
  border: 1px solid var(--grid-border);
  font-weight: 500;
  text-align: center;
  user-select: none;
  position: sticky;
  z-index: 2;
}

#grid-table th.col-header {
  top: 0;
  height: var(--row-height);
  min-width: var(--cell-width);
  width: var(--cell-width);
  font-size: 12px;
  color: #5f6368;
}

#grid-table th.row-header {
  left: 0;
  width: 48px;
  min-width: 48px;
  font-size: 12px;
  color: #5f6368;
  z-index: 3;
}

#grid-table th.corner {
  top: 0;
  left: 0;
  z-index: 4;
  width: 48px;
  min-width: 48px;
}

#grid-table td {
  border: 1px solid var(--grid-border);
  height: var(--row-height);
  padding: 0 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: cell;
  vertical-align: middle;
  min-width: var(--cell-width);
  max-width: var(--cell-width);
  font-size: 13px;
}

td.selected {
  outline: 2px solid var(--selection-color);
  outline-offset: -1px;
  z-index: 1;
  position: relative;
  background: rgba(26, 115, 232, 0.04);
}

td.editing {
  padding: 0;
}

td.editing input {
  width: 100%;
  height: 100%;
  border: none;
  outline: 2px solid var(--selection-color);
  outline-offset: -1px;
  padding: 0 4px;
  font-family: inherit;
  font-size: 13px;
  background: white;
}

td.text-cell { text-align: left; }
td.number-cell { text-align: right; }
td.error-cell { text-align: right; color: var(--error-color); font-weight: 500; }

#context-menu {
  position: absolute;
  background: var(--context-menu-bg);
  border: 1px solid var(--grid-border);
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
  z-index: 100;
  min-width: 160px;
  padding: 4px 0;
}

#context-menu .menu-item {
  padding: 6px 16px;
  cursor: pointer;
  font-size: 13px;
}

#context-menu .menu-item:hover {
  background: #e8eaed;
}

#context-menu .divider {
  height: 1px;
  background: var(--grid-border);
  margin: 4px 0;
}

.hidden { display: none !important; }
</style>
</head>
<body>
<div id="formula-bar">
  <span id="address-label"></span>
  <input type="text" id="formula-input">
</div>
<div id="grid-container">
  <table id="grid-table">
    <thead id="grid-head"></thead>
    <tbody id="grid-body"></tbody>
  </table>
</div>
<div id="context-menu" class="hidden"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./store.js":"src/store.ts","./ui.js":"src/ui.ts"},"src/formula-parser.ts":{"./types.js":"src/types.ts"},"src/engine.ts":{"./types.js":"src/types.ts","./formula-parser.js":"src/formula-parser.ts"},"src/store.ts":{"./types.js":"src/types.ts","./engine.js":"src/engine.ts","./formula-parser.js":"src/formula-parser.ts"},"src/ui.ts":{"./types.js":"src/types.ts","./store.js":"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 });
const store_js_1 = require("./store.js");
const ui_js_1 = require("./ui.js");
// ============================================================
// Entry point - wire up Store and UI
// DOMContentLoaded has already fired per bootstrap template
// ============================================================
const container = document.getElementById('grid-container');
const formulaBar = document.getElementById('formula-bar');
if (container && formulaBar) {
    const store = new store_js_1.CellStore();
    const ui = new ui_js_1.SpreadsheetUI(container, formulaBar, store);
    // Expose for testing via probe
    window.__spreadsheetStore = store;
    window.__spreadsheetUI = ui;
    // Load saved state
    ui.loadState();
}
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ROW_HEADER_WIDTH = exports.HEADER_HEIGHT = exports.CELL_WIDTH = exports.ROWS = exports.COLS = void 0;
exports.colIndexToLetter = colIndexToLetter;
exports.letterToColIndex = letterToColIndex;
exports.cellAddress = cellAddress;
exports.parseCellRef = parseCellRef;
exports.parseRange = parseRange;
// ============================================================
// Tunable constants
// ============================================================
exports.COLS = 26; // A-Z
exports.ROWS = 100;
exports.CELL_WIDTH = 100; // px
exports.HEADER_HEIGHT = 24; // px
exports.ROW_HEADER_WIDTH = 48; // px
// ============================================================
// Address helpers
// ============================================================
function colIndexToLetter(c) {
    let s = '';
    c++;
    while (c > 0) {
        c--;
        s = String.fromCharCode(65 + (c % 26)) + s;
        c = Math.floor(c / 26);
    }
    return s;
}
function letterToColIndex(letter) {
    let idx = 0;
    for (const ch of letter.toUpperCase()) {
        idx = idx * 26 + (ch.charCodeAt(0) - 64);
    }
    return idx - 1;
}
function cellAddress(col, row) {
    return colIndexToLetter(col) + (row + 1);
}
function parseCellRef(addr) {
    const m = addr.match(/^([A-Z]+)(\d+)$/);
    if (!m)
        return null;
    const col = letterToColIndex(m[1]);
    const row = parseInt(m[2]) - 1;
    if (col < 0 || col >= exports.COLS || row < 0 || row >= exports.ROWS)
        return null;
    return [col, row];
}
function parseRange(rangeStr) {
    // expects "ADDR:ADDR"
    const m = rangeStr.match(/^([A-Z]+\d+):([A-Z]+\d+)$/);
    if (!m)
        return null;
    const start = parseCellRef(m[1]);
    const end = parseCellRef(m[2]);
    if (!start || !end)
        return null;
    const top = Math.min(start[1], end[1]);
    const bot = Math.max(start[1], end[1]);
    const left = Math.min(start[0], end[0]);
    const right = Math.max(start[0], end[0]);
    return [left, top, right, bot];
}
};

// ── module: src/formula-parser.ts ──
__mods["src/formula-parser.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tokenize = tokenize;
exports.parseFormula = parseFormula;
function tokenize(source) {
    const tokens = [];
    let pos = 0;
    while (pos < source.length) {
        if (/\s/.test(source[pos])) {
            pos++;
            continue;
        }
        if (/[0-9]/.test(source[pos]) || (source[pos] === '.' && pos + 1 < source.length && /[0-9]/.test(source[pos + 1]))) {
            let start = pos;
            while (pos < source.length && (/[0-9]/.test(source[pos]) || source[pos] === '.'))
                pos++;
            tokens.push({ kind: 'NUMBER', value: source.slice(start, pos) });
            continue;
        }
        if (source[pos] === '"') {
            let start = pos;
            pos++;
            while (pos < source.length && source[pos] !== '"') {
                if (source[pos] === '\\')
                    pos++;
                pos++;
            }
            if (pos < source.length)
                pos++;
            tokens.push({ kind: 'STRING', value: source.slice(start + 1, pos - 1).replace(/\\"/g, '"') });
            continue;
        }
        if (source[pos] === '(') {
            tokens.push({ kind: 'LPAREN', value: '(' });
            pos++;
            continue;
        }
        if (source[pos] === ')') {
            tokens.push({ kind: 'RPAREN', value: ')' });
            pos++;
            continue;
        }
        if (source[pos] === ',') {
            tokens.push({ kind: 'COMMA', value: ',' });
            pos++;
            continue;
        }
        if (source[pos] === ':') {
            tokens.push({ kind: 'COLON', value: ':' });
            pos++;
            continue;
        }
        if ('+-*/'.includes(source[pos])) {
            tokens.push({ kind: 'OP', value: source[pos] });
            pos++;
            continue;
        }
        if (/[A-Za-z]/.test(source[pos])) {
            let start = pos;
            while (pos < source.length && /[A-Za-z]/.test(source[pos]))
                pos++;
            if (pos < source.length && /[0-9]/.test(source[pos])) {
                while (pos < source.length && /[0-9]/.test(source[pos]))
                    pos++;
                tokens.push({ kind: 'COLREF', value: source.slice(start, pos).toUpperCase() });
            }
            else {
                tokens.push({ kind: 'IDENT', value: source.slice(start, pos).toUpperCase() });
            }
            continue;
        }
        pos++;
    }
    tokens.push({ kind: 'EOF', value: '' });
    return tokens;
}
// ============================================================
// Recursive Descent Parser
// Grammar:
//   expr       -> term (('+' | '-') term)*
//   term       -> factor (('*' | '/') factor)*
//   factor     -> ['+'|'-'] primary
//   primary    -> NUMBER | STRING | IDENT(args) | COLREF | RANGE(COLREF:COLREF) | '(' expr ')'
// ============================================================
function makeNumber(v) { return { kind: 'number', value: v }; }
function makeString(v) { return { kind: 'string', value: v }; }
function makeCellref(addr) { return { kind: 'cellref', address: addr }; }
function makeRange(addr) { return { kind: 'range', address: addr }; }
function makeBinary(op, l, r) { return { kind: 'binary', op, left: l, right: r }; }
function makeUnary(op, o) { return { kind: 'unary', op, operand: o }; }
function makeFunc(name, args) { return { kind: 'func', name, args }; }
function parseFormula(source) {
    const tokens = tokenize(source);
    const lexer = { pos: 0, source };
    function peek() { return tokens[lexer.pos]; }
    function advance() { return tokens[lexer.pos++]; }
    function expect(kind, value) {
        const tok = peek();
        if (tok.kind !== kind || (value !== undefined && tok.value !== value)) {
            throw new Error(`Expected ${kind}${value ? "='" + value + "'" : ''}, got '${tok.kind}'='${tok.value}'`);
        }
        advance();
    }
    function parseExpr() {
        let left = parseTerm();
        while (peek().kind === 'OP' && (peek().value === '+' || peek().value === '-')) {
            const op = advance().value;
            const right = parseTerm();
            left = makeBinary(op, left, right);
        }
        return left;
    }
    function parseTerm() {
        let left = parseFactor();
        while (peek().kind === 'OP' && (peek().value === '*' || peek().value === '/')) {
            const op = advance().value;
            const right = parseFactor();
            left = makeBinary(op, left, right);
        }
        return left;
    }
    function parseFactor() {
        if (peek().kind === 'OP' && (peek().value === '+' || peek().value === '-')) {
            const op = advance().value;
            const operand = parseFactor();
            return makeUnary(op, operand);
        }
        return parsePrimary();
    }
    function parsePrimary() {
        const tok = peek();
        if (tok.kind === 'NUMBER') {
            advance();
            return makeNumber(parseFloat(tok.value));
        }
        if (tok.kind === 'STRING') {
            advance();
            return makeString(tok.value);
        }
        if (tok.kind === 'LPAREN') {
            advance();
            const expr = parseExpr();
            expect('RPAREN');
            return expr;
        }
        // Function call: IDENT LPAREN ...
        if (tok.kind === 'IDENT' && tokens[lexer.pos + 1]?.kind === 'LPAREN') {
            advance();
            advance(); // consume (
            const args = [];
            if (peek().kind !== 'RPAREN') {
                args.push(parseExpr());
                while (peek().kind === 'COMMA') {
                    advance();
                    args.push(parseExpr());
                }
            }
            expect('RPAREN');
            return makeFunc(tok.value, args);
        }
        // Cell reference or range
        if (tok.kind === 'COLREF') {
            advance();
            const addr = tok.value;
            if (peek().kind === 'COLON') {
                advance();
                const rightTok = peek();
                if (rightTok.kind === 'COLREF') {
                    advance();
                    return makeRange(addr + ':' + rightTok.value);
                }
                throw new Error(`Invalid range syntax near ${addr}:`);
            }
            return makeCellref(addr);
        }
        throw new Error(`Unexpected token '${tok.kind}'='${tok.value}' at position ${lexer.pos}`);
    }
    const ast = parseExpr();
    if (peek().kind !== 'EOF') {
        throw new Error(`Unexpected token after expression: '${peek().kind}'`);
    }
    return ast;
}
};

// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CalculationEngine = void 0;
const formula_parser_js_1 = require("./formula-parser.js");
function isError(v) {
    return typeof v === 'object' && v !== null && 'kind' in v && v.kind === 'error';
}
// ============================================================
// Calculation engine with dependency tracking
// ============================================================
class CalculationEngine {
    constructor() {
        // Store raw cell data
        this.store = new Map();
        // dependencyMap[cellId] = Set of cellIds this cell depends on
        this.depMap = new Map();
        // reverseDepMap[depId] = Set of cellIds that depend on it
        this.revDepMap = new Map();
    }
    evaluate(raw, resolveFn) {
        if (raw === null || raw.trim() === '')
            return '';
        if (!raw.startsWith('=')) {
            const trimmed = raw.trim();
            const num = Number(trimmed);
            return isNaN(num) ? trimmed : num;
        }
        try {
            const ast = (0, formula_parser_js_1.parseFormula)(raw.slice(1));
            const result = this.evalAst(ast, resolveFn);
            if (isError(result))
                return '';
            return result;
        }
        catch {
            return '';
        }
    }
    extractDeps(ast) {
        const deps = new Set();
        this.collectDeps(ast, deps);
        return deps;
    }
    setDependency(cellId, deps) {
        this.depMap.set(cellId, deps);
        for (const d of deps) {
            if (!this.revDepMap.has(d))
                this.revDepMap.set(d, new Set());
            this.revDepMap.get(d).add(cellId);
        }
    }
    getDependents(cellId) {
        return this.revDepMap.get(cellId) ?? new Set();
    }
    /** Get all cells (for iteration during full rebuild) */
    getAllCellIds() {
        const ids = new Set();
        for (const [k] of this.depMap)
            ids.add(k);
        for (const [k] of this.revDepMap)
            ids.add(k);
        // Only return cells that actually have data stored
        return Array.from(ids);
    }
    removeCellDeps(cellId) {
        const deps = this.depMap.get(cellId);
        if (deps) {
            for (const d of deps) {
                const r = this.revDepMap.get(d);
                if (r)
                    r.delete(cellId);
            }
        }
        this.depMap.delete(cellId);
        this.revDepMap.delete(cellId);
    }
    /** Evaluate the entire sheet given a row of updates, topologically sorted. */
    evalAst(node, resolveFn) {
        switch (node.kind) {
            case 'number': return node.value;
            case 'string': return node.value;
            case 'cellref': {
                const addr = node.address;
                const val = resolveFn(addr);
                if (val === null)
                    return { kind: 'error', message: '#REF!' };
                return val;
            }
            case 'range':
                // Only used inside functions; standalone is an error
                return { kind: 'error', message: '#ERR!' };
            case 'binary': {
                const left = this.evalAst(node.left, resolveFn);
                if (isError(left))
                    return left;
                const right = this.evalAst(node.right, resolveFn);
                if (isError(right))
                    return right;
                const l = this.asNumber(left), r = this.asNumber(right);
                switch (node.op) {
                    case '+': return l + r;
                    case '-': return l - r;
                    case '*': return l * r;
                    case '/': return r === 0 ? { kind: 'error', message: '#DIV/0!' } : l / r;
                    default: throw new Error('Unknown op');
                }
            }
            case 'unary': {
                const operand = this.evalAst(node.operand, resolveFn);
                if (isError(operand))
                    return operand;
                return (node.op === '-') ? -this.asNumber(operand) : this.asNumber(operand);
            }
            case 'func': {
                const name = node.name.toUpperCase();
                const args = node.args;
                const valsOrErr = this.flattenArgs(args, resolveFn);
                if ('kind' in valsOrErr && valsOrErr.kind === 'error')
                    return valsOrErr;
                const values = valsOrErr;
                switch (name) {
                    case 'SUM': return values.reduce((a, b) => a + b, 0);
                    case 'AVERAGE':
                    case 'AVG': return values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
                    case 'MIN': return values.length > 0 ? Math.min(...values) : 0;
                    case 'MAX': return values.length > 0 ? Math.max(...values) : 0;
                    case 'COUNT': return values.length;
                    default: return 0;
                }
            }
            default: return '';
        }
    }
    collectDeps(node, deps) {
        switch (node.kind) {
            case 'cellref':
                deps.add(node.address);
                break;
            case 'range': {
                // Collect all cells in the range
                const addr = node.address;
                const parts = addr.split(':');
                if (parts.length === 2) {
                    deps.add(parts[0]);
                    deps.add(parts[1]);
                }
                break;
            }
            case 'binary':
                this.collectDeps(node.left, deps);
                this.collectDeps(node.right, deps);
                break;
            case 'unary':
                this.collectDeps(node.operand, deps);
                break;
            case 'func':
                for (const arg of node.args)
                    this.collectDeps(arg, deps);
                break;
        }
    }
    flattenArgs(args, resolveFn) {
        const values = [];
        for (const arg of args) {
            switch (arg.kind) {
                case 'number':
                    values.push(arg.value);
                    break;
                case 'string': break;
                case 'cellref': {
                    const addr = arg.address;
                    const val = resolveFn(addr);
                    if (val === null)
                        return { kind: 'error', message: '#REF!' };
                    values.push(this.asNumber(val));
                    break;
                }
                case 'range': {
                    const valsOrErr = this.rangeValues(arg.address, resolveFn);
                    if ('kind' in valsOrErr && valsOrErr.kind === 'error')
                        return valsOrErr;
                    values.push(...valsOrErr);
                    break;
                }
                case 'binary': {
                    const res = this.evalAst(arg, resolveFn);
                    if (isError(res))
                        return res;
                    values.push(this.asNumber(res));
                    break;
                }
                case 'unary': {
                    const res = this.evalAst(arg, resolveFn);
                    if (isError(res))
                        return res;
                    values.push(this.asNumber(res));
                    break;
                }
                case 'func': {
                    const res = this.evalAst(arg, resolveFn);
                    if (isError(res))
                        return res;
                    values.push(this.asNumber(res));
                    break;
                }
            }
        }
        return values;
    }
    rangeValues(rangeStr, resolveFn) {
        const parts = rangeStr.split(':');
        if (parts.length !== 2)
            return [];
        const startM = parts[0].match(/^([A-Z]+)(\d+)$/);
        const endM = parts[1].match(/^([A-Z]+)(\d+)$/);
        if (!startM || !endM)
            return [];
        const colStart = letterToCol(startM[1]), rowStart = parseInt(startM[2]) - 1;
        const colEnd = letterToCol(endM[1]), rowEnd = parseInt(endM[2]) - 1;
        const values = [];
        for (let c = Math.min(colStart, colEnd); c <= Math.max(colStart, colEnd); c++) {
            for (let r = Math.min(rowStart, rowEnd); r <= Math.max(rowStart, rowEnd); r++) {
                const addr = colToLetter(c) + (r + 1);
                const val = resolveFn(addr);
                if (val === null)
                    return { kind: 'error', message: '#REF!' };
                if (typeof val === 'number')
                    values.push(val);
                else if (typeof val === 'boolean')
                    values.push(val ? 1 : 0);
                // strings count as 0 for SUM etc.
            }
        }
        return values;
    }
    asNumber(v) {
        if (typeof v === 'number')
            return v;
        if (typeof v === 'boolean')
            return v ? 1 : 0;
        const n = Number(v);
        return isNaN(n) ? 0 : n;
    }
}
exports.CalculationEngine = CalculationEngine;
function letterToCol(name) {
    let idx = 0;
    for (const ch of name)
        idx = idx * 26 + (ch.charCodeAt(0) - 64);
    return idx - 1;
}
function colToLetter(c) {
    let s = '';
    c++;
    while (c > 0) {
        c--;
        s = String.fromCharCode(65 + (c % 26)) + s;
        c = Math.floor(c / 26);
    }
    return s;
}
};

// ── module: src/store.ts ──
__mods["src/store.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CellStore = void 0;
const types_js_1 = require("./types.js");
const engine_js_1 = require("./engine.js");
const formula_parser_js_1 = require("./formula-parser.js");
// ============================================================
// CellStore: manages all cells, dependencies, and recalculation
// ============================================================
class CellStore {
    constructor() {
        this.data = new Map();
        this.engine = new engine_js_1.CalculationEngine();
        this.dirtyCells = new Set();
        this.pendingUpdates = new Set();
    }
    // Public accessor
    getRaw(addr) { return this.data.get(addr)?.raw ?? null; }
    setRaw(addr, raw) {
        const oldData = this.data.get(addr);
        this.data.set(addr, { raw, computed: oldData?.computed ?? '', error: null });
        this.markDirty(addr);
    }
    getValue(addr) {
        return this.data.get(addr)?.computed ?? '';
    }
    hasError(addr) {
        return !!this.data.get(addr)?.error;
    }
    /** Get all cell addresses */
    getAllKeys() {
        return Array.from(this.data.keys());
    }
    /** Recalculate a cell and its dependents in topological order */
    recalc(addr) {
        // Collect ALL affected cells including upstream dependencies
        const affected = new Set();
        this.collectUpstreamAndDownstream(addr, affected);
        const topo = this.topologicalSortFromStore(affected);
        for (const cellAddr of topo) {
            this.recalcOne(cellAddr);
        }
    }
    /** Recursively collect both upstream dependencies and downstream dependents */
    collectUpstreamAndDownstream(addr, affected) {
        if (affected.has(addr))
            return;
        affected.add(addr);
        // Follow upstream: expand full deps from AST (including range expansion)
        const cellData = this.data.get(addr);
        if (cellData && cellData.raw && cellData.raw.startsWith('=')) {
            try {
                const ast = (0, formula_parser_js_1.parseFormula)(cellData.raw.slice(1));
                const fwdDeps = this.expandFullRangeDeps(ast);
                for (const dep of fwdDeps) {
                    if (!affected.has(dep)) {
                        this.collectUpstreamAndDownstream(dep, affected);
                    }
                }
            }
            catch { /* skip parse errors */ }
        }
        // Follow downstream: use engine's public method  
        const revDeps = this.engine.getDependents(addr);
        for (const rdep of revDeps) {
            if (!affected.has(rdep)) {
                this.collectUpstreamAndDownstream(rdep, affected);
            }
        }
    }
    /** Expand all AST references into individual cell addresses (expanding ranges) */
    expandFullRangeDeps(node) {
        const deps = [];
        this._expandNode(node, deps);
        return new Set(deps);
    }
    _expandNode(node, out) {
        switch (node.kind) {
            case 'cellref':
                out.push(node.address);
                break;
            case 'range':
                this._expandRangeAddr(node.address, out);
                break;
            case 'binary':
                this._expandNode(node.left, out);
                this._expandNode(node.right, out);
                break;
            case 'unary':
                this._expandNode(node.operand, out);
                break;
            case 'func':
                for (const arg of node.args)
                    this._expandNode(arg, out);
                break;
        }
    }
    _expandRangeAddr(rangeStr, out) {
        const parts = rangeStr.split(':');
        if (parts.length !== 2)
            return;
        const sm = parts[0].match(/^([A-Z]+)(\d+)$/);
        const em = parts[1].match(/^([A-Z]+)(\d+)$/);
        if (!sm || !em)
            return;
        const cs = letterToCol(sm[1]), rs = parseInt(sm[2]) - 1;
        const ce = letterToCol(em[1]), re = parseInt(em[2]) - 1;
        for (let c = Math.min(cs, ce); c <= Math.max(cs, ce); c++) {
            for (let r = Math.min(rs, re); r <= Math.max(rs, re); r++) {
                out.push(colToLetter(c) + (r + 1));
            }
        }
    }
    /** Recalculate just one cell (given it was marked dirty) */
    recalcOne(addr) {
        const cellData = this.data.get(addr);
        if (!cellData || cellData.raw === null || cellData.raw.trim() === '') {
            if (cellData) {
                cellData.computed = '';
                cellData.error = null;
            }
            return;
        }
        // Check if this is a formula
        const isFormula = cellData.raw.startsWith('=');
        // Build resolver that reads current values from store
        const resolveFn = (a) => {
            const d = this.data.get(a);
            return d ? d.computed : null;
        };
        if (isFormula) {
            try {
                const ast = (0, formula_parser_js_1.parseFormula)(cellData.raw.slice(1));
                // Check for circular dependency
                if (this.containsCycle(cellData.raw, addr)) {
                    cellData.computed = '';
                    cellData.error = '#CYCLE!';
                    return;
                }
                // Evaluate with resolution through live values
                const result = this.engine.evalAst(ast, resolveFn);
                if (typeof result === 'object' && result !== null && 'kind' in result && result.kind === 'error') {
                    cellData.computed = '';
                    cellData.error = result.message;
                }
                else {
                    cellData.computed = result;
                    cellData.error = null;
                }
                // Update dependency graph
                const deps = this.engine.extractDeps(ast);
                this.engine.setDependency(addr, deps);
            }
            catch {
                cellData.computed = '';
                cellData.error = '#ERR!';
            }
        }
        else {
            // Plain value
            const trimmed = cellData.raw.trim();
            const num = Number(trimmed);
            cellData.computed = isNaN(num) ? trimmed : num;
            cellData.error = null;
            this.engine.setDependency(addr, new Set());
        }
    }
    containsCycle(rawFormula, cellId) {
        // Simple cycle detection: check if traversing dependencies from this cell
        // eventually leads back to it
        try {
            const ast = (0, formula_parser_js_1.parseFormula)(rawFormula.slice(1));
            const deps = this.engine.extractDeps(ast);
            // BFS/DFS from deps to see if cellId is reachable
            const visited = new Set();
            const stack = Array.from(deps);
            while (stack.length > 0) {
                const current = stack.pop();
                if (current === cellId)
                    return true;
                if (visited.has(current))
                    continue;
                visited.add(current);
                const d = this.data.get(current);
                if (d && d.raw && d.raw.startsWith('=')) {
                    const a = (0, formula_parser_js_1.parseFormula)(d.raw.slice(1));
                    const subDeps = this.engine.extractDeps(a);
                    for (const dep of subDeps) {
                        if (!visited.has(dep))
                            stack.push(dep);
                    }
                }
            }
        }
        catch {
            // If parsing fails, assume no cycle
        }
        return false;
    }
    collectDependents(startAddr) {
        const result = new Set();
        const stack = [startAddr];
        while (stack.length > 0) {
            const addr = stack.pop();
            const deps = this.engine.getDependents(addr);
            for (const d of deps) {
                if (!result.has(d)) {
                    result.add(d);
                    stack.push(d);
                }
            }
        }
        return result;
    }
    /** Build topo order using AST-based deps instead of stored deps */
    topologicalSortFromStore(cells) {
        if (cells.size === 0)
            return [];
        const adj = new Map();
        const inDeg = new Map();
        for (const cell of cells) {
            adj.set(cell, new Set());
            inDeg.set(cell, 0);
        }
        // Build edges from parsed AST dependencies
        for (const cell of cells) {
            const cellData = this.data.get(cell);
            if (!cellData || !cellData.raw || !cellData.raw.startsWith('='))
                continue;
            try {
                const ast = (0, formula_parser_js_1.parseFormula)(cellData.raw.slice(1));
                const deps = this.expandFullRangeDeps(ast);
                for (const dep of deps) {
                    if (cells.has(dep)) {
                        adj.get(dep).add(cell); // dep → cell (cell depends on dep)
                        inDeg.set(cell, (inDeg.get(cell) || 0) + 1);
                    }
                }
            }
            catch { /* skip errors */ }
        }
        // Kahn's algorithm
        const queue = [];
        for (const [cell, deg] of inDeg) {
            if (deg === 0)
                queue.push(cell);
        }
        const result = [];
        while (queue.length > 0) {
            const cell = queue.shift();
            result.push(cell);
            for (const dep of adj.get(cell)) {
                inDeg.set(dep, inDeg.get(dep) - 1);
                if (inDeg.get(dep) === 0)
                    queue.push(dep);
            }
        }
        // If not all cells were processed, they form cycles
        // Add remaining unprocessed cells to result (they'll get #CYCLE! during recalcOne)
        if (result.length < cells.size) {
            for (const cell of cells) {
                if (!result.includes(cell)) {
                    result.push(cell);
                }
            }
        }
        return result;
    }
    topologicalSort(cells) {
        if (cells.size === 0)
            return [];
        // Build adjacency within the subset
        const adj = new Map();
        const inDeg = new Map();
        for (const cell of cells) {
            adj.set(cell, new Set());
            inDeg.set(cell, 0);
        }
        for (const cell of cells) {
            const deps = this.engine.getDependents(cell); // Actually these are reverse deps
            for (const dep of deps) {
                if (cells.has(dep)) {
                    // dep depends on cell, so edge from cell -> dep
                    // But we want processing order: cells with no unprocessed deps first
                    // So edge direction: cell must be processed before dep
                    adj.get(cell).add(dep);
                    inDeg.set(dep, (inDeg.get(dep) || 0) + 1);
                }
            }
        }
        // Kahn's algorithm
        const queue = [];
        for (const [cell, deg] of inDeg) {
            if (deg === 0)
                queue.push(cell);
        }
        const result = [];
        while (queue.length > 0) {
            const cell = queue.shift();
            result.push(cell);
            for (const dep of adj.get(cell)) {
                inDeg.set(dep, inDeg.get(dep) - 1);
                if (inDeg.get(dep) === 0)
                    queue.push(dep);
            }
        }
        return result;
    }
    // ============================================================
    // Row/Column Operations with Formula Rewriting
    // ============================================================
    /** Insert a column at index `col`. All formulas referencing columns >= col shift right. */
    insertColumn(col) {
        // Shift all existing cells: A->B, B->C, etc. for columns >= col
        const keys = Array.from(this.data.keys()).sort().reverse();
        const newData = new Map();
        for (const key of keys) {
            const parsed = (0, types_js_1.parseCellRef)(key);
            if (!parsed) {
                newData.set(key, this.data.get(key));
                continue;
            }
            if (parsed[0] >= col) {
                // Shift right
                const shiftedKey = (0, types_js_1.cellAddress)(parsed[0] + 1, parsed[1]);
                newData.set(shiftedKey, { ...this.data.get(key) });
            }
            else {
                newData.set(key, this.data.get(key));
            }
        }
        this.data = newData;
        this.rebuildDependencies();
    }
    /** Delete column at index `col`. All formulas lose references to deleted column. */
    deleteColumn(col) {
        const keys = Array.from(this.data.keys());
        const newData = new Map();
        for (const key of keys) {
            const parsed = (0, types_js_1.parseCellRef)(key);
            if (!parsed) {
                newData.set(key, this.data.get(key));
                continue;
            }
            if (parsed[0] === col) {
                // Remove this cell entirely
                continue;
            }
            else if (parsed[0] > col) {
                // Shift left
                const shiftedKey = (0, types_js_1.cellAddress)(parsed[0] - 1, parsed[1]);
                newData.set(shiftedKey, { ...this.data.get(key) });
            }
            else {
                newData.set(key, this.data.get(key));
            }
        }
        this.data = newData;
        this.rebuildDependencies();
    }
    /** Insert a row at index `row`. */
    insertRow(row) {
        const keys = Array.from(this.data.keys()).sort().reverse();
        const newData = new Map();
        for (const key of keys) {
            const parsed = (0, types_js_1.parseCellRef)(key);
            if (!parsed) {
                newData.set(key, this.data.get(key));
                continue;
            }
            if (parsed[1] >= row) {
                const shiftedKey = (0, types_js_1.cellAddress)(parsed[0], parsed[1] + 1);
                newData.set(shiftedKey, { ...this.data.get(key) });
            }
            else {
                newData.set(key, this.data.get(key));
            }
        }
        this.data = newData;
        this.rebuildDependencies();
    }
    /** Delete row at index `row`. */
    deleteRow(row) {
        const keys = Array.from(this.data.keys());
        const newData = new Map();
        for (const key of keys) {
            const parsed = (0, types_js_1.parseCellRef)(key);
            if (!parsed) {
                newData.set(key, this.data.get(key));
                continue;
            }
            if (parsed[1] === row)
                continue; // skip deleted row
            else if (parsed[1] > row) {
                const shiftedKey = (0, types_js_1.cellAddress)(parsed[0], parsed[1] - 1);
                newData.set(shiftedKey, { ...this.data.get(key) });
            }
            else {
                newData.set(key, this.data.get(key));
            }
        }
        this.data = newData;
        this.rebuildDependencies();
    }
    rebuildDependencies() {
        // Re-clear all deps and rebuild from scratch
        // This is safe because recalcOne also updates deps
        this.engine = new engine_js_1.CalculationEngine();
        // Recompute all cells from their stored raw values
        for (const addr of Array.from(this.data.keys())) {
            this.recalcOne(addr);
        }
    }
    clear(addr) {
        const old = this.data.get(addr);
        this.data.delete(addr);
        if (old)
            this.recalc(addr); // notify dependents
    }
    markDirty(addr) {
        this.pendingUpdates.add(addr);
    }
    flush() {
        for (const addr of this.pendingUpdates) {
            this.recalcOne(addr);
            this.recalc(addr);
        }
        this.pendingUpdates.clear();
    }
    /** Serialize entire state */
    serialize() {
        const obj = {};
        for (const [key, cell] of this.data) {
            obj[key] = cell.raw;
        }
        return obj;
    }
    /** Restore from serialized state */
    deserialize(data) {
        this.data.clear();
        for (const [key, raw] of Object.entries(data)) {
            this.data.set(key, { raw, computed: '', error: null });
        }
        // Full re-eval
        for (const addr of Array.from(this.data.keys())) {
            this.recalcOne(addr);
        }
    }
    /** Clear all data and dependency graph (for testing) */
    /** Clear all data and dependency graph (for testing) */
    clearAll() {
        this.data.clear();
        this.engine = new engine_js_1.CalculationEngine();
        this.pendingUpdates.clear();
    }
}
exports.CellStore = CellStore;
// ============================================================
// Range helper functions
// ============================================================
function letterToCol(name) {
    let idx = 0;
    for (const ch of name)
        idx = idx * 26 + (ch.charCodeAt(0) - 64);
    return idx - 1;
}
function colToLetter(c) {
    let s = '';
    c++;
    while (c > 0) {
        c--;
        s = String.fromCharCode(65 + (c % 26)) + s;
        c = Math.floor(c / 26);
    }
    return s;
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpreadsheetUI = void 0;
const types_js_1 = require("./types.js");
// ============================================================
// Grid UI: table creation, selection, editing, events
// ============================================================
class SpreadsheetUI {
    constructor(container, formulaBar, store) {
        this.selectedCol = 0;
        this.selectedRow = 0;
        this.editingCell = null;
        this.editingInput = null;
        this._fiHandler = null;
        this.store = store;
        this.container = container;
        this.formulaInput = formulaBar.querySelector('#formula-input');
        this.addressLabel = formulaBar.querySelector('#address-label');
        this.contextMenu = document.getElementById('context-menu');
        this.table = document.getElementById('grid-table');
        this.head = document.getElementById('grid-head');
        this.body = document.getElementById('grid-body');
        this.buildGrid();
        this.bindEvents();
        this.selectCell(0, 0);
    }
    setOnCellValueChange(fn) {
        this.onCellValueChange = fn;
    }
    buildGrid() {
        // Column headers row
        const headerRow = document.createElement('tr');
        const corner = document.createElement('th');
        corner.className = 'corner';
        headerRow.appendChild(corner);
        for (let c = 0; c < types_js_1.COLS; c++) {
            const th = document.createElement('th');
            th.className = 'col-header';
            th.style.width = types_js_1.CELL_WIDTH + 'px';
            th.textContent = (0, types_js_1.colIndexToLetter)(c);
            th.addEventListener('contextmenu', (e) => { e.preventDefault(); this.showColumnMenu(e, c); });
            headerRow.appendChild(th);
        }
        this.head.appendChild(headerRow);
        // Body rows
        for (let r = 0; r < types_js_1.ROWS; r++) {
            const tr = document.createElement('tr');
            const rowHeader = document.createElement('td');
            rowHeader.className = 'row-header';
            rowHeader.style.width = types_js_1.ROW_HEADER_WIDTH + 'px';
            rowHeader.style.height = types_js_1.HEADER_HEIGHT + 'px';
            rowHeader.textContent = String(r + 1);
            rowHeader.addEventListener('contextmenu', (e) => { e.preventDefault(); this.showRowMenu(e, r); });
            tr.appendChild(rowHeader);
            for (let c = 0; c < types_js_1.COLS; c++) {
                const td = document.createElement('td');
                td.id = 'cell-' + (0, types_js_1.cellAddress)(c, r);
                td.style.height = types_js_1.HEADER_HEIGHT + 'px';
                td.addEventListener('click', () => {
                    if (this.editingInput)
                        this.commitEdit();
                    this.selectCell(c, r);
                });
                td.addEventListener('dblclick', () => {
                    this.startEditCell(td, c, r);
                });
                td.addEventListener('contextmenu', (e) => { e.preventDefault(); this.showCellContextMenu(e, c, r); });
                tr.appendChild(td);
            }
            this.body.appendChild(tr);
        }
    }
    selectCell(col, row) {
        this.selectedCol = col;
        this.selectedRow = row;
        const addr = (0, types_js_1.cellAddress)(col, row);
        const prevSelected = this.table.querySelector('td.selected');
        if (prevSelected)
            prevSelected.classList.remove('selected');
        const cell = this.table.querySelector('#cell-' + addr);
        if (cell)
            cell.classList.add('selected');
        this.addressLabel.textContent = addr;
        this.formulaInput.value = this.store.getRaw(addr) ?? '';
        cell?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
        this.refreshCellDisplay(cell);
    }
    refreshCellDisplay(cell) {
        if (!cell)
            return;
        const id = cell.id.replace('cell-', '');
        const addrMatch = id.match(/^([A-Z]+)(\d+)$/);
        if (!addrMatch)
            return;
        const colName = addrMatch[1];
        const rowNum = parseInt(addrMatch[2]);
        const colIdx = (0, types_js_1.letterToColIndex)(colName);
        const rowIdx = rowNum - 1;
        const addr = (0, types_js_1.cellAddress)(colIdx, rowIdx);
        const cellData = this.store['data'].get(addr);
        const hasError = !!cellData?.error;
        const val = cellData?.computed ?? '';
        const isSelected = cell.classList.contains('selected');
        // Show error message in cell if present, otherwise show computed value
        const displayText = hasError && cellData.error ? cellData.error : String(val);
        cell.textContent = displayText;
        var classes = [];
        if (isSelected)
            classes.push('selected');
        if (hasError)
            classes.push('error-cell');
        else if (typeof val === 'number')
            classes.push('number-cell');
        else
            classes.push('text-cell');
        cell.className = classes.join(' ');
    }
    startEditCell(cell, col, row) {
        this.stopEditing();
        this.editingCell = cell;
        cell.classList.add('editing');
        const input = document.createElement('input');
        input.type = 'text';
        cell.appendChild(input);
        this.editingInput = input;
        const addr = (0, types_js_1.cellAddress)(col, row);
        input.value = this.store.getRaw(addr) ?? '';
        input.focus();
        input.select();
        input.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                this.commitMoveDown(col, row);
            }
            else if (e.key === 'Tab') {
                e.preventDefault();
                this.commitMoveRight(col, row);
            }
            else if (e.key === 'Escape') {
                this.cancelEdit();
            }
        });
        input.addEventListener('input', () => {
            this.formulaInput.value = input.value;
        });
        // Sync from formula bar back to input
        this._fiHandler = () => { input.value = this.formulaInput.value; };
        this.formulaInput.addEventListener('input', this._fiHandler);
    }
    commitEdit() {
        if (!this.editingInput || !this.editingCell)
            return;
        const addr = this.editingCell.id.replace('cell-', '');
        const newRaw = this.editingInput.value.trim() !== '' ? this.editingInput.value : null;
        this.store.setRaw(addr, newRaw);
        this.recalcAndRender(addr);
    }
    cancelEdit() {
        if (this.editingInput && this.editingCell) {
            if (this.editingCell.contains(this.editingInput)) {
                this.editingCell.removeChild(this.editingInput);
            }
            this.editingCell.classList.remove('editing');
        }
        this.editingInput = null;
        this.editingCell = null;
        if (this._fiHandler) {
            this.formulaInput.removeEventListener('input', this._fiHandler);
            this._fiHandler = null;
        }
        const addr = (0, types_js_1.cellAddress)(this.selectedCol, this.selectedRow);
        this.formulaInput.value = this.store.getRaw(addr) ?? '';
    }
    commitMoveDown(col, row) {
        this.commitEdit();
        this._fiHandler = null;
        this.selectCell(col, Math.min(row + 1, types_js_1.ROWS - 1));
    }
    commitMoveRight(col, row) {
        this.commitEdit();
        this._fiHandler = null;
        this.selectCell(Math.min(col + 1, types_js_1.COLS - 1), row);
    }
    recalcAndRender(startAddr) {
        this.store.recalc(startAddr);
        // Refresh display of changed cells
        for (const key of Object.keys(this.store.serialize())) {
            const cell = this.table.querySelector('#cell-' + key);
            if (cell)
                this.refreshCellDisplay(cell);
        }
    }
    stopEditing() {
        if (this.editingInput && this.editingCell) {
            if (this.editingCell.contains(this.editingInput)) {
                this.editingCell.removeChild(this.editingInput);
            }
            this.editingCell.classList.remove('editing');
        }
        this.editingInput = null;
        this.editingCell = null;
        this._fiHandler = null;
    }
    handleKeyDown(e) {
        if (this.editingInput)
            return false;
        switch (e.key) {
            case 'ArrowUp':
                e.preventDefault();
                this.selectCell(this.selectedCol, Math.max(0, this.selectedRow - 1));
                return true;
            case 'ArrowDown':
                e.preventDefault();
                this.selectCell(this.selectedCol, Math.min(types_js_1.ROWS - 1, this.selectedRow + 1));
                return true;
            case 'ArrowLeft':
                e.preventDefault();
                this.selectCell(Math.max(0, this.selectedCol - 1), this.selectedRow);
                return true;
            case 'ArrowRight':
                e.preventDefault();
                this.selectCell(Math.min(types_js_1.COLS - 1, this.selectedCol + 1), this.selectedRow);
                return true;
            case 'Enter':
                e.preventDefault();
                this.startEditInCurrentCell(false);
                return true;
            case 'Tab':
                e.preventDefault();
                if (e.shiftKey) {
                    this.selectCell(Math.max(0, this.selectedCol - 1), this.selectedRow);
                }
                else {
                    this.selectCell(Math.min(types_js_1.COLS - 1, this.selectedCol + 1), this.selectedRow);
                }
                return true;
            case 'Delete':
                if (!e.ctrlKey && !e.metaKey) {
                    this.clearCurrentCell();
                    e.preventDefault();
                    return true;
                }
                break;
            case 'F2':
                e.preventDefault();
                this.startEditInCurrentCell(false);
                return true;
        }
        if (e.ctrlKey || e.metaKey) {
            if (e.key === 's') {
                e.preventDefault();
                this.saveState();
            }
            return false;
        }
        if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
            this.startEditInCurrentCell(true, e.key);
            return true;
        }
        return false;
    }
    startEditInCurrentCell(_isEnter, initialChar) {
        const cell = this.table.querySelector('#cell-' + (0, types_js_1.cellAddress)(this.selectedCol, this.selectedRow));
        if (!cell)
            return;
        this.stopEditing();
        this.startEditCell(cell, this.selectedCol, this.selectedRow);
        if (initialChar) {
            this.editingInput.value = initialChar;
            this.formulaInput.value = initialChar;
        }
        if (this.editingInput) {
            this.editingInput.focus();
            this.editingInput.select();
        }
    }
    clearCurrentCell() {
        const addr = (0, types_js_1.cellAddress)(this.selectedCol, this.selectedRow);
        const oldRaw = this.store.getRaw(addr);
        this.store.setRaw(addr, null);
        this.store.recalc(addr);
        const cell = this.table.querySelector('#cell-' + addr);
        if (cell) {
            cell.textContent = '';
            cell.className = 'selected text-cell';
        }
        this.formulaInput.value = '';
    }
    saveState() {
        localStorage.setItem('spreadsheet-data', JSON.stringify(this.store.serialize()));
    }
    loadState() {
        const saved = localStorage.getItem('spreadsheet-data');
        if (saved) {
            try {
                this.store.deserialize(JSON.parse(saved));
                // Full grid refresh after deserialization
                for (const addr of this.store.getAllKeys()) {
                    const cell = this.table.querySelector(`#cell-${addr}`);
                    if (cell)
                        this.refreshCellDisplay(cell);
                }
            }
            catch { /* ignore corrupt data */ }
        }
    }
    showCellContextMenu(e, col, row) {
        this.showMenu(e.clientX, e.clientY, [
            { label: 'Insert Row Above', action: () => this.insertRowAbove(row) },
            { label: 'Insert Row Below', action: () => this.insertRowBelow(row) },
            { label: 'Delete Row', action: () => this.deleteRowOp(row) },
            { divider: true },
            { label: 'Clear Cell', action: () => { this.clearCurrentCell(); } },
        ]);
    }
    showColumnMenu(e, col) {
        this.showMenu(e.clientX, e.clientY, [
            { label: 'Insert Column Left', action: () => this.insertColLeft(col) },
            { label: 'Insert Column Right', action: () => this.insertColRight(col) },
            { label: 'Delete Column', action: () => this.deleteColOp(col) },
        ]);
    }
    showRowMenu(e, row) {
        this.showMenu(e.clientX, e.clientY, [
            { label: 'Insert Row Above', action: () => this.insertRowAbove(row) },
            { label: 'Insert Row Below', action: () => this.insertRowBelow(row) },
            { label: 'Delete Row', action: () => this.deleteRowOp(row) },
        ]);
    }
    showMenu(x, y, items) {
        this.hideContextMenu();
        for (const item of items) {
            if (item.divider) {
                const div = document.createElement('div');
                div.className = 'divider';
                this.contextMenu.appendChild(div);
            }
            else if (item.label && item.action) {
                const el = document.createElement('div');
                el.className = 'menu-item';
                el.textContent = item.label;
                el.addEventListener('click', () => {
                    this.hideContextMenu();
                    item.action();
                });
                this.contextMenu.appendChild(el);
            }
        }
        // Ensure menu stays within viewport
        const rect = this.contextMenu.getBoundingClientRect();
        if (rect.width > 0 && x + rect.width > window.innerWidth)
            x -= rect.width;
        if (y + 200 > window.innerHeight)
            y = window.innerHeight - 200;
        this.contextMenu.style.left = x + 'px';
        this.contextMenu.style.top = y + 'px';
        this.contextMenu.classList.remove('hidden');
    }
    hideContextMenu() {
        this.contextMenu.innerHTML = '';
        this.contextMenu.classList.add('hidden');
    }
    insertRowAbove(row) { this._insertRow(row); }
    insertRowBelow(row) { this._insertRow(row + 1); }
    deleteRowOp(row) { this.store.deleteRow(row); this.rebuildAll(); this.saveState(); }
    insertColLeft(col) { this._insertCol(col); }
    insertColRight(col) { this._insertCol(col + 1); }
    deleteColOp(col) { this.store.deleteColumn(col); this.rebuildAll(); this.saveState(); }
    _insertRow(row) {
        this.stopEditing();
        this.store.insertRow(row);
        this.rebuildAll();
        this.saveState();
    }
    _insertCol(col) {
        this.stopEditing();
        this.store.insertColumn(col);
        this.rebuildAll();
        this.saveState();
    }
    rebuildAll() {
        // Refresh all cells that have stored data
        for (const addr of this.store.getAllKeys()) {
            const cell = this.table.querySelector(`#cell-${addr}`);
            if (cell)
                this.refreshCellDisplay(cell);
        }
    }
    /** Refresh display for every single cell in the grid */
    refreshAllCells() {
        for (let r = 0; r < types_js_1.ROWS; r++) {
            for (let c = 0; c < types_js_1.COLS; c++) {
                const addr = (0, types_js_1.cellAddress)(c, r);
                const cell = this.table.querySelector(`#cell-${addr}`);
                if (cell)
                    this.refreshCellDisplay(cell);
            }
        }
    }
    bindEvents() {
        window.addEventListener('keydown', (e) => {
            if (this.handleKeyDown(e))
                return;
            if (e.key === 'Escape') {
                this.stopEditing();
                this.hideContextMenu();
            }
        });
        document.addEventListener('click', (e) => {
            if (!this.contextMenu.contains(e.target)) {
                this.hideContextMenu();
            }
        });
        // Formula bar sync
        let fiTimer = null;
        this.formulaInput.addEventListener('input', () => {
            if (this.editingInput) {
                this.editingInput.value = this.formulaInput.value;
            }
        });
        this.formulaInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                const addr = (0, types_js_1.cellAddress)(this.selectedCol, this.selectedRow);
                const newVal = this.formulaInput.value.trim() !== '' ? this.formulaInput.value : null;
                this.store.setRaw(addr, newVal);
                this.recalcAndRender(addr);
                if (this.editingInput)
                    this.cancelEdit();
            }
            else if (e.key === 'Escape') {
                const addr = (0, types_js_1.cellAddress)(this.selectedCol, this.selectedRow);
                this.formulaInput.value = this.store.getRaw(addr) ?? '';
                if (this.editingInput)
                    this.cancelEdit();
            }
        });
        // Save state on any change
        const origSetRaw = this.store.setRaw.bind(this.store);
        this.store.setRaw = (addr, raw) => {
            origSetRaw(addr, raw);
            if (fiTimer)
                clearTimeout(fiTimer);
            fiTimer = setTimeout(() => this.saveState(), 500);
        };
    }
}
exports.SpreadsheetUI = SpreadsheetUI;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.7-flash","provider":"openrouter","persona":"typescript","sessionId":"e8feda4f-fc46-4cd0-b36b-31becf973955","tokensIn":12139620,"tokensOut":80869,"tokensTotal":12220489,"cost":0.4369330939999999,"turns":167,"toolCalls":166,"failedToolCalls":8,"timestamp":"2026-08-02T03:26:26.747Z"} -->