Spreadsheet
Tiel Coder 35B A3B GGUF MTP · full

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 35,414 bytes · SHA-256 a2d78d342589
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sheet — Formula Spreadsheet</title>
<script>
/* ============================================================
TUNABLE CONSTANTS
============================================================ */
const CONFIG = {
COLS: 26, // A–Z
ROWS: 100,
CELL_WIDTH: 100,
CELL_HEIGHT: 22,
HEADER_WIDTH: 50,
HEADER_HEIGHT: 22,
STORAGE_KEY: 'sheet.app.v1',
COLORS: {
selection: '#1a73e8',
headerBg: '#f1f3f4',
headerText: '#202124',
gridline: '#e0e0e0',
cellBg: '#ffffff',
toolbarBg: '#ffffff',
error: '#c5221f',
accent: '#1a73e8'
}
};
</script>
</head>
<body>
<div id="app">
<div id="toolbar">
<button data-op="insertColL" title="Insert column to the left">+ Col ◀</button>
<button data-op="insertColR" title="Insert column to the right">+ Col ▶</button>
<button data-op="delCol" title="Delete selected column">✕ Col</button>
<span class="sep"></span>
<button data-op="insertRowU" title="Insert row above">+ Row ▲</button>
<button data-op="insertRowD" title="Insert row below">+ Row ▼</button>
<button data-op="delRow" title="Delete selected row">✕ Row</button>
<span class="sep"></span>
<button data-op="clear" title="Clear selected cell (Delete)">Clear</button>
</div>
<div id="formula-bar">
<span id="cell-name">A1</span>
<span class="fx">fx</span>
<input id="formula-input" type="text" autocomplete="off" spellcheck="false" placeholder="value or formula (=...)">
</div>
<div id="grid-scroll">
<table id="grid">
<colgroup></colgroup>
<thead></thead>
<tbody></tbody>
</table>
<input id="edit-input" type="text" autocomplete="off" spellcheck="false">
</div>
<div id="context-menu"></div>
</div>
<script>
/* ============================================================
1. CONFIG / HELPERS
============================================================ */
const COLS = CONFIG.COLS, ROWS = CONFIG.ROWS;
const C = CONFIG.COLORS;
function key(r, c) { return r + ',' + c; }
function splitKey(k) { const i = k.indexOf(','); return [+k.slice(0, i), +k.slice(i + 1)]; }
function inBounds(r, c) { return r >= 0 && r < ROWS && c >= 0 && c < COLS; }
function colToLetters(col) {
let s = '';
while (col >= 0) { const rem = col % 26; s = String.fromCharCode(65 + rem) + s; col = Math.floor(col / 26) - 1; }
return s;
}
function lettersToCol(letters) {
let c = 0;
for (let i = 0; i < letters.length; i++) c = c * 26 + (letters.charCodeAt(i) - 65);
return c;
}
function refName(r, c) { return colToLetters(c) + (r + 1); }
/* ============================================================
2. STYLES (injected, driven by CONFIG.COLORS)
============================================================ */
function injectStyles() {
const vars = {
'--cell-width': CONFIG.CELL_WIDTH + 'px',
'--cell-height': CONFIG.CELL_HEIGHT + 'px',
'--header-width': CONFIG.HEADER_WIDTH + 'px',
'--header-height': CONFIG.HEADER_HEIGHT + 'px',
'--sel': C.selection,
'--gridline': C.gridline,
'--header-bg': C.headerBg,
'--header-text': C.headerText,
'--error': C.error,
'--cell-bg': C.cellBg,
'--toolbar-bg': C.toolbarBg,
'--accent': C.accent
};
let css = ':root{';
for (const k in vars) css += k + ':' + vars[k] + ';';
css += '}' + `
*{box-sizing:border-box}
html,body{margin:0;height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;font-size:13px;color:#202124}
#app{display:flex;flex-direction:column;height:100vh;position:relative}
#toolbar{display:flex;gap:4px;padding:4px 6px;background:var(--toolbar-bg);border-bottom:1px solid var(--gridline);align-items:center;flex:0 0 auto}
#toolbar button{font:inherit;font-size:12px;padding:4px 9px;border:1px solid #dadce0;background:#fff;border-radius:4px;cursor:pointer;color:#202124}
#toolbar button:hover{background:#e8f0fe;border-color:#c9d7f2}
#toolbar button:active{background:#d3e3fb}
#toolbar .sep{width:1px;height:20px;background:#dadce0;margin:0 2px}
#formula-bar{display:flex;align-items:center;gap:6px;padding:3px 6px;background:#fff;border-bottom:1px solid var(--gridline);flex:0 0 auto}
#cell-name{font-weight:600;min-width:46px;text-align:center;padding:0 6px;border:1px solid #dadce0;border-radius:3px;background:#fafafa;color:#202124}
#formula-bar .fx{color:#9aa0a6;font-style:italic;font-weight:600}
#formula-input{flex:1;border:1px solid transparent;padding:4px 6px;font:inherit;border-radius:3px;background:transparent;color:#202124}
#formula-input:focus{border-color:var(--accent);background:#fff;outline:none}
#grid-scroll{position:relative;overflow:auto;flex:1 1 auto;background:#fff}
table#grid{border-collapse:collapse;table-layout:fixed}
th,td{border:1px solid var(--gridline);height:var(--cell-height);min-width:var(--cell-width);padding:0 4px;overflow:hidden;white-space:nowrap;position:relative;background:var(--cell-bg)}
thead th{position:sticky;top:0;z-index:2;background:var(--header-bg);color:var(--header-text);font-weight:600;text-align:center;height:var(--header-height);user-select:none}
thead th.corner{left:0;z-index:4;background:var(--header-bg)}
tbody th{position:sticky;left:0;z-index:1;background:var(--header-bg);color:var(--header-text);font-weight:600;text-align:center;height:var(--cell-height);width:var(--header-width);min-width:var(--header-width);user-select:none}
td{cursor:cell}
td.cell-num{text-align:right}
td.cell-text{text-align:left}
td.cell-error{color:var(--error);font-style:normal}
td.sel{box-shadow:inset 0 0 0 2px var(--sel)}
#edit-input{position:absolute;display:none;border:2px solid var(--accent);outline:none;padding:0 4px;font:inherit;background:#fff;z-index:5;color:#202124;height:100%;width:100%;margin:0}
#context-menu{position:absolute;display:none;background:#fff;border:1px solid #dadce0;box-shadow:0 2px 10px rgba(0,0,0,.18);z-index:20;min-width:170px;padding:4px;border-radius:4px}
#context-menu button{display:block;width:100%;text-align:left;border:none;background:none;padding:7px 10px;cursor:pointer;font:inherit;font-size:13px;border-radius:3px;color:#202124}
#context-menu button:hover{background:#e8f0fe}
#context-menu .cm-sep{height:1px;background:#dadce0;margin:4px 2px}
`;
const s = document.createElement('style');
s.textContent = css;
document.head.appendChild(s);
}
/* ============================================================
3. SHEET STATE
============================================================ */
let grid = []; // grid[r][c] = raw string ('' if empty)
let computed = []; // computed[r][c] = {t:'num'|'str'|'err'|'null', v}
let deps = []; // deps[r][c] = Set('r,c') this cell depends on
let dependents = []; // dependents[r][c] = Set('r,c') that depend on this cell
function initState() {
grid = Array.from({length: ROWS}, () => Array(COLS).fill(''));
computed = Array.from({length: ROWS}, () => Array.from({length: COLS}, () => ({t: 'null'})));
deps = Array.from({length: ROWS}, () => Array.from({length: COLS}, () => new Set()));
dependents = Array.from({length: ROWS}, () => Array.from({length: COLS}, () => new Set()));
}
/* ============================================================
4. TOKENIZER
============================================================ */
function tokenize(str) {
const toks = [];
let i = 0, n = str.length;
const isDigit = c => c >= '0' && c <= '9';
const isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
while (i < n) {
const ch = str[i];
if (ch === ' ' || ch === '\t') { i++; continue; }
if (isDigit(ch)) {
let j = i;
while (j < n && isDigit(str[j])) j++;
if (j < n && str[j] === '.') { j++; while (j < n && isDigit(str[j])) j++; }
toks.push({type: 'num', value: parseFloat(str.slice(i, j))});
i = j; continue;
}
if (ch === '"') {
let j = i + 1, val = '';
while (j < n && str[j] !== '"') { if (str[j] === '"' && str[j + 1] === '"') { val += '"'; j += 2; } else { val += str[j]; j++; } }
if (j >= n) throw new Error('unterminated string');
toks.push({type: 'str', value: val});
i = j + 1; continue;
}
if (ch === '#') {
let j = i;
while (j < n && /[A-Za-z0-9/#!]/.test(str[j])) j++;
toks.push({type: 'err', value: str.slice(i, j)});
i = j; continue;
}
if (isLetter(ch)) {
let j = i;
while (j < n && isLetter(str[j])) j++;
if (j < n && isDigit(str[j])) {
let dj = j; while (dj < n && isDigit(str[dj])) dj++;
const letters = str.slice(i, j);
const row = parseInt(str.slice(j, dj), 10) - 1;
toks.push({type: 'ref', col: lettersToCol(letters), row: row});
i = dj;
} else {
toks.push({type: 'ident', value: str.slice(i, j)});
i = j;
}
continue;
}
if ('+-*/(),:='.indexOf(ch) !== -1) { toks.push({type: 'op', value: ch}); i++; continue; }
throw new Error('unexpected char: ' + ch);
}
toks.push({type: 'eof'});
return toks;
}
/* ============================================================
5. RECURSIVE-DESCENT PARSER -> AST
============================================================ */
function parseFormula(raw) {
const toks = tokenize(raw);
let pos = 0;
const peek = () => toks[pos];
const next = () => toks[pos++];
const expectOp = v => {
if (peek().type === 'op' && peek().value === v) { pos++; return; }
throw new Error('expected ' + v);
};
const parseExpr = () => {
let node = parseTerm();
while (peek().type === 'op' && (peek().value === '+' || peek().value === '-')) {
const op = next().value;
node = {type: 'binop', op, left: node, right: parseTerm()};
}
return node;
};
const parseTerm = () => {
let node = parseFactor();
while (peek().type === 'op' && (peek().value === '*' || peek().value === '/')) {
const op = next().value;
node = {type: 'binop', op, left: node, right: parseFactor()};
}
return node;
};
const parseFactor = () => {
if (peek().type === 'op' && (peek().value === '+' || peek().value === '-')) {
const op = next().value;
return {type: 'unary', op, operand: parseFactor()};
}
return parsePrimary();
};
const parsePrimary = () => {
const t = peek();
if (t.type === 'op' && t.value === '(') {
next();
const node = parseExpr();
expectOp(')');
return node;
}
if (t.type === 'num') { next(); return {type: 'num', v: t.value}; }
if (t.type === 'str') { next(); return {type: 'str', v: t.value}; }
if (t.type === 'err') { next(); return {type: 'err', v: t.value}; }
if (t.type === 'ident') {
const name = t.value.toUpperCase();
next();
expectOp('(');
const args = [];
if (!(peek().type === 'op' && peek().value === ')')) {
args.push(parseExpr());
while (peek().type === 'op' && peek().value === ',') { next(); args.push(parseExpr()); }
}
expectOp(')');
return {type: 'func', name, args};
}
if (t.type === 'ref') {
next();
if (peek().type === 'op' && peek().value === ':') {
next();
const right = parsePrimary();
if (right.type !== 'ref') throw new Error('range end must be a reference');
const c1 = {row: Math.min(t.row, right.row), col: Math.min(t.col, right.col)};
const c2 = {row: Math.max(t.row, right.row), col: Math.max(t.col, right.col)};
return {type: 'range', c1, c2};
}
return {type: 'ref', row: t.row, col: t.col};
}
throw new Error('unexpected token');
};
if (peek().type !== 'op' || peek().value !== '=') throw new Error('formula must start with =');
next(); // consume '='
const node = parseExpr();
if (peek().type !== 'eof') throw new Error('trailing content');
return node;
}
/* ============================================================
6. DEPENDENCY COLLECTION (AST walk)
============================================================ */
function walkRefs(node, out) {
switch (node.type) {
case 'ref':
if (inBounds(node.row, node.col)) out.add(key(node.row, node.col));
break;
case 'range': {
const {c1, c2} = node;
for (let r = c1.row; r <= c2.row; r++)
for (let col = c1.col; col <= c2.col; col++)
if (inBounds(r, col)) out.add(key(r, col));
break;
}
case 'binop': walkRefs(node.left, out); walkRefs(node.right, out); break;
case 'unary': walkRefs(node.operand, out); break;
case 'func': node.args.forEach(a => walkRefs(a, out)); break;
}
}
function computeDepsFromRaw(raw) {
const d = new Set();
if (raw && raw[0] === '=') {
try { walkRefs(parseFormula(raw), d); } catch (e) { /* parse error => no deps */ }
}
return d;
}
/* ============================================================
7. DEPENDENCY GRAPH UPDATE
============================================================ */
function applyDeps(r, c, newDeps) {
const oldDeps = deps[r][c];
if (oldDeps) for (const k of oldDeps) { const [dr, dc] = splitKey(k); dependents[dr][dc].delete(key(r, c)); }
const nd = new Set(newDeps);
for (const k of nd) { const [dr, dc] = splitKey(k); dependents[dr][dc].add(key(r, c)); }
deps[r][c] = nd;
}
/* ============================================================
8. VALUE EVALUATION
============================================================ */
// CellVal: {t:'num'|'str'|'err'|'null'|'arr', v}
function toScalar(val) {
switch (val.t) {
case 'num': return val.v;
case 'null': return 0;
case 'str': { const n = Number(val.v); return isNaN(n) ? '#ERR!' : n; }
case 'err': return val.v;
case 'arr': {
let sum = 0;
for (const x of val.v) { const s = toScalar(x); if (typeof s === 'string') return s; sum += s; }
return sum;
}
}
}
function collectNumbers(args) {
const vals = [], err = {v: null};
for (const val of args) {
if (err.v) break;
switch (val.t) {
case 'err': err.v = val.v; break;
case 'null': break;
case 'num': vals.push(val.v); break;
case 'str': { const n = Number(val.v); if (!isNaN(n)) vals.push(n); break; }
case 'arr': {
const sub = collectNumbers(val.v);
if (sub.err.v) { err.v = sub.err.v; } else for (const x of sub.vals) vals.push(x);
break;
}
}
}
return {vals, err};
}
function evalFunc(name, args) {
const {vals, err} = collectNumbers(args);
if (err.v) return {t: 'err', v: err.v};
switch (name) {
case 'SUM': return {t: 'num', v: vals.reduce((a, b) => a + b, 0)};
case 'AVG':
case 'AVERAGE':
return vals.length ? {t: 'num', v: vals.reduce((a, b) => a + b, 0) / vals.length}
: {t: 'err', v: '#DIV/0!'};
case 'MIN': return vals.length ? {t: 'num', v: Math.min(...vals)} : {t: 'num', v: 0};
case 'MAX': return vals.length ? {t: 'num', v: Math.max(...vals)} : {t: 'num', v: 0};
case 'COUNT': return {t: 'num', v: vals.length};
default: return {t: 'err', v: '#ERR!'};
}
}
function evalAST(node) {
switch (node.type) {
case 'num': return {t: 'num', v: node.v};
case 'str': return {t: 'str', v: node.v};
case 'err': return {t: 'err', v: node.v};
case 'ref':
if (!inBounds(node.row, node.col)) return {t: 'err', v: '#REF!'};
return computed[node.row][node.col];
case 'range': {
const arr = [];
const {c1, c2} = node;
for (let r = c1.row; r <= c2.row; r++)
for (let col = c1.col; col <= c2.col; col++)
arr.push(inBounds(r, col) ? computed[r][col] : {t: 'err', v: '#REF!'});
return {t: 'arr', v: arr};
}
case 'unary': {
const s = toScalar(evalAST(node.operand));
if (typeof s === 'string') return {t: 'err', v: s};
return {t: 'num', v: node.op === '-' ? -s : s};
}
case 'binop': {
const a = toScalar(evalAST(node.left));
const b = toScalar(evalAST(node.right));
if (typeof a === 'string') return {t: 'err', v: a};
if (typeof b === 'string') return {t: 'err', v: b};
switch (node.op) {
case '+': return {t: 'num', v: a + b};
case '-': return {t: 'num', v: a - b};
case '*': return {t: 'num', v: a * b};
case '/': return b === 0 ? {t: 'err', v: '#DIV/0!'} : {t: 'num', v: a / b};
}
return {t: 'err', v: '#ERR!'};
}
case 'func': return evalFunc(node.name, node.args.map(evalAST));
}
return {t: 'err', v: '#ERR!'};
}
function normalize(val) {
switch (val.t) {
case 'arr': {
const s = toScalar(val);
return typeof s === 'string' ? {t: 'err', v: s} : {t: 'num', v: s};
}
default: return val;
}
}
function evalCell(r, c) {
const raw = grid[r][c];
if (!raw || raw.trim() === '') { applyDeps(r, c, new Set()); return {t: 'null'}; }
if (raw[0] !== '=') {
applyDeps(r, c, new Set());
const t = raw.trim();
if (t === '') return {t: 'null'};
const n = Number(t);
return !isNaN(n) && isFinite(n) ? {t: 'num', v: n} : {t: 'str', v: raw};
}
let ast;
try { ast = parseFormula(raw); }
catch (e) { applyDeps(r, c, new Set()); return {t: 'err', v: '#ERR!'}; }
const d = new Set();
walkRefs(ast, d);
applyDeps(r, c, d);
return normalize(evalAST(ast));
}
/* ============================================================
9. RECALCULATION (topological, cycle-aware)
============================================================ */
function expand(startKeys) {
const affected = new Set();
const queue = [];
for (const k of startKeys) if (!affected.has(k)) { affected.add(k); queue.push(k); }
while (queue.length) {
const k = queue.shift();
const [r, c] = splitKey(k);
for (const succ of dependents[r][c]) if (!affected.has(succ)) { affected.add(succ); queue.push(succ); }
}
return affected;
}
function topoSort(affected) {
const indeg = new Map();
for (const k of affected) indeg.set(k, 0);
for (const k of affected) {
const [r, c] = splitKey(k);
let cnt = 0;
for (const dep of deps[r][c]) if (affected.has(dep)) cnt++;
indeg.set(k, cnt);
}
const queue = [];
for (const k of affected) if (indeg.get(k) === 0) queue.push(k);
const order = [];
const processed = new Set();
while (queue.length) {
const k = queue.shift();
order.push(k); processed.add(k);
const [r, c] = splitKey(k);
for (const succ of dependents[r][c]) {
if (!affected.has(succ)) continue;
const nv = indeg.get(succ) - 1;
indeg.set(succ, nv);
if (nv === 0) queue.push(succ);
}
}
const cycle = [];
for (const k of affected) if (!processed.has(k)) cycle.push(k);
return {order, cycle};
}
function recompute(startKeys) {
// Update dependency edges for the changed cells first, so the dependent BFS is correct.
for (const k of startKeys) {
const [r, c] = splitKey(k);
const d = computeDepsFromRaw(grid[r][c]);
applyDeps(r, c, d);
}
const affected = expand(startKeys);
const {order, cycle} = topoSort(affected);
for (const k of order) { const [r, c] = splitKey(k); computed[r][c] = evalCell(r, c); }
for (const k of cycle) { const [r, c] = splitKey(k); computed[r][c] = {t: 'err', v: '#CYCLE!'}; }
for (const k of affected) { const [r, c] = splitKey(k); updateCellDisplay(r, c); }
}
function evaluateAll() {
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
if (grid[r][c] && grid[r][c][0] === '=') {
try { const d = computeDepsFromRaw(grid[r][c]); applyDeps(r, c, d); } catch (e) {}
}
const all = new Set();
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) all.add(key(r, c));
const {order, cycle} = topoSort(all);
for (const k of order) { const [r, c] = splitKey(k); computed[r][c] = evalCell(r, c); }
for (const k of cycle) { const [r, c] = splitKey(k); computed[r][c] = {t: 'err', v: '#CYCLE!'}; }
for (const k of all) { const [r, c] = splitKey(k); updateCellDisplay(r, c); }
}
/* ============================================================
10. FORMULA REWRITING (insert/delete row & column)
============================================================ */
function rewriteFormula(raw, transform) {
if (!raw || raw[0] !== '=') return raw;
return raw.replace(/"([^"]*)"|([A-Za-z]+\d+)/g, (m, strLit, ref) => {
if (ref === undefined) return m;
const letters = ref.match(/[A-Za-z]+/)[0];
const digits = ref.match(/\d+/)[0];
const col = lettersToCol(letters);
const row = parseInt(digits, 10) - 1;
const res = transform(col, row);
if (res == null) return '#REF!';
return colToLetters(res.col) + (res.row + 1);
});
}
const insertColT = idx => (col, row) => (col >= idx ? {col: col + 1, row} : {col, row});
const delColT = idx => (col, row) => (col === idx ? null : (col > idx ? {col: col - 1, row} : {col, row}));
const insertRowT = idx => (col, row) => (row >= idx ? {col, row: row + 1} : {col, row});
const delRowT = idx => (col, row) => (row === idx ? null : (row > idx ? {col, row: row - 1} : {col, row}));
function rewriteAll(transform) {
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++) grid[r][c] = rewriteFormula(grid[r][c], transform);
}
function insertColAt(index) {
for (let r = 0; r < ROWS; r++) { grid[r].splice(index, 0, ''); grid[r].pop(); }
rewriteAll(insertColT(index));
evaluateAll();
}
function deleteColAt(index) {
for (let r = 0; r < ROWS; r++) { grid[r].splice(index, 1); grid[r].push(''); }
rewriteAll(delColT(index));
evaluateAll();
}
function insertRowAt(index) {
grid.splice(index, 0, Array(COLS).fill(''));
grid.pop();
rewriteAll(insertRowT(index));
evaluateAll();
}
function deleteRowAt(index) {
grid.splice(index, 1);
grid.push(Array(COLS).fill(''));
rewriteAll(delRowT(index));
evaluateAll();
}
/* ============================================================
11. RENDERING
============================================================ */
let tdGrid = [];
let scroll, cellName, formulaInput, editInput, contextMenu, appEl;
function displayString(v) {
if (v.t === 'null') return '';
if (v.t === 'num') return formatNumber(v.v);
return v.v;
}
function formatNumber(n) {
if (Number.isInteger(n)) return String(n);
return String(parseFloat(n.toPrecision(12)));
}
function updateCellDisplay(r, c) {
const td = tdGrid[r][c];
if (!td) return;
const v = computed[r][c];
td.textContent = displayString(v);
td.classList.remove('cell-num', 'cell-text', 'cell-error');
if (v.t === 'err') td.classList.add('cell-error');
else if (v.t === 'num') td.classList.add('cell-num');
else if (v.t === 'str') td.classList.add('cell-text');
}
function buildGridDOM() {
const table = document.getElementById('grid');
const cg = table.querySelector('colgroup');
let colHtml = '<col style="width:' + CONFIG.HEADER_WIDTH + 'px">';
for (let c = 0; c < COLS; c++) colHtml += '<col style="width:' + CONFIG.CELL_WIDTH + 'px">';
cg.innerHTML = colHtml;
const thead = table.querySelector('thead');
let headTr = '<tr><th class="corner"> </th>';
for (let c = 0; c < COLS; c++) headTr += '<th data-col="' + c + '">' + colToLetters(c) + '</th>';
headTr += '</tr>';
thead.innerHTML = headTr;
const tbody = table.querySelector('tbody');
tdGrid = Array.from({length: ROWS}, () => Array(COLS));
let rowsHtml = '';
for (let r = 0; r < ROWS; r++) {
rowsHtml += '<tr><th data-row="' + r + '">' + (r + 1) + '</th>';
for (let c = 0; c < COLS; c++) rowsHtml += '<td data-r="' + r + '" data-c="' + c + '" data-key="' + key(r, c) + '"> </td>';
rowsHtml += '</tr>';
}
tbody.innerHTML = rowsHtml;
// Populate tdGrid references
const cells = table.querySelectorAll('td[data-r]');
cells.forEach(td => { const [r, c] = [+td.dataset.r, +td.dataset.c]; tdGrid[r][c] = td; });
}
/* ============================================================
12. SELECTION & FORMULA BAR
============================================================ */
let selR = 0, selC = 0;
function updateFormulaBar() {
if (document.activeElement === formulaInput) return;
formulaInput.value = grid[selR][selC] || '';
}
function updateCornerLabel() { cellName.textContent = refName(selR, selC); }
function selectCell(r, c) {
if (selR != null && selC != null && tdGrid[selR] && tdGrid[selR][selC])
tdGrid[selR][selC].classList.remove('sel');
selR = r; selC = c;
tdGrid[r][c].classList.add('sel');
updateCornerLabel();
updateFormulaBar();
ensureVisible(r, c);
}
function ensureVisible(r, c) {
const td = tdGrid[r][c];
const rect = td.getBoundingClientRect();
const sr = scroll.getBoundingClientRect();
if (rect.left < sr.left) scroll.scrollLeft += rect.left - sr.left;
else if (rect.right > sr.right) scroll.scrollLeft += rect.right - sr.right;
if (rect.top < sr.top) scroll.scrollTop += rect.top - sr.top;
else if (rect.bottom > sr.bottom) scroll.scrollTop += rect.bottom - sr.bottom;
}
/* ============================================================
13. EDITING (grid overlay + formula bar)
============================================================ */
let editing = false; // true when grid overlay is active
let editOriginal = null;
function setRaw(r, c, val) {
grid[r][c] = val;
recompute([key(r, c)]);
scheduleSave();
}
function positionGridInput(r, c) {
const td = tdGrid[r][c];
const cr = td.getBoundingClientRect();
const sr = scroll.getBoundingClientRect();
editInput.style.left = (cr.left - sr.left) + 'px';
editInput.style.top = (cr.top - sr.top) + 'px';
editInput.style.width = cr.width + 'px';
editInput.style.height = cr.height + 'px';
editInput.style.display = 'flex';
}
function startEdit(mode, firstChar) {
commitGrid();
const r = selR, c = selC;
editOriginal = grid[r][c];
positionGridInput(r, c);
if (mode === 'type') {
editInput.value = firstChar || '';
if (editInput.value) setRaw(r, c, editInput.value);
editInput.focus();
editInput.setSelectionRange(editInput.value.length, editInput.value.length);
} else {
editInput.value = grid[r][c];
editInput.focus();
editInput.setSelectionRange(editInput.value.length, editInput.value.length);
}
editing = true;
formulaInput.value = editInput.value;
}
function commitGrid() {
if (!editing) return;
editing = false;
editInput.style.display = 'none';
setRaw(selR, selC, editInput.value);
}
function cancelEdit() {
if (!editing) return;
editing = false;
editInput.style.display = 'none';
grid[selR][selC] = editOriginal;
recompute([key(selR, selC)]);
}
function clearCell() {
setRaw(selR, selC, '');
}
/* ============================================================
14. MOVEMENT
============================================================ */
function moveDown() { selectCell(Math.min(selR + 1, ROWS - 1), selC); }
function moveUp() { selectCell(Math.max(selR - 1, 0), selC); }
function moveRight() { selectCell(selR, Math.min(selC + 1, COLS - 1)); }
function moveLeft() { selectCell(selR, Math.max(selC - 1, 0)); }
/* ============================================================
15. LOCALSTORAGE
============================================================ */
let saveTimer = null;
function scheduleSave() {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(persist, 250);
}
function persist() {
const obj = {};
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
if (grid[r][c] && grid[r][c] !== '') obj[key(r, c)] = grid[r][c];
try { localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(obj)); } catch (e) {}
}
function loadFromStorage() {
let s;
try { s = localStorage.getItem(CONFIG.STORAGE_KEY); } catch (e) { s = null; }
if (!s) return;
let obj;
try { obj = JSON.parse(s); } catch (e) { return; }
for (const k in obj) {
const [r, c] = splitKey(k);
if (inBounds(r, c)) grid[r][c] = obj[k];
}
}
/* ============================================================
16. CONTEXT MENU
============================================================ */
function showContextMenu(x, y, items) {
contextMenu.innerHTML = '';
for (const it of items) {
if (it === '-') { const s = document.createElement('div'); s.className = 'cm-sep'; contextMenu.appendChild(s); continue; }
const b = document.createElement('button');
b.textContent = it.label;
b.onclick = () => { hideContextMenu(); it.action(); };
contextMenu.appendChild(b);
}
const ar = appEl.getBoundingClientRect();
contextMenu.style.left = (x - ar.left) + 'px';
contextMenu.style.top = (y - ar.top) + 'px';
contextMenu.style.display = 'block';
}
function hideContextMenu() { contextMenu.style.display = 'none'; }
function cellContextMenu(e, r, c) {
showContextMenu(e.clientX, e.clientY, [
{label: 'Insert Row Above', action: () => insertRowAt(r)},
{label: 'Insert Row Below', action: () => insertRowAt(r + 1)},
{label: 'Delete Row', action: () => deleteRowAt(r)},
'-',
{label: 'Insert Column Left', action: () => insertColAt(c)},
{label: 'Insert Column Right', action: () => insertColAt(c + 1)},
{label: 'Delete Column', action: () => deleteColAt(c)},
'-',
{label: 'Clear Cell', action: () => clearCell()}
]);
}
function columnContextMenu(e, col) {
showContextMenu(e.clientX, e.clientY, [
{label: 'Insert Column Left', action: () => insertColAt(col)},
{label: 'Insert Column Right', action: () => insertColAt(col + 1)},
{label: 'Delete Column', action: () => deleteColAt(col)}
]);
}
function rowContextMenu(e, row) {
showContextMenu(e.clientX, e.clientY, [
{label: 'Insert Row Above', action: () => insertRowAt(row)},
{label: 'Insert Row Below', action: () => insertRowAt(row + 1)},
{label: 'Delete Row', action: () => deleteRowAt(row)}
]);
}
/* ============================================================
17. EVENT WIRING
============================================================ */
function wireEvents() {
// Grid click -> select
scroll.addEventListener('click', e => {
const td = e.target.closest('td[data-r]');
if (td) selectCell(+td.dataset.r, +td.dataset.c);
});
// Double-click -> edit
scroll.addEventListener('dblclick', e => {
const td = e.target.closest('td[data-r]');
if (td) { e.preventDefault(); startEdit('dblclick'); }
});
// Right-click -> context menu
scroll.addEventListener('contextmenu', e => {
e.preventDefault();
const td = e.target.closest('td[data-r]');
if (td) { cellContextMenu(e, +td.dataset.r, +td.dataset.c); return; }
const ch = e.target.closest('th[data-col]');
if (ch) { columnContextMenu(e, +ch.dataset.col); return; }
const rh = e.target.closest('th[data-row]');
if (rh) { rowContextMenu(e, +rh.dataset.row); return; }
});
// Toolbar
document.getElementById('toolbar').addEventListener('click', e => {
const btn = e.target.closest('button[data-op]');
if (!btn) return;
const op = btn.dataset.op;
switch (op) {
case 'insertColL': insertColAt(selC); break;
case 'insertColR': insertColAt(selC + 1); break;
case 'delCol': deleteColAt(selC); break;
case 'insertRowU': insertRowAt(selR); break;
case 'insertRowD': insertRowAt(selR + 1); break;
case 'delRow': deleteRowAt(selR); break;
case 'clear': clearCell(); break;
}
});
// Grid edit overlay
editInput.addEventListener('input', () => {
setRaw(selR, selC, editInput.value);
formulaInput.value = editInput.value;
});
editInput.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); commitGrid(); moveDown(); }
else if (e.key === 'Tab') { e.preventDefault(); commitGrid(); e.shiftKey ? moveLeft() : moveRight(); }
else if (e.key === 'Escape') { e.preventDefault(); cancelEdit(); }
});
editInput.addEventListener('blur', () => commitGrid());
// Formula bar
formulaInput.addEventListener('focus', () => { if (editing) commitGrid(); });
formulaInput.addEventListener('input', () => {
if (editing) editInput.value = formulaInput.value;
setRaw(selR, selC, formulaInput.value);
});
formulaInput.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); commitGrid(); moveDown(); }
else if (e.key === 'Tab') { e.preventDefault(); commitGrid(); e.shiftKey ? moveLeft() : moveRight(); }
else if (e.key === 'Escape') { e.preventDefault(); grid[selR][selC] = editOriginal; commitGrid(); }
});
// Global keyboard (when not editing in the overlay)
window.addEventListener('keydown', e => {
if (editing) return;
const key = e.key;
if (key === 'ArrowUp') { e.preventDefault(); moveUp(); }
else if (key === 'ArrowDown') { e.preventDefault(); moveDown(); }
else if (key === 'ArrowLeft') { e.preventDefault(); moveLeft(); }
else if (key === 'ArrowRight') { e.preventDefault(); moveRight(); }
else if (key === 'Enter') { e.preventDefault(); moveDown(); }
else if (key === 'Tab') { e.preventDefault(); e.shiftKey ? moveLeft() : moveRight(); }
else if (key === 'Delete') { clearCell(); }
else if (key === 'F2') { e.preventDefault(); startEdit('dblclick'); }
else if (key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
startEdit('type', key);
}
});
// Scroll: keep edit overlay on the active cell
scroll.addEventListener('scroll', () => { if (editing) positionGridInput(selR, selC); });
// Hide context menu on outside interaction
document.addEventListener('click', e => { if (!contextMenu.contains(e.target)) hideContextMenu(); });
window.addEventListener('beforeunload', () => { if (saveTimer) clearTimeout(saveTimer); persist(); });
}
/* ============================================================
18. INIT
============================================================ */
function init() {
injectStyles();
scroll = document.getElementById('grid-scroll');
cellName = document.getElementById('cell-name');
formulaInput = document.getElementById('formula-input');
editInput = document.getElementById('edit-input');
contextMenu = document.getElementById('context-menu');
appEl = document.getElementById('app');
initState();
buildGridDOM();
wireEvents();
loadFromStorage();
evaluateAll();
selectCell(0, 0);
scheduleSave();
}
document.addEventListener('DOMContentLoaded', init);
/* Expose internals for programmatic verification */
window.__sheet = {
recompute, evaluateAll, evalCell, setRaw, rewriteFormula,
delRowT, delColT, insertRowT, insertColT, CONFIG,
insertColAt, deleteColAt, insertRowAt, deleteRowAt,
persist, loadFromStorage,
get grid() { return grid; },
get computed() { return computed; },
get deps() { return deps; },
get dependents() { return dependents; }
};
</script>
</body>
</html>
<!-- agent-meta {"model":"peculiar-ragdoll/Tiel-Coder-35B-A3B-GGUF-MTP","provider":"brigid","persona":"full","sessionId":"aaf0f4ad-6026-4ad4-852e-d09f29436f2f","tokensIn":3933387,"tokensOut":80691,"tokensTotal":4014078,"cost":0,"turns":83,"toolCalls":82,"failedToolCalls":0,"timestamp":"2026-09-04T19:42:27.653Z"} -->