Spreadsheet
North Mini Code 1.0 GGUF · 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 32,569 bytes · SHA-256 02f243756986
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spreadsheet</title>
<style>
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/parser.ts":{"./types":"src/types.ts"},"src/spreadsheet.ts":{"./types":"src/types.ts","./parser":"src/parser.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";
// Entry point. Runs after the DOM is parsed and after any classic
// <script> imports, so document queries and CDN globals are available.
// Import your other modules from here.
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
function start() {
// Replace this with the real startup sequence.
}
start();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ErrorType = void 0;
var ErrorType;
(function (ErrorType) {
ErrorType["REF"] = "#REF!";
ErrorType["DIV0"] = "#DIV/0!";
ErrorType["ERR"] = "#ERR!";
ErrorType["CYCLE"] = "#CYCLE!";
})(ErrorType || (exports.ErrorType = ErrorType = {}));
};
// ── module: src/parser.ts ──
__mods["src/parser.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tokenize = tokenize;
exports.parse = parse;
exports.evaluateAST = evaluateAST;
exports.parseFormula = parseFormula;
// Tokenizes a formula string into tokens.
// Handles numbers, identifiers (references, functions, range), operators, parentheses, etc.
const types_1 = require("./types");
function tokenize(input) {
const tokens = [];
let pos = 0;
const length = input.length;
const isAlpha = (c) => /[A-Za-z]/.test(c);
const isDigit = (c) => /[0-9]/.test(c);
while (pos < length) {
const c = input[pos];
if (c === ' ') {
pos++;
continue;
}
if (isDigit(c)) {
let num = '';
while (pos < length && isDigit(input[pos])) {
num += input[pos];
pos++;
}
if (pos < length && input[pos] === '.') {
num += '.';
pos++;
while (pos < length && isDigit(input[pos])) {
num += input[pos];
pos++;
}
}
tokens.push({ type: 'NUMBER', value: parseFloat(num) });
continue;
}
if (isAlpha(c)) {
let name = '';
while (pos < length && (isAlpha(input[pos]) || isDigit(input[pos]))) {
name += input[pos];
pos++;
}
// Distinguish references (single letter + number) from functions (all caps)
const isRef = name.length > 1 && isNaN(parseInt(name[name.length - 1])) && /^[A-Z][0-9]+$/.test(name);
const isFunc = /^[A-Z]{3,}$/.test(name.toUpperCase());
if (isRef) {
tokens.push({ type: 'REF', value: name });
}
else if (isFunc) {
tokens.push({ type: 'FUNC', value: name.toUpperCase() });
}
else {
// fallback identifier (treated as literal text)
tokens.push({ type: 'TEXT', value: name });
}
continue;
}
if (c === ',') {
tokens.push({ type: 'COMMA' });
pos++;
continue;
}
if (c === '=') {
tokens.push({ type: 'EQUALS' });
pos++;
continue;
}
if (c === ':' && tokens.length > 0 && tokens[tokens.length - 1].type === 'REF') {
// Range token starts with REF; we'll handle range detection later
// For now, push colon as part of REF token and handle in parser
// Simpler: treat colon as separate token and parser will combine REF + ':' + REF
tokens.pop(); // remove last REF token
tokens.push({ type: 'RANGE', start: tokens[tokens.length - 1].value, end: '' });
pos++;
continue;
}
if (c === '(') {
tokens.push({ type: 'PAREN_OPEN' });
pos++;
continue;
}
if (c === ')') {
tokens.push({ type: 'PAREN_CLOSE' });
pos++;
continue;
}
if ('+-*/'.includes(c)) {
tokens.push({ type: 'OP', value: c });
pos++;
continue;
}
// If unknown character, treat as TEXT literal
let text = '';
while (pos < length && !/[,=():*/+-\s]/.test(input[pos])) {
text += input[pos];
pos++;
}
if (text.length > 0) {
tokens.push({ type: 'TEXT', value: text });
}
}
tokens.push({ type: 'EOF' });
return tokens;
}
function isReferenceToken(token) {
return token.type === 'REF' || token.type === 'RANGE';
}
function parse(tokens) {
let current = 0;
function peek() {
return tokens[current];
}
function consume(type) {
const token = peek();
if (type !== undefined && token.type !== type) {
throw new Error(`Expected ${type} but got ${token.type}`);
}
current++;
return token;
}
function parseExpression() {
return parseAdditive();
}
function parseAdditive() {
let left = parseMultiplicative();
while (peek().type === 'OP') {
const op = peek().value;
if (op !== '+' && op !== '-')
throw new Error('Unexpected operator');
consume('OP');
const right = parseMultiplicative();
left = { type: 'binary', op, left, right };
}
return left;
}
function parseMultiplicative() {
let left = parseUnary();
while (peek().type === 'OP') {
const op = peek().value;
if (op !== '*' && op !== '/')
throw new Error('Unexpected operator');
consume('OP');
const right = parseUnary();
left = { type: 'binary', op, left, right };
}
return left;
}
function parseUnary() {
if (peek().type === 'OP' && (peek().value === '+' || peek().value === '-')) {
const op = peek().value;
consume('OP');
const operand = parseUnary();
return { type: 'unary', op, operand };
}
return parsePrimary();
}
function parsePrimary() {
const token = peek();
if (token.type === 'NUMBER') {
consume('NUMBER');
return { type: 'literal', value: token.value };
}
if (token.type === 'TEXT') {
consume('TEXT');
return { type: 'literal', value: token.value };
}
if (token.type === 'REF') {
consume('REF');
return { type: 'reference', address: token.value };
}
if (token.type === 'RANGE') {
// For now, treat RANGE as a single token; need to parse start/end
// This is a simplification; real parsing should handle A1:A10
// We'll implement later
consume('RANGE');
// For now, just treat as reference to start address
return { type: 'reference', address: token.value };
}
if (token.type === 'FUNC') {
consume('FUNC');
consume('PAREN_OPEN');
const args = [];
if (peek().type !== 'PAREN_CLOSE') {
while (true) {
const arg = parseExpression();
args.push(arg);
if (peek().type === 'COMMA') {
consume('COMMA');
continue;
}
break;
}
}
consume('PAREN_CLOSE');
return { type: 'function', name: token.value, args };
}
if (token.type === 'PAREN_OPEN') {
consume('PAREN_OPEN');
const expr = parseExpression();
consume('PAREN_CLOSE');
return expr;
}
throw new Error(`Unexpected token: ${token.type}`);
}
const ast = parseExpression();
if (peek().type !== 'EOF') {
throw new Error('Unexpected extra tokens');
}
return ast;
}
function evaluateAST(node, sheet, address) {
try {
if (node.type === 'binary') {
const left = evaluateAST(node.left, sheet, address);
if (left.error)
return left;
const right = evaluateAST(node.right, sheet, address);
if (right.error)
return right;
if (typeof left.value === 'number' && typeof right.value === 'number') {
switch (node.op) {
case '+': return { value: left.value + right.value };
case '-': return { value: left.value - right.value };
case '*': return { value: left.value * right.value };
case '/': {
if (right.value === 0)
return { value: 0, error: types_1.ErrorType.DIV0 };
return { value: left.value / right.value };
}
}
}
return { value: null, error: types_1.ErrorType.ERR };
}
if (node.type === 'unary') {
const operand = evaluateAST(node.operand, sheet, address);
if (operand.error)
return operand;
if (typeof operand.value === 'number') {
if (node.op === '-')
return { value: -operand.value };
return { value: operand.value };
}
return { value: null, error: types_1.ErrorType.ERR };
}
if (node.type === 'literal') {
return { value: node.value };
}
if (node.type === 'reference') {
const ref = sheet.cells.get(node.address);
if (!ref)
return { value: null, error: types_1.ErrorType.REF };
return { value: ref.value !== null && !ref.error ? ref.value : null, error: ref.error || undefined };
}
if (node.type === 'function') {
const results = [];
for (const arg of node.args) {
const res = evaluateAST(arg, sheet, address);
if (res.error)
return res;
if (typeof res.value === 'number') {
results.push(res.value);
}
else {
results.push(null);
}
}
switch (node.name) {
case 'SUM':
let sum = 0;
for (const v of results) {
if (v !== null)
sum += v;
}
return { value: sum };
case 'AVG':
case 'AVERAGE':
const nonNull = results.filter(v => v !== null);
const avg = nonNull.reduce((acc, v) => acc + v, 0) / Math.max(nonNull.length, 1);
return { value: avg };
case 'MIN':
const min = Math.min(...results.filter(v => v !== null));
return { value: min };
case 'MAX':
const max = Math.max(...results.filter(v => v !== null));
return { value: max };
case 'COUNT':
return { value: results.filter(v => v !== null).length };
default:
return { value: null, error: types_1.ErrorType.ERR };
}
}
throw new Error('Unknown AST node type');
}
catch (e) {
return { value: null, error: types_1.ErrorType.ERR };
}
}
function parseFormula(formula) {
const tokens = tokenize(formula);
return parse(tokens);
}
};
// ── module: src/spreadsheet.ts ──
__mods["src/spreadsheet.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Spreadsheet = void 0;
const types_1 = require("./types");
const parser_1 = require("./parser");
function addressToString(addr) {
const col = String.fromCharCode(65 + addr.col);
return col + addr.row;
}
function parseAddress(str) {
const match = /^([A-Z])(\d+)$/.exec(str);
if (!match)
throw new Error('Invalid address format');
return { col: match[1].charCodeAt(0) - 65, row: parseInt(match[2], 10) };
}
function addressFromString(str) {
const match = /^([A-Z])(\d+)$/.exec(str);
if (!match)
throw new Error('Invalid address format');
return { col: match[1].charCodeAt(0) - 65, row: parseInt(match[2], 10) };
}
function isValidAddress(str) {
return /^[A-Z]\d+$/.test(str);
}
function getColumnLetter(col) {
return String.fromCharCode(65 + col);
}
function resolveRange(start, end, sheet) {
const startAddr = parseAddress(start);
const endAddr = parseAddress(end);
const cells = [];
for (let c = startAddr.col; c <= endAddr.col; c++) {
for (let r = startAddr.row; r <= endAddr.row; r++) {
const addr = addressToString({ col: c, row: r });
const cell = sheet.cells.get(addr);
if (cell)
cells.push(cell);
}
}
return cells;
}
function extractReferences(node, sheet) {
if (!node)
return new Set();
if (node.type === 'reference') {
return new Set([node.address]);
}
if (node.type === 'range') {
const cells = resolveRange(node.start, node.end, sheet);
const refs = new Set();
for (const cell of cells) {
refs.add(cell.address);
}
return refs;
}
const refs = new Set();
if (node.type === 'binary' || node.type === 'unary') {
const leftRefs = extractReferences(node.left, sheet);
for (const ref of leftRefs)
refs.add(ref);
const rightRefs = extractReferences(node.right, sheet);
for (const ref of rightRefs)
refs.add(ref);
}
if (node.type === 'function') {
for (const arg of node.args) {
const argRefs = extractReferences(arg, sheet);
for (const ref of argRefs)
refs.add(ref);
}
}
return refs;
}
function topologicalSort(sheet) {
const visited = new Set();
const temp = new Set();
const order = [];
const cycle = [];
function visit(addr) {
if (temp.has(addr)) {
cycle.push(addr);
return;
}
if (visited.has(addr))
return;
temp.add(addr);
const cell = sheet.cells.get(addr);
if (!cell) {
temp.delete(addr);
return;
}
for (const dep of cell.dependencies) {
visit(dep);
}
temp.delete(addr);
visited.add(addr);
order.push(addr);
}
for (const addr of sheet.cells.keys()) {
if (!visited.has(addr)) {
visit(addr);
}
}
return cycle.length > 0 ? cycle.map(() => types_1.ErrorType.CYCLE) : order;
}
function computeCell(cell, sheet) {
if (!cell.formula) {
return { value: cell.raw === '' ? null : cell.raw };
}
if (cell.error) {
return { value: null, error: cell.error };
}
try {
const ast = (0, parser_1.parseFormula)(cell.raw.slice(1)); // strip '='
const result = (0, parser_1.evaluateAST)(ast, sheet, cell.address);
return result;
}
catch (e) {
return { value: null, error: types_1.ErrorType.ERR };
}
}
class Spreadsheet {
constructor(container, config = {}) {
this.selected = null;
this.editing = false;
this.formulaBar = null;
this.grid = null;
this.cellsCache = new Map();
this.config = {
cols: 26,
rows: 100,
cellWidth: 120,
cellHeight: 30,
headerHeight: 40,
...config,
};
this.sheet = { cells: new Map(), size: { cols: this.config.cols, rows: this.config.rows } };
this.initGrid(container);
this.initFormulaBar(container);
this.initEventListeners();
this.renderGrid();
}
initGrid(container) {
this.grid = document.createElement('div');
this.grid.style.position = 'absolute';
this.grid.style.top = `${this.config.headerHeight}px`;
this.grid.style.left = '0';
this.grid.style.width = `${this.config.cols * this.config.cellWidth}px`;
this.grid.style.height = `${this.config.rows * this.config.cellHeight}px`;
this.grid.style.border = '1px solid #ccc';
this.grid.style.display = 'grid';
this.grid.style.gridTemplateColumns = `repeat(${this.config.cols}, ${this.config.cellWidth}px)`;
this.grid.style.gridTemplateRows = `repeat(${this.config.rows}, ${this.config.cellHeight}px)`;
container.appendChild(this.grid);
}
initFormulaBar(container) {
this.formulaBar = document.createElement('div');
this.formulaBar.style.height = `${this.config.headerHeight}px`;
this.formulaBar.style.borderBottom = '1px solid #ccc';
this.formulaBar.style.display = 'flex';
this.formulaBar.style.alignItems = 'center';
this.formulaBar.style.padding = '5px';
this.formulaBar.style.backgroundColor = '#f8f9fa';
container.prepend(this.formulaBar);
}
initEventListeners() {
document.addEventListener('keydown', (e) => this.handleKeyDown(e));
window.addEventListener('beforeunload', () => this.save());
}
handleKeyDown(e) {
if (!this.selected)
return;
if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault();
this.moveSelection(e.key === 'Enter' ? 0 : 1, 0);
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
this.moveSelection(0, -1);
}
else if (e.key === 'ArrowDown') {
e.preventDefault();
this.moveSelection(0, 1);
}
else if (e.key === 'ArrowLeft') {
e.preventDefault();
this.moveSelection(-1, 0);
}
else if (e.key === 'ArrowRight') {
e.preventDefault();
this.moveSelection(1, 0);
}
else if (e.key === 'Delete') {
e.preventDefault();
this.clearCell();
}
else if (e.key.length === 1 || e.key === 'Backspace') {
this.enterEditMode();
}
}
moveSelection(dCol, dRow) {
if (!this.selected)
return;
let newCol = this.selected.col + dCol;
let newRow = this.selected.row + dRow;
newCol = Math.max(0, Math.min(newCol, this.config.cols - 1));
newRow = Math.max(1, Math.min(newRow, this.config.rows));
this.setSelected({ col: newCol, row: newRow });
}
setSelected(addr) {
this.selected = addr;
this.renderFormulaBar();
this.updateGridSelection();
}
renderFormulaBar() {
if (!this.formulaBar)
return;
this.formulaBar.innerHTML = '';
if (this.selected) {
const ref = addressToString(this.selected);
const refEl = document.createElement('div');
refEl.textContent = ref;
refEl.style.fontWeight = 'bold';
refEl.style.marginRight = '10px';
this.formulaBar.appendChild(refEl);
const input = document.createElement('input');
input.type = 'text';
input.value = this.sheet.cells.get(addressToString(this.selected))?.raw || '';
input.style.flex = '1';
input.addEventListener('change', (e) => this.setCellRaw(addressToString(this.selected), e.target.value));
this.formulaBar.appendChild(input);
}
}
updateGridSelection() {
this.cellsCache.forEach((el, addr) => {
const selected = this.selected && addr === addressToString(this.selected);
el.style.backgroundColor = selected ? '#e3f2fd' : '';
el.style.border = selected ? '2px solid #2196f3' : '1px solid #ddd';
});
}
renderGrid() {
if (!this.grid)
return;
this.grid.innerHTML = '';
this.cellsCache.clear();
for (let r = 1; r <= this.config.rows; r++) {
for (let c = 0; c < this.config.cols; c++) {
const addr = addressToString({ col: c, row: r });
const cell = this.sheet.cells.get(addr);
const cellEl = document.createElement('div');
cellEl.style.border = '1px solid #ddd';
cellEl.style.display = 'flex';
cellEl.style.alignItems = 'center';
cellEl.style.justifyContent = 'center';
cellEl.style.fontSize = '12px';
cellEl.style.cursor = 'pointer';
if (cell) {
if (cell.value !== null) {
cellEl.textContent = String(cell.value);
cellEl.style.textAlign = 'right';
}
else if (cell.error) {
cellEl.textContent = cell.error;
cellEl.style.color = '#d32f2f';
}
else {
cellEl.textContent = cell.raw || '';
cellEl.style.textAlign = 'left';
}
}
else {
cellEl.textContent = '';
}
cellEl.addEventListener('click', () => this.setSelected({ col: c, row: r }));
cellEl.addEventListener('dblclick', () => this.enterEditMode());
this.cellsCache.set(addr, cellEl);
this.grid.appendChild(cellEl);
}
}
}
enterEditMode() {
if (!this.selected)
return;
this.editing = true;
const cellEl = this.cellsCache.get(addressToString(this.selected));
if (cellEl)
cellEl.contentEditable = 'true';
cellEl?.focus();
}
setCellRaw(address, raw) {
let cell = this.sheet.cells.get(address);
const wasFormula = cell?.formula;
const oldDeps = new Set(cell?.dependencies || []);
if (!cell) {
const addr = parseAddress(address);
cell = {
address,
raw,
value: null,
formula: raw.startsWith('='),
error: null,
dependencies: new Set(),
dependents: new Set(),
};
this.sheet.cells.set(address, cell);
}
else {
cell.raw = raw;
cell.formula = raw.startsWith('=');
cell.error = null;
}
const newDeps = new Set();
if (cell.formula) {
try {
const ast = (0, parser_1.parseFormula)(raw.slice(1));
newDeps.add(...extractReferences(ast, this.sheet));
}
catch (e) {
cell.error = types_1.ErrorType.ERR;
}
}
cell.dependencies = newDeps;
for (const oldDep of oldDeps) {
if (!newDeps.has(oldDep)) {
const oldCell = this.sheet.cells.get(oldDep);
if (oldCell)
oldCell.dependents.delete(address);
}
}
for (const newDep of newDeps) {
const depCell = this.sheet.cells.get(newDep);
if (depCell)
depCell.dependents.add(address);
}
this.recompute(address);
this.renderGrid();
this.renderFormulaBar();
this.editing = false;
}
recompute(address) {
const cell = this.sheet.cells.get(address);
if (!cell)
return;
const result = computeCell(cell, this.sheet);
cell.value = result.value;
cell.error = result.error || null;
for (const dep of cell.dependents) {
this.recompute(dep);
}
}
insertRow(row) {
if (row < 1 || row > this.config.rows)
return;
for (let r = this.config.rows; r >= row; r--) {
for (let c = 0; c < this.config.cols; c++) {
const addr = addressToString({ col: c, row: r });
const oldCell = this.sheet.cells.get(addr);
if (oldCell) {
const newAddr = addressToString({ col: c, row: r + 1 });
oldCell.address = newAddr;
this.sheet.cells.delete(addr);
this.sheet.cells.set(newAddr, oldCell);
oldCell.dependencies.forEach(dep => {
const depCell = this.sheet.cells.get(dep);
if (depCell) {
depCell.dependents.delete(addr);
depCell.dependents.add(newAddr);
}
});
oldCell.dependents.forEach(dep => {
const depCell = this.sheet.cells.get(dep);
if (depCell) {
depCell.dependencies.delete(addr);
depCell.dependencies.add(newAddr);
}
});
}
}
}
this.config.rows++;
this.renderGrid();
this.save();
}
deleteRow(row) {
if (row < 1 || row > this.config.rows)
return;
for (let r = row; r < this.config.rows; r++) {
for (let c = 0; c < this.config.cols; c++) {
const addr = addressToString({ col: c, row: r });
const cell = this.sheet.cells.get(addr);
if (cell) {
const newAddr = addressToString({ col: c, row: r + 1 });
cell.address = newAddr;
this.sheet.cells.delete(addr);
this.sheet.cells.set(newAddr, cell);
cell.dependencies.forEach(dep => {
const depCell = this.sheet.cells.get(dep);
if (depCell) {
depCell.dependents.delete(addr);
depCell.dependents.add(newAddr);
}
});
cell.dependents.forEach(dep => {
const depCell = this.sheet.cells.get(dep);
if (depCell) {
depCell.dependencies.delete(addr);
depCell.dependencies.add(newAddr);
}
});
}
}
}
this.config.rows--;
this.renderGrid();
this.save();
}
insertColumn(col) {
if (col < 0 || col >= this.config.cols)
return;
for (let c = this.config.cols - 1; c >= col; c--) {
for (let r = 1; r <= this.config.rows; r++) {
const addr = addressToString({ col: c, row: r });
const oldCell = this.sheet.cells.get(addr);
if (oldCell) {
const newAddr = addressToString({ col: c + 1, row: r });
oldCell.address = newAddr;
this.sheet.cells.delete(addr);
this.sheet.cells.set(newAddr, oldCell);
oldCell.dependencies.forEach(dep => {
const depCell = this.sheet.cells.get(dep);
if (depCell) {
depCell.dependents.delete(addr);
depCell.dependents.add(newAddr);
}
});
oldCell.dependents.forEach(dep => {
const depCell = this.sheet.cells.get(dep);
if (depCell) {
depCell.dependencies.delete(addr);
depCell.dependencies.add(newAddr);
}
});
}
}
}
this.config.cols++;
this.renderGrid();
this.save();
}
deleteColumn(col) {
if (col < 0 || col >= this.config.cols)
return;
for (let c = col; c < this.config.cols; c++) {
for (let r = 1; r <= this.config.rows; r++) {
const addr = addressToString({ col: c, row: r });
const cell = this.sheet.cells.get(addr);
if (cell) {
const newAddr = addressToString({ col: c, row: r });
cell.address = newAddr;
this.sheet.cells.delete(addr);
this.sheet.cells.set(newAddr, cell);
cell.dependencies.forEach(dep => {
const depCell = this.sheet.cells.get(dep);
if (depCell) {
depCell.dependents.delete(addr);
depCell.dependents.add(newAddr);
}
});
cell.dependents.forEach(dep => {
const depCell = this.sheet.cells.get(dep);
if (depCell) {
depCell.dependencies.delete(addr);
depCell.dependencies.add(newAddr);
}
});
}
}
}
this.config.cols--;
this.renderGrid();
this.save();
}
clearCell() {
if (!this.selected)
return;
const addr = addressToString(this.selected);
const cell = this.sheet.cells.get(addr);
if (!cell)
return;
const oldDeps = new Set(cell.dependencies);
cell.raw = '';
cell.formula = false;
cell.value = null;
cell.error = null;
cell.dependencies.clear();
oldDeps.forEach(dep => {
const depCell = this.sheet.cells.get(dep);
if (depCell)
depCell.dependents.delete(addr);
});
this.recompute(addr);
this.renderGrid();
this.renderFormulaBar();
}
save() {
const data = {
cells: Array.from(this.sheet.cells.entries()).map(([addr, cell]) => ({ addr, ...cell, dependencies: Array.from(cell.dependencies), dependents: Array.from(cell.dependents) })),
config: this.config,
};
localStorage.setItem('spreadsheet', JSON.stringify(data));
}
load() {
const data = localStorage.getItem('spreadsheet');
if (!data)
return;
const { cells, config } = JSON.parse(data);
this.config = { ...this.config, ...config };
this.sheet.cells.clear();
cells.forEach((c) => {
const cell = { ...c, dependencies: new Set(c.dependencies), dependents: new Set(c.dependents) };
this.sheet.cells.set(c.addr, cell);
});
this.renderGrid();
this.renderFormulaBar();
}
getSelected() {
return this.selected;
}
setSelectedFromAddress(addr) {
try {
const parsed = parseAddress(addr);
this.setSelected(parsed);
}
catch (e) { }
}
getCell(address) {
return this.sheet.cells.get(address);
}
}
exports.Spreadsheet = Spreadsheet;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>