Spreadsheet
Qwen 3.6 27b · typescript

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,908 bytes · SHA-256 eb530af6034a
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spreadsheet</title>
<style>
/* ── css: layout ── */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
overflow: hidden;
font-family: 'Google Sans', 'Roboto', 'Arial', sans-serif;
font-size: 13px;
color: #202124;
background: #fff;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.toolbar {
display: flex;
gap: 4px;
padding: 6px 8px;
background: #edf2fa;
border-bottom: 1px solid #dadce0;
flex-shrink: 0;
}
.toolbar button {
padding: 4px 10px;
border: 1px solid #dadce0;
border-radius: 4px;
background: #fff;
color: #3c4043;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
}
.toolbar button:hover {
background: #f1f3f4;
border-color: #c0c4c8;
}
.formula-bar-container {
display: flex;
align-items: center;
padding: 4px 8px;
background: #f8f9fa;
border-bottom: 1px solid #dadce0;
flex-shrink: 0;
gap: 6px;
}
.cell-ref {
width: 50px;
text-align: center;
font-weight: 600;
color: #5f6368;
font-size: 12px;
flex-shrink: 0;
user-select: none;
}
.formula-bar {
flex: 1;
padding: 4px 8px;
border: 1px solid #dadce0;
border-radius: 4px;
font-size: 13px;
font-family: inherit;
outline: none;
background: #fff;
}
.formula-bar:focus {
border-color: #1a73e8;
box-shadow: 0 0 0 1px #1a73e8;
}
.grid-container {
flex: 1;
overflow: auto;
position: relative;
}
/* ── css: grid ── */
.spreadsheet-table {
border-collapse: collapse;
table-layout: fixed;
width: max-content;
}
.spreadsheet-table th,
.spreadsheet-table td {
border: 1px solid #e0e0e0;
padding: 0;
font-size: 13px;
line-height: 24px;
height: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.corner-header {
background: #f8f9fa;
position: sticky;
top: 0;
z-index: 3;
border-color: #dadce0;
}
.col-header {
background: #f8f9fa;
position: sticky;
top: 0;
z-index: 3;
text-align: center;
font-weight: 500;
color: #5f6368;
user-select: none;
border-color: #dadce0;
}
.row-header {
background: #f8f9fa;
position: sticky;
left: 0;
z-index: 2;
text-align: center;
font-weight: 500;
color: #5f6368;
user-select: none;
border-color: #dadce0;
}
.cell {
background: #fff;
cursor: cell;
padding: 0 4px;
overflow: hidden;
text-overflow: ellipsis;
}
.cell.selected {
outline: 2px solid #1a73e8;
outline-offset: -1px;
z-index: 1;
position: relative;
}
.cell-input {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 0;
font: inherit;
background: transparent;
}
/* Header hover effects */
.col-header:hover,
.row-header:hover {
background: #e8eaed;
}
</style>
</head>
<body>
<div id="app">
<div id="toolbar" class="toolbar">
<button id="btn-insert-row" title="Insert row above">Insert Row</button>
<button id="btn-delete-row" title="Delete selected row">Delete Row</button>
<button id="btn-insert-col" title="Insert column left">Insert Col</button>
<button id="btn-delete-col" title="Delete selected column">Delete Col</button>
</div>
<div id="formula-bar-container" class="formula-bar-container">
<span id="cell-ref" class="cell-ref">A1</span>
<input id="formula-bar" class="formula-bar" type="text" placeholder="Formula bar">
</div>
<div id="grid-container" class="grid-container"></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./types":"src/types.ts","./state":"src/state.ts","./grid":"src/grid.ts","./dependency":"src/dependency.ts","./operations":"src/operations.ts","./persistence":"src/persistence.ts","./input":"src/input.ts"},"src/types.ts":{"./config":"src/config.ts"},"src/state.ts":{"./config":"src/config.ts","./types":"src/types.ts"},"src/tokenizer.ts":{"./types":"src/types.ts"},"src/parser.ts":{"./types":"src/types.ts","./tokenizer":"src/tokenizer.ts"},"src/evaluator.ts":{"./config":"src/config.ts","./types":"src/types.ts","./state":"src/state.ts","./parser":"src/parser.ts"},"src/dependency.ts":{"./config":"src/config.ts","./types":"src/types.ts","./state":"src/state.ts","./parser":"src/parser.ts","./evaluator":"src/evaluator.ts"},"src/grid.ts":{"./config":"src/config.ts","./types":"src/types.ts","./state":"src/state.ts"},"src/operations.ts":{"./config":"src/config.ts","./types":"src/types.ts","./state":"src/state.ts"},"src/persistence.ts":{"./types":"src/types.ts"},"src/input.ts":{"./config":"src/config.ts","./types":"src/types.ts","./state":"src/state.ts","./grid":"src/grid.ts","./dependency":"src/dependency.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const config_1 = require("./config");
const state_1 = require("./state");
const grid_1 = require("./grid");
const dependency_1 = require("./dependency");
const operations_1 = require("./operations");
const persistence_1 = require("./persistence");
const input_1 = require("./input");
// AST cache for recalculation
const astCache = new Map();
function start() {
const app = document.getElementById('app');
const gridContainer = document.getElementById('grid-container');
const formulaBar = document.getElementById('formula-bar');
const cellRef = document.getElementById('cell-ref');
// Set up formula bar
(0, input_1.setFormulaBarInput)(formulaBar);
// Init grid
(0, grid_1.initGrid)(gridContainer);
// Load from localStorage
(0, persistence_1.loadSheet)(state_1.sheet);
// Full recalculation
const revDeps = (0, dependency_1.buildReverseDeps)(state_1.sheet);
(0, dependency_1.fullRecalc)(state_1.sheet, revDeps, astCache);
// Update all cell displays
(0, grid_1.updateCellDisplayFromSheet)(state_1.sheet);
// Select initial cell
(0, grid_1.selectCell)(state_1.selection.col, state_1.selection.row);
(0, input_1.updateFormulaBar)();
updateCellRef();
// Selection change handler
function onSelectionChange(col, row) {
updateCellRef();
(0, input_1.updateFormulaBar)();
}
// Wire up selection change callback
(0, grid_1.setSelectionChange)(onSelectionChange);
// Cell reference display
function updateCellRef() {
cellRef.textContent = (0, state_1.addr)(state_1.selection.col, state_1.selection.row);
}
// Formula bar keydown
formulaBar.addEventListener('keydown', (e) => {
(0, input_1.handleFormulaBarKeydown)(e);
});
// Formula bar input — sync to cell on blur or enter
formulaBar.addEventListener('input', () => {
// If editing in-cell, sync the cell input
const cellEl = document.querySelector('.cell.selected .cell-input');
if (cellEl) {
cellEl.value = formulaBar.value;
}
});
// Grid keydown (delegate from grid container)
gridContainer.addEventListener('keydown', (e) => {
(0, input_1.handleGridKeydown)(e, state_1.sheet);
});
// Make grid container focusable for keyboard events
gridContainer.setAttribute('tabindex', '0');
gridContainer.style.outline = 'none';
// Double-click to edit
gridContainer.addEventListener('dblclick', (e) => {
const td = e.target.closest('td.cell');
if (td) {
(0, input_1.startEdit)();
}
});
// Commit handler
function onCommit(raw) {
// Determine if Enter was pressed (move down) or Tab (move right)
(0, input_1.commitCell)(state_1.sheet, raw);
(0, persistence_1.saveSheet)(state_1.sheet);
}
// Override commitEdit to handle Enter key behavior
// We need to intercept the Enter key in the edit input
const origGridContainer = gridContainer;
origGridContainer.addEventListener('keydown', (e) => {
if ((0, input_1.isEditing)() && e.key === 'Enter') {
// Move down after commit
const cell = (0, state_1.getCell)(state_1.selection.col, state_1.selection.row);
const raw = cell ? cell.raw : '';
// This will be handled by the input's keydown handler
}
});
// 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');
if (btnInsertRow) {
btnInsertRow.addEventListener('click', () => {
(0, operations_1.insertRow)(state_1.sheet, state_1.selection.row);
const revDeps = (0, dependency_1.buildReverseDeps)(state_1.sheet);
(0, dependency_1.fullRecalc)(state_1.sheet, revDeps, astCache);
(0, grid_1.updateCellDisplayFromSheet)(state_1.sheet);
updateSelectionAfterGridChange();
(0, persistence_1.saveSheet)(state_1.sheet);
});
}
if (btnDeleteRow) {
btnDeleteRow.addEventListener('click', () => {
(0, operations_1.deleteRow)(state_1.sheet, state_1.selection.row);
const revDeps = (0, dependency_1.buildReverseDeps)(state_1.sheet);
(0, dependency_1.fullRecalc)(state_1.sheet, revDeps, astCache);
(0, grid_1.updateCellDisplayFromSheet)(state_1.sheet);
updateSelectionAfterGridChange();
(0, persistence_1.saveSheet)(state_1.sheet);
});
}
if (btnInsertCol) {
btnInsertCol.addEventListener('click', () => {
(0, operations_1.insertCol)(state_1.sheet, state_1.selection.col);
const revDeps = (0, dependency_1.buildReverseDeps)(state_1.sheet);
(0, dependency_1.fullRecalc)(state_1.sheet, revDeps, astCache);
(0, grid_1.updateCellDisplayFromSheet)(state_1.sheet);
updateSelectionAfterGridChange();
(0, persistence_1.saveSheet)(state_1.sheet);
});
}
if (btnDeleteCol) {
btnDeleteCol.addEventListener('click', () => {
(0, operations_1.deleteCol)(state_1.sheet, state_1.selection.col);
const revDeps = (0, dependency_1.buildReverseDeps)(state_1.sheet);
(0, dependency_1.fullRecalc)(state_1.sheet, revDeps, astCache);
(0, grid_1.updateCellDisplayFromSheet)(state_1.sheet);
updateSelectionAfterGridChange();
(0, persistence_1.saveSheet)(state_1.sheet);
});
}
// Right-click context menu on headers
gridContainer.addEventListener('contextmenu', (e) => {
const colHeader = e.target.closest('.col-header');
const rowHeader = e.target.closest('.row-header');
if (colHeader) {
e.preventDefault();
const col = parseInt(colHeader.dataset.col);
showColContextMenu(col, e.clientX, e.clientY);
}
else if (rowHeader) {
e.preventDefault();
const row = parseInt(rowHeader.dataset.row);
showRowContextMenu(row, e.clientX, e.clientY);
}
});
// Focus the grid container so keyboard events work
gridContainer.focus();
}
function updateSelectionAfterGridChange() {
state_1.selection.col = Math.min(state_1.selection.col, config_1.COLS - 1);
state_1.selection.row = Math.min(state_1.selection.row, config_1.ROWS - 1);
(0, grid_1.selectCell)(state_1.selection.col, state_1.selection.row);
(0, input_1.updateFormulaBar)();
}
function showColContextMenu(col, x, y) {
// Simple context menu
const menu = document.createElement('div');
menu.style.cssText = `
position: fixed; left: ${x}px; top: ${y}px;
background: #fff; border: 1px solid #dadce0; border-radius: 4px;
box-shadow: 0 2px 6px rgba(0,0,0,0.2); padding: 4px 0; z-index: 1000;
min-width: 150px;
`;
const insertItem = createMenuItem('Insert column left', () => {
(0, operations_1.insertCol)(state_1.sheet, col);
recalcAndRender();
});
const deleteItem = createMenuItem('Delete column', () => {
(0, operations_1.deleteCol)(state_1.sheet, col);
recalcAndRender();
});
menu.appendChild(insertItem);
menu.appendChild(deleteItem);
document.body.appendChild(menu);
const close = () => {
document.body.removeChild(menu);
document.removeEventListener('mousedown', close);
};
document.addEventListener('mousedown', close);
}
function showRowContextMenu(row, x, y) {
const menu = document.createElement('div');
menu.style.cssText = `
position: fixed; left: ${x}px; top: ${y}px;
background: #fff; border: 1px solid #dadce0; border-radius: 4px;
box-shadow: 0 2px 6px rgba(0,0,0,0.2); padding: 4px 0; z-index: 1000;
min-width: 150px;
`;
const insertItem = createMenuItem('Insert row above', () => {
(0, operations_1.insertRow)(state_1.sheet, row);
recalcAndRender();
});
const deleteItem = createMenuItem('Delete row', () => {
(0, operations_1.deleteRow)(state_1.sheet, row);
recalcAndRender();
});
menu.appendChild(insertItem);
menu.appendChild(deleteItem);
document.body.appendChild(menu);
const close = () => {
document.body.removeChild(menu);
document.removeEventListener('mousedown', close);
};
document.addEventListener('mousedown', close);
}
function createMenuItem(label, onClick) {
const item = document.createElement('div');
item.textContent = label;
item.style.cssText = `
padding: 6px 16px; cursor: pointer; font-size: 13px; color: #202124;
`;
item.addEventListener('mouseenter', () => {
item.style.background = '#f1f3f4';
});
item.addEventListener('mouseleave', () => {
item.style.background = '';
});
item.addEventListener('click', () => {
onClick();
(0, persistence_1.saveSheet)(state_1.sheet);
});
return item;
}
function recalcAndRender() {
const revDeps = (0, dependency_1.buildReverseDeps)(state_1.sheet);
(0, dependency_1.fullRecalc)(state_1.sheet, revDeps, astCache);
(0, grid_1.updateCellDisplayFromSheet)(state_1.sheet);
updateSelectionAfterGridChange();
}
// Wire up the commit handler
(0, input_1.setOnCommit)((raw) => {
(0, input_1.commitCell)(state_1.sheet, raw);
(0, persistence_1.saveSheet)(state_1.sheet);
});
start();
};
// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ERR_GENERAL = exports.ERR_DIV0 = exports.ERR_REF = exports.ERR_CYCLE = exports.COLORS = exports.HEADER_HEIGHT = exports.HEADER_WIDTH = exports.CELL_HEIGHT = exports.CELL_WIDTH = exports.ROWS = exports.COLS = void 0;
// Tunable constants
exports.COLS = 26;
exports.ROWS = 100;
exports.CELL_WIDTH = 100;
exports.CELL_HEIGHT = 24;
exports.HEADER_WIDTH = 40;
exports.HEADER_HEIGHT = 24;
// Colors
exports.COLORS = {
bg: '#ffffff',
gridLine: '#e0e0e0',
headerBg: '#f8f9fa',
headerText: '#333333',
selectionBorder: '#1a73e8',
selectionBg: '#e8f0fe',
errorColor: '#d32f2f',
formulaBarBg: '#f1f3f4',
formulaBarBorder: '#dadce0',
textLeft: '#000000',
textRight: '#000000',
};
// Error strings
exports.ERR_CYCLE = '#CYCLE!';
exports.ERR_REF = '#REF!';
exports.ERR_DIV0 = '#DIV/0!';
exports.ERR_GENERAL = '#ERR!';
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.selection = exports.sheet = void 0;
exports.colName = colName;
exports.addr = addr;
exports.parseAddr = parseAddr;
exports.getCell = getCell;
exports.setCell = setCell;
exports.removeCell = removeCell;
exports.hasCell = hasCell;
exports.getSelectionAddr = getSelectionAddr;
const config_1 = require("./config");
exports.sheet = new Map();
exports.selection = { col: 0, row: 0 };
function colName(col) {
return String.fromCharCode(65 + col);
}
function addr(col, row) {
return colName(col) + (row + 1);
}
function parseAddr(s) {
const m = s.match(/^([A-Z])(\d+)$/);
if (!m)
return null;
const col = m[1].charCodeAt(0) - 65;
const row = parseInt(m[2], 10) - 1;
if (col < 0 || col >= config_1.COLS || row < 0 || row >= config_1.ROWS)
return null;
return { col, row };
}
function getCell(col, row) {
return exports.sheet.get(addr(col, row));
}
function setCell(col, row, cell) {
exports.sheet.set(addr(col, row), cell);
}
function removeCell(col, row) {
exports.sheet.delete(addr(col, row));
}
function hasCell(col, row) {
return exports.sheet.has(addr(col, row));
}
function getSelectionAddr() {
return addr(exports.selection.col, exports.selection.row);
}
};
// ── module: src/tokenizer.ts ──
__mods["src/tokenizer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tokenize = tokenize;
function tokenize(input) {
const tokens = [];
let i = 0;
const s = input.trim();
while (i < s.length) {
// Skip whitespace
if (s[i] === ' ' || s[i] === '\t') {
i++;
continue;
}
// Numbers (including decimals)
if ((s[i] >= '0' && s[i] <= '9') || s[i] === '.') {
let num = '';
const hasDot = s[i] === '.';
while (i < s.length && ((s[i] >= '0' && s[i] <= '9') || (s[i] === '.' && !hasDot))) {
num += s[i];
if (s[i] === '.')
break;
i++;
}
tokens.push({ kind: 'NUMBER', value: num, pos: i - num.length });
continue;
}
// Identifiers (cell refs like A1, B7) and function names (SUM, AVG, etc.)
if (s[i] >= 'A' && s[i] <= 'Z') {
let ident = '';
while (i < s.length && (s[i] >= 'A' && s[i] <= 'Z')) {
ident += s[i];
i++;
}
// Check if followed by digits → cell reference
if (i < s.length && s[i] >= '0' && s[i] <= '9') {
let num = '';
while (i < s.length && s[i] >= '0' && s[i] <= '9') {
num += s[i];
i++;
}
tokens.push({ kind: 'IDENT', value: ident + num, pos: i - (ident + num).length });
}
else {
// Function name
tokens.push({ kind: 'IDENT', value: ident, pos: i - ident.length });
}
continue;
}
// Operators and punctuation
switch (s[i]) {
case ':':
tokens.push({ kind: 'COLON', value: ':', pos: i });
break;
case ',':
tokens.push({ kind: 'COMMA', value: ',', pos: i });
break;
case '(':
tokens.push({ kind: 'LPAREN', value: '(', pos: i });
break;
case ')':
tokens.push({ kind: 'RPAREN', value: ')', pos: i });
break;
case '+':
tokens.push({ kind: 'PLUS', value: '+', pos: i });
break;
case '-':
tokens.push({ kind: 'MINUS', value: '-', pos: i });
break;
case '*':
tokens.push({ kind: 'STAR', value: '*', pos: i });
break;
case '/':
tokens.push({ kind: 'SLASH', value: '/', pos: i });
break;
default:
// Unknown character — skip
break;
}
i++;
}
tokens.push({ kind: 'EOF', value: '', pos: i });
return tokens;
}
};
// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseFormula = parseFormula;
const tokenizer_1 = require("./tokenizer");
class Parser {
constructor(tokens) {
this.error = null;
this.tokens = tokens;
this.pos = 0;
}
peek() {
return this.tokens[this.pos];
}
consume() {
return this.tokens[this.pos++];
}
expect(kind) {
const t = this.peek();
if (t.kind !== kind) {
this.error = `Expected ${kind}, got ${t.kind}`;
return t;
}
return this.consume();
}
// expr -> addExpr
parse() {
const node = this.parseAdd();
if (this.error)
return { kind: 'error', msg: this.error };
if (this.peek().kind !== 'EOF') {
return { kind: 'error', msg: 'Unexpected token' };
}
return node;
}
// addExpr -> mulExpr (('+' | '-') mulExpr)*
parseAdd() {
let left = this.parseMul();
while (this.peek().kind === 'PLUS' || this.peek().kind === 'MINUS') {
const op = this.consume().value;
const right = this.parseMul();
left = { kind: 'binary', op, left, right };
}
return left;
}
// mulExpr -> unary (('*' | '/') unary)*
parseMul() {
let left = this.parseUnary();
while (this.peek().kind === 'STAR' || this.peek().kind === 'SLASH') {
const op = this.consume().value;
const right = this.parseUnary();
left = { kind: 'binary', op, left, right };
}
return left;
}
// unary -> '-' unary | primary
parseUnary() {
if (this.peek().kind === 'MINUS') {
this.consume();
const operand = this.parseUnary();
return { kind: 'unaryMinus', operand };
}
return this.parsePrimary();
}
// primary -> NUMBER | IDENT | '(' expr ')' | functionCall
parsePrimary() {
const t = this.peek();
// Number literal
if (t.kind === 'NUMBER') {
this.consume();
return { kind: 'number', value: parseFloat(t.value) };
}
// Parenthesized expression
if (t.kind === 'LPAREN') {
this.consume();
const expr = this.parseAdd();
this.expect('RPAREN');
return expr;
}
// Cell reference, range, or function call
if (t.kind === 'IDENT') {
this.consume();
const name = t.value;
// Check if next is a colon -> range
if (this.peek().kind === 'COLON') {
this.consume();
const endToken = this.peek();
if (endToken.kind !== 'IDENT') {
this.error = 'Expected cell reference after colon';
return { kind: 'error', msg: this.error };
}
this.consume();
const start = parseCellRef(name);
const end = parseCellRef(endToken.value);
if (!start || !end) {
return { kind: 'error', msg: 'Invalid cell reference in range' };
}
return {
kind: 'range',
startCol: start.col,
startRow: start.row,
endCol: end.col,
endRow: end.row,
};
}
// Check if next is '(' -> function call
if (this.peek().kind === 'LPAREN') {
this.consume();
const args = [];
if (this.peek().kind !== 'RPAREN') {
args.push(this.parseAdd());
while (this.peek().kind === 'COMMA') {
this.consume();
args.push(this.parseAdd());
}
}
this.expect('RPAREN');
return { kind: 'function', name: name.toUpperCase(), args };
}
// Plain cell reference
const ref = parseCellRef(name);
if (!ref) {
return { kind: 'error', msg: 'Invalid cell reference: ' + name };
}
return { kind: 'cellRef', col: ref.col, row: ref.row };
}
this.error = 'Unexpected token: ' + t.kind;
return { kind: 'error', msg: this.error };
}
}
function parseCellRef(s) {
const m = s.match(/^([A-Z])(\d+)$/);
if (!m)
return null;
return {
col: m[1].charCodeAt(0) - 65,
row: parseInt(m[2], 10) - 1,
};
}
function parseFormula(formula) {
try {
const input = formula.startsWith('=') ? formula.slice(1) : formula;
const tokens = (0, tokenizer_1.tokenize)(input);
const parser = new Parser(tokens);
return parser.parse();
}
catch (e) {
return { kind: 'error', msg: String(e) };
}
}
};
// ── module: src/evaluator.ts ──
__mods["src/evaluator.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.evaluate = evaluate;
exports.toNumber = toNumber;
const config_1 = require("./config");
const state_1 = require("./state");
const parser_1 = require("./parser");
// Evaluate an AST node, collecting dependencies
function evaluate(node, ctx) {
if (ctx.depth > 100) {
return { value: config_1.ERR_CYCLE, error: config_1.ERR_CYCLE };
}
switch (node.kind) {
case 'number':
return { value: node.value };
case 'cellRef': {
const a = (0, state_1.addr)(node.col, node.row);
ctx.deps.push(a);
const cell = ctx.sheet.get(a);
if (!cell) {
return { value: 0 }; // empty cell = 0
}
if (cell.error) {
return { value: cell.error, error: cell.error };
}
// Check for circular dependency
if (ctx.visiting.has(a)) {
return { value: config_1.ERR_CYCLE, error: config_1.ERR_CYCLE };
}
// Recursively evaluate the referenced cell
const result = evalCell(cell, { ...ctx, depth: ctx.depth + 1 });
return result;
}
case 'range': {
const values = [];
// Collect all cells in the range
const sc = Math.min(node.startCol, node.endCol);
const ec = Math.max(node.startCol, node.endCol);
const sr = Math.min(node.startRow, node.endRow);
const er = Math.max(node.startRow, node.endRow);
for (let r = sr; r <= er; r++) {
for (let c = sc; c <= ec; c++) {
const a = (0, state_1.addr)(c, r);
ctx.deps.push(a);
const cell = ctx.sheet.get(a);
if (cell) {
if (cell.error) {
return { value: cell.error, error: cell.error };
}
const result = evalCell(cell, { ...ctx, depth: ctx.depth + 1 });
if (result.error) {
return { value: result.error, error: result.error };
}
const n = toNumber(result.value);
if (n !== null)
values.push(n);
}
}
}
return { value: values };
}
case 'unaryMinus': {
const result = evaluate(node.operand, ctx);
if (result.error)
return result;
const n = toNumber(result.value);
if (n === null)
return { value: config_1.ERR_GENERAL, error: config_1.ERR_GENERAL };
return { value: -n };
}
case 'binary': {
const left = evaluate(node.left, ctx);
if (left.error)
return left;
const right = evaluate(node.right, ctx);
if (right.error)
return right;
const ln = toNumber(left.value);
const rn = toNumber(right.value);
if (ln === null || rn === null)
return { value: config_1.ERR_GENERAL, error: config_1.ERR_GENERAL };
switch (node.op) {
case '+': return { value: ln + rn };
case '-': return { value: ln - rn };
case '*': return { value: ln * rn };
case '/':
if (rn === 0)
return { value: config_1.ERR_DIV0, error: config_1.ERR_DIV0 };
return { value: ln / rn };
}
}
case 'function': {
const fn = node.name.toUpperCase();
const values = [];
for (const arg of node.args) {
const result = evaluate(arg, ctx);
if (result.error)
return result;
// Flatten arrays (from ranges) into individual numbers
if (Array.isArray(result.value)) {
for (const v of result.value) {
const n = toNumber(v);
if (n !== null)
values.push(n);
}
}
else {
const n = toNumber(result.value);
if (n !== null)
values.push(n);
}
}
switch (fn) {
case 'SUM':
return { value: values.reduce((a, b) => a + b, 0) };
case 'AVG':
case 'AVERAGE':
return values.length > 0
? { value: values.reduce((a, b) => a + b, 0) / values.length }
: { value: 0 };
case 'MIN':
return values.length > 0 ? { value: Math.min(...values) } : { value: 0 };
case 'MAX':
return values.length > 0 ? { value: Math.max(...values) } : { value: 0 };
case 'COUNT':
return { value: values.length };
default:
return { value: config_1.ERR_GENERAL, error: config_1.ERR_GENERAL };
}
}
case 'error':
return { value: config_1.ERR_GENERAL, error: config_1.ERR_GENERAL };
default:
return { value: config_1.ERR_GENERAL, error: config_1.ERR_GENERAL };
}
}
// Evaluate a cell's formula
function evalCell(cell, ctx) {
if (cell.error) {
return { value: cell.error, error: cell.error };
}
const raw = cell.raw;
if (typeof raw !== 'string' || !raw.startsWith('=')) {
// Not a formula
const n = toNumber(raw);
if (n !== null)
return { value: n };
return { value: String(raw) };
}
// Re-evaluate the formula to detect cycles
const ast = (0, parser_1.parseFormula)(raw);
if (ast.kind === 'error') {
return { value: config_1.ERR_GENERAL, error: config_1.ERR_GENERAL };
}
const result = evaluate(ast, { ...ctx, depth: ctx.depth + 1 });
if (result.error)
return { value: result.error, error: result.error };
const n = toNumber(result.value);
if (n !== null)
return { value: n };
return { value: String(result.value) };
}
function toNumber(v) {
if (typeof v === 'number')
return v;
if (typeof v === 'string') {
const n = parseFloat(v);
if (!isNaN(n) && v.trim() !== '')
return n;
return null;
}
if (Array.isArray(v))
return null;
return null;
}
};
// ── module: src/dependency.ts ──
__mods["src/dependency.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.collectDeps = collectDeps;
exports.buildReverseDeps = buildReverseDeps;
exports.recomputeCell = recomputeCell;
exports.recalcFrom = recalcFrom;
exports.fullRecalc = fullRecalc;
const config_1 = require("./config");
const state_1 = require("./state");
const parser_1 = require("./parser");
const evaluator_1 = require("./evaluator");
// Parse a formula and collect all cell dependencies
function collectDeps(raw) {
if (!raw.startsWith('='))
return [];
const ast = (0, parser_1.parseFormula)(raw);
const deps = [];
collectDepsFromNode(ast, deps);
return deps;
}
function collectDepsFromNode(node, deps) {
if (!node || node.kind === 'error')
return;
switch (node.kind) {
case 'cellRef':
deps.push((0, state_1.addr)(node.col, node.row));
break;
case 'range': {
const sc = Math.min(node.startCol, node.endCol);
const ec = Math.max(node.startCol, node.endCol);
const sr = Math.min(node.startRow, node.endRow);
const er = Math.max(node.startRow, node.endRow);
for (let r = sr; r <= er; r++) {
for (let c = sc; c <= ec; c++) {
deps.push((0, state_1.addr)(c, r));
}
}
break;
}
case 'unaryMinus':
collectDepsFromNode(node.operand, deps);
break;
case 'binary':
collectDepsFromNode(node.left, deps);
collectDepsFromNode(node.right, deps);
break;
case 'function':
for (const arg of node.args) {
collectDepsFromNode(arg, deps);
}
break;
}
}
// Build reverse dependency map: for each cell, which cells depend on it
function buildReverseDeps(sheet) {
const revDeps = new Map();
for (const [cellAddr, cell] of sheet) {
if (cell.raw.startsWith('=')) {
const deps = collectDeps(cell.raw);
for (const dep of deps) {
if (!revDeps.has(dep))
revDeps.set(dep, new Set());
revDeps.get(dep).add(cellAddr);
}
}
}
return revDeps;
}
// Recompute a single cell
function recomputeCell(cellAddr, sheet, revDeps, astCache) {
const cell = sheet.get(cellAddr);
if (!cell)
return { raw: '', value: '' };
const raw = cell.raw;
// Non-formula cells just display their raw value
if (!raw.startsWith('=')) {
const n = (0, evaluator_1.toNumber)(raw);
if (n !== null)
return { raw, value: n };
return { raw, value: raw };
}
// Parse formula (or get from cache)
let ast;
if (astCache.has(cellAddr)) {
ast = astCache.get(cellAddr);
}
else {
ast = (0, parser_1.parseFormula)(raw);
astCache.set(cellAddr, ast);
}
// Check for parse error
if (ast.kind === 'error') {
return { raw, value: config_1.ERR_GENERAL, error: config_1.ERR_GENERAL };
}
// Evaluate with cycle detection
const ctx = {
sheet,
deps: [],
visiting: new Set([cellAddr]),
depth: 0,
};
const result = (0, evaluator_1.evaluate)(ast, ctx);
if (result.error) {
return { raw, value: result.error, error: result.error };
}
const n = (0, evaluator_1.toNumber)(result.value);
if (n !== null) {
return { raw, value: n };
}
return { raw, value: String(result.value) };
}
// Recompute a cell and cascade to all dependents
// Returns the list of affected cell addresses
function recalcFrom(cellAddr, sheet, revDeps, astCache) {
// BFS to find all affected cells in topological order
const visited = new Set();
const queue = [cellAddr];
const order = [];
while (queue.length > 0) {
const current = queue.shift();
if (visited.has(current))
continue;
visited.add(current);
order.push(current);
// Add dependents
const deps = revDeps.get(current);
if (deps) {
for (const d of deps) {
if (!visited.has(d)) {
queue.push(d);
}
}
}
}
// Recompute in order
for (const ca of order) {
const cell = recomputeCell(ca, sheet, revDeps, astCache);
sheet.set(ca, cell);
}
return order;
}
// Full recalculation of all formula cells (used on init)
function fullRecalc(sheet, revDeps, astCache) {
// Topological sort of all cells
const inDegree = new Map();
const allCells = new Set(sheet.keys());
// Initialize in-degrees
for (const addr of allCells) {
if (!inDegree.has(addr))
inDegree.set(addr, 0);
}
// Build forward dependency graph
const fwdDeps = new Map();
for (const [addr, cell] of sheet) {
if (cell.raw.startsWith('=')) {
const deps = collectDeps(cell.raw);
for (const dep of deps) {
if (allCells.has(dep)) {
if (!fwdDeps.has(dep))
fwdDeps.set(dep, new Set());
fwdDeps.get(dep).add(addr);
inDegree.set(addr, (inDegree.get(addr) || 0) + 1);
}
}
}
}
// Kahn's algorithm
const queue = [];
for (const [addr, deg] of inDegree) {
if (deg === 0)
queue.push(addr);
}
const order = [];
while (queue.length > 0) {
const current = queue.shift();
order.push(current);
const deps = fwdDeps.get(current);
if (deps) {
for (const d of deps) {
inDegree.set(d, (inDegree.get(d) || 0) - 1);
if (inDegree.get(d) === 0) {
queue.push(d);
}
}
}
}
// Cells not in order are in cycles
for (const addr of allCells) {
if (!order.includes(addr)) {
const cell = sheet.get(addr);
if (cell && cell.raw.startsWith('=')) {
sheet.set(addr, { raw: cell.raw, value: config_1.ERR_CYCLE, error: config_1.ERR_CYCLE });
}
}
}
// Recompute in topological order
for (const ca of order) {
const cell = recomputeCell(ca, sheet, revDeps, astCache);
sheet.set(ca, cell);
}
}
};
// ── module: src/grid.ts ──
__mods["src/grid.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setSelectionChange = setSelectionChange;
exports.initGrid = initGrid;
exports.selectCell = selectCell;
exports.updateSelection = updateSelection;
exports.updateCellDisplay = updateCellDisplay;
exports.updateCellDisplayFromSheet = updateCellDisplayFromSheet;
exports.getCellElement = getCellElement;
const config_1 = require("./config");
const state_1 = require("./state");
let tableEl = null;
let tbodyEl = null;
let onSelectionChange = null;
function setSelectionChange(fn) {
onSelectionChange = fn;
}
function initGrid(container) {
// Create the table
tableEl = document.createElement('table');
tableEl.className = 'spreadsheet-table';
tableEl.style.borderCollapse = 'collapse';
tableEl.style.tableLayout = 'fixed';
// Create thead with column headers
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
// Corner cell (top-left)
const corner = document.createElement('th');
corner.className = 'corner-header';
corner.style.width = config_1.HEADER_WIDTH + 'px';
corner.style.height = config_1.HEADER_HEIGHT + 'px';
corner.style.minWidth = config_1.HEADER_WIDTH + 'px';
corner.style.minHeight = config_1.HEADER_HEIGHT + 'px';
headerRow.appendChild(corner);
// Column headers
for (let c = 0; c < config_1.COLS; c++) {
const th = document.createElement('th');
th.className = 'col-header';
th.textContent = (0, state_1.colName)(c);
th.style.width = config_1.CELL_WIDTH + 'px';
th.style.minWidth = config_1.CELL_WIDTH + 'px';
th.style.height = config_1.HEADER_HEIGHT + 'px';
th.style.minHeight = config_1.HEADER_HEIGHT + 'px';
th.dataset.col = String(c);
headerRow.appendChild(th);
}
thead.appendChild(headerRow);
tableEl.appendChild(thead);
// Create tbody
tbodyEl = document.createElement('tbody');
for (let r = 0; r < config_1.ROWS; r++) {
const tr = document.createElement('tr');
tr.dataset.row = String(r);
// Row header
const th = document.createElement('td');
th.className = 'row-header';
th.textContent = String(r + 1);
th.style.width = config_1.HEADER_WIDTH + 'px';
th.style.minWidth = config_1.HEADER_WIDTH + 'px';
th.style.height = config_1.CELL_HEIGHT + 'px';
th.style.minHeight = config_1.CELL_HEIGHT + 'px';
tr.appendChild(th);
// Cells
for (let c = 0; c < config_1.COLS; c++) {
const td = document.createElement('td');
td.className = 'cell';
td.dataset.col = String(c);
td.dataset.row = String(r);
td.style.width = config_1.CELL_WIDTH + 'px';
td.style.height = config_1.CELL_HEIGHT + 'px';
tr.appendChild(td);
}
tbodyEl.appendChild(tr);
}
tableEl.appendChild(tbodyEl);
container.appendChild(tableEl);
// Click handler for cell selection
tbodyEl.addEventListener('click', handleCellClick);
}
function handleCellClick(e) {
const td = e.target.closest('td.cell');
if (!td)
return;
const col = parseInt(td.dataset.col);
const row = parseInt(td.dataset.row);
selectCell(col, row);
}
function selectCell(col, row) {
state_1.selection.col = col;
state_1.selection.row = row;
updateSelection();
if (onSelectionChange)
onSelectionChange(col, row);
}
function updateSelection() {
if (!tbodyEl)
return;
// Remove old selection
const old = tbodyEl.querySelector('.cell.selected');
if (old)
old.classList.remove('selected');
// Add new selection
const row = tbodyEl.querySelector(`tr[data-row="${state_1.selection.row}"]`);
if (row) {
const cell = row.querySelector(`td.cell[data-col="${state_1.selection.col}"]`);
if (cell)
cell.classList.add('selected');
}
}
function updateCellDisplay(col, row) {
if (!tbodyEl)
return;
const rowEl = tbodyEl.querySelector(`tr[data-row="${row}"]`);
if (!rowEl)
return;
const cellEl = rowEl.querySelector(`td.cell[data-col="${col}"]`);
if (!cellEl)
return;
const cell = (0, state_1.getCell)(col, row);
if (!cell) {
cellEl.textContent = '';
cellEl.style.color = '';
cellEl.style.textAlign = '';
return;
}
const val = cell.value;
if (typeof val === 'string' && val.startsWith('#')) {
cellEl.textContent = val;
cellEl.style.color = config_1.COLORS.errorColor;
cellEl.style.textAlign = 'right';
}
else if (typeof val === 'number') {
cellEl.textContent = formatNumber(val);
cellEl.style.color = '';
cellEl.style.textAlign = 'right';
}
else {
cellEl.textContent = String(val);
cellEl.style.color = '';
cellEl.style.textAlign = 'left';
}
}
function updateCellDisplayFromSheet(sheet) {
if (!tbodyEl)
return;
for (const [cellAddr, cell] of sheet) {
const m = cellAddr.match(/^([A-Z])(\d+)$/);
if (!m)
continue;
const col = m[1].charCodeAt(0) - 65;
const row = parseInt(m[2], 10) - 1;
updateCellDisplay(col, row);
}
}
function formatNumber(n) {
if (Number.isInteger(n))
return String(n);
// Limit decimal places
const s = n.toFixed(10);
// Remove trailing zeros
return s.replace(/\.?0+$/, '');
}
function getCellElement(col, row) {
if (!tbodyEl)
return null;
const rowEl = tbodyEl.querySelector(`tr[data-row="${row}"]`);
if (!rowEl)
return null;
return rowEl.querySelector(`td.cell[data-col="${col}"]`);
}
};
// ── module: src/operations.ts ──
__mods["src/operations.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.rewriteFormula = rewriteFormula;
exports.insertRow = insertRow;
exports.deleteRow = deleteRow;
exports.insertCol = insertCol;
exports.deleteCol = deleteCol;
const config_1 = require("./config");
const state_1 = require("./state");
// Rewrite a cell address when a row/col is inserted or deleted
function rewriteAddr(addrStr, op, index) {
const parsed = (0, state_1.parseAddr)(addrStr);
if (!parsed)
return null;
let { col, row } = parsed;
if (op === 'insertRow' && row >= index)
row++;
if (op === 'deleteRow' && row > index)
row--;
if (op === 'insertCol' && col >= index)
col++;
if (op === 'deleteCol' && col > index)
col--;
// Out of bounds
if (col < 0 || col >= config_1.COLS || row < 0 || row >= config_1.ROWS)
return null;
return (0, state_1.addr)(col, row);
}
// Rewrite all cell references in a formula string
function rewriteFormula(raw, op, index) {
if (!raw.startsWith('='))
return raw;
// Find all cell references and ranges in the formula
// Pattern: letter followed by digits (cell ref), optionally followed by : and another cell ref (range)
const cellRefPattern = /([A-Z]\d+)(?::([A-Z]\d+))?/g;
let result = raw;
let hasRefError = false;
// We need to process matches carefully since we're modifying the string
// Use a replacement function
result = raw.replace(cellRefPattern, (match, startRef, endRef) => {
const newStart = rewriteAddr(startRef, op, index);
if (!newStart) {
hasRefError = true;
return match;
}
if (endRef) {
const newEnd = rewriteAddr(endRef, op, index);
if (!newEnd) {
hasRefError = true;
return match;
}
return newStart + ':' + newEnd;
}
return newStart;
});
return result;
}
// Insert a row at the given index
function insertRow(sheet, rowIndex) {
if (rowIndex < 0 || rowIndex >= config_1.ROWS)
return;
const newSheet = new Map(sheet);
// Move all cells below the inserted row down by 1
const toRemove = [];
const toAdd = new Map();
for (const [cellAddr, cell] of sheet) {
const parsed = (0, state_1.parseAddr)(cellAddr);
if (!parsed)
continue;
if (parsed.row >= rowIndex) {
// Move this cell down
const newAddr = (0, state_1.addr)(parsed.col, parsed.row + 1);
if (parsed.row + 1 < config_1.ROWS) {
toAdd.set(newAddr, { ...cell });
toRemove.push(cellAddr);
}
}
}
// Rewrite formulas to account for the new row
for (const [newAddr, cell] of toAdd) {
cell.raw = rewriteFormula(cell.raw, 'insertRow', rowIndex);
}
// Also rewrite formulas in cells above the inserted row
for (const [cellAddr, cell] of sheet) {
const parsed = (0, state_1.parseAddr)(cellAddr);
if (!parsed || parsed.row >= rowIndex)
continue;
if (cell.raw.startsWith('=')) {
cell.raw = rewriteFormula(cell.raw, 'insertRow', rowIndex);
}
}
// Apply changes
for (const addr of toRemove) {
newSheet.delete(addr);
}
for (const [addr, cell] of toAdd) {
newSheet.set(addr, cell);
}
sheet.clear();
for (const [addr, cell] of newSheet) {
sheet.set(addr, cell);
}
}
// Delete a row at the given index
function deleteRow(sheet, rowIndex) {
if (rowIndex < 0 || rowIndex >= config_1.ROWS)
return;
const newSheet = new Map(sheet);
// Remove all cells in the deleted row
for (const cellAddr of sheet.keys()) {
const parsed = (0, state_1.parseAddr)(cellAddr);
if (parsed && parsed.row === rowIndex) {
newSheet.delete(cellAddr);
}
}
// Move all cells below the deleted row up by 1
const toRemove = [];
const toAdd = new Map();
for (const [cellAddr, cell] of newSheet) {
const parsed = (0, state_1.parseAddr)(cellAddr);
if (!parsed)
continue;
if (parsed.row > rowIndex) {
const newAddr = (0, state_1.addr)(parsed.col, parsed.row - 1);
toAdd.set(newAddr, { ...cell });
toRemove.push(cellAddr);
}
}
// Rewrite formulas
for (const [newAddr, cell] of toAdd) {
cell.raw = rewriteFormula(cell.raw, 'deleteRow', rowIndex);
}
// Also rewrite formulas in cells above the deleted row
for (const [cellAddr, cell] of newSheet) {
const parsed = (0, state_1.parseAddr)(cellAddr);
if (!parsed || parsed.row > rowIndex)
continue;
if (cell.raw.startsWith('=')) {
cell.raw = rewriteFormula(cell.raw, 'deleteRow', rowIndex);
}
}
// Apply changes
for (const addr of toRemove) {
newSheet.delete(addr);
}
for (const [addr, cell] of toAdd) {
newSheet.set(addr, cell);
}
sheet.clear();
for (const [addr, cell] of newSheet) {
sheet.set(addr, cell);
}
}
// Insert a column at the given index
function insertCol(sheet, colIndex) {
if (colIndex < 0 || colIndex >= config_1.COLS)
return;
const newSheet = new Map(sheet);
const toRemove = [];
const toAdd = new Map();
for (const [cellAddr, cell] of sheet) {
const parsed = (0, state_1.parseAddr)(cellAddr);
if (!parsed)
continue;
if (parsed.col >= colIndex) {
const newAddr = (0, state_1.addr)(parsed.col + 1, parsed.row);
if (parsed.col + 1 < config_1.COLS) {
toAdd.set(newAddr, { ...cell });
toRemove.push(cellAddr);
}
}
}
for (const [newAddr, cell] of toAdd) {
cell.raw = rewriteFormula(cell.raw, 'insertCol', colIndex);
}
for (const [cellAddr, cell] of sheet) {
const parsed = (0, state_1.parseAddr)(cellAddr);
if (!parsed || parsed.col >= colIndex)
continue;
if (cell.raw.startsWith('=')) {
cell.raw = rewriteFormula(cell.raw, 'insertCol', colIndex);
}
}
for (const a of toRemove)
newSheet.delete(a);
for (const [a, cell] of toAdd)
newSheet.set(a, cell);
sheet.clear();
for (const [a, cell] of newSheet)
sheet.set(a, cell);
}
// Delete a column at the given index
function deleteCol(sheet, colIndex) {
if (colIndex < 0 || colIndex >= config_1.COLS)
return;
const newSheet = new Map(sheet);
for (const cellAddr of sheet.keys()) {
const parsed = (0, state_1.parseAddr)(cellAddr);
if (parsed && parsed.col === colIndex) {
newSheet.delete(cellAddr);
}
}
const toRemove = [];
const toAdd = new Map();
for (const [cellAddr, cell] of newSheet) {
const parsed = (0, state_1.parseAddr)(cellAddr);
if (!parsed)
continue;
if (parsed.col > colIndex) {
const newAddr = (0, state_1.addr)(parsed.col - 1, parsed.row);
toAdd.set(newAddr, { ...cell });
toRemove.push(cellAddr);
}
}
for (const [newAddr, cell] of toAdd) {
cell.raw = rewriteFormula(cell.raw, 'deleteCol', colIndex);
}
for (const [cellAddr, cell] of newSheet) {
const parsed = (0, state_1.parseAddr)(cellAddr);
if (!parsed || parsed.col > colIndex)
continue;
if (cell.raw.startsWith('=')) {
cell.raw = rewriteFormula(cell.raw, 'deleteCol', colIndex);
}
}
for (const a of toRemove)
newSheet.delete(a);
for (const [a, cell] of toAdd)
newSheet.set(a, cell);
sheet.clear();
for (const [a, cell] of newSheet)
sheet.set(a, cell);
}
};
// ── module: src/persistence.ts ──
__mods["src/persistence.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveSheet = saveSheet;
exports.loadSheet = loadSheet;
const STORAGE_KEY = 'spreadsheet_data';
function saveSheet(sheet) {
try {
const data = {};
for (const [addr, cell] of sheet) {
data[addr] = { raw: cell.raw };
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
catch (e) {
// Storage might be unavailable
}
}
function loadSheet(sheet) {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw)
return false;
const data = JSON.parse(raw);
sheet.clear();
for (const [addr, cellData] of Object.entries(data)) {
sheet.set(addr, { raw: cellData.raw, value: '' });
}
return true;
}
catch (e) {
return false;
}
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setFormulaBarInput = setFormulaBarInput;
exports.setOnCommit = setOnCommit;
exports.setOnFormulaBarChange = setOnFormulaBarChange;
exports.getFormulaBar = getFormulaBar;
exports.updateFormulaBar = updateFormulaBar;
exports.startEdit = startEdit;
exports.commitEdit = commitEdit;
exports.cancelEdit = cancelEdit;
exports.isEditing = isEditing;
exports.handleFormulaBarKeydown = handleFormulaBarKeydown;
exports.handleGridKeydown = handleGridKeydown;
exports.clearCell = clearCell;
exports.commitCell = commitCell;
exports.moveSelection = moveSelection;
const config_1 = require("./config");
const state_1 = require("./state");
const grid_1 = require("./grid");
const dependency_1 = require("./dependency");
let formulaBarInput = null;
let editingCell = null;
let editInput = null;
let onCommit = null;
let onFormulaBarChange = null;
function setFormulaBarInput(el) {
formulaBarInput = el;
}
function setOnCommit(fn) {
onCommit = fn;
}
function setOnFormulaBarChange(fn) {
onFormulaBarChange = fn;
}
function getFormulaBar() {
return formulaBarInput;
}
// Update formula bar to show the raw content of the selected cell
function updateFormulaBar() {
if (!formulaBarInput)
return;
const cell = (0, state_1.getCell)(state_1.selection.col, state_1.selection.row);
formulaBarInput.value = cell ? cell.raw : '';
}
// Enter edit mode for the selected cell
function startEdit() {
const cellEl = (0, grid_1.getCellElement)(state_1.selection.col, state_1.selection.row);
if (!cellEl || !formulaBarInput)
return;
// Create an input element inside the cell
cellEl.textContent = '';
editInput = document.createElement('input');
editInput.type = 'text';
editInput.className = 'cell-input';
editInput.style.width = '100%';
editInput.style.height = '100%';
editInput.style.border = 'none';
editInput.style.outline = 'none';
editInput.style.padding = '2px 4px';
editInput.style.font = 'inherit';
editInput.style.boxSizing = 'border-box';
const cell = (0, state_1.getCell)(state_1.selection.col, state_1.selection.row);
editInput.value = cell ? cell.raw : '';
cellEl.appendChild(editInput);
editInput.focus();
editInput.select();
// Sync formula bar
formulaBarInput.value = editInput.value;
// Handle input events
editInput.addEventListener('input', () => {
if (formulaBarInput && editInput)
formulaBarInput.value = editInput.value;
});
// Handle Enter/Escape
editInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (editInput)
commitEdit(editInput.value);
}
else if (e.key === 'Escape') {
e.preventDefault();
cancelEdit();
}
});
editingCell = cellEl;
}
// Commit the current edit
function commitEdit(raw) {
if (onCommit) {
onCommit(raw);
}
finishEdit();
}
// Cancel the current edit
function cancelEdit() {
finishEdit();
}
// Finish editing (remove input element, restore display)
function finishEdit() {
if (editInput && editingCell) {
editingCell.removeChild(editInput);
editInput = null;
editingCell = null;
}
(0, grid_1.updateCellDisplay)(state_1.selection.col, state_1.selection.row);
updateFormulaBar();
}
// Check if we're currently editing
function isEditing() {
return editInput !== null;
}
// Handle keyboard input on the formula bar
function handleFormulaBarKeydown(e) {
if (e.key === 'Enter') {
e.preventDefault();
if (formulaBarInput) {
commitEdit(formulaBarInput.value);
}
}
else if (e.key === 'Escape') {
e.preventDefault();
if (formulaBarInput) {
updateFormulaBar();
}
}
}
// Handle keyboard input on the grid
function handleGridKeydown(e, sheet) {
// If editing, let the input handle it
if (isEditing())
return;
const key = e.key;
// Navigation
if (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight') {
e.preventDefault();
let { col, row } = state_1.selection;
if (key === 'ArrowUp')
row = Math.max(0, row - 1);
if (key === 'ArrowDown')
row = Math.min(config_1.ROWS - 1, row + 1);
if (key === 'ArrowLeft')
col = Math.max(0, col - 1);
if (key === 'ArrowRight')
col = Math.min(config_1.COLS - 1, col + 1);
state_1.selection.col = col;
state_1.selection.row = row;
(0, grid_1.selectCell)(col, row);
updateFormulaBar();
return;
}
// Delete key
if (key === 'Delete' || key === 'Backspace') {
e.preventDefault();
clearCell(sheet);
return;
}
// Tab moves right
if (key === 'Tab') {
e.preventDefault();
let col = Math.min(config_1.COLS - 1, state_1.selection.col + 1);
state_1.selection.col = col;
(0, grid_1.selectCell)(col, state_1.selection.row);
updateFormulaBar();
return;
}
// Any other printable key starts editing
if (key.length === 1 && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
startEditWithInitial(key);
}
}
// Start editing with an initial character
function startEditWithInitial(ch) {
const cellEl = (0, grid_1.getCellElement)(state_1.selection.col, state_1.selection.row);
if (!cellEl || !formulaBarInput)
return;
cellEl.textContent = '';
editInput = document.createElement('input');
editInput.type = 'text';
editInput.className = 'cell-input';
editInput.style.width = '100%';
editInput.style.height = '100%';
editInput.style.border = 'none';
editInput.style.outline = 'none';
editInput.style.padding = '2px 4px';
editInput.style.font = 'inherit';
editInput.style.boxSizing = 'border-box';
editInput.value = ch;
cellEl.appendChild(editInput);
editInput.focus();
formulaBarInput.value = ch;
editInput.addEventListener('input', () => {
if (formulaBarInput && editInput)
formulaBarInput.value = editInput.value;
});
editInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (editInput)
commitEdit(editInput.value);
}
else if (e.key === 'Escape') {
e.preventDefault();
cancelEdit();
}
});
editingCell = cellEl;
}
// Clear the selected cell
function clearCell(sheet) {
const cellAddr = (0, state_1.getSelectionAddr)();
const cell = sheet.get(cellAddr);
if (!cell)
return;
// Remove the cell
sheet.delete(cellAddr);
(0, grid_1.updateCellDisplay)(state_1.selection.col, state_1.selection.row);
// Recalculate dependents
const revDeps = (0, dependency_1.buildReverseDeps)(sheet);
(0, dependency_1.recalcFrom)(cellAddr, sheet, revDeps, new Map());
updateFormulaBar();
}
// Commit a cell value
function commitCell(sheet, raw) {
const cellAddr = (0, state_1.getSelectionAddr)();
// Build AST cache
const astCache = new Map();
// Set the cell
if (raw.trim() === '') {
sheet.delete(cellAddr);
}
else {
const cell = { raw, value: '' };
sheet.set(cellAddr, cell);
}
// Recalculate and track which cells were affected
const revDeps = (0, dependency_1.buildReverseDeps)(sheet);
const affectedCells = (0, dependency_1.recalcFrom)(cellAddr, sheet, revDeps, astCache);
// Update display for all affected cells
for (const addr of affectedCells) {
const m = addr.match(/^([A-Z])(\d+)$/);
if (m) {
const col = m[1].charCodeAt(0) - 65;
const row = parseInt(m[2], 10) - 1;
(0, grid_1.updateCellDisplay)(col, row);
}
}
updateFormulaBar();
}
// Move selection after commit
function moveSelection(direction) {
if (direction === 'down') {
state_1.selection.row = Math.min(config_1.ROWS - 1, state_1.selection.row + 1);
}
else {
state_1.selection.col = Math.min(config_1.COLS - 1, state_1.selection.col + 1);
}
(0, grid_1.selectCell)(state_1.selection.col, state_1.selection.row);
updateFormulaBar();
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"71271cc6-cdee-470c-9f33-830f172461b0","tokensIn":5739720,"tokensOut":64214,"tokensTotal":5803934,"cost":0,"turns":124,"toolCalls":123,"failedToolCalls":3,"timestamp":"2026-08-01T07:33:50.218Z"} -->