← Spreadsheet results

Spreadsheet

inkling · 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 50,014 bytes · SHA-256 4f6c83f06b8f
<!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 ── */
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
#app-inner { display: flex; flex-direction: column; height: 100vh; font-family: 'Segoe UI', sans-serif; }
#toolbar { padding: 8px 12px; background: #f1f3f4; border-bottom: 1px solid #e0e0e0; display: flex; gap: 8px; align-items: center; }
#toolbar button { background: #fff; border: 1px solid #dadce0; padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 13px; }
#toolbar button:hover { background: #e8eaed; }
#formula-bar-area { padding: 6px 12px; background: #fff; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 8px; font-family: monospace; font-size: 14px; }
#name-box { font-weight: bold; color: #1a73e8; width: 60px; text-align: center; border-right: 1px solid #e0e0e0; padding-right: 8px; flex-shrink: 0; }
#formula-bar { flex: 1; border: 1px solid #d0d0d0; padding: 5px 8px; border-radius: 4px; font-family: monospace; font-size: 14px; outline: none; }
#formula-bar:focus { border-color: #1a73e8; }
#grid-container { flex: 1; overflow: auto; position: relative; background: #fff; }
.grid-table { border-collapse: collapse; table-layout: fixed; user-select: none; }
.grid-table th, .grid-table td { border: 1px solid #e0e0e0; height: 28px; padding: 0 6px; font-size: 13px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.grid-table th { background: #f8f9fa; font-weight: 600; color: #5f6368; text-align: center; position: sticky; top: 0; z-index: 10; height: 28px; }
.grid-table th:first-child, .grid-table td:first-child { position: sticky; left: 0; background: #f8f9fa; z-index: 11; width: 50px; text-align: center; }
.grid-table td:first-child { background: #f8f9fa; font-weight: 500; color: #5f6368; }
.grid-table td { text-align: right; color: #202124; cursor: cell; }
.grid-table td.text-left { text-align: left; }
.grid-table td.selected { border: 2px solid #1a73e8; background: #e8f0fe; outline: none; z-index: 5; }
.grid-table td.error { color: #d93025; }
.grid-table td[data-col="0"] { position: sticky; left: 0; z-index: 5; }
</style>
</head>
<body>
<div id="app-inner" style="display:flex;flex-direction:column;height:100vh;"></div><div id="toolbar" style="padding:8px;background:#f1f3f4;border-bottom:1px solid #e0e0e0;display:flex;gap:8px;align-items:center;"><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><button id="btn-save">Save</button></div><div id="formula-bar-area" style="padding:6px 12px;background:#fff;border-bottom:1px solid #e0e0e0;display:flex;align-items:center;gap:8px;font-family:monospace;font-size:14px;"><span id="name-box" style="font-weight:bold;color:#1a73e8;width:50px;text-align:center;border-right:1px solid #e0e0e0;padding-right:8px;">A1</span><input id="formula-bar" type="text" style="flex:1;border:1px solid #d0d0d0;padding:4px 8px;border-radius:4px;font-family:monospace;font-size:14px;outline:none;"></div><div id="grid-container" style="flex:1;overflow:auto;position:relative;background:#fff;"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./parser":"src/parser.ts","./renderer":"src/renderer.ts","./config":"src/config.ts"},"src/state.ts":{"./config":"src/config.ts","./parser":"src/parser.ts"},"src/renderer.ts":{"./config":"src/config.ts","./state":"src/state.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 });
exports.start = start;
const state_1 = require("./state");
const parser_1 = require("./parser");
const renderer_1 = require("./renderer");
const config_1 = require("./config");
function start() {
    const state = new state_1.SpreadsheetState();
    const saved = localStorage.getItem('spreadsheet_data');
    if (saved) {
        state.deserialize(saved);
    }
    else {
        state.recalcAll();
    }
    const renderer = new renderer_1.Renderer('grid-container', state);
    window.__sheetState = state;
    window.__sheetRenderer = renderer;
    window.__parseFormula = parser_1.parseFormula;
    renderer.updateGrid();
    // Toolbar buttons wire-up
    document.getElementById('btn-insert-row')?.addEventListener('click', () => {
        renderer.insertRow(renderer.selectedRow);
    });
    document.getElementById('btn-delete-row')?.addEventListener('click', () => {
        renderer.deleteRow(renderer.selectedRow);
    });
    document.getElementById('btn-insert-col')?.addEventListener('click', () => {
        renderer.insertCol(renderer.selectedCol);
    });
    document.getElementById('btn-delete-col')?.addEventListener('click', () => {
        renderer.deleteCol(renderer.selectedCol);
    });
    document.getElementById('btn-save')?.addEventListener('click', () => {
        localStorage.setItem('spreadsheet_data', state.serialize());
    });
    // Auto-save on change (throttled)
    let saveTimer = null;
    const scheduleSave = () => {
        if (saveTimer)
            clearTimeout(saveTimer);
        saveTimer = window.setTimeout(() => {
            localStorage.setItem('spreadsheet_data', state.serialize());
        }, 500);
    };
    // Override state.setRaw to trigger save
    const originalSetRaw = state.setRaw.bind(state);
    state.setRaw = (c, r, raw) => {
        originalSetRaw(c, r, raw);
        scheduleSave();
    };
    // Keyboard navigation
    document.addEventListener('keydown', (e) => {
        if (renderer.editMode)
            return; // Let input handle
        if (e.target && e.target.tagName === 'INPUT')
            return;
        switch (e.key) {
            case 'ArrowUp':
                e.preventDefault();
                renderer.selectCell(renderer.selectedCol, Math.max(0, renderer.selectedRow - 1));
                break;
            case 'ArrowDown':
                e.preventDefault();
                renderer.selectCell(renderer.selectedCol, Math.min(config_1.ROWS - 1, renderer.selectedRow + 1));
                break;
            case 'ArrowLeft':
                e.preventDefault();
                renderer.selectCell(Math.max(0, renderer.selectedCol - 1), renderer.selectedRow);
                break;
            case 'ArrowRight':
                e.preventDefault();
                renderer.selectCell(Math.min(config_1.COLS - 1, renderer.selectedCol + 1), renderer.selectedRow);
                break;
            case 'Enter':
                e.preventDefault();
                renderer.enterEdit(renderer.selectedCol, renderer.selectedRow);
                break;
            case 'Delete':
                e.preventDefault();
                renderer.state.clearCell(renderer.selectedCol, renderer.selectedRow);
                renderer.updateCellDisplay(renderer.selectedCol, renderer.selectedRow);
                renderer.state.processDirty();
                renderer.updateGrid();
                scheduleSave();
                break;
            case 'Tab':
                e.preventDefault();
                const newCol = Math.min(config_1.COLS - 1, renderer.selectedCol + 1);
                renderer.selectCell(newCol, renderer.selectedRow);
                break;
        }
    });
    // Click outside to deselect edit
    document.addEventListener('click', (e) => {
        const target = e.target;
        if (renderer.editMode && target !== renderer.editInput && !renderer.container.contains(target)) {
            renderer.commitEdit(renderer.selectedCol, renderer.selectedRow);
        }
    });
}
start();
};

// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.COLORS = exports.COLS_BY_LABEL = exports.HEADER_WIDTH = exports.HEADER_HEIGHT = exports.CELL_HEIGHT = exports.CELL_WIDTH = exports.ROWS = exports.COLS = void 0;
exports.COLS = 26;
exports.ROWS = 100;
exports.CELL_WIDTH = 100;
exports.CELL_HEIGHT = 28;
exports.HEADER_HEIGHT = 28;
exports.HEADER_WIDTH = 50;
exports.COLS_BY_LABEL = [...Array(exports.COLS)].map((_, i) => String.fromCharCode(65 + i));
exports.COLORS = {
    bg: '#ffffff',
    headerBg: '#f8f9fa',
    selectedBorder: '#1a73e8',
    selectedFill: '#e8f0fe',
    gridLine: '#e0e0e0',
    errorText: '#d93025',
    formulaBg: '#fff',
    toolbarBg: '#f1f3f4',
    text: '#202124',
    muted: '#5f6368',
};
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpreadsheetState = void 0;
const config_1 = require("./config");
const parser_1 = require("./parser");
class SpreadsheetState {
    constructor() {
        this.cells = new Map();
        this.dependents = new Map();
        this.dependencies = new Map();
        this.dirty = new Set();
        this.cycleCells = new Set();
    }
    getKey(col, row) {
        return `${String.fromCharCode(65 + col)}${row + 1}`;
    }
    getCell(col, row) {
        const k = this.getKey(col, row);
        if (!this.cells.has(k)) {
            this.cells.set(k, { raw: '', value: null, error: null });
        }
        return this.cells.get(k);
    }
    setRaw(col, row, raw) {
        const k = this.getKey(col, row);
        const data = this.getCell(col, row);
        data.raw = raw;
        data.value = null;
        data.error = null;
        data.parsed = undefined;
        this.cells.set(k, data);
        this.clearDependencies(k);
        if (raw.startsWith('=')) {
            try {
                const parsed = (0, parser_1.parseFormula)(raw.slice(1));
                data.parsed = parsed;
                for (const r of parsed.refs) {
                    this.addDependency(k, r);
                }
            }
            catch (e) {
                data.error = '#ERR!';
                data.value = '#ERR!';
            }
        }
        this.markDirty(k);
    }
    clearCell(col, row) {
        this.setRaw(col, row, '');
    }
    clearDependencies(key) {
        const oldDeps = this.dependencies.get(key) || new Set();
        for (const dep of oldDeps) {
            const dset = this.dependents.get(dep);
            if (dset) {
                dset.delete(key);
                if (dset.size === 0)
                    this.dependents.delete(dep);
            }
        }
        this.dependencies.set(key, new Set());
    }
    addDependency(from, to) {
        if (!this.dependencies.has(from))
            this.dependencies.set(from, new Set());
        this.dependencies.get(from).add(to);
        if (!this.dependents.has(to))
            this.dependents.set(to, new Set());
        this.dependents.get(to).add(from);
    }
    markDirty(key) {
        this.dirty.add(key);
        // Also mark all dependents dirty
        const toPropagate = new Set();
        const stack = [key];
        const visited = new Set();
        visited.add(key);
        while (stack.length > 0) {
            const curr = stack.pop();
            for (const d of (this.dependents.get(curr) || [])) {
                if (!visited.has(d)) {
                    visited.add(d);
                    toPropagate.add(d);
                    stack.push(d);
                }
            }
        }
        for (const k of toPropagate) {
            this.dirty.add(k);
        }
    }
    processDirty() {
        // Compute dirty cells in topological order (simplified iterative approach)
        const computed = new Set();
        let changed = true;
        while (changed && this.dirty.size > 0) {
            changed = false;
            const currentDirty = Array.from(this.dirty);
            for (const k of currentDirty) {
                if (computed.has(k)) {
                    this.dirty.delete(k);
                    continue;
                }
                // Check if all dependencies are computed (not dirty and not computing cycle)
                const deps = this.dependencies.get(k) || new Set();
                const uncomputedDeps = Array.from(deps).filter(d => !computed.has(d));
                if (uncomputedDeps.length === 0) {
                    computed.add(k);
                    this.dirty.delete(k);
                    this.computeCellInternal(k, new Set());
                    changed = true;
                }
                else {
                    // Some dependencies not yet computed, skip for now
                }
            }
        }
        // For remaining dirty cells that have cycles, compute them to detect cycles
        for (const k of Array.from(this.dirty)) {
            this.computeCellInternal(k, new Set());
            this.dirty.delete(k);
        }
    }
    computeCellInternal(key, computing) {
        const data = this.cells.get(key);
        if (!data)
            return;
        if (!data.raw) {
            data.value = null;
            data.error = null;
            return;
        }
        if (!data.raw.startsWith('=')) {
            data.error = null;
            let val = data.raw;
            const num = Number(data.raw);
            if (!isNaN(num) && data.raw !== '')
                val = num;
            data.value = val;
            return;
        }
        if (!data.parsed) {
            try {
                data.parsed = (0, parser_1.parseFormula)(data.raw.slice(1));
                for (const r of data.parsed.refs) {
                    this.addDependency(key, r);
                }
            }
            catch (e) {
                data.error = '#ERR!';
                data.value = '#ERR!';
                return;
            }
        }
        // Detect cycle
        if (computing.has(key)) {
            data.error = '#CYCLE!';
            data.value = '#CYCLE!';
            this.cycleCells.add(key);
            return;
        }
        computing.add(key);
        try {
            const resolve = (ref) => {
                if (computing.has(ref)) {
                    return { value: null, error: '#CYCLE!' };
                }
                const refData = this.cells.get(ref);
                if (!refData) {
                    return { value: null, error: '#REF!' };
                }
                if (refData.error === '#CYCLE!') {
                    return { value: null, error: '#CYCLE!' };
                }
                // Ensure dependency computed
                if (refData.raw && (refData.value === null && refData.error === null)) {
                    // It needs computation; trigger computation if not dirty
                }
                // We rely on processDirty to handle order; here just return current value or compute on demand
                if (refData.raw.startsWith('=') && refData.value === null && refData.error === null) {
                    // Compute on demand for nested evaluation
                    this.computeCellInternal(ref, new Set(computing));
                }
                if (refData.error) {
                    return { value: null, error: refData.error };
                }
                if (refData.value === null || refData.value === undefined) {
                    return { value: null, error: null };
                }
                if (typeof refData.value === 'number')
                    return { value: refData.value, error: null };
                const num = Number(refData.value);
                if (isNaN(num))
                    return { value: null, error: null };
                return { value: num, error: null };
            };
            const result = data.parsed.evaluate(resolve);
            computing.delete(key);
            if (typeof result === 'string' && (result === '#DIV/0!' || result === '#REF!' || result === '#CYCLE!' || result === '#ERR!')) {
                data.error = result;
                data.value = result;
            }
            else {
                data.error = null;
                data.value = result === null ? null : result;
            }
        }
        catch (e) {
            computing.delete(key);
            data.error = '#ERR!';
            data.value = '#ERR!';
        }
    }
    recalcAll() {
        for (let r = 0; r < config_1.ROWS; r++) {
            for (let c = 0; c < config_1.COLS; c++) {
                this.dirty.add(this.getKey(c, r));
            }
        }
        this.processDirty();
    }
    insertRow(afterRow) {
        // Shift all rows >= afterRow down by 1, update references
        const newCells = new Map();
        for (const [k, data] of this.cells) {
            const match = k.match(/^([A-Z]+)([0-9]+)$/);
            if (match) {
                const col = match[1];
                const row = parseInt(match[2], 10);
                if (row > afterRow) {
                    const newRow = row + 1;
                    const newKey = col + newRow;
                    newCells.set(newKey, data);
                }
                else if (row === afterRow) {
                    // Keep as is? Actually inserting after a row means rows below shift.
                    // If inserting afterRow (0-indexed), then row afterRow+1 and above shift.
                    // Let's assume afterRow is 0-indexed row number. Insert after that row -> rows > afterRow shift.
                    newCells.set(k, data);
                }
                else {
                    newCells.set(k, data);
                }
            }
            else {
                newCells.set(k, data);
            }
        }
        this.cells = newCells;
        // Rewrite formulas: any reference to a row > afterRow needs +1
        for (const [k, data] of this.cells) {
            if (data.raw.startsWith('=')) {
                // Parse and rewrite
                const rewritten = this.rewriteFormula(data.raw, afterRow, 'row', 1);
                data.raw = rewritten;
                data.value = null;
                data.error = null;
                data.parsed = undefined;
                this.clearDependencies(k);
                try {
                    const parsed = (0, parser_1.parseFormula)(rewritten.slice(1));
                    data.parsed = parsed;
                    for (const r of parsed.refs) {
                        this.addDependency(k, r);
                    }
                }
                catch (e) {
                    data.error = '#ERR!';
                }
            }
        }
        this.recalcAll();
    }
    deleteRow(rowIdx) {
        const newCells = new Map();
        for (const [k, data] of this.cells) {
            const match = k.match(/^([A-Z]+)([0-9]+)$/);
            if (match) {
                const col = match[1];
                const r = parseInt(match[2], 10);
                if (r === rowIdx + 1) {
                    // Delete this row
                    continue;
                }
                else if (r > rowIdx + 1) {
                    const newR = r - 1;
                    newCells.set(col + newR, data);
                }
                else {
                    newCells.set(k, data);
                }
            }
            else {
                newCells.set(k, data);
            }
        }
        this.cells = newCells;
        for (const [k, data] of this.cells) {
            if (data.raw.startsWith('=')) {
                const rewritten = this.rewriteFormula(data.raw, rowIdx, 'row', -1);
                data.raw = rewritten;
                data.value = null;
                data.error = null;
                data.parsed = undefined;
                this.clearDependencies(k);
                try {
                    const parsed = (0, parser_1.parseFormula)(rewritten.slice(1));
                    data.parsed = parsed;
                    for (const r of parsed.refs) {
                        this.addDependency(k, r);
                    }
                }
                catch (e) {
                    data.error = '#ERR!';
                }
            }
        }
        this.recalcAll();
    }
    insertCol(afterCol) {
        const newCells = new Map();
        for (const [k, data] of this.cells) {
            const match = k.match(/^([A-Z]+)([0-9]+)$/);
            if (match) {
                const colChar = match[1];
                const row = match[2];
                const colIdx = colChar.charCodeAt(0) - 65;
                if (colIdx > afterCol) {
                    const newColChar = String.fromCharCode(colChar.charCodeAt(0) + 1);
                    newCells.set(newColChar + row, data);
                }
                else {
                    newCells.set(k, data);
                }
            }
            else {
                newCells.set(k, data);
            }
        }
        this.cells = newCells;
        for (const [k, data] of this.cells) {
            if (data.raw.startsWith('=')) {
                const rewritten = this.rewriteFormula(data.raw, afterCol, 'col', 1);
                data.raw = rewritten;
                data.value = null;
                data.error = null;
                data.parsed = undefined;
                this.clearDependencies(k);
                try {
                    const parsed = (0, parser_1.parseFormula)(rewritten.slice(1));
                    data.parsed = parsed;
                    for (const r of parsed.refs) {
                        this.addDependency(k, r);
                    }
                }
                catch (e) {
                    data.error = '#ERR!';
                }
            }
        }
        this.recalcAll();
    }
    deleteCol(colIdx) {
        const newCells = new Map();
        for (const [k, data] of this.cells) {
            const match = k.match(/^([A-Z]+)([0-9]+)$/);
            if (match) {
                const colChar = match[1];
                const row = match[2];
                const cIdx = colChar.charCodeAt(0) - 65;
                if (cIdx === colIdx) {
                    continue;
                }
                else if (cIdx > colIdx) {
                    const newColChar = String.fromCharCode(colChar.charCodeAt(0) - 1);
                    newCells.set(newColChar + row, data);
                }
                else {
                    newCells.set(k, data);
                }
            }
            else {
                newCells.set(k, data);
            }
        }
        this.cells = newCells;
        for (const [k, data] of this.cells) {
            if (data.raw.startsWith('=')) {
                const rewritten = this.rewriteFormula(data.raw, colIdx, 'col', -1);
                data.raw = rewritten;
                data.value = null;
                data.error = null;
                data.parsed = undefined;
                this.clearDependencies(k);
                try {
                    const parsed = (0, parser_1.parseFormula)(rewritten.slice(1));
                    data.parsed = parsed;
                    for (const r of parsed.refs) {
                        this.addDependency(k, r);
                    }
                }
                catch (e) {
                    data.error = '#ERR!';
                }
            }
        }
        this.recalcAll();
    }
    rewriteFormula(raw, index, type, delta) {
        // Rewrite cell references in formula string
        // Simple regex-based approach
        const regex = /([A-Z]+)([0-9]+)/g;
        return raw.replace(regex, (match, letters, digits) => {
            if (type === 'row') {
                const rowNum = parseInt(digits, 10) - 1; // 0-indexed
                if (rowNum === index)
                    return match;
                if (delta > 0 && rowNum > index) {
                    return letters + (rowNum + delta + 1);
                }
                if (delta < 0 && rowNum > index) {
                    return letters + (rowNum + delta + 1);
                }
                return match;
            }
            else {
                const colIdx = letters.charCodeAt(0) - 65;
                if (colIdx === index)
                    return match;
                if (delta > 0 && colIdx > index) {
                    return String.fromCharCode(letters.charCodeAt(0) + delta) + digits;
                }
                if (delta < 0 && colIdx > index) {
                    return String.fromCharCode(letters.charCodeAt(0) + delta) + digits;
                }
                return match;
            }
        });
    }
    serialize() {
        const obj = {};
        for (const [k, data] of this.cells) {
            if (data.raw !== '' || data.value !== null) {
                obj[k] = data.raw;
            }
        }
        return JSON.stringify(obj);
    }
    deserialize(json) {
        this.cells.clear();
        this.dependencies.clear();
        this.dependents.clear();
        this.dirty.clear();
        this.cycleCells.clear();
        try {
            const obj = JSON.parse(json);
            for (const [k, raw] of Object.entries(obj)) {
                const data = { raw: raw, value: null, error: null };
                this.cells.set(k, data);
                if (raw.startsWith('=')) {
                    try {
                        const parsed = (0, parser_1.parseFormula)(raw.slice(1));
                        data.parsed = parsed;
                        for (const r of parsed.refs) {
                            this.addDependency(k, r);
                        }
                    }
                    catch (e) {
                        data.error = '#ERR!';
                    }
                }
            }
        }
        catch (e) {
            // ignore
        }
        this.recalcAll();
    }
}
exports.SpreadsheetState = SpreadsheetState;
};

// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tokenize = tokenize;
exports.parseFormula = parseFormula;
function tokenize(input) {
    const tokens = [];
    let i = 0;
    while (i < input.length) {
        const ch = input[i];
        if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
            i++;
            continue;
        }
        if (ch === '+' || ch === '-' || ch === '*' || ch === '/') {
            tokens.push({ type: 'OP', value: ch, raw: ch });
            i++;
            continue;
        }
        if (ch === '(') {
            tokens.push({ type: 'LPAREN', value: '(', raw: '(' });
            i++;
            continue;
        }
        if (ch === ')') {
            tokens.push({ type: 'RPAREN', value: ')', raw: ')' });
            i++;
            continue;
        }
        if (ch === ',') {
            tokens.push({ type: 'COMMA', value: ',', raw: ',' });
            i++;
            continue;
        }
        if (/[A-Z]/.test(ch)) {
            let j = i;
            while (j < input.length && /[A-Z]/.test(input[j]))
                j++;
            const letters = input.slice(i, j);
            let k = j;
            while (k < input.length && /[0-9]/.test(input[k]))
                k++;
            const digits = input.slice(j, k);
            if (digits.length > 0) {
                tokens.push({ type: 'REF', value: letters + digits, raw: letters + digits });
            }
            else {
                if (k < input.length && input[k] === '(') {
                    tokens.push({ type: 'FUNC', value: letters, raw: letters });
                }
                else {
                    tokens.push({ type: 'REF', value: letters, raw: letters });
                }
            }
            i = k;
            continue;
        }
        if (/[0-9]/.test(ch) || ch === '.') {
            let j = i;
            let hasDot = false;
            if (ch === '.') {
                hasDot = true;
                j++;
            }
            while (j < input.length) {
                if (/[0-9]/.test(input[j])) {
                    j++;
                }
                else if (input[j] === '.' && !hasDot) {
                    hasDot = true;
                    j++;
                }
                else {
                    break;
                }
            }
            tokens.push({ type: 'NUMBER', value: input.slice(i, j), raw: input.slice(i, j) });
            i = j;
            continue;
        }
        if (ch === ':') {
            tokens.push({ type: 'OP', value: ':', raw: ':' });
            i++;
            continue;
        }
        i++;
    }
    tokens.push({ type: 'EOF', value: '', raw: '' });
    return tokens;
}
function current(s) {
    return s.tokens[s.pos];
}
function advance(s) {
    return s.tokens[s.pos++];
}
function expect(s, type) {
    if (current(s).type !== type) {
        throw new Error(`Expected ${type} but got ${current(s).type}`);
    }
    return advance(s);
}
function parseFormula(input) {
    const tokens = tokenize(input);
    const s = { tokens, pos: 0 };
    const refs = new Set();
    function parseExpression() {
        return parseAddSub();
    }
    function parseAddSub() {
        let left = parseMulDiv();
        while (current(s).type === 'OP' && (current(s).value === '+' || current(s).value === '-')) {
            const op = advance(s).value;
            const right = parseMulDiv();
            left = { type: 'binop', op, left, right, refs: combineRefs(left, right) };
        }
        return left;
    }
    function parseMulDiv() {
        let left = parseUnary();
        while (current(s).type === 'OP' && (current(s).value === '*' || current(s).value === '/')) {
            const op = advance(s).value;
            const right = parseUnary();
            left = { type: 'binop', op, left, right, refs: combineRefs(left, right) };
        }
        return left;
    }
    function parseUnary() {
        if (current(s).type === 'OP' && current(s).value === '-') {
            advance(s);
            const val = parseUnary();
            return { type: 'unary', op: '-', val, refs: val.refs ? val.refs : new Set() };
        }
        return parsePrimary();
    }
    function parsePrimary() {
        const tok = current(s);
        if (tok.type === 'NUMBER') {
            advance(s);
            return { type: 'number', value: Number(tok.value), refs: new Set() };
        }
        if (tok.type === 'REF') {
            advance(s);
            refs.add(tok.value);
            return { type: 'ref', ref: tok.value, refs: new Set([tok.value]) };
        }
        if (tok.type === 'FUNC') {
            const name = advance(s).value;
            expect(s, 'LPAREN');
            const args = [];
            if (current(s).type !== 'RPAREN') {
                args.push(parseArgument());
                while (current(s).type === 'COMMA') {
                    advance(s);
                    args.push(parseArgument());
                }
            }
            expect(s, 'RPAREN');
            const combinedRefs = new Set();
            for (const a of args) {
                for (const r of (a.refs || []))
                    combinedRefs.add(r);
            }
            return { type: 'func', name, args, refs: combinedRefs };
        }
        if (tok.type === 'LPAREN') {
            advance(s);
            const inner = parseExpression();
            expect(s, 'RPAREN');
            return inner;
        }
        throw new Error(`Unexpected token: ${tok.type}`);
    }
    function parseArgument() {
        const tok = current(s);
        if (tok.type === 'REF') {
            const ref1 = tok.value;
            advance(s);
            if (current(s).type === 'OP' && current(s).value === ':') {
                advance(s);
                const tok2 = current(s);
                if (tok2.type === 'REF') {
                    const ref2 = tok2.value;
                    advance(s);
                    const rangeRefs = expandRange(ref1, ref2);
                    const combinedRefs = new Set();
                    for (const r of rangeRefs)
                        combinedRefs.add(r);
                    return { type: 'range', refs: combinedRefs, start: ref1, end: ref2, rangeRefs };
                }
                else {
                    throw new Error('Invalid range');
                }
            }
            else {
                return { type: 'ref', ref: ref1, refs: new Set([ref1]) };
            }
        }
        if (tok.type === 'NUMBER') {
            advance(s);
            return { type: 'number', value: Number(tok.value), refs: new Set() };
        }
        if (tok.type === 'FUNC') {
            const name = advance(s).value;
            expect(s, 'LPAREN');
            const args = [];
            if (current(s).type !== 'RPAREN') {
                args.push(parseArgument());
                while (current(s).type === 'COMMA') {
                    advance(s);
                    args.push(parseArgument());
                }
            }
            expect(s, 'RPAREN');
            const combinedRefs = new Set();
            for (const a of args) {
                for (const r of (a.refs || []))
                    combinedRefs.add(r);
            }
            return { type: 'func', name, args, refs: combinedRefs };
        }
        return parseExpression();
    }
    const ast = parseExpression();
    expect(s, 'EOF');
    // Collect refs from AST
    function collectRefs(node) {
        const s = new Set();
        if (node.type === 'ref') {
            s.add(node.ref);
        }
        else if (node.type === 'range') {
            for (const r of node.rangeRefs || [])
                s.add(r);
        }
        else if (node.type === 'func') {
            for (const arg of node.args || []) {
                for (const r of collectRefs(arg))
                    s.add(r);
            }
        }
        else if (node.type === 'binop' || node.type === 'unary') {
            for (const r of collectRefs(node.left || node.val))
                s.add(r);
            if (node.type === 'binop') {
                for (const r of collectRefs(node.right))
                    s.add(r);
            }
        }
        for (const r of node.refs || [])
            s.add(r);
        return s;
    }
    for (const r of collectRefs(ast))
        refs.add(r);
    return {
        refs,
        evaluate: (resolve) => {
            return evaluateNode(ast, resolve);
        }
    };
}
function combineRefs(a, b) {
    const s = new Set();
    for (const r of (a.refs || []))
        s.add(r);
    for (const r of (b.refs || []))
        s.add(r);
    return s;
}
function expandRange(start, end) {
    const match1 = start.match(/^([A-Z]+)([0-9]+)$/);
    const match2 = end.match(/^([A-Z]+)([0-9]+)$/);
    if (!match1 || !match2)
        return [];
    const c1 = match1[1];
    const r1 = parseInt(match1[2], 10);
    const c2 = match2[1];
    const r2 = parseInt(match2[2], 10);
    const refs = [];
    const startCol = c1.charCodeAt(0) - 65;
    const endCol = c2.charCodeAt(0) - 65;
    for (let c = startCol; c <= endCol; c++) {
        for (let r = r1; r <= r2; r++) {
            refs.push(String.fromCharCode(65 + c) + r);
        }
    }
    return refs;
}
function evaluateNode(node, resolve) {
    switch (node.type) {
        case 'number':
            return node.value;
        case 'ref': {
            const res = resolve(node.ref);
            if (res.error)
                return res.error;
            if (res.value === null || res.value === undefined)
                return 0;
            if (typeof res.value === 'number')
                return res.value;
            const num = Number(res.value);
            if (isNaN(num))
                return 0;
            return num;
        }
        case 'binop': {
            const left = evaluateNode(node.left, resolve);
            const right = evaluateNode(node.right, resolve);
            if (typeof left === 'string' || typeof right === 'string')
                return '#ERR!';
            if (typeof left === 'number' && typeof right === 'number') {
                if (node.op === '+')
                    return left + right;
                if (node.op === '-')
                    return left - right;
                if (node.op === '*')
                    return left * right;
                if (node.op === '/') {
                    if (right === 0)
                        return '#DIV/0!';
                    return left / right;
                }
            }
            return '#ERR!';
        }
        case 'unary': {
            const val = evaluateNode(node.val, resolve);
            if (typeof val === 'string')
                return '#ERR!';
            if (typeof val !== 'number')
                return '#ERR!';
            return -val;
        }
        case 'func': {
            const args = node.args;
            const name = node.name.toUpperCase();
            let values = [];
            for (const arg of args) {
                if (arg.type === 'range') {
                    for (const ref of arg.rangeRefs) {
                        const res = resolve(ref);
                        if (res.error)
                            return res.error;
                        if (res.value === null || res.value === undefined)
                            continue;
                        const num = Number(res.value);
                        if (!isNaN(num))
                            values.push(num);
                    }
                }
                else if (arg.type === 'ref') {
                    const res = resolve(arg.ref);
                    if (res.error)
                        return res.error;
                    if (res.value === null || res.value === undefined)
                        continue;
                    const num = Number(res.value);
                    if (!isNaN(num))
                        values.push(num);
                }
                else if (arg.type === 'number') {
                    values.push(arg.value);
                }
                else if (arg.type === 'func') {
                    const nested = evaluateNode(arg, resolve);
                    if (typeof nested === 'number')
                        values.push(nested);
                    else if (nested === '#DIV/0!' || nested === '#REF!' || nested === '#CYCLE!' || nested === '#ERR!')
                        return nested;
                }
                else {
                    const val = evaluateNode(arg, resolve);
                    if (typeof val === 'number')
                        values.push(val);
                    else if (typeof val === 'string')
                        return val;
                }
            }
            if (name === 'SUM') {
                return values.reduce((a, b) => a + b, 0);
            }
            if (name === 'AVG' || name === 'AVERAGE') {
                if (values.length === 0)
                    return 0;
                return values.reduce((a, b) => a + b, 0) / values.length;
            }
            if (name === 'MIN') {
                if (values.length === 0)
                    return 0;
                return Math.min(...values);
            }
            if (name === 'MAX') {
                if (values.length === 0)
                    return 0;
                return Math.max(...values);
            }
            if (name === 'COUNT') {
                return values.length;
            }
            return '#ERR!';
        }
        default:
            return '#ERR!';
    }
}
};

// ── 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");
class Renderer {
    constructor(containerId, state) {
        this.selectedCol = 0;
        this.selectedRow = 0;
        this.editMode = false;
        this.formulaKeyHandler = (e) => { };
        this.container = document.getElementById(containerId);
        this.state = state;
        this.table = document.createElement('table');
        this.table.className = 'grid-table';
        this.container.appendChild(this.table);
        this.renderHeaders();
        this.renderCells();
        this.selectCell(0, 0);
    }
    renderHeaders() {
        const thead = document.createElement('thead');
        const tr = document.createElement('tr');
        // Empty corner
        const thCorner = document.createElement('th');
        thCorner.style.width = config_1.HEADER_WIDTH + 'px';
        thCorner.textContent = '';
        tr.appendChild(thCorner);
        for (let c = 0; c < config_1.COLS; c++) {
            const th = document.createElement('th');
            th.textContent = config_1.COLS_BY_LABEL[c];
            th.style.width = config_1.CELL_WIDTH + 'px';
            th.addEventListener('contextmenu', (e) => {
                e.preventDefault();
                this.showHeaderMenu(c, 'col');
            });
            tr.appendChild(th);
        }
        thead.appendChild(tr);
        this.table.appendChild(thead);
    }
    renderCells() {
        const tbody = document.createElement('tbody');
        for (let r = 0; r < config_1.ROWS; r++) {
            const tr = document.createElement('tr');
            // Row header
            const thRow = document.createElement('th');
            thRow.textContent = String(r + 1);
            thRow.style.width = config_1.HEADER_WIDTH + 'px';
            thRow.addEventListener('contextmenu', (e) => {
                e.preventDefault();
                this.showHeaderMenu(r, 'row');
            });
            tr.appendChild(thRow);
            for (let c = 0; c < config_1.COLS; c++) {
                const td = document.createElement('td');
                td.setAttribute('data-col', String(c));
                td.setAttribute('data-row', String(r));
                td.style.height = config_1.CELL_HEIGHT + 'px';
                td.style.width = config_1.CELL_WIDTH + 'px';
                td.addEventListener('click', () => {
                    this.selectCell(c, r);
                });
                td.addEventListener('dblclick', () => {
                    this.enterEdit(c, r);
                });
                tr.appendChild(td);
            }
            tbody.appendChild(tr);
        }
        this.table.appendChild(tbody);
    }
    showHeaderMenu(index, type) {
        // Simple context menu using a temporary element
        const menu = document.createElement('div');
        menu.style.position = 'fixed';
        menu.style.background = '#fff';
        menu.style.border = '1px solid #ccc';
        menu.style.zIndex = '100';
        menu.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
        menu.style.padding = '4px 0';
        menu.innerHTML = `<div style="padding:6px 16px;cursor:pointer;font-size:13px;hover:bg:#e8f0fe;" onclick="this.getRootNode().host.insertRow()">Insert ${type}</div>
      <div style="padding:6px 16px;cursor:pointer;font-size:13px;hover:bg:#e8f0fe;" onclick="this.getRootNode().host.deleteRow()">Delete ${type}</div>`;
        // Actually we'll wire events properly below
        menu.style.left = '100px';
        menu.style.top = '100px';
        document.body.appendChild(menu);
        const insertBtn = menu.querySelector('div:nth-child(1)');
        const deleteBtn = menu.querySelector('div:nth-child(2)');
        insertBtn.addEventListener('click', () => {
            if (type === 'row')
                this.insertRow(index);
            else
                this.insertCol(index);
            document.body.removeChild(menu);
        });
        deleteBtn.addEventListener('click', () => {
            if (type === 'row')
                this.deleteRow(index);
            else
                this.deleteCol(index);
            document.body.removeChild(menu);
        });
        setTimeout(() => {
            document.addEventListener('click', () => {
                if (document.body.contains(menu))
                    document.body.removeChild(menu);
            }, { once: true });
        }, 0);
    }
    insertRow(rowIdx) {
        this.state.insertRow(rowIdx);
        this.updateGrid();
    }
    deleteRow(rowIdx) {
        this.state.deleteRow(rowIdx);
        this.updateGrid();
    }
    insertCol(colIdx) {
        this.state.insertCol(colIdx);
        this.updateGrid();
    }
    deleteCol(colIdx) {
        this.state.deleteCol(colIdx);
        this.updateGrid();
    }
    selectCell(c, r) {
        this.selectedCol = c;
        this.selectedRow = r;
        this.updateSelectionUI();
        this.updateFormulaBar();
    }
    enterEdit(c, r) {
        this.selectCell(c, r);
        this.editMode = true;
        const td = this.getTd(c, r);
        if (!td)
            return;
        const currentText = this.state.getCell(c, r).raw;
        const input = document.createElement('input');
        input.type = 'text';
        input.value = currentText;
        input.style.width = '100%';
        input.style.height = '100%';
        input.style.border = 'none';
        input.style.padding = '0 6px';
        input.style.fontFamily = 'inherit';
        input.style.fontSize = '13px';
        input.style.background = '#e8f0fe';
        td.innerHTML = '';
        td.appendChild(input);
        input.focus();
        input.select();
        this.editInput = input;
        input.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                this.commitEdit(c, r);
            }
            else if (e.key === 'Escape') {
                this.cancelEdit();
            }
        });
        input.addEventListener('blur', () => {
            this.commitEdit(c, r);
        });
    }
    commitEdit(c, r) {
        if (!this.editInput)
            return;
        const raw = this.editInput.value;
        this.editMode = false;
        this.editInput = undefined;
        this.state.setRaw(c, r, raw);
        this.updateCellDisplay(c, r);
        this.state.processDirty();
        this.updateGrid();
    }
    cancelEdit() {
        this.editMode = false;
        this.editInput = undefined;
        this.updateGrid();
    }
    updateSelectionUI() {
        const tds = this.table.querySelectorAll('td');
        tds.forEach(td => {
            td.classList.remove('selected');
        });
        const td = this.getTd(this.selectedCol, this.selectedRow);
        if (td)
            td.classList.add('selected');
    }
    updateFormulaBar() {
        const nameBox = document.getElementById('name-box');
        const formulaBar = document.getElementById('formula-bar');
        const colLabel = config_1.COLS_BY_LABEL[this.selectedCol];
        const rowNum = this.selectedRow + 1;
        if (nameBox)
            nameBox.textContent = colLabel + rowNum;
        const data = this.state.getCell(this.selectedCol, this.selectedRow);
        if (formulaBar) {
            formulaBar.value = data.raw;
            formulaBar.removeEventListener('keydown', this.formulaKeyHandler);
            this.formulaKeyHandler = (e) => {
                const ke = e;
                if (ke.key === 'Enter') {
                    const raw = e.target.value;
                    this.state.setRaw(this.selectedCol, this.selectedRow, raw);
                    this.updateCellDisplay(this.selectedCol, this.selectedRow);
                    this.state.processDirty();
                    this.updateGrid();
                    // Move selection down
                    const newRow = Math.min(this.selectedRow + 1, config_1.ROWS - 1);
                    this.selectCell(this.selectedCol, newRow);
                }
            };
            formulaBar.addEventListener('keydown', this.formulaKeyHandler);
        }
    }
    getTd(c, r) {
        return this.table.querySelector(`td[data-col="${c}"][data-row="${r}"]`);
    }
    updateCellDisplay(c, r) {
        const td = this.getTd(c, r);
        if (!td)
            return;
        const data = this.state.getCell(c, r);
        td.innerHTML = '';
        if (data.error) {
            td.textContent = data.error;
            td.classList.add('error');
            td.classList.remove('text-left');
        }
        else {
            td.classList.remove('error');
            if (data.value === null || data.value === '') {
                td.textContent = '';
            }
            else {
                td.textContent = String(data.value);
                if (typeof data.value === 'number' || (typeof data.value === 'string' && !isNaN(Number(data.value)))) {
                    td.style.textAlign = 'right';
                    td.classList.remove('text-left');
                }
                else {
                    td.style.textAlign = 'left';
                    td.classList.add('text-left');
                }
            }
        }
    }
    updateGrid() {
        for (let r = 0; r < config_1.ROWS; r++) {
            for (let c = 0; c < config_1.COLS; c++) {
                this.updateCellDisplay(c, r);
            }
        }
        this.updateSelectionUI();
        this.updateFormulaBar();
    }
}
exports.Renderer = Renderer;
};

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