← Spreadsheet results

Spreadsheet

thinkingcap qwen3.6 27b · 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 53,872 bytes · SHA-256 4c058ddbcf5a
<!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; }
html, body { height: 100%; overflow: hidden; font-family: 'Segoe UI', Roboto, sans-serif; background: #ffffff; color: #202124; }
#app { display: flex; flex-direction: column; height: 100vh; }
#toolbar button { padding: 4px 10px; font-size: 12px; border: 1px solid #dadce0; background: #ffffff; cursor: pointer; border-radius: 3px; color: #5f6368; }
#toolbar button:hover { background: #f1f3f4; }
</style>
</head>
<body>
<div id="toolbar" style="display:flex;align-items:center;padding:4px 8px;background:#edf2fa;border-bottom:1px solid #e2e8f0;gap:6px;">
  <button id="btn-insert-row">Insert Row</button>
  <button id="btn-delete-row">Delete Row</button>
  <button id="btn-insert-col">Insert Col</button>
  <button id="btn-delete-col">Delete Col</button>
  <span style="flex:1"></span>
  <button id="btn-clear-all" title="Clear all cells">Clear All</button>
</div>
<div id="formula-bar-row" style="display:flex;align-items:center;padding:4px 8px;background:#ffffff;border-bottom:2px solid #e2e8f0;gap:4px;">
  <span id="cell-ref" style="font-weight:600;font-size:13px;color:#5f6368;width:40px;text-align:center;font-family:'Segoe UI',Roboto,sans-serif;">A1</span>
  <input id="formula-bar" type="text" placeholder="Formula bar" style="flex:1;height:26px;border:1px solid #dadce0;padding:0 8px;font-size:13px;font-family:'Segoe UI',Roboto,sans-serif;outline:none;">
</div>
<div id="grid-container" style="overflow:auto;flex:1;"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./state":"src/state.ts","./renderer":"src/renderer.ts","./input":"src/input.ts"},"src/types.ts":{"./config":"src/config.ts"},"src/parser.ts":{"./types":"src/types.ts","./config":"src/config.ts"},"src/state.ts":{"./config":"src/config.ts","./types":"src/types.ts","./parser":"src/parser.ts"},"src/renderer.ts":{"./config":"src/config.ts","./state":"src/state.ts","./types":"src/types.ts"},"src/input.ts":{"./config":"src/config.ts","./state":"src/state.ts","./renderer":"src/renderer.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 config_1 = require("./config");
const state_1 = require("./state");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
function main() {
    const state = new state_1.SheetState();
    const container = document.getElementById('grid-container');
    const renderer = new renderer_1.Renderer(container, state);
    const input = new input_1.InputHandler(state, renderer);
    // Formula bar setup
    const formulaBarInput = document.getElementById('formula-bar');
    input.setFormulaBar(formulaBarInput);
    // Toolbar buttons
    const btnInsertRow = document.getElementById('btn-insert-row');
    const btnDeleteRow = document.getElementById('btn-delete-row');
    const btnInsertCol = document.getElementById('btn-insert-col');
    const btnDeleteCol = document.getElementById('btn-delete-col');
    const btnClearAll = document.getElementById('btn-clear-all');
    if (btnInsertRow) {
        btnInsertRow.addEventListener('click', () => {
            const row = renderer.getSelectedRow();
            state.insertRow(row + 1); // insert before current row (1-indexed for the function)
            renderer.updateAllCells();
            renderer.selectCell(renderer.getSelectedCol(), Math.min(row, config_1.ROWS - 1));
        });
    }
    if (btnDeleteRow) {
        btnDeleteRow.addEventListener('click', () => {
            const row = renderer.getSelectedRow();
            state.deleteRow(row);
            renderer.updateAllCells();
            renderer.selectCell(renderer.getSelectedCol(), Math.min(row, config_1.ROWS - 1));
        });
    }
    if (btnInsertCol) {
        btnInsertCol.addEventListener('click', () => {
            const col = renderer.getSelectedCol();
            state.insertCol(col + 1);
            renderer.updateAllCells();
            renderer.selectCell(Math.min(col, config_1.COLS - 1), renderer.getSelectedRow());
        });
    }
    if (btnDeleteCol) {
        btnDeleteCol.addEventListener('click', () => {
            const col = renderer.getSelectedCol();
            state.deleteCol(col);
            renderer.updateAllCells();
            renderer.selectCell(Math.min(col, config_1.COLS - 1), renderer.getSelectedRow());
        });
    }
    if (btnClearAll) {
        btnClearAll.addEventListener('click', () => {
            state.clear();
            renderer.updateAllCells();
            formulaBarInput.value = '';
        });
    }
    // Load saved state
    try {
        const saved = localStorage.getItem('spreadsheet-data');
        if (saved) {
            state.deserialize(saved);
            renderer.updateAllCells();
        }
    }
    catch {
        // ignore
    }
    // Select A1 initially
    renderer.selectCell(0, 0);
    input.init();
}
main();
};

// ── 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_DIV0 = exports.ERR_REF = exports.ERR_CYCLE = exports.COLORS = exports.HEADER_HEIGHT = exports.HEADER_WIDTH = exports.CELL_HEIGHT = exports.CELL_WIDTH = exports.ROWS = exports.COLS = void 0;
// Tunable constants
exports.COLS = 26;
exports.ROWS = 100;
exports.CELL_WIDTH = 100;
exports.CELL_HEIGHT = 28;
exports.HEADER_WIDTH = 45;
exports.HEADER_HEIGHT = 25;
// Colors
exports.COLORS = {
    bg: '#ffffff',
    headerBg: '#f8fafc',
    headerBorder: '#e2e8f0',
    gridLine: '#e2e8f0',
    selectionBorder: '#1a73e8',
    selectedCellBg: '#e8f0fe',
    errorColor: '#d32f2f',
    textPrimary: '#202124',
    textSecondary: '#5f6368',
    formulaBarBg: '#ffffff',
    toolbarBg: '#edf2fa',
};
// Error strings
exports.ERR_CYCLE = '#CYCLE!';
exports.ERR_REF = '#REF!';
exports.ERR_DIV0 = '#DIV/0!';
exports.ERR_PARSE = '#ERR!';
};

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

// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Parser = void 0;
exports.parseFormula = parseFormula;
const config_1 = require("./config");
function tokenize(input) {
    const tokens = [];
    let i = 0;
    const s = input.trim();
    while (i < s.length) {
        // Skip whitespace
        if (s[i] === ' ') {
            i++;
            continue;
        }
        // Number (including decimals and leading minus handled by parser)
        if ((s[i] >= '0' && s[i] <= '9') || s[i] === '.') {
            let num = '';
            while (i < s.length && ((s[i] >= '0' && s[i] <= '9') || s[i] === '.')) {
                num += s[i++];
            }
            tokens.push({ type: 'NUMBER', value: num });
            continue;
        }
        // Cell reference (A1, Z100) or function name
        if (s[i] >= 'A' && s[i] <= 'Z') {
            let word = '';
            while (i < s.length && ((s[i] >= 'A' && s[i] <= 'Z') || (s[i] >= 'a' && s[i] <= 'z'))) {
                word += s[i++];
            }
            // Check if followed by digits → cell ref
            if (i < s.length && s[i] >= '0' && s[i] <= '9') {
                let num = '';
                while (i < s.length && s[i] >= '0' && s[i] <= '9') {
                    num += s[i++];
                }
                tokens.push({ type: 'CELLREF', value: word.toUpperCase() + num });
            }
            else {
                // Function name
                const upper = word.toUpperCase();
                if (['SUM', 'AVG', 'AVERAGE', 'MIN', 'MAX', 'COUNT'].includes(upper)) {
                    tokens.push({ type: 'FUNC', value: upper === 'AVG' ? 'AVERAGE' : upper });
                }
                else {
                    // Unknown identifier → treat as text error
                    throw new Error('Unknown identifier');
                }
            }
            continue;
        }
        // Operators and punctuation
        switch (s[i]) {
            case '+':
                tokens.push({ type: 'PLUS', value: '+' });
                i++;
                break;
            case '-':
                tokens.push({ type: 'MINUS', value: '-' });
                i++;
                break;
            case '*':
                tokens.push({ type: 'STAR', value: '*' });
                i++;
                break;
            case '/':
                tokens.push({ type: 'SLASH', value: '/' });
                i++;
                break;
            case '(':
                tokens.push({ type: 'LPAREN', value: '(' });
                i++;
                break;
            case ')':
                tokens.push({ type: 'RPAREN', value: ')' });
                i++;
                break;
            case ',':
                tokens.push({ type: 'COMMA', value: ',' });
                i++;
                break;
            case ':':
                tokens.push({ type: 'RANGE_OP', value: ':' });
                i++;
                break;
            default: throw new Error('Unexpected character');
        }
    }
    return tokens;
}
// --- Recursive Descent Parser ---
class Parser {
    constructor(tokens) {
        this.pos = 0;
        this.refs = []; // collected cell references
        this.tokens = tokens;
    }
    peek() { return this.tokens[this.pos]; }
    consume(expected) {
        const t = this.tokens[this.pos];
        if (t && (expected === undefined || t.type === expected)) {
            this.pos++;
            return t;
        }
        throw new Error(`Expected ${expected ?? 'token'}, got ${t?.type}`);
    }
    parse() {
        const node = this.parseExpression();
        if (this.pos < this.tokens.length) {
            throw new Error('Unexpected token after expression');
        }
        return node;
    }
    // Expression → Additive
    parseExpression() {
        return this.parseAdditive();
    }
    // Additive → Multiplicative (('+' | '-') Multiplicative)*
    parseAdditive() {
        let left = this.parseMultiplicative();
        while (this.peek() && (this.peek().type === 'PLUS' || this.peek().type === 'MINUS')) {
            const op = this.consume().value;
            const right = this.parseMultiplicative();
            left = { kind: 'binary', op, left, right };
        }
        return left;
    }
    // Multiplicative → Unary (('*' | '/') Unary)*
    parseMultiplicative() {
        let left = this.parseUnary();
        while (this.peek() && (this.peek().type === 'STAR' || this.peek().type === 'SLASH')) {
            const op = this.consume().value;
            const right = this.parseUnary();
            left = { kind: 'binary', op, left, right };
        }
        return left;
    }
    // Unary → '-' Unary | Primary
    parseUnary() {
        if (this.peek() && this.peek().type === 'MINUS') {
            this.consume();
            const operand = this.parseUnary();
            return { kind: 'unaryMinus', operand };
        }
        return this.parsePrimary();
    }
    // Primary → NUMBER | CELLREF [':' CELLREF] | FUNC '(' args ')' | '(' Expression ')'
    parsePrimary() {
        const t = this.peek();
        if (!t)
            throw new Error('Unexpected end of expression');
        switch (t.type) {
            case 'NUMBER': {
                this.consume();
                return { kind: 'literal', value: parseFloat(t.value) };
            }
            case 'CELLREF': {
                const ref = t.value;
                this.consume();
                if (this.peek() && this.peek().type === 'RANGE_OP') {
                    this.consume(); // consume ':'
                    const endRef = this.consume('CELLREF').value;
                    return this.parseRange(ref, endRef);
                }
                return this.parseCellRefNode(ref);
            }
            case 'FUNC': {
                const name = t.value;
                this.consume();
                this.consume('LPAREN');
                const args = [];
                if (this.peek() && this.peek().type !== 'RPAREN') {
                    args.push(this.parseExpression());
                    while (this.peek() && this.peek().type === 'COMMA') {
                        this.consume(); // consume ','
                        args.push(this.parseExpression());
                    }
                }
                this.consume('RPAREN');
                return { kind: 'func', name, args };
            }
            case 'LPAREN': {
                this.consume();
                const expr = this.parseExpression();
                this.consume('RPAREN');
                return expr;
            }
            default:
                throw new Error(`Unexpected token: ${t.type}`);
        }
    }
    parseCellRefNode(ref) {
        const match = ref.match(/^([A-Z]+)(\d+)$/);
        if (!match)
            throw new Error('Invalid cell reference');
        const col = this.colNameToNum(match[1]);
        const row = parseInt(match[2], 10) - 1; // 0-indexed
        if (col < 0 || col >= config_1.COLS || row < 0 || row >= config_1.ROWS)
            throw new Error('Cell out of range');
        this.refs.push(ref);
        return { kind: 'cellRef', col, row };
    }
    parseRange(startRef, endRef) {
        const sm = startRef.match(/^([A-Z]+)(\d+)$/);
        const em = endRef.match(/^([A-Z]+)(\d+)$/);
        if (!sm || !em)
            throw new Error('Invalid range');
        const sc = this.colNameToNum(sm[1]);
        const sr = parseInt(sm[2], 10) - 1;
        const ec = this.colNameToNum(em[1]);
        const er = parseInt(em[2], 10) - 1;
        if (sc < 0 || sc >= config_1.COLS || sr < 0 || sr >= config_1.ROWS || ec < 0 || ec >= config_1.COLS || er < 0 || er >= config_1.ROWS) {
            throw new Error('Range out of range');
        }
        this.refs.push(startRef);
        this.refs.push(endRef);
        return { kind: 'range', startCol: sc, startRow: sr, endCol: ec, endRow: er };
    }
    colNameToNum(name) {
        let n = 0;
        for (let i = 0; i < name.length; i++) {
            n = n * 26 + (name.charCodeAt(i) - 64);
        }
        return n - 1; // A=0, B=1, ...
    }
    static colNumToName(n) {
        let name = '';
        while (n >= 0) {
            name = String.fromCharCode(65 + (n % 26)) + name;
            n = Math.floor(n / 26) - 1;
        }
        return name || 'A';
    }
    static rowIdxToNum(idx) {
        return idx + 1; // 0-indexed → 1-indexed
    }
    static cellId(col, row) {
        return Parser.colNumToName(col) + Parser.rowIdxToNum(row);
    }
}
exports.Parser = Parser;
// Public API
function parseFormula(input) {
    try {
        const tokens = tokenize(input);
        if (tokens.length === 0)
            return null;
        const parser = new Parser(tokens);
        const ast = parser.parse();
        return { ast, refs: parser.refs };
    }
    catch {
        return null;
    }
}
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SheetState = void 0;
const config_1 = require("./config");
const types_1 = require("./types");
const parser_1 = require("./parser");
// --- Sheet State ---
class SheetState {
    constructor() {
        this.cells = [];
        for (let c = 0; c < config_1.COLS; c++) {
            const col = [];
            for (let r = 0; r < config_1.ROWS; r++)
                col.push(undefined);
            this.cells.push(col);
        }
        this.astCache = new Map();
        this.dependents = new Map();
        this.dependencies = new Map();
    }
    id(c, r) {
        return parser_1.Parser.cellId(c, r);
    }
    getCell(col, row) {
        if (col < 0 || col >= config_1.COLS || row < 0 || row >= config_1.ROWS)
            return undefined;
        return this.cells[col][row];
    }
    hasRaw(col, row) {
        const cell = this.getCell(col, row);
        return !!cell && cell.raw !== '';
    }
    // Set a cell's raw value and recalculate dependents
    // Returns list of [col, row] pairs that were updated (including the original cell)
    setCell(col, row, raw) {
        const id = this.id(col, row);
        const oldRaw = this.getCell(col, row)?.raw ?? '';
        if (oldRaw === raw)
            return []; // no change
        const updated = [[col, row]];
        // Remove old dependencies for this cell
        this.clearDeps(id);
        // Store raw value
        const cellData = { raw, value: '' };
        this.cells[col][row] = cellData;
        // Parse formula
        const isFormula = raw.startsWith('=');
        let parseResult = null;
        if (isFormula) {
            parseResult = (0, parser_1.parseFormula)(raw.substring(1));
            if (!parseResult) {
                cellData.value = config_1.ERR_PARSE;
                return updated;
            }
            // Register dependencies
            this.astCache.set(id, parseResult);
            for (const ref of parseResult.refs) {
                const depSet = this.dependents.get(ref) || new Set();
                depSet.add(id);
                this.dependents.set(ref, depSet);
                const myDeps = this.dependencies.get(id) || new Set();
                myDeps.add(ref);
                this.dependencies.set(id, myDeps);
            }
        }
        else {
            this.astCache.set(id, null);
            if (raw === '') {
                delete this.cells[col][row];
            }
            else {
                // Try to parse as number
                const num = parseFloat(raw);
                cellData.value = isNaN(num) ? raw : num;
            }
        }
        // Check for cycles using DFS from this cell through its dependencies
        const cycleCells = this.detectCycles(id);
        // Evaluate this cell first (unless it's part of a cycle)
        if (!cycleCells.has(id)) {
            const evalResult = this.evaluateCell(id);
            if (this.cells[col]?.[row]) {
                this.cells[col][row].value = evalResult.value;
            }
        }
        else {
            if (this.cells[col]?.[row]) {
                this.cells[col][row].value = config_1.ERR_CYCLE;
            }
        }
        // Mark all cycle cells as #CYCLE!
        for (const cycleId of cycleCells) {
            const [cc, cr] = this.parseId(cycleId);
            if (this.cells[cc]?.[cr]) {
                this.cells[cc][cr].value = config_1.ERR_CYCLE;
                updated.push([cc, cr]);
            }
        }
        // Propagate change to all dependents of this cell, collecting updated cells
        this.propagateChange(id, new Set(), updated);
        return updated;
    }
    clearDeps(cellId) {
        // Remove from dependents of cells we reference
        const myDeps = this.dependencies.get(cellId);
        if (myDeps) {
            for (const dep of myDeps) {
                const dSet = this.dependents.get(dep);
                if (dSet) {
                    dSet.delete(cellId);
                    if (dSet.size === 0)
                        this.dependents.delete(dep);
                }
            }
        }
        this.dependencies.delete(cellId);
    }
    // Recalculate a cell and propagate to its dependents
    recalcDependents(cellId, visited) {
        if (visited.has(cellId))
            return;
        visited.add(cellId);
        const evalResult = this.evaluateCell(cellId);
        const [col, row] = this.parseId(cellId);
        if (this.cells[col]?.[row]) {
            this.cells[col][row].value = evalResult.value;
        }
        // Propagate to dependents
        const deps = this.dependents.get(cellId);
        if (deps) {
            for (const dep of deps) {
                this.recalcDependents(dep, visited);
            }
        }
    }
    // Detect cycles by doing DFS through dependencies from the given cell
    detectCycles(startId) {
        const cycleCells = new Set();
        // DFS to find if we can reach startId again through dependency chain
        const dfs = (cellId, path) => {
            if (path.has(cellId))
                return false; // already in current path
            path.add(cellId);
            const deps = this.dependencies.get(cellId);
            if (!deps) {
                path.delete(cellId);
                return false;
            }
            for (const dep of deps) {
                if (dep === startId) {
                    // Found a cycle - mark all cells in the path
                    for (const p of path)
                        cycleCells.add(p);
                    cycleCells.add(startId);
                    path.delete(cellId);
                    return true;
                }
                if (dfs(dep, path)) {
                    cycleCells.add(cellId);
                    path.delete(cellId);
                    return true;
                }
            }
            path.delete(cellId);
            return false;
        };
        dfs(startId, new Set());
        return cycleCells;
    }
    propagateChange(cellId, visited, updated) {
        const deps = this.dependents.get(cellId);
        if (!deps)
            return;
        for (const dep of deps) {
            if (visited.has(dep))
                continue;
            visited.add(dep);
            const [dc, dr] = this.parseId(dep);
            const cellData = this.cells[dc]?.[dr];
            if (!cellData || !this.astCache.get(dep))
                continue;
            // Check for cycle during propagation
            const evalResult = this.evaluateCell(dep);
            cellData.value = evalResult.value;
            if (updated)
                updated.push([dc, dr]);
            this.propagateChange(dep, visited, updated);
        }
    }
    evaluateCell(cellId) {
        const [col, row] = this.parseId(cellId);
        const cellData = this.cells[col]?.[row];
        if (!cellData)
            return { value: '' };
        const parseResult = this.astCache.get(cellId);
        if (!parseResult) {
            // Non-formula cell — already has its value set
            return { value: cellData.value };
        }
        try {
            const val = this.evalAst(parseResult.ast, new Set());
            return { value: val };
        }
        catch (e) {
            if (e.message === 'CYCLE')
                return { value: config_1.ERR_CYCLE };
            if (e.message === 'DIV0')
                return { value: config_1.ERR_DIV0 };
            return { value: config_1.ERR_PARSE };
        }
    }
    evalAst(node, visiting) {
        switch (node.kind) {
            case 'literal': return node.value;
            case 'text': return node.value;
            case 'cellRef': {
                const refId = this.id(node.col, node.row);
                if (visiting.has(refId))
                    throw new Error('CYCLE');
                visiting.add(refId);
                const cellData = this.cells[node.col]?.[node.row];
                if (!cellData)
                    return 0; // empty cell → 0 in arithmetic context
                // If the referenced cell is still being computed (has formula but not yet evaluated), recurse
                const refParseResult = this.astCache.get(refId);
                if (refParseResult && !(0, types_1.isError)(cellData.value)) {
                    // Already has a valid value from prior computation, use it
                    const v = cellData.value;
                    visiting.delete(refId);
                    return typeof v === 'number' ? v : 0;
                }
                const v = cellData.value;
                visiting.delete(refId);
                if ((0, types_1.isError)(v))
                    return v; // propagate errors
                return typeof v === 'number' ? v : parseFloat(String(v)) || 0;
            }
            case 'range': {
                const values = [];
                for (let c = Math.min(node.startCol, node.endCol); c <= Math.max(node.startCol, node.endCol); c++) {
                    for (let r = Math.min(node.startRow, node.endRow); r <= Math.max(node.startRow, node.endRow); r++) {
                        const cellData = this.cells[c]?.[r];
                        if (!cellData)
                            continue;
                        const v = cellData.value;
                        if ((0, types_1.isError)(v))
                            return v;
                        const num = typeof v === 'number' ? v : parseFloat(String(v));
                        if (!isNaN(num))
                            values.push(num);
                    }
                }
                // Return as array-like for functions to consume — we handle this in func evaluation
                return NaN; // sentinel: range should be consumed by a function, not directly evaluated
            }
            case 'unaryMinus': {
                const v = this.evalAst(node.operand, visiting);
                if (typeof v === 'string')
                    return v;
                return -v;
            }
            case 'binary': {
                const left = this.evalAst(node.left, visiting);
                const right = this.evalAst(node.right, visiting);
                if (typeof left === 'string' || typeof right === 'string') {
                    // If either is an error string, propagate it
                    return typeof left === 'string' ? left : right;
                }
                switch (node.op) {
                    case '+': return left + right;
                    case '-': return left - right;
                    case '*': return left * right;
                    case '/':
                        if (right === 0)
                            throw new Error('DIV0');
                        return left / right;
                }
            }
            case 'func': {
                const values = [];
                for (const arg of node.args) {
                    if (arg.kind === 'range') {
                        // Evaluate range directly to collect all cell values
                        for (let c = Math.min(arg.startCol, arg.endCol); c <= Math.max(arg.startCol, arg.endCol); c++) {
                            for (let r = Math.min(arg.startRow, arg.endRow); r <= Math.max(arg.startRow, arg.endRow); r++) {
                                const cellData = this.cells[c]?.[r];
                                if (!cellData)
                                    continue;
                                const v = cellData.value;
                                if ((0, types_1.isError)(v))
                                    return v;
                                const num = typeof v === 'number' ? v : parseFloat(String(v));
                                if (!isNaN(num))
                                    values.push(num);
                            }
                        }
                    }
                    else {
                        const v = this.evalAst(arg, visiting);
                        if (typeof v === 'string')
                            return v; // error propagation
                        if (!isNaN(v))
                            values.push(v);
                    }
                }
                return this.applyFunc(node.name, values);
            }
        }
    }
    applyFunc(name, values) {
        switch (name) {
            case 'SUM': return values.reduce((a, b) => a + b, 0);
            case 'AVERAGE': 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: throw new Error('Unknown function');
        }
    }
    parseId(id) {
        const match = id.match(/^([A-Z]+)(\d+)$/);
        if (!match)
            throw new Error('Invalid cell ID');
        let col = 0;
        for (let i = 0; i < match[1].length; i++) {
            col = col * 26 + (match[1].charCodeAt(i) - 64);
        }
        col -= 1;
        const row = parseInt(match[2], 10) - 1;
        return [col, row];
    }
    // --- Row/Column operations ---
    insertRow(before) {
        if (before <= 0 || before > config_1.ROWS)
            return;
        // Shift all rows down from 'before' onwards
        for (let c = 0; c < config_1.COLS; c++) {
            const newRowData = undefined;
            // We need to shift the column array — but we can't grow beyond ROWS
            // So we drop the last row if needed
            this.cells[c].splice(before, 0, newRowData);
            this.cells[c].pop(); // keep size at ROWS
        }
        this.rebuildAll();
    }
    deleteRow(at) {
        if (at < 0 || at >= config_1.ROWS)
            return;
        for (let c = 0; c < config_1.COLS; c++) {
            this.cells[c].splice(at, 1);
            this.cells[c].push(undefined); // keep size at ROWS
        }
        this.rebuildAll();
    }
    insertCol(before) {
        if (before <= 0 || before > config_1.COLS)
            return;
        for (let r = 0; r < config_1.ROWS; r++) {
            const cols = [];
            // Collect all cell data at this row across columns
            const rowData = this.cells.map(c => c[r]);
            rowData.splice(before, 0, undefined);
            rowData.pop();
            for (let c = 0; c < config_1.COLS; c++) {
                this.cells[c][r] = rowData[c];
            }
        }
        this.rebuildAll();
    }
    deleteCol(at) {
        if (at < 0 || at >= config_1.COLS)
            return;
        for (let r = 0; r < config_1.ROWS; r++) {
            const rowData = this.cells.map(c => c[r]);
            rowData.splice(at, 1);
            rowData.push(undefined);
            for (let c = 0; c < config_1.COLS; c++) {
                this.cells[c][r] = rowData[c];
            }
        }
        this.rebuildAll();
    }
    // Full rebuild after structural changes — re-parses all formulas and recalculates
    rebuildAll() {
        this.astCache.clear();
        this.dependents.clear();
        this.dependencies.clear();
        // First pass: parse all formulas and build dependency graph
        for (let c = 0; c < config_1.COLS; c++) {
            for (let r = 0; r < config_1.ROWS; r++) {
                const cellData = this.cells[c]?.[r];
                if (!cellData)
                    continue;
                const id = this.id(c, r);
                const isFormula = cellData.raw.startsWith('=');
                if (isFormula) {
                    const parseResult = (0, parser_1.parseFormula)(cellData.raw.substring(1));
                    this.astCache.set(id, parseResult);
                    if (parseResult) {
                        for (const ref of parseResult.refs) {
                            const depSet = this.dependents.get(ref) || new Set();
                            depSet.add(id);
                            this.dependents.set(ref, depSet);
                            const myDeps = this.dependencies.get(id) || new Set();
                            myDeps.add(ref);
                            this.dependencies.set(id, myDeps);
                        }
                    }
                    else {
                        cellData.value = config_1.ERR_PARSE;
                    }
                }
                else {
                    if (cellData.raw === '') {
                        delete this.cells[c][r];
                    }
                    else {
                        const num = parseFloat(cellData.raw);
                        cellData.value = isNaN(num) ? cellData.raw : num;
                    }
                }
            }
        }
        // Second pass: topological sort and evaluate all formula cells
        this.topoEvaluate();
    }
    topoEvaluate() {
        const inDegree = new Map();
        const formulaCells = new Set();
        for (let c = 0; c < config_1.COLS; c++) {
            for (let r = 0; r < config_1.ROWS; r++) {
                const cellData = this.cells[c]?.[r];
                if (!cellData)
                    continue;
                const id = this.id(c, r);
                if (!this.astCache.get(id))
                    continue;
                formulaCells.add(id);
                const deps = this.dependencies.get(id);
                inDegree.set(id, deps ? deps.size : 0);
            }
        }
        // Kahn's algorithm
        const queue = [];
        for (const id of formulaCells) {
            if ((inDegree.get(id) ?? 0) === 0)
                queue.push(id);
        }
        const evaluated = new Set();
        while (queue.length > 0) {
            const cellId = queue.shift();
            evaluated.add(cellId);
            const [col, row] = this.parseId(cellId);
            const evalResult = this.evaluateCell(cellId);
            if (this.cells[col]?.[row]) {
                this.cells[col][row].value = evalResult.value;
            }
            const deps = this.dependents.get(cellId);
            if (deps) {
                for (const dep of deps) {
                    inDegree.set(dep, (inDegree.get(dep) ?? 0) - 1);
                    if ((inDegree.get(dep) ?? 0) === 0)
                        queue.push(dep);
                }
            }
        }
        // Any formula cells not evaluated are part of cycles
        for (const id of formulaCells) {
            if (!evaluated.has(id)) {
                const [col, row] = this.parseId(id);
                if (this.cells[col]?.[row]) {
                    this.cells[col][row].value = config_1.ERR_CYCLE;
                }
            }
        }
    }
    // --- Serialization ---
    serialize() {
        const data = {};
        for (let c = 0; c < config_1.COLS; c++) {
            for (let r = 0; r < config_1.ROWS; r++) {
                const cellData = this.cells[c]?.[r];
                if (cellData && cellData.raw !== '') {
                    data[this.id(c, r)] = cellData.raw;
                }
            }
        }
        return JSON.stringify(data);
    }
    deserialize(json) {
        try {
            const data = JSON.parse(json);
            for (const [id, raw] of Object.entries(data)) {
                const match = id.match(/^([A-Z]+)(\d+)$/);
                if (!match)
                    continue;
                let col = 0;
                for (let i = 0; i < match[1].length; i++) {
                    col = col * 26 + (match[1].charCodeAt(i) - 64);
                }
                col -= 1;
                const row = parseInt(match[2], 10) - 1;
                if (col < 0 || col >= config_1.COLS || row < 0 || row >= config_1.ROWS)
                    continue;
                this.setCell(col, row, raw);
            }
        }
        catch {
            // ignore bad data
        }
    }
    clear() {
        for (let c = 0; c < config_1.COLS; c++) {
            for (let r = 0; r < config_1.ROWS; r++) {
                delete this.cells[c][r];
            }
        }
        this.astCache.clear();
        this.dependents.clear();
        this.dependencies.clear();
    }
}
exports.SheetState = SheetState;
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const config_1 = require("./config");
const types_1 = require("./types");
class Renderer {
    constructor(container, state) {
        this.selectedCol = -1;
        this.selectedRow = -1;
        this.state = state;
        this.cellMap = new Map();
        this.table = document.createElement('table');
        this.table.style.borderCollapse = 'collapse';
        this.table.style.tableLayout = 'fixed';
        container.appendChild(this.table);
        this.buildTable();
    }
    buildTable() {
        // Create thead with column headers
        const thead = document.createElement('thead');
        const headerRow = document.createElement('tr');
        // Corner cell (top-left)
        const corner = document.createElement('th');
        corner.style.width = config_1.HEADER_WIDTH + 'px';
        corner.style.height = config_1.HEADER_HEIGHT + 'px';
        corner.style.minWidth = config_1.HEADER_WIDTH + 'px';
        corner.style.background = config_1.COLORS.headerBg;
        corner.style.borderBottom = `2px solid ${config_1.COLORS.gridLine}`;
        corner.style.borderRight = `2px solid ${config_1.COLORS.gridLine}`;
        headerRow.appendChild(corner);
        for (let c = 0; c < config_1.COLS; c++) {
            const th = document.createElement('th');
            const colName = String.fromCharCode(65 + c);
            th.textContent = colName;
            th.style.width = config_1.CELL_WIDTH + 'px';
            th.style.minWidth = config_1.CELL_WIDTH + 'px';
            th.style.height = config_1.HEADER_HEIGHT + 'px';
            th.style.background = config_1.COLORS.headerBg;
            th.style.borderBottom = `2px solid ${config_1.COLORS.gridLine}`;
            th.style.color = config_1.COLORS.textSecondary;
            th.style.fontSize = '12px';
            th.style.fontWeight = '500';
            th.setAttribute('data-col', String(c));
            headerRow.appendChild(th);
        }
        thead.appendChild(headerRow);
        this.table.appendChild(thead);
        // Create tbody with row data
        const tbody = document.createElement('tbody');
        for (let r = 0; r < config_1.ROWS; r++) {
            const tr = document.createElement('tr');
            // Row header
            const th = document.createElement('th');
            th.textContent = String(r + 1);
            th.style.width = config_1.HEADER_WIDTH + 'px';
            th.style.minWidth = config_1.HEADER_WIDTH + 'px';
            th.style.height = config_1.CELL_HEIGHT + 'px';
            th.style.background = config_1.COLORS.headerBg;
            th.style.borderRight = `2px solid ${config_1.COLORS.gridLine}`;
            th.style.color = config_1.COLORS.textSecondary;
            th.style.fontSize = '12px';
            th.style.fontWeight = '500';
            tr.appendChild(th);
            for (let c = 0; c < config_1.COLS; c++) {
                const td = document.createElement('td');
                td.style.width = config_1.CELL_WIDTH + 'px';
                td.style.height = config_1.CELL_HEIGHT + 'px';
                td.style.borderBottom = `1px solid ${config_1.COLORS.gridLine}`;
                td.style.borderRight = `1px solid ${config_1.COLORS.gridLine}`;
                td.style.padding = '2px 4px';
                td.style.fontSize = '13px';
                td.style.fontFamily = '"Segoe UI", Roboto, sans-serif';
                td.style.overflow = 'hidden';
                td.style.whiteSpace = 'nowrap';
                td.style.textOverflow = 'ellipsis';
                td.setAttribute('data-col', String(c));
                td.setAttribute('data-row', String(r));
                tr.appendChild(td);
                const key = `${c},${r}`;
                this.cellMap.set(key, td);
            }
            tbody.appendChild(tr);
        }
        this.table.appendChild(tbody);
    }
    selectCell(col, row) {
        // Deselect previous
        if (this.selectedCol >= 0 && this.selectedRow >= 0) {
            const prevKey = `${this.selectedCol},${this.selectedRow}`;
            const prevTd = this.cellMap.get(prevKey);
            if (prevTd) {
                prevTd.style.outline = 'none';
                prevTd.style.background = '';
            }
        }
        this.selectedCol = col;
        this.selectedRow = row;
        // Select new cell
        const key = `${col},${row}`;
        const td = this.cellMap.get(key);
        if (td) {
            td.style.outline = `2px solid ${config_1.COLORS.selectionBorder}`;
            td.style.outlineOffset = '-1px';
            td.style.background = config_1.COLORS.selectedCellBg;
        }
        // Scroll cell into view
        if (td)
            td.scrollIntoView({ block: 'nearest', inline: 'nearest' });
    }
    updateCell(col, row) {
        const key = `${col},${row}`;
        const td = this.cellMap.get(key);
        if (!td)
            return;
        const isSelected = col === this.selectedCol && row === this.selectedRow;
        const cellData = this.state.getCell(col, row);
        if (!cellData || cellData.raw === '') {
            td.textContent = '';
            td.style.textAlign = 'left';
            td.style.color = config_1.COLORS.textPrimary;
            if (isSelected) {
                td.style.outline = `2px solid ${config_1.COLORS.selectionBorder}`;
                td.style.outlineOffset = '-1px';
                td.style.background = config_1.COLORS.selectedCellBg;
            }
            else {
                td.style.outline = 'none';
                td.style.background = '';
            }
            return;
        }
        const val = cellData.value;
        if ((0, types_1.isError)(val)) {
            td.textContent = String(val);
            td.style.color = config_1.COLORS.errorColor;
            td.style.fontWeight = '600';
            td.style.textAlign = 'left';
        }
        else if (typeof val === 'number') {
            // Format number: avoid long decimals
            const formatted = Number.isInteger(val) ? String(val) : parseFloat(val.toFixed(10)).toString();
            td.textContent = formatted;
            td.style.color = config_1.COLORS.textPrimary;
            td.style.textAlign = 'right';
        }
        else {
            td.textContent = String(val);
            td.style.color = config_1.COLORS.textPrimary;
            td.style.textAlign = 'left';
        }
        if (isSelected) {
            td.style.outline = `2px solid ${config_1.COLORS.selectionBorder}`;
            td.style.outlineOffset = '-1px';
            td.style.background = config_1.COLORS.selectedCellBg;
        }
        else {
            td.style.outline = 'none';
            td.style.background = '';
        }
    }
    updateAllCells() {
        for (let c = 0; c < config_1.COLS; c++) {
            for (let r = 0; r < config_1.ROWS; r++) {
                this.updateCell(c, r);
            }
        }
    }
    getSelectedCol() { return this.selectedCol; }
    getSelectedRow() { return this.selectedRow; }
    // Get the TD element for a cell
    getTd(col, row) {
        return this.cellMap.get(`${col},${row}`);
    }
    // Start editing mode on selected cell
    startEdit() {
        const td = this.cellMap.get(`${this.selectedCol},${this.selectedRow}`);
        if (!td)
            return null;
        const input = document.createElement('input');
        input.type = 'text';
        input.style.width = '100%';
        input.style.height = '100%';
        input.style.border = 'none';
        input.style.outline = 'none';
        input.style.padding = '0';
        input.style.fontSize = '13px';
        input.style.fontFamily = '"Segoe UI", Roboto, sans-serif';
        input.style.background = 'transparent';
        const cellData = this.state.getCell(this.selectedCol, this.selectedRow);
        input.value = cellData?.raw ?? '';
        td.textContent = '';
        td.appendChild(input);
        input.focus();
        input.select();
        return input;
    }
    // Cancel edit mode (remove input from cell)
    cancelEdit() {
        const td = this.cellMap.get(`${this.selectedCol},${this.selectedRow}`);
        if (!td)
            return;
        const input = td.querySelector('input');
        if (input)
            input.remove();
        this.updateCell(this.selectedCol, this.selectedRow);
    }
    // Commit edit mode
    commitEdit() {
        const td = this.cellMap.get(`${this.selectedCol},${this.selectedRow}`);
        let value = '';
        if (td) {
            const input = td.querySelector('input');
            if (input) {
                value = input.value;
                input.remove();
            }
        }
        return value;
    }
    getTable() { return this.table; }
}
exports.Renderer = Renderer;
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputHandler = void 0;
const config_1 = require("./config");
class InputHandler {
    constructor(state, renderer) {
        this.editing = false;
        this.formulaBarInput = null;
        this.cellEditInput = null;
        this.state = state;
        this.renderer = renderer;
    }
    setFormulaBar(input) {
        this.formulaBarInput = input;
    }
    init() {
        const table = this.renderer.getTable();
        // Click on cell to select it
        table.addEventListener('mousedown', (e) => {
            const target = e.target;
            const td = target.closest('td');
            if (!td)
                return;
            const col = parseInt(td.getAttribute('data-col') ?? '-1', 10);
            const row = parseInt(td.getAttribute('data-row') ?? '-1', 10);
            if (col < 0 || row < 0)
                return;
            // If we're editing and click elsewhere, commit first
            if (this.editing && !(td === this.renderer.getTd(this.renderer.getSelectedCol(), this.renderer.getSelectedRow()))) {
                this.commitEdit();
            }
            this.renderer.selectCell(col, row);
            this.updateFormulaBar();
        });
        // Double-click to enter edit mode
        table.addEventListener('dblclick', (e) => {
            const target = e.target;
            const td = target.closest('td');
            if (!td)
                return;
            const col = parseInt(td.getAttribute('data-col') ?? '-1', 10);
            const row = parseInt(td.getAttribute('data-row') ?? '-1', 10);
            if (col < 0 || row < 0)
                return;
            this.renderer.selectCell(col, row);
            this.startEdit();
        });
        // Keyboard handling
        document.addEventListener('keydown', (e) => {
            if (this.editing) {
                this.handleEditingKey(e);
            }
            else {
                this.handleNormalKey(e);
            }
        });
        // Formula bar input
        if (this.formulaBarInput) {
            this.formulaBarInput.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') {
                    e.preventDefault();
                    this.commitFormulaBarEdit();
                }
                else if (e.key === 'Escape') {
                    this.cancelFormulaBarEdit();
                }
            });
            // Sync formula bar changes to cell display
            this.formulaBarInput.addEventListener('input', () => {
                // We don't update the cell live during typing in formula bar,
                // only on commit. But we do want to show what's being typed.
            });
        }
    }
    handleNormalKey(e) {
        const col = this.renderer.getSelectedCol();
        const row = this.renderer.getSelectedRow();
        if (col < 0 || row < 0)
            return;
        // Arrow keys move selection
        if (e.key === 'ArrowUp' && row > 0) {
            e.preventDefault();
            this.renderer.selectCell(col, row - 1);
            this.updateFormulaBar();
            return;
        }
        if (e.key === 'ArrowDown' && row < config_1.ROWS - 1) {
            e.preventDefault();
            this.renderer.selectCell(col, row + 1);
            this.updateFormulaBar();
            return;
        }
        if (e.key === 'ArrowLeft' && col > 0) {
            e.preventDefault();
            this.renderer.selectCell(col - 1, row);
            this.updateFormulaBar();
            return;
        }
        if (e.key === 'ArrowRight' && col < config_1.COLS - 1) {
            e.preventDefault();
            this.renderer.selectCell(col + 1, row);
            this.updateFormulaBar();
            return;
        }
        // Delete key clears cell
        if (e.key === 'Delete') {
            e.preventDefault();
            const updated = this.state.setCell(col, row, '');
            for (const [c, r] of updated) {
                this.renderer.updateCell(c, r);
            }
            this.updateFormulaBar();
            this.saveState();
            return;
        }
        // Tab moves right and enters edit mode
        if (e.key === 'Tab') {
            e.preventDefault();
            const newCol = Math.min(col + 1, config_1.COLS - 1);
            this.renderer.selectCell(newCol, row);
            this.updateFormulaBar();
            return;
        }
        // If key is a printable character, start editing
        if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
            e.preventDefault();
            this.startEdit();
            // Type the character into the edit input
            if (this.cellEditInput) {
                this.cellEditInput.value = e.key;
            }
        }
    }
    handleEditingKey(e) {
        if (!this.cellEditInput)
            return;
        if (e.key === 'Enter') {
            e.preventDefault();
            const value = this.commitEdit();
            // Move down
            const newRow = Math.min(this.renderer.getSelectedRow() + 1, config_1.ROWS - 1);
            this.renderer.selectCell(this.renderer.getSelectedCol(), newRow);
            this.updateFormulaBar();
        }
        else if (e.key === 'Tab') {
            e.preventDefault();
            const value = this.commitEdit();
            // Move right
            const newCol = Math.min(this.renderer.getSelectedCol() + 1, config_1.COLS - 1);
            this.renderer.selectCell(newCol, this.renderer.getSelectedRow());
            this.updateFormulaBar();
        }
        else if (e.key === 'Escape') {
            e.preventDefault();
            this.cancelEdit();
        }
    }
    startEdit() {
        this.editing = true;
        this.cellEditInput = this.renderer.startEdit();
        // Sync formula bar to show raw value
        this.updateFormulaBar();
    }
    commitEdit() {
        const value = this.renderer.commitEdit();
        this.editing = false;
        this.cellEditInput = null;
        const col = this.renderer.getSelectedCol();
        const row = this.renderer.getSelectedRow();
        const updated = this.state.setCell(col, row, value);
        for (const [c, r] of updated) {
            this.renderer.updateCell(c, r);
        }
        this.saveState();
        return value;
    }
    cancelEdit() {
        this.editing = false;
        this.cellEditInput = null;
        this.renderer.cancelEdit();
    }
    commitFormulaBarEdit() {
        if (!this.formulaBarInput)
            return;
        const value = this.formulaBarInput.value;
        const col = this.renderer.getSelectedCol();
        const row = this.renderer.getSelectedRow();
        const updated = this.state.setCell(col, row, value);
        for (const [c, r] of updated) {
            this.renderer.updateCell(c, r);
        }
        this.saveState();
    }
    cancelFormulaBarEdit() {
        if (!this.formulaBarInput)
            return;
        // Restore formula bar to current cell's raw value
        const col = this.renderer.getSelectedCol();
        const row = this.renderer.getSelectedRow();
        const cellData = this.state.getCell(col, row);
        this.formulaBarInput.value = cellData?.raw ?? '';
    }
    updateFormulaBar() {
        if (!this.formulaBarInput)
            return;
        const col = this.renderer.getSelectedCol();
        const row = this.renderer.getSelectedRow();
        const cellData = this.state.getCell(col, row);
        this.formulaBarInput.value = cellData?.raw ?? '';
        // Update the cell reference display (e.g. "B4")
        const refDisplay = document.getElementById('cell-ref');
        if (refDisplay) {
            const colName = String.fromCharCode(65 + col);
            refDisplay.textContent = `${colName}${row + 1}`;
        }
    }
    saveState() {
        try {
            localStorage.setItem('spreadsheet-data', this.state.serialize());
        }
        catch {
            // ignore storage errors
        }
    }
}
exports.InputHandler = InputHandler;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>