← Spreadsheet results

Spreadsheet

Qwen 3.6 27B MTP GGUF IQ4 NL · 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 62,889 bytes · SHA-256 52e3d8bf4488
<!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: spreadsheet ── */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  height: 100%;
  font-family: 'Google Sans', 'Roboto', 'Segoe UI', Arial, sans-serif;
  font-size: 13px;
  color: #202124;
  background: #ffffff;
  overflow: hidden;
}

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

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

#toolbar button {
  background: #ffffff;
  border: 1px solid #dadce0;
  border-radius: 4px;
  padding: 4px 10px;
  font-size: 12px;
  color: #3c4043;
  cursor: pointer;
  white-space: nowrap;
}

#toolbar button:hover {
  background: #e8eaed;
  border-color: #c0c0c0;
}

.toolbar-separator {
  width: 1px;
  height: 20px;
  background: #dadce0;
  margin: 0 8px;
}

.toolbar-hint {
  font-size: 11px;
  color: #5f6368;
  margin-left: auto;
}

/* Formula bar */
#formula-bar {
  display: flex;
  align-items: center;
  padding: 4px 8px;
  border-bottom: 1px solid #dadce0;
  background: #ffffff;
  flex-shrink: 0;
  height: 36px;
}

#cell-ref {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 28px;
  border: 1px solid #dadce0;
  border-right: none;
  border-radius: 4px 0 0 4px;
  background: #f1f3f4;
  font-size: 12px;
  font-weight: 600;
  color: #3c4043;
  flex-shrink: 0;
  cursor: default;
}

#formula-input {
  flex: 1;
  height: 28px;
  border: 1px solid #dadce0;
  border-left: none;
  border-radius: 0 4px 4px 0;
  padding: 0 8px;
  font-size: 13px;
  font-family: inherit;
  color: #202124;
  outline: none;
}

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

/* Grid area */
#grid-area {
  flex: 1;
  overflow: auto;
  position: relative;
}

/* Table styles */
.sheet-table {
  border-collapse: collapse;
  table-layout: fixed;
  width: max-content;
}

.sheet-table th,
.sheet-table td {
  border-right: 1px solid #e0e0e0;
  border-bottom: 1px solid #e0e0e0;
  padding: 0 6px;
  font-size: 13px;
  line-height: 22px;
  height: 24px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: middle;
}

/* Column headers */
.col-header {
  background: #f5f5f5;
  color: #333;
  font-weight: 600;
  font-size: 12px;
  text-align: center;
  position: sticky;
  top: 0;
  z-index: 3;
  border-bottom: 2px solid #c0c0c0;
  border-right: 1px solid #e0e0e0;
  user-select: none;
  cursor: default;
}

/* Row headers */
.row-header {
  background: #f5f5f5;
  color: #333;
  font-weight: 600;
  font-size: 12px;
  text-align: center;
  position: sticky;
  left: 0;
  z-index: 2;
  border-right: 2px solid #c0c0c0;
  user-select: none;
  cursor: default;
}

/* Corner header */
.corner-header {
  background: #f5f5f5;
  position: sticky;
  top: 0;
  left: 0;
  z-index: 4;
  border-bottom: 2px solid #c0c0c0;
  border-right: 2px solid #c0c0c0;
  user-select: none;
}

/* Cell styles */
.cell {
  background: #ffffff;
  color: #202124;
  cursor: cell;
  position: relative;
}

.cell:hover {
  outline: 1px solid #1a73e8;
  outline-offset: -1px;
}

.cell.selected {
  outline: 2px solid #1a73e8;
  outline-offset: -1px;
  z-index: 1;
}

.cell.cell-number {
  text-align: right;
}

.cell.cell-text {
  text-align: left;
}

.cell.cell-error {
  color: #d32f2f;
  font-size: 11px;
  font-style: italic;
}

.cell.editing {
  padding: 0;
}

.cell-edit-input {
  width: 100%;
  height: 100%;
  border: none;
  outline: 2px solid #1a73e8;
  padding: 0 6px;
  font-size: 13px;
  font-family: inherit;
  color: #202124;
  background: #fffde7;
}

/* Context menu */
.context-menu {
  position: fixed;
  background: #ffffff;
  border: 1px solid #dadce0;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  z-index: 1000;
  min-width: 160px;
  padding: 4px 0;
}

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

.context-menu-item:hover {
  background: #f1f3f4;
}
</style>
</head>
<body>
<div id="spreadsheet-container">
  <!-- Toolbar -->
  <div id="toolbar">
    <button id="btn-insert-row" title="Insert row above selection">+ Row</button>
    <button id="btn-delete-row" title="Delete selected row">- Row</button>
    <button id="btn-insert-col" title="Insert column before selection">+ Col</button>
    <button id="btn-delete-col" title="Delete selected column">- Col</button>
    <div class="toolbar-separator"></div>
    <span class="toolbar-hint">Right-click headers for more options • Delete to clear • Enter/Tab to navigate</span>
  </div>
  <!-- Formula bar -->
  <div id="formula-bar">
    <span id="cell-ref">A1</span>
    <input id="formula-input" type="text" spellcheck="false" autocomplete="off">
  </div>
  <!-- Grid area -->
  <div id="grid-area"></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./sheet":"src/sheet.ts","./renderer":"src/renderer.ts","./types":"src/types.ts"},"src/parser.ts":{"./types":"src/types.ts"},"src/sheet.ts":{"./types":"src/types.ts","./parser":"src/parser.ts"},"src/renderer.ts":{"./sheet":"src/sheet.ts","./types":"src/types.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 sheet_1 = require("./sheet");
const renderer_1 = require("./renderer");
const types_1 = require("./types");
function main() {
    const sheet = new sheet_1.Sheet();
    const depGraph = new sheet_1.DependencyGraph();
    // Load saved data
    const renderer = new renderer_1.Renderer(sheet, depGraph);
    const loaded = renderer.load();
    if (loaded) {
        depGraph.updateAll(sheet);
        renderer.fullRecalculate();
    }
    // Wire up grid keyboard events
    const gridArea = document.getElementById('grid-area');
    gridArea.addEventListener('keydown', (e) => renderer.handleGridKeydown(e));
    // Wire up formula bar
    const formulaInput = document.getElementById('formula-input');
    formulaInput.addEventListener('keydown', (e) => renderer.handleFormulaBarKeydown(e));
    formulaInput.addEventListener('input', () => renderer.handleFormulaBarInput());
    // Focus formula bar on click
    const cellRef = document.getElementById('cell-ref');
    cellRef.addEventListener('click', () => {
        formulaInput.focus();
    });
    // Wire up 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');
    btnInsertRow.addEventListener('click', () => {
        renderer.insertRow(renderer.selection.row);
        // Adjust selection if needed
        if (renderer.selection.row >= renderer.sheet.numRows) {
            renderer.selection.row = renderer.sheet.numRows - 1;
        }
        renderer.highlightCell();
        renderer.updateFormulaBar();
    });
    btnDeleteRow.addEventListener('click', () => {
        renderer.deleteRow(renderer.selection.row);
        if (renderer.selection.row >= renderer.sheet.numRows) {
            renderer.selection.row = Math.max(0, renderer.sheet.numRows - 1);
        }
        renderer.highlightCell();
        renderer.updateFormulaBar();
    });
    btnInsertCol.addEventListener('click', () => {
        renderer.insertCol(renderer.selection.col);
        if (renderer.selection.col >= renderer.sheet.numCols) {
            renderer.selection.col = renderer.sheet.numCols - 1;
        }
        renderer.highlightCell();
        renderer.updateFormulaBar();
    });
    btnDeleteCol.addEventListener('click', () => {
        renderer.deleteCol(renderer.selection.col);
        if (renderer.selection.col >= renderer.sheet.numCols) {
            renderer.selection.col = Math.max(0, renderer.sheet.numCols - 1);
        }
        renderer.highlightCell();
        renderer.updateFormulaBar();
    });
    // Initial cell selection highlight
    renderer.highlightCell();
    renderer.updateFormulaBar();
    // Save on page unload
    window.addEventListener('beforeunload', () => renderer.save());
    // Expose internals for testing
    const expose = window;
    expose.__sheet = sheet;
    expose.__renderer = renderer;
    expose.__depGraph = depGraph;
    expose.__recalculate = sheet_1.recalculate;
    expose.__evaluateCell = sheet_1.evaluateCell;
    expose.__addrKey = types_1.addrKey;
}
main();
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.addrKey = addrKey;
exports.parseAddrKey = parseAddrKey;
exports.colName = colName;
exports.parseColName = parseColName;
exports.cellLabel = cellLabel;
function addrKey(col, row) {
    return col + ',' + row;
}
function parseAddrKey(key) {
    const idx = key.indexOf(',');
    return { col: Number(key.slice(0, idx)), row: Number(key.slice(idx + 1)) };
}
function colName(col) {
    return String.fromCharCode(65 + col);
}
function parseColName(s) {
    const c = s.toUpperCase();
    if (c.length === 1 && c >= 'A' && c <= 'Z')
        return c.charCodeAt(0) - 65;
    return null;
}
function cellLabel(col, row) {
    return colName(col) + (row + 1);
}
};

// ── 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;
const types_1 = require("./types");
function tokenize(input) {
    const tokens = [];
    let i = 0;
    while (i < input.length) {
        const c = input[i];
        // Skip whitespace
        if (c === ' ' || c === '\t' || c === '\n') {
            i++;
            continue;
        }
        // Numbers (including decimal point)
        if (c >= '0' && c <= '9' || (c === '.' && i + 1 < input.length && input[i + 1] >= '0' && input[i + 1] <= '9')) {
            let s = '';
            while (i < input.length && (input[i] >= '0' && input[i] <= '9' || input[i] === '.')) {
                s += input[i];
                i++;
            }
            tokens.push({ type: 'NUMBER', value: s, pos: tokens.length });
            continue;
        }
        // Cell reference or function name (letters followed by optional digits for cell refs)
        if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
            let s = '';
            while (i < input.length && ((input[i] >= 'A' && input[i] <= 'Z') || (input[i] >= 'a' && input[i] <= 'z') || (input[i] >= '0' && input[i] <= '9'))) {
                s += input[i];
                i++;
            }
            // Check if it's a cell reference (e.g. A1, B7)
            const upper = s.toUpperCase();
            const lastChar = upper[upper.length - 1];
            // Could be a cell ref: letter(s) then digits
            if (upper.length >= 2 && lastChar >= '0' && lastChar <= '9') {
                tokens.push({ type: 'CELL', value: upper, pos: tokens.length });
            }
            else {
                tokens.push({ type: 'FUNC', value: upper, pos: tokens.length });
            }
            continue;
        }
        // Operators and punctuation
        if (c === '+') {
            tokens.push({ type: 'PLUS', value: '+', pos: tokens.length });
            i++;
            continue;
        }
        if (c === '-') {
            tokens.push({ type: 'MINUS', value: '-', pos: tokens.length });
            i++;
            continue;
        }
        if (c === '*') {
            tokens.push({ type: 'STAR', value: '*', pos: tokens.length });
            i++;
            continue;
        }
        if (c === '/') {
            tokens.push({ type: 'SLASH', value: '/', pos: tokens.length });
            i++;
            continue;
        }
        if (c === '(') {
            tokens.push({ type: 'LPAREN', value: '(', pos: tokens.length });
            i++;
            continue;
        }
        if (c === ')') {
            tokens.push({ type: 'RPAREN', value: ')', pos: tokens.length });
            i++;
            continue;
        }
        if (c === ',') {
            tokens.push({ type: 'COMMA', value: ',', pos: tokens.length });
            i++;
            continue;
        }
        if (c === ':') {
            tokens.push({ type: 'RANGE', value: ':', pos: tokens.length });
            i++;
            continue;
        }
        // Unknown character - skip it
        i++;
    }
    tokens.push({ type: 'EOF', value: '', pos: tokens.length });
    return tokens;
}
function peek(p) { return p.tokens[p.pos]; }
function consume(p, type) {
    const t = p.tokens[p.pos];
    if (t.type !== type)
        throw new Error(`Unexpected token '${t.value}' (expected ${type})`);
    p.pos++;
    return t;
}
function parseCellRef(token) {
    const s = token.value;
    // Find where letters end and digits begin
    let ci = 0;
    while (ci < s.length && s[ci] >= 'A' && s[ci] <= 'Z')
        ci++;
    const letters = s.slice(0, ci);
    const digits = s.slice(ci);
    const col = (0, types_1.parseColName)(letters);
    const row = Number(digits) - 1;
    if (col === null || isNaN(row) || row < 0)
        return null;
    return { col, row };
}
function parseExpr(p) {
    let left = parseTerm(p);
    while (peek(p).type === 'PLUS' || peek(p).type === 'MINUS') {
        const op = consume(p, peek(p).type);
        const right = parseTerm(p);
        left = { type: 'binary', op: op.value, left, right };
    }
    return left;
}
function parseTerm(p) {
    let left = parseUnary(p);
    while (peek(p).type === 'STAR' || peek(p).type === 'SLASH') {
        const op = consume(p, peek(p).type);
        const right = parseUnary(p);
        left = { type: 'binary', op: op.value, left, right };
    }
    return left;
}
function parseUnary(p) {
    if (peek(p).type === 'MINUS') {
        consume(p, 'MINUS');
        const operand = parseUnary(p);
        return { type: 'unary', op: '-', operand };
    }
    if (peek(p).type === 'PLUS') {
        consume(p, 'PLUS');
        return parseUnary(p);
    }
    return parsePrimary(p);
}
function parsePrimary(p) {
    const t = peek(p);
    // Number literal
    if (t.type === 'NUMBER') {
        consume(p, 'NUMBER');
        return { type: 'number', value: Number(t.value) };
    }
    // Parenthesized expression
    if (t.type === 'LPAREN') {
        consume(p, 'LPAREN');
        const expr = parseExpr(p);
        consume(p, 'RPAREN');
        return expr;
    }
    // Function call
    if (t.type === 'FUNC') {
        const name = consume(p, 'FUNC').value;
        consume(p, 'LPAREN');
        const args = [];
        if (peek(p).type !== 'RPAREN') {
            args.push(parseExpr(p));
            while (peek(p).type === 'COMMA') {
                consume(p, 'COMMA');
                args.push(parseExpr(p));
            }
        }
        consume(p, 'RPAREN');
        return { type: 'func', name, args };
    }
    // Cell reference (possibly with range)
    if (t.type === 'CELL') {
        const addr = parseCellRef(consume(p, 'CELL'));
        if (!addr)
            throw new Error(`Invalid cell reference '${t.value}'`);
        if (addr.col > p.col || addr.row > p.row || addr.row < 0)
            throw new Error(`Invalid cell reference '${t.value}'`);
        // Check for range operator
        if (peek(p).type === 'RANGE') {
            consume(p, 'RANGE');
            const endToken = consume(p, 'CELL');
            const end = parseCellRef(endToken);
            if (!end)
                throw new Error(`Invalid cell reference '${endToken.value}'`);
            if (end.col > p.col || end.row > p.row || end.row < 0)
                throw new Error(`Invalid cell reference '${endToken.value}'`);
            // Normalize range so start <= end
            const startCol = Math.min(addr.col, end.col);
            const startRow = Math.min(addr.row, end.row);
            const endCol = Math.max(addr.col, end.col);
            const endRow = Math.max(addr.row, end.row);
            return { type: 'range', start: { col: startCol, row: startRow }, end: { col: endCol, row: endRow } };
        }
        return { type: 'cell', col: addr.col, row: addr.row };
    }
    throw new Error(`Unexpected token '${t.value}'`);
}
function parseFormula(input, maxCol, maxRow) {
    const tokens = tokenize(input);
    const p = { tokens, pos: 0, col: maxCol, row: maxRow };
    const expr = parseExpr(p);
    if (peek(p).type !== 'EOF')
        throw new Error(`Unexpected token '${peek(p).value}'`);
    return expr;
}
};

// ── module: src/sheet.ts ──
__mods["src/sheet.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DependencyGraph = exports.Sheet = exports.COLORS = exports.HEADER_HEIGHT = exports.HEADER_WIDTH = exports.CELL_HEIGHT = exports.CELL_WIDTH = exports.NUM_ROWS = exports.NUM_COLS = void 0;
exports.evaluateCell = evaluateCell;
exports.recalculate = recalculate;
const types_1 = require("./types");
const parser_1 = require("./parser");
// ---- Tunable Constants ----
exports.NUM_COLS = 26;
exports.NUM_ROWS = 100;
exports.CELL_WIDTH = 100;
exports.CELL_HEIGHT = 24;
exports.HEADER_WIDTH = 48;
exports.HEADER_HEIGHT = 26;
exports.COLORS = {
    bg: '#ffffff',
    gridLine: '#e0e0e0',
    headerBg: '#f5f5f5',
    headerText: '#333333',
    headerBorder: '#c0c0c0',
    selection: '#1a73e8',
    cellText: '#202124',
    errorColor: '#d32f2f',
    formulaBarBg: '#ffffff',
    formulaBarBorder: '#dadce0',
    toolbarBg: '#f1f3f4',
    toolbarBorder: '#dadce0',
};
// ---- Sheet State ----
class Sheet {
    constructor() {
        this.cells = new Map();
        this.numCols = exports.NUM_COLS;
        this.numRows = exports.NUM_ROWS;
    }
    getCell(col, row) {
        if (col < 0 || col >= this.numCols || row < 0 || row >= this.numRows)
            return undefined;
        return this.cells.get((0, types_1.addrKey)(col, row));
    }
    setCellRaw(col, row, raw) {
        const key = (0, types_1.addrKey)(col, row);
        if (raw === '') {
            this.cells.delete(key);
        }
        else {
            this.cells.set(key, { raw, computed: { type: 'null' } });
        }
    }
    // ---- Row/Column Insert/Delete with formula rewriting ----
    rewriteCellRaw(cell, colShift, rowShift) {
        const raw = cell.raw;
        if (!raw.startsWith('='))
            return raw;
        // We need to rewrite cell references in the raw formula
        // Strategy: tokenize, then rebuild the formula string with shifted references
        const tokens = (0, parser_1.tokenize)(raw.slice(1)); // skip '='
        let result = '=';
        for (const t of tokens) {
            if (t.type === 'CELL') {
                const addr = this.parseCellFromToken(t.value);
                if (addr) {
                    const newCol = colShift(addr.col);
                    const newRow = rowShift(addr.row);
                    if (newCol >= 0 && newRow >= 0) {
                        result += (0, types_1.cellLabel)(newCol, newRow);
                    }
                    else {
                        // Reference would be out of bounds - keep original (will be #REF!)
                        result += t.value;
                    }
                }
                else {
                    result += t.value;
                }
            }
            else {
                result += t.value;
            }
        }
        return result;
    }
    parseCellFromToken(s) {
        let ci = 0;
        while (ci < s.length && s[ci] >= 'A' && s[ci] <= 'Z')
            ci++;
        if (ci === 0)
            return null;
        const letters = s.slice(0, ci);
        const digits = s.slice(ci);
        if (digits.length === 0)
            return null;
        const col = letters.charCodeAt(0) - 65;
        const row = Number(digits) - 1;
        if (isNaN(row) || row < 0)
            return null;
        return { col, row };
    }
    insertRow(before) {
        if (before < 0 || before > this.numRows)
            return;
        // Rewrite all formulas to shift row references
        const newCells = new Map();
        for (const [key, cell] of this.cells) {
            const parts = key.split(',');
            const c = Number(parts[0]);
            const r = Number(parts[1]);
            const newRow = r >= before ? r + 1 : r;
            const newRaw = cell.raw.startsWith('=')
                ? this.rewriteCellRaw(cell, (col) => col, (row) => row >= before ? row + 1 : row)
                : cell.raw;
            newCells.set((0, types_1.addrKey)(c, newRow), { raw: newRaw, computed: { type: 'null' } });
        }
        this.cells = newCells;
        this.numRows++;
    }
    deleteRow(row) {
        if (row < 0 || row >= this.numRows)
            return;
        // Rewrite formulas first, then remove cells in that row
        const newCells = new Map();
        for (const [key, cell] of this.cells) {
            const parts = key.split(',');
            const c = Number(parts[0]);
            const r = Number(parts[1]);
            if (r === row)
                continue; // remove this cell
            const newRow = r > row ? r - 1 : r;
            const newRaw = cell.raw.startsWith('=')
                ? this.rewriteCellRaw(cell, (col) => col, (rowIdx) => rowIdx > row ? rowIdx - 1 : rowIdx)
                : cell.raw;
            newCells.set((0, types_1.addrKey)(c, newRow), { raw: newRaw, computed: { type: 'null' } });
        }
        this.cells = newCells;
        this.numRows--;
    }
    insertCol(before) {
        if (before < 0 || before > this.numCols)
            return;
        const newCells = new Map();
        for (const [key, cell] of this.cells) {
            const parts = key.split(',');
            const c = Number(parts[0]);
            const r = Number(parts[1]);
            const newCol = c >= before ? c + 1 : c;
            const newRaw = cell.raw.startsWith('=')
                ? this.rewriteCellRaw(cell, (col) => col >= before ? col + 1 : col, (row) => row)
                : cell.raw;
            newCells.set((0, types_1.addrKey)(newCol, r), { raw: newRaw, computed: { type: 'null' } });
        }
        this.cells = newCells;
        this.numCols++;
    }
    deleteCol(col) {
        if (col < 0 || col >= this.numCols)
            return;
        const newCells = new Map();
        for (const [key, cell] of this.cells) {
            const parts = key.split(',');
            const c = Number(parts[0]);
            const r = Number(parts[1]);
            if (c === col)
                continue;
            const newCol = c > col ? c - 1 : c;
            const newRaw = cell.raw.startsWith('=')
                ? this.rewriteCellRaw(cell, (colIdx) => colIdx > col ? colIdx - 1 : colIdx, (row) => row)
                : cell.raw;
            newCells.set((0, types_1.addrKey)(newCol, r), { raw: newRaw, computed: { type: 'null' } });
        }
        this.cells = newCells;
        this.numCols--;
    }
}
exports.Sheet = Sheet;
// ---- Formula Evaluator ----
function evalExpr(expr, sheet, visiting) {
    switch (expr.type) {
        case 'number':
            return { type: 'number', value: expr.value };
        case 'cell': {
            const key = (0, types_1.addrKey)(expr.col, expr.row);
            if (visiting.has(key))
                return { type: 'error', value: '#CYCLE!' };
            const cell = sheet.getCell(expr.col, expr.row);
            if (!cell)
                return { type: 'null' };
            return cell.computed;
        }
        case 'range': {
            // Collect values in range for use by functions
            const values = [];
            for (let r = expr.start.row; r <= expr.end.row; r++) {
                for (let c = expr.start.col; c <= expr.end.col; c++) {
                    const cell = sheet.getCell(c, r);
                    if (cell)
                        values.push(cell.computed);
                }
            }
            return { type: 'text', value: '' }; // ranges are only valid inside functions
        }
        case 'unary': {
            if (expr.op === '-') {
                const v = evalExpr(expr.operand, sheet, visiting);
                if (v.type === 'number')
                    return { type: 'number', value: -v.value };
                if (v.type === 'error')
                    return v;
                return { type: 'error', value: '#ERR!' };
            }
            return evalExpr(expr.operand, sheet, visiting);
        }
        case 'binary': {
            const left = evalExpr(expr.left, sheet, visiting);
            if (left.type === 'error')
                return left;
            const right = evalExpr(expr.right, sheet, visiting);
            if (right.type === 'error')
                return right;
            if (left.type !== 'number' || right.type !== 'number')
                return { type: 'error', value: '#ERR!' };
            switch (expr.op) {
                case '+': return { type: 'number', value: left.value + right.value };
                case '-': return { type: 'number', value: left.value - right.value };
                case '*': return { type: 'number', value: left.value * right.value };
                case '/':
                    if (right.value === 0)
                        return { type: 'error', value: '#DIV/0!' };
                    return { type: 'number', value: left.value / right.value };
            }
            return { type: 'error', value: '#ERR!' };
        }
        case 'func': {
            return evalFunc(expr.name, expr.args, sheet, visiting);
        }
    }
}
function evalFunc(name, args, sheet, visiting) {
    const upperName = name.toUpperCase();
    // Collect all numeric values and cell values from args
    const allValues = [];
    const numbers = [];
    for (const arg of args) {
        if (arg.type === 'number') {
            numbers.push(arg.value);
        }
        else if (arg.type === 'cell') {
            const key = (0, types_1.addrKey)(arg.col, arg.row);
            if (visiting.has(key))
                return { type: 'error', value: '#CYCLE!' };
            const cell = sheet.getCell(arg.col, arg.row);
            if (cell) {
                allValues.push(cell.computed);
                if (cell.computed.type === 'number')
                    numbers.push(cell.computed.value);
            }
        }
        else if (arg.type === 'range') {
            for (let r = arg.start.row; r <= arg.end.row; r++) {
                for (let c = arg.start.col; c <= arg.end.col; c++) {
                    const key = (0, types_1.addrKey)(c, r);
                    if (visiting.has(key))
                        return { type: 'error', value: '#CYCLE!' };
                    const cell = sheet.getCell(c, r);
                    if (cell) {
                        allValues.push(cell.computed);
                        if (cell.computed.type === 'number')
                            numbers.push(cell.computed.value);
                    }
                }
            }
        }
        else if (arg.type === 'binary' || arg.type === 'unary') {
            const v = evalExpr(arg, sheet, visiting);
            if (v.type === 'error')
                return v;
            if (v.type === 'number')
                numbers.push(v.value);
        }
    }
    const allNums = numbers;
    switch (upperName) {
        case 'SUM':
            return allNums.length === 0 ? { type: 'number', value: 0 } : { type: 'number', value: allNums.reduce((a, b) => a + b, 0) };
        case 'AVG':
        case 'AVERAGE':
            return allNums.length === 0 ? { type: 'error', value: '#DIV/0!' } : { type: 'number', value: allNums.reduce((a, b) => a + b, 0) / allNums.length };
        case 'MIN':
            return allNums.length === 0 ? { type: 'number', value: 0 } : { type: 'number', value: Math.min(...allNums) };
        case 'MAX':
            return allNums.length === 0 ? { type: 'number', value: 0 } : { type: 'number', value: Math.max(...allNums) };
        case 'COUNT':
            return { type: 'number', value: allNums.length };
        default:
            return { type: 'error', value: '#ERR!' };
    }
}
function evaluateCell(col, row, sheet, visiting) {
    const cell = sheet.getCell(col, row);
    if (!cell)
        return { type: 'null' };
    const raw = cell.raw;
    if (!raw.startsWith('=')) {
        // Check if it's a number
        const num = Number(raw);
        if (!isNaN(num) && raw.trim() !== '') {
            return { type: 'number', value: num };
        }
        return { type: 'text', value: raw };
    }
    // Parse and evaluate formula
    try {
        const expr = (0, parser_1.parseFormula)(raw.slice(1), sheet.numCols - 1, sheet.numRows - 1);
        return evalExpr(expr, sheet, visiting);
    }
    catch (e) {
        return { type: 'error', value: '#ERR!' };
    }
}
// ---- Dependency Graph & Recalculation ----
function collectReferences(raw, maxCol, maxRow) {
    if (!raw.startsWith('='))
        return [];
    const refs = new Set();
    try {
        const tokens = (0, parser_1.tokenize)(raw.slice(1));
        for (const t of tokens) {
            if (t.type === 'CELL') {
                const addr = parseCellRefFromToken(t.value);
                if (addr)
                    refs.add((0, types_1.addrKey)(addr.col, addr.row));
            }
        }
    }
    catch (e) {
        // parse error - no valid references
    }
    return Array.from(refs);
}
function parseCellRefFromToken(s) {
    let ci = 0;
    while (ci < s.length && s[ci] >= 'A' && s[ci] <= 'Z')
        ci++;
    if (ci === 0)
        return null;
    const digits = s.slice(ci);
    if (digits.length === 0)
        return null;
    const col = s.slice(0, ci).charCodeAt(0) - 65;
    const row = Number(digits) - 1;
    if (isNaN(row) || row < 0 || col < 0)
        return null;
    return { col, row };
}
class DependencyGraph {
    constructor() {
        // cell -> set of cells it depends on
        this.dependsOn = new Map();
        // cell -> set of cells that depend on it
        this.dependents = new Map();
    }
    clear() {
        this.dependsOn.clear();
        this.dependents.clear();
    }
    updateCell(col, row, sheet) {
        const key = (0, types_1.addrKey)(col, row);
        const raw = sheet.getCell(col, row)?.raw || '';
        const refs = collectReferences(raw, sheet.numCols - 1, sheet.numRows - 1);
        // Remove old edges
        const oldRefs = this.dependsOn.get(key);
        if (oldRefs) {
            for (const ref of oldRefs) {
                const deps = this.dependents.get(ref);
                if (deps)
                    deps.delete(key);
            }
            this.dependsOn.delete(key);
        }
        // Add new edges
        if (refs.length > 0) {
            this.dependsOn.set(key, new Set(refs));
            for (const ref of refs) {
                if (!this.dependents.has(ref))
                    this.dependents.set(ref, new Set());
                this.dependents.get(ref).add(key);
            }
        }
    }
    updateAll(sheet) {
        this.clear();
        for (const [key, cell] of sheet.cells) {
            const parts = key.split(',');
            const col = Number(parts[0]);
            const row = Number(parts[1]);
            this.updateCell(col, row, sheet);
        }
    }
    // Find all cells that need recalculation, in topological order (dependencies first)
    // Starting from a set of changed cells
    recalcOrder(changed) {
        // BFS from changed cells through dependents, collecting all affected cells
        // Then do topological sort on the subgraph
        const affected = new Set();
        const queue = [...changed];
        const visited = new Set();
        while (queue.length > 0) {
            const current = queue.shift();
            if (visited.has(current))
                continue;
            visited.add(current);
            affected.add(current);
            const deps = this.dependents.get(current);
            if (deps) {
                for (const dep of deps) {
                    if (!visited.has(dep))
                        queue.push(dep);
                }
            }
        }
        // Topological sort using Kahn's algorithm
        // Count in-degree (only from within affected set)
        const inDegree = new Map();
        for (const cell of affected) {
            const deps = this.dependsOn.get(cell);
            let count = 0;
            if (deps) {
                for (const d of deps) {
                    if (affected.has(d))
                        count++;
                }
            }
            inDegree.set(cell, count);
        }
        // Start with cells that have no dependencies in the affected set
        const q = [];
        for (const [cell, deg] of inDegree) {
            if (deg === 0)
                q.push(cell);
        }
        const result = [];
        while (q.length > 0) {
            const current = q.shift();
            result.push(current);
            const deps = this.dependents.get(current);
            if (deps) {
                for (const dep of deps) {
                    if (affected.has(dep)) {
                        const newDeg = (inDegree.get(dep) || 0) - 1;
                        inDegree.set(dep, newDeg);
                        if (newDeg === 0)
                            q.push(dep);
                    }
                }
            }
        }
        // If not all affected cells are in result, there's a cycle
        if (result.length < affected.size) {
            return null; // cycle detected
        }
        return result;
    }
}
exports.DependencyGraph = DependencyGraph;
function recalculate(sheet, depGraph, changedKey) {
    const changed = new Set();
    changed.add(changedKey);
    // Find all affected cells
    const affected = new Set();
    const q = [...changed];
    const visited = new Set();
    while (q.length > 0) {
        const cur = q.shift();
        if (visited.has(cur))
            continue;
        visited.add(cur);
        affected.add(cur);
        const deps = depGraph.dependents.get(cur);
        if (deps)
            for (const d of deps)
                if (!visited.has(d))
                    q.push(d);
    }
    const recalcSet = new Set();
    // Find cycle cells
    const cycleCells = findCycleCells(depGraph, changed);
    if (cycleCells.size > 0) {
        // Mark cycle cells
        for (const key of cycleCells) {
            const { col, row } = (0, types_1.parseAddrKey)(key);
            const cell = sheet.getCell(col, row);
            if (cell) {
                cell.computed = { type: 'error', value: '#CYCLE!' };
                recalcSet.add(key);
            }
        }
        // Recalculate non-cycle cells in topological order
        const nonCycle = new Set();
        for (const k of affected)
            if (!cycleCells.has(k))
                nonCycle.add(k);
        const order = topologicalSort(depGraph, nonCycle);
        for (const key of order) {
            const { col, row } = (0, types_1.parseAddrKey)(key);
            const cell = sheet.getCell(col, row);
            if (!cell)
                continue;
            const visiting = new Set();
            visiting.add(key);
            cell.computed = evaluateCell(col, row, sheet, visiting);
            recalcSet.add(key);
        }
    }
    else {
        // No cycle - recalculate all affected in topological order
        const order = depGraph.recalcOrder(changed) || [];
        for (const key of order) {
            const { col, row } = (0, types_1.parseAddrKey)(key);
            const cell = sheet.getCell(col, row);
            if (!cell)
                continue;
            const visiting = new Set();
            visiting.add(key);
            cell.computed = evaluateCell(col, row, sheet, visiting);
            recalcSet.add(key);
        }
    }
    return recalcSet;
}
function topologicalSort(depGraph, nodes) {
    const inDegree = new Map();
    for (const cell of nodes) {
        const deps = depGraph.dependsOn.get(cell);
        let count = 0;
        if (deps)
            for (const d of deps)
                if (nodes.has(d))
                    count++;
        inDegree.set(cell, count);
    }
    const q = [];
    for (const [cell, deg] of inDegree)
        if (deg === 0)
            q.push(cell);
    const result = [];
    while (q.length > 0) {
        const cur = q.shift();
        result.push(cur);
        const deps = depGraph.dependents.get(cur);
        if (deps) {
            for (const dep of deps) {
                if (nodes.has(dep)) {
                    const newDeg = (inDegree.get(dep) || 0) - 1;
                    inDegree.set(dep, newDeg);
                    if (newDeg === 0)
                        q.push(dep);
                }
            }
        }
    }
    return result;
}
function findCycleCells(depGraph, changed) {
    // Find all cells involved in cycles via DFS
    const cycleCells = new Set();
    // Collect all affected cells first
    const affected = new Set();
    const queue = [...changed];
    const visited = new Set();
    while (queue.length > 0) {
        const current = queue.shift();
        if (visited.has(current))
            continue;
        visited.add(current);
        affected.add(current);
        const deps = depGraph.dependents.get(current);
        if (deps)
            for (const dep of deps)
                if (!visited.has(dep))
                    queue.push(dep);
    }
    // DFS from each affected cell to find cycles
    for (const start of affected) {
        const path = new Map(); // cell -> depth
        const stack = [{ cell: start, isReturn: false }];
        while (stack.length > 0) {
            const top = stack[stack.length - 1];
            const current = top.cell;
            const isReturn = top.isReturn;
            if (!isReturn) {
                top.isReturn = true;
                if (path.has(current)) {
                    // Found a cycle - mark all cells in the cycle
                    const cycleStart = path.get(current);
                    for (const [cell, depth] of path) {
                        if (depth >= cycleStart)
                            cycleCells.add(cell);
                    }
                    continue;
                }
                path.set(current, path.size);
                const deps = depGraph.dependents.get(current);
                if (deps) {
                    for (const dep of deps) {
                        if (affected.has(dep)) {
                            stack.push({ cell: dep, isReturn: false });
                        }
                    }
                }
            }
            else {
                path.delete(current);
                stack.pop();
            }
        }
    }
    return cycleCells;
}
};

// ── 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 sheet_1 = require("./sheet");
const types_1 = require("./types");
class Renderer {
    constructor(sheet, depGraph) {
        this.selection = { col: 0, row: 0 };
        this.editing = false;
        this.editCell = null;
        this.editInput = null;
        // Cell DOM cache: Map<key, td>
        this.cellElements = new Map();
        this.sheet = sheet;
        this.depGraph = depGraph;
        this.container = document.getElementById('spreadsheet-container');
        this.grid = document.getElementById('grid-area');
        this.formulaInput = document.getElementById('formula-input');
        this.cellRefDisplay = document.getElementById('cell-ref');
        this.buildTable();
    }
    buildTable() {
        this.grid.innerHTML = '';
        this.cellElements.clear();
        const table = document.createElement('table');
        table.className = 'sheet-table';
        this.table = table;
        // Thead with corner cell + column headers
        const thead = document.createElement('thead');
        const headerRow = document.createElement('tr');
        // Corner cell
        const corner = document.createElement('th');
        corner.className = 'corner-header';
        corner.style.width = sheet_1.HEADER_WIDTH + 'px';
        corner.style.height = sheet_1.HEADER_HEIGHT + 'px';
        headerRow.appendChild(corner);
        // Column headers
        for (let c = 0; c < this.sheet.numCols; c++) {
            const th = document.createElement('th');
            th.className = 'col-header';
            th.textContent = (0, types_1.colName)(c);
            th.style.width = sheet_1.CELL_WIDTH + 'px';
            th.style.height = sheet_1.HEADER_HEIGHT + 'px';
            th.dataset.col = String(c);
            th.addEventListener('contextmenu', (e) => {
                e.preventDefault();
                this.showColumnMenu(e, c);
            });
            headerRow.appendChild(th);
        }
        thead.appendChild(headerRow);
        table.appendChild(thead);
        // Tbody with all cells
        const tbody = document.createElement('tbody');
        for (let r = 0; r < this.sheet.numRows; r++) {
            const tr = document.createElement('tr');
            // Row header
            const rowHeader = document.createElement('th');
            rowHeader.className = 'row-header';
            rowHeader.textContent = String(r + 1);
            rowHeader.style.width = sheet_1.HEADER_WIDTH + 'px';
            rowHeader.style.height = sheet_1.CELL_HEIGHT + 'px';
            rowHeader.dataset.row = String(r);
            rowHeader.addEventListener('contextmenu', (e) => {
                e.preventDefault();
                this.showRowMenu(e, r);
            });
            tr.appendChild(rowHeader);
            // Data cells
            for (let c = 0; c < this.sheet.numCols; c++) {
                const td = document.createElement('td');
                td.className = 'cell';
                td.style.width = sheet_1.CELL_WIDTH + 'px';
                td.style.height = sheet_1.CELL_HEIGHT + 'px';
                td.dataset.col = String(c);
                td.dataset.row = String(r);
                const key = (0, types_1.addrKey)(c, r);
                this.cellElements.set(key, td);
                // Click to select
                td.addEventListener('mousedown', (e) => {
                    if (this.editing) {
                        this.commitEdit();
                    }
                    if (e.detail === 2) {
                        // Double-click - enter edit mode
                        this.selection = { col: c, row: r };
                        this.selectCell();
                        this.startEdit();
                    }
                    else if (e.detail === 1) {
                        this.selection = { col: c, row: r };
                        this.selectCell();
                    }
                });
                tr.appendChild(td);
            }
            tbody.appendChild(tr);
        }
        table.appendChild(tbody);
        this.grid.appendChild(table);
    }
    selectCell() {
        // Remove old selection
        const oldTd = this.cellElements.get((0, types_1.addrKey)(this.selection.col, this.selection.row));
        // We'll re-select below, but first update the formula bar
        this.updateFormulaBar();
        this.highlightCell();
    }
    highlightCell() {
        // Remove selection from all cells
        for (const td of this.cellElements.values()) {
            td.classList.remove('selected');
        }
        // Add selection to current cell
        const key = (0, types_1.addrKey)(this.selection.col, this.selection.row);
        const td = this.cellElements.get(key);
        if (td)
            td.classList.add('selected');
        // Scroll into view
        td?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
    }
    updateFormulaBar() {
        const { col, row } = this.selection;
        this.cellRefDisplay.textContent = (0, types_1.cellLabel)(col, row);
        const cell = this.sheet.getCell(col, row);
        if (cell) {
            this.formulaInput.value = cell.raw;
        }
        else {
            this.formulaInput.value = '';
        }
    }
    updateCellDisplay(col, row) {
        const key = (0, types_1.addrKey)(col, row);
        const td = this.cellElements.get(key);
        if (!td)
            return;
        const cell = this.sheet.getCell(col, row);
        if (!cell) {
            td.textContent = '';
            td.classList.remove('cell-number', 'cell-text', 'cell-error');
            return;
        }
        const computed = cell.computed;
        switch (computed.type) {
            case 'null':
                td.textContent = '';
                td.classList.remove('cell-number', 'cell-text', 'cell-error');
                break;
            case 'text':
                td.textContent = computed.value;
                td.classList.add('cell-text');
                td.classList.remove('cell-number', 'cell-error');
                break;
            case 'number': {
                // Format number - avoid floating point display issues
                const n = computed.value;
                const text = Number.isInteger(n) ? String(n) : parseFloat(n.toPrecision(12)).toString();
                td.textContent = text;
                td.classList.add('cell-number');
                td.classList.remove('cell-text', 'cell-error');
                break;
            }
            case 'error':
                td.textContent = computed.value;
                td.classList.add('cell-error');
                td.classList.remove('cell-number', 'cell-text');
                break;
        }
    }
    startEdit() {
        if (this.editing)
            return;
        this.editing = true;
        const { col, row } = this.selection;
        const key = (0, types_1.addrKey)(col, row);
        const td = this.cellElements.get(key);
        if (!td)
            return;
        const cell = this.sheet.getCell(col, row);
        const raw = cell ? cell.raw : '';
        // Replace cell content with input
        td.classList.add('editing');
        const input = document.createElement('input');
        input.type = 'text';
        input.value = raw;
        input.className = 'cell-edit-input';
        td.textContent = '';
        td.appendChild(input);
        input.focus();
        input.select();
        this.editCell = td;
        this.editInput = input;
        // Sync formula bar
        this.formulaInput.value = raw;
        // Handle input events
        input.addEventListener('keydown', (e) => this.handleEditKeydown(e));
        input.addEventListener('input', () => {
            this.formulaInput.value = input.value;
        });
    }
    commitEdit() {
        if (!this.editing || !this.editInput || !this.editCell)
            return;
        const { col, row } = this.selection;
        const raw = this.editInput.value;
        const td = this.editCell;
        // Restore cell display
        td.classList.remove('editing');
        this.editing = false;
        this.editCell = null;
        this.editInput = null;
        // Update cell data
        this.sheet.setCellRaw(col, row, raw);
        this.depGraph.updateCell(col, row, this.sheet);
        // Recalculate
        const changed = (0, sheet_1.recalculate)(this.sheet, this.depGraph, (0, types_1.addrKey)(col, row));
        // Update displays
        for (const key of changed) {
            const { col: c, row: r } = (0, types_1.parseAddrKey)(key);
            this.updateCellDisplay(c, r);
        }
        // Save to localStorage
        this.save();
        // Update formula bar
        this.updateFormulaBar();
    }
    handleEditKeydown(e) {
        if (e.key === 'Enter') {
            e.preventDefault();
            this.commitEdit();
            this.moveSelection(0, 1); // Move down
        }
        else if (e.key === 'Tab') {
            e.preventDefault();
            this.commitEdit();
            this.moveSelection(1, 0); // Move right
        }
        else if (e.key === 'Escape') {
            e.preventDefault();
            // Cancel edit - restore original value
            if (this.editCell) {
                this.editCell.classList.remove('editing');
                const { col, row } = this.selection;
                this.updateCellDisplay(col, row);
            }
            this.editing = false;
            this.editCell = null;
            this.editInput = null;
            this.updateFormulaBar();
        }
    }
    moveSelection(dCol, dRow) {
        let newCol = this.selection.col + dCol;
        let newRow = this.selection.row + dRow;
        if (newCol < 0)
            newCol = 0;
        if (newCol >= this.sheet.numCols)
            newCol = this.sheet.numCols - 1;
        if (newRow < 0)
            newRow = 0;
        if (newRow >= this.sheet.numRows)
            newRow = this.sheet.numRows - 1;
        this.selection = { col: newCol, row: newRow };
        this.highlightCell();
        this.updateFormulaBar();
    }
    handleGridKeydown(e) {
        if (this.editing)
            return; // Edit mode handles its own keys
        const { col, row } = this.selection;
        switch (e.key) {
            case 'ArrowUp':
                e.preventDefault();
                this.moveSelection(0, -1);
                break;
            case 'ArrowDown':
                e.preventDefault();
                this.moveSelection(0, 1);
                break;
            case 'ArrowLeft':
                e.preventDefault();
                this.moveSelection(-1, 0);
                break;
            case 'ArrowRight':
                e.preventDefault();
                this.moveSelection(1, 0);
                break;
            case 'Enter':
                e.preventDefault();
                this.startEdit();
                break;
            case 'Tab':
                e.preventDefault();
                this.moveSelection(e.shiftKey ? -1 : 1, 0);
                break;
            case 'Delete':
            case 'Backspace':
                e.preventDefault();
                this.sheet.setCellRaw(col, row, '');
                this.depGraph.updateCell(col, row, this.sheet);
                const changed = (0, sheet_1.recalculate)(this.sheet, this.depGraph, (0, types_1.addrKey)(col, row));
                for (const key of changed) {
                    const { col: c, row: r } = (0, types_1.parseAddrKey)(key);
                    this.updateCellDisplay(c, r);
                }
                this.updateFormulaBar();
                this.save();
                break;
            default:
                // If it's a printable character, start editing
                if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
                    e.preventDefault();
                    this.startEdit();
                    if (this.editInput) {
                        this.editInput.value = e.key;
                        this.formulaInput.value = e.key;
                    }
                }
        }
    }
    handleFormulaBarInput() {
        if (this.editing && this.editInput) {
            this.editInput.value = this.formulaInput.value;
        }
    }
    handleFormulaBarKeydown(e) {
        if (e.key === 'Enter') {
            e.preventDefault();
            const { col, row } = this.selection;
            const raw = this.formulaInput.value;
            // Update the cell
            this.sheet.setCellRaw(col, row, raw);
            this.depGraph.updateCell(col, row, this.sheet);
            // If we were editing, commit
            if (this.editing && this.editInput) {
                this.editInput.value = raw;
            }
            this.commitEdit();
            // Move down
            this.moveSelection(0, 1);
        }
        else if (e.key === 'Escape') {
            e.preventDefault();
            this.updateFormulaBar(); // Reset to cell value
            if (this.editing) {
                this.editing = false;
                this.editCell = null;
                this.editInput = null;
                const { col, row } = this.selection;
                this.updateCellDisplay(col, row);
            }
        }
    }
    // ---- Context menus for insert/delete row/col ----
    showColumnMenu(e, col) {
        // Create a context menu
        const menu = document.createElement('div');
        menu.className = 'context-menu';
        menu.style.left = e.pageX + 'px';
        menu.style.top = e.pageY + 'px';
        const insertBefore = document.createElement('div');
        insertBefore.className = 'context-menu-item';
        insertBefore.textContent = 'Insert column before';
        insertBefore.addEventListener('click', () => {
            document.body.removeChild(menu);
            this.insertCol(col);
        });
        menu.appendChild(insertBefore);
        const insertAfter = document.createElement('div');
        insertAfter.className = 'context-menu-item';
        insertAfter.textContent = 'Insert column after';
        insertAfter.addEventListener('click', () => {
            document.body.removeChild(menu);
            this.insertCol(col + 1);
        });
        menu.appendChild(insertAfter);
        const deleteCol = document.createElement('div');
        deleteCol.className = 'context-menu-item';
        deleteCol.textContent = 'Delete column';
        deleteCol.addEventListener('click', () => {
            document.body.removeChild(menu);
            this.deleteCol(col);
        });
        menu.appendChild(deleteCol);
        document.body.appendChild(menu);
        // Close menu on outside click
        const close = () => {
            if (document.body.contains(menu)) {
                document.body.removeChild(menu);
            }
            document.removeEventListener('click', close);
        };
        setTimeout(() => document.addEventListener('click', close), 0);
    }
    showRowMenu(e, row) {
        const menu = document.createElement('div');
        menu.className = 'context-menu';
        menu.style.left = e.pageX + 'px';
        menu.style.top = e.pageY + 'px';
        const insertBefore = document.createElement('div');
        insertBefore.className = 'context-menu-item';
        insertBefore.textContent = 'Insert row above';
        insertBefore.addEventListener('click', () => {
            document.body.removeChild(menu);
            this.insertRow(row);
        });
        menu.appendChild(insertBefore);
        const insertAfter = document.createElement('div');
        insertAfter.className = 'context-menu-item';
        insertAfter.textContent = 'Insert row below';
        insertAfter.addEventListener('click', () => {
            document.body.removeChild(menu);
            this.insertRow(row + 1);
        });
        menu.appendChild(insertAfter);
        const deleteRow = document.createElement('div');
        deleteRow.className = 'context-menu-item';
        deleteRow.textContent = 'Delete row';
        deleteRow.addEventListener('click', () => {
            document.body.removeChild(menu);
            this.deleteRow(row);
        });
        menu.appendChild(deleteRow);
        document.body.appendChild(menu);
        const close = () => {
            if (document.body.contains(menu)) {
                document.body.removeChild(menu);
            }
            document.removeEventListener('click', close);
        };
        setTimeout(() => document.addEventListener('click', close), 0);
    }
    insertCol(col) {
        if (this.editing)
            this.commitEdit();
        this.sheet.insertCol(col);
        this.depGraph.updateAll(this.sheet);
        this.fullRecalculate();
        this.buildTable();
        this.save();
    }
    deleteCol(col) {
        if (this.editing)
            this.commitEdit();
        this.sheet.deleteCol(col);
        this.depGraph.updateAll(this.sheet);
        this.fullRecalculate();
        this.buildTable();
        this.save();
    }
    insertRow(row) {
        if (this.editing)
            this.commitEdit();
        this.sheet.insertRow(row);
        this.depGraph.updateAll(this.sheet);
        this.fullRecalculate();
        this.buildTable();
        this.save();
    }
    deleteRow(row) {
        if (this.editing)
            this.commitEdit();
        this.sheet.deleteRow(row);
        this.depGraph.updateAll(this.sheet);
        this.fullRecalculate();
        this.buildTable();
        this.save();
    }
    // Recalculate all cells (used on load, after insert/delete row/col)
    fullRecalculate() {
        this.depGraph.updateAll(this.sheet);
        // Collect all cells (including non-formula cells that may have computed values)
        const allCellKeys = new Set(this.sheet.cells.keys());
        // Find cycle cells first
        const cycleCells = findCycleCellsForRecalc(this.depGraph, allCellKeys);
        // Build topological order for non-cycle cells
        const nonCycle = new Set();
        for (const k of allCellKeys)
            if (!cycleCells.has(k))
                nonCycle.add(k);
        const order = topoSortGraph(this.depGraph, nonCycle);
        // Mark cycle cells
        for (const key of cycleCells) {
            const { col: c, row: r } = (0, types_1.parseAddrKey)(key);
            const cell = this.sheet.getCell(c, r);
            if (cell) {
                cell.computed = { type: 'error', value: '#CYCLE!' };
                this.updateCellDisplay(c, r);
            }
        }
        // Recalculate non-cycle cells in order
        for (const key of order) {
            const { col: c, row: r } = (0, types_1.parseAddrKey)(key);
            const cell = this.sheet.getCell(c, r);
            if (!cell)
                continue;
            cell.computed = (0, sheet_1.evaluateCell)(c, r, this.sheet, new Set([key]));
            this.updateCellDisplay(c, r);
        }
    }
    // ---- localStorage persistence ----
    save() {
        const data = {};
        for (const [key, cell] of this.sheet.cells) {
            data[key] = cell.raw;
        }
        const meta = {
            numCols: this.sheet.numCols,
            numRows: this.sheet.numRows,
            cells: data,
        };
        try {
            localStorage.setItem('spreadsheet-data', JSON.stringify(meta));
        }
        catch (e) {
            // Storage full or unavailable
        }
    }
    load() {
        try {
            const raw = localStorage.getItem('spreadsheet-data');
            if (!raw)
                return false;
            const meta = JSON.parse(raw);
            this.sheet.numCols = meta.numCols || sheet_1.NUM_COLS;
            this.sheet.numRows = meta.numRows || sheet_1.NUM_ROWS;
            this.sheet.cells = new Map();
            for (const [key, val] of Object.entries(meta.cells)) {
                this.sheet.cells.set(key, { raw: val, computed: { type: 'null' } });
            }
            return true;
        }
        catch (e) {
            return false;
        }
    }
}
exports.Renderer = Renderer;
function topoSortGraph(depGraph, nodes) {
    const inDegree = new Map();
    for (const cell of nodes) {
        const deps = depGraph.dependsOn.get(cell);
        let count = 0;
        if (deps)
            for (const d of deps)
                if (nodes.has(d))
                    count++;
        inDegree.set(cell, count);
    }
    const q = [];
    for (const [cell, deg] of inDegree)
        if (deg === 0)
            q.push(cell);
    const result = [];
    while (q.length > 0) {
        const cur = q.shift();
        result.push(cur);
        const deps = depGraph.dependents.get(cur);
        if (deps) {
            for (const dep of deps) {
                if (nodes.has(dep)) {
                    const newDeg = (inDegree.get(dep) || 0) - 1;
                    inDegree.set(dep, newDeg);
                    if (newDeg === 0)
                        q.push(dep);
                }
            }
        }
    }
    return result;
}
// Helper function used both in sheet.ts recalculation and here
function findCycleCellsForRecalc(depGraph, affected) {
    const cycleCells = new Set();
    for (const start of affected) {
        const path = new Map();
        const stack = [{ cell: start, isReturn: false }];
        while (stack.length > 0) {
            const top = stack[stack.length - 1];
            const current = top.cell;
            const isReturn = top.isReturn;
            if (!isReturn) {
                top.isReturn = true;
                if (path.has(current)) {
                    const cycleStart = path.get(current);
                    for (const [cell, depth] of path) {
                        if (depth >= cycleStart)
                            cycleCells.add(cell);
                    }
                    continue;
                }
                path.set(current, path.size);
                const deps = depGraph.dependents.get(current);
                if (deps) {
                    for (const dep of deps) {
                        if (affected.has(dep)) {
                            stack.push({ cell: dep, isReturn: false });
                        }
                    }
                }
            }
            else {
                path.delete(current);
                stack.pop();
            }
        }
    }
    return cycleCells;
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.6-27B-MTP-GGUF-IQ4_NL","provider":"brigid","persona":"typescript","sessionId":"2dceb99a-b5ea-43a6-905b-dd125de372b9","tokensIn":1690771,"tokensOut":59650,"tokensTotal":1750421,"cost":0,"turns":52,"toolCalls":51,"failedToolCalls":1,"timestamp":"2026-08-04T01:07:30.267Z"} -->