Spreadsheet
Deepseek v4.1 Flash · 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 38,725 bytes · SHA-256 84016e11533d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sheet — Spreadsheet</title>
<style>
/* ==========================================================================
THEME + LAYOUT
========================================================================== */
:root{
--gridline:#e8eaed;
--hdr-bg:#f8f9fa;
--hdr-fg:#5f6368;
--hdr-border:#dadce0;
--sel:#1a73e8;
--sel-tint:#e8f0fe;
--err:#d93025;
--fg:#202124;
--col-w:96px;
--row-h:26px;
--hdr-w:48px;
--hdr-h:26px;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0}
body{
font:13px/1.25 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
color:var(--fg);background:#fff;overflow:hidden;
-webkit-font-smoothing:antialiased;
}
#app{display:flex;flex-direction:column;height:100vh;min-height:0}
/* ---------- toolbar ---------- */
.toolbar{
flex:0 0 auto;display:flex;align-items:center;gap:6px;
padding:9px 12px;border-bottom:1px solid var(--hdr-border);background:#fff;
}
.brand{display:flex;align-items:center;gap:8px;font-weight:600;font-size:14px;margin-right:12px;letter-spacing:.1px}
.logo{width:16px;height:16px;border-radius:3px;background:linear-gradient(135deg,#1a73e8,#34a853);box-shadow:inset 0 0 0 1px rgba(0,0,0,.06)}
.sep{width:1px;height:20px;background:var(--hdr-border);margin:0 6px}
button.tool{
font:12px/1 inherit;color:#3c4043;background:#fff;border:1px solid var(--hdr-border);
border-radius:4px;padding:6px 10px;cursor:pointer;white-space:nowrap;
}
button.tool:hover{background:#f1f3f4}
button.tool:active{background:#e8eaed}
button.tool.danger:hover{background:#fce8e6;border-color:#f5c6c2;color:#c5221f}
.spacer{flex:1}
.hint{font-size:11px;color:#80868b}
/* ---------- formula bar ---------- */
.fbar{
flex:0 0 auto;display:flex;align-items:stretch;height:32px;
border-bottom:1px solid var(--hdr-border);background:#fff;
}
#cellRef{
width:74px;flex:0 0 74px;display:flex;align-items:center;justify-content:center;
border-right:1px solid var(--hdr-border);font-size:12px;font-weight:600;color:#3c4043;
letter-spacing:.3px;background:#fff;
}
.fx{
display:flex;align-items:center;padding:0 11px;color:#80868b;font-style:italic;
font-family:Georgia,"Times New Roman",serif;font-size:14px;border-right:1px solid var(--hdr-border);
user-select:none;
}
#finput{
flex:1;border:0;outline:0;padding:0 12px;background:#fff;color:var(--fg);
font:13px/1 inherit;
}
#finput:focus{background:#fbfcfe}
/* ---------- grid ---------- */
.grid-wrap{
flex:1 1 auto;min-height:0;overflow:auto;background:#fff;position:relative;
border-bottom:1px solid var(--hdr-border);
}
table#grid{border-collapse:separate;border-spacing:0;table-layout:fixed}
table#grid th,table#grid td{
padding:0 6px;margin:0;height:var(--row-h);line-height:calc(var(--row-h) - 1px);
font-weight:400;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:clip;
border-right:1px solid var(--gridline);border-bottom:1px solid var(--gridline);
background:#fff;text-align:left;vertical-align:middle;
}
thead th{
position:sticky;top:0;z-index:3;height:var(--hdr-h);line-height:calc(var(--hdr-h) - 1px);
background:var(--hdr-bg);color:var(--hdr-fg);font-size:11px;font-weight:500;
text-align:center;text-transform:uppercase;letter-spacing:.4px;
border-bottom:1px solid var(--hdr-border);user-select:none;cursor:default;
border-right:1px solid var(--hdr-border);
}
tbody th{
position:sticky;left:0;z-index:2;width:var(--hdr-w);min-width:var(--hdr-w);max-width:var(--hdr-w);
background:var(--hdr-bg);color:var(--hdr-fg);font-size:11px;font-weight:500;text-align:center;
border-right:1px solid var(--hdr-border);user-select:none;
}
thead th.corner{left:0;z-index:4;width:var(--hdr-w);min-width:var(--hdr-w)}
thead th.colh{width:var(--col-w);min-width:var(--col-w)}
thead th.hl{background:var(--sel-tint);color:#174ea6}
tbody th.hl{background:var(--sel-tint);color:#174ea6}
td.cell{cursor:cell;user-select:none}
td.cell.num{text-align:right;font-variant-numeric:tabular-nums}
td.cell.err{color:var(--err);text-align:right;font-weight:500}
td.cell.sel{box-shadow:inset 0 0 0 2px var(--sel);background:#fff}
td.cell.editing{color:transparent}
/* corner handle on active cell */
td.cell.sel::after{
content:"";position:absolute;right:0;bottom:0;width:2px;height:2px;background:var(--sel);
}
td.cell{position:relative}
/* ---------- in-cell editor ---------- */
#editor{
position:fixed;display:none;z-index:60;margin:0;
border:2px solid var(--sel);outline:0;background:#fff;color:var(--fg);
font:13px/1.25 inherit;padding:0 7px;box-shadow:0 1px 3px rgba(60,64,67,.28);
border-radius:1px;
}
/* ---------- context menu ---------- */
.ctx{
position:fixed;z-index:80;display:none;min-width:186px;background:#fff;
border:1px solid #dadce0;border-radius:6px;box-shadow:0 4px 14px rgba(60,64,67,.22);
padding:5px;font-size:13px;
}
.ctx button{
display:block;width:100%;text-align:left;background:none;border:0;font:13px/1 inherit;
color:#3c4043;padding:8px 10px;border-radius:4px;cursor:pointer;
}
.ctx button:hover{background:#f1f3f4}
.ctx hr{border:0;border-top:1px solid var(--hdr-border);margin:4px 2px}
/* ---------- status ---------- */
.status{
flex:0 0 auto;display:flex;align-items:center;gap:14px;height:24px;
padding:0 12px;background:var(--hdr-bg);font-size:11px;color:#80868b;
}
.status b{color:#5f6368;font-weight:600}
</style>
</head>
<body>
<div id="app">
<header class="toolbar">
<div class="brand"><span class="logo"></span>Sheet</div>
<button class="tool" data-act="insRow">Insert row</button>
<button class="tool" data-act="delRow">Delete row</button>
<span class="sep"></span>
<button class="tool" data-act="insCol">Insert column</button>
<button class="tool" data-act="delCol">Delete column</button>
<span class="sep"></span>
<button class="tool danger" data-act="clear">Clear cell</button>
<span class="spacer"></span>
<span class="hint">Right-click a row / column header for more</span>
</header>
<div class="fbar">
<span id="cellRef">A1</span>
<span class="fx">fx</span>
<input id="finput" spellcheck="false" autocomplete="off" autocapitalize="off">
</div>
<div class="grid-wrap" id="gridWrap" tabindex="-1">
<table id="grid"></table>
</div>
<div class="status">
<span id="statSel">A1</span>
<span id="statVal"></span>
<span class="spacer"></span>
<span>26 × 100 · formulas: <b>=SUM(A1:A5)</b> · <b>=B1*2+1</b></span>
</div>
</div>
<input id="editor" spellcheck="false" autocomplete="off">
<div class="ctx" id="ctx"></div>
<script>
/* ==========================================================================
1. CONFIG — tunable constants
========================================================================== */
const CONFIG = {
ROWS: 100,
COLS: 26,
COL_WIDTH: 96,
ROW_HEIGHT: 26,
HEADER_WIDTH: 48,
HEADER_HEIGHT: 26,
STORAGE_KEY: 'spreadsheet.artifact.v1',
NUM_PRECISION: 12, // significant digits kept for display
SAVE_DEBOUNCE: 250 // ms
};
const ROWS = CONFIG.ROWS, COLS = CONFIG.COLS;
/* ==========================================================================
2. SMALL UTILITIES / ERROR VALUES
========================================================================== */
const ERR_REF = { err: '#REF!' };
const ERR_DIV = { err: '#DIV/0!' };
const ERR_PARSE = { err: '#ERR!' };
const ERR_CYCLE = { err: '#CYCLE!' };
const ERRS = { '#REF!': ERR_REF, '#DIV/0!': ERR_DIV, '#ERR!': ERR_PARSE, '#CYCLE!': ERR_CYCLE };
const BLANK = '';
const isErr = v => !!v && typeof v === 'object' && typeof v.err === 'string';
/** stable numeric key for a (row, col) pair */
const K = (r, c) => r * 10000 + c;
const KR = k => Math.floor(k / 10000);
const KC = k => k % 10000;
function colName(c) {
let s = '', n = c + 1;
while (n > 0) { const m = (n - 1) % 26; s = String.fromCharCode(65 + m) + s; n = Math.floor((n - 1) / 26); }
return s;
}
const REF_RE = /^(\$?)([A-Za-z]+)(\$?)(\d+)$/;
/** "A1" -> {col,row} (0-based). Returns null when not a reference. */
function parseRef(str) {
const m = REF_RE.exec(str);
if (!m) return null;
const L = m[2].toUpperCase();
let col = 0;
for (let i = 0; i < L.length; i++) {
const code = L.charCodeAt(i);
if (code < 65 || code > 90) return null;
col = col * 26 + (code - 64);
}
const row = parseInt(m[4], 10) - 1;
if (!isFinite(col) || !isFinite(row)) return null;
return { col: col - 1, row: row, absC: !!m[1], absR: !!m[3] };
}
/** rebuild reference text keeping $ anchors: "$A$1" style */
function refText(orig, col, row) {
const m = REF_RE.exec(orig);
return (m ? m[1] : '') + colName(col) + (m ? m[3] : '') + (row + 1);
}
class FormulaError extends Error {
constructor(code) { super(code); this.code = code || '#ERR!'; }
}
/* ==========================================================================
3. FORMULA ENGINE — tokenizer
========================================================================== */
const OPS = '+-*/^(),:%<>=&';
/** tokenize(source without leading '=') -> [{t,v,raw}] */
function tokenize(src) {
const tokens = [];
let i = 0;
while (i < src.length) {
const ch = src[i];
const start = i;
if (ch === ' ' || ch === '\t' || ch === '\n') { i++; tokens.push({ t: 'ws', v: ch, raw: ch }); continue; }
if (ch >= '0' && ch <= '9' || (ch === '.' && /[0-9]/.test(src[i + 1] || ''))) {
let j = i;
while (j < src.length && /[0-9.]/.test(src[j])) j++;
if (/[eE][+-]?[0-9]/.test(src.slice(j, j + 3))) { j += 1; if (src[j] === '+' || src[j] === '-') j++; while (j < src.length && /[0-9]/.test(src[j])) j++; }
const raw = src.slice(i, j);
const v = parseFloat(raw);
if (!isFinite(v)) throw new FormulaError('#ERR!');
tokens.push({ t: 'num', v: v, raw: raw });
i = j; continue;
}
if (/[A-Za-z_$]/.test(ch)) {
let j = i;
while (j < src.length && /[A-Za-z0-9_$.]/.test(src[j])) j++;
tokens.push({ t: 'id', v: src.slice(i, j), raw: src.slice(i, j) });
i = j; continue;
}
if (ch === '"') {
let j = i + 1, s = '';
while (j < src.length && src[j] !== '"') { s += src[j]; j++; }
if (j >= src.length) throw new FormulaError('#ERR!');
tokens.push({ t: 'str', v: s, raw: src.slice(i, j + 1) });
i = j + 1; continue;
}
if (OPS.indexOf(ch) >= 0) {
tokens.push({ t: 'op', v: ch, raw: ch });
i++; continue;
}
throw new FormulaError('#ERR!');
}
return tokens;
}
/* ==========================================================================
4. FORMULA ENGINE — recursive-descent parser -> AST
Ast nodes: {k:'num'|'str'|'ref'|'range'|'un'|'bin'|'fn'}
========================================================================== */
const FUNCS = { SUM: 1, AVG: 1, AVERAGE: 1, MIN: 1, MAX: 1, COUNT: 1, ABS: 1, ROUND: 1, SQRT: 1 };
function parseFormula(src) {
/* a rewritten reference that died is carried as literal #REF! */
if (src.indexOf('#REF!') >= 0) throw new FormulaError('#REF!');
const tokens = tokenize(src).filter(t => t.t !== 'ws');
let pos = 0;
const peek = () => tokens[pos];
const next = () => tokens[pos++];
const isOp = tok => tok && tok.t === 'op';
function parseExpr() {
let left = parseTerm();
while (isOp(peek()) && (peek().v === '+' || peek().v === '-')) {
const op = next().v;
left = { k: 'bin', op: op, l: left, r: parseTerm() };
}
return left;
}
function parseTerm() {
let left = parseFactor();
while (isOp(peek()) && (peek().v === '*' || peek().v === '/' || peek().v === '^')) {
const op = next().v;
left = { k: 'bin', op: op, l: left, r: parseFactor() };
}
return left;
}
function parseFactor() {
if (isOp(peek()) && (peek().v === '-' || peek().v === '+')) {
const op = next().v;
return { k: 'un', op: op, x: parseFactor() };
}
return parsePrimary();
}
function parsePrimary() {
const tok = next();
if (!tok) throw new FormulaError('#ERR!');
if (tok.t === 'num') return { k: 'num', v: tok.v };
if (tok.t === 'str') return { k: 'str', v: tok.v };
if (isOp(tok) && tok.v === '(') {
const e = parseExpr();
const close = next();
if (!isOp(close) || close.v !== ')') throw new FormulaError('#ERR!');
return e;
}
if (tok.t === 'id') {
const up = tok.v.toUpperCase();
if (FUNCS[up] && isOp(peek()) && peek().v === '(') {
next(); // eat '('
const args = [];
if (!(isOp(peek()) && peek().v === ')')) {
args.push(parseExpr());
while (isOp(peek()) && peek().v === ',') { next(); args.push(parseExpr()); }
}
const close = next();
if (!isOp(close) || close.v !== ')') throw new FormulaError('#ERR!');
return { k: 'fn', name: up, args: args };
}
const ref = parseRef(tok.v);
if (!ref) throw new FormulaError('#ERR!');
if (isOp(peek()) && peek().v === ':') {
next();
const t2 = next();
if (!t2 || t2.t !== 'id') throw new FormulaError('#ERR!');
const ref2 = parseRef(t2.v);
if (!ref2) throw new FormulaError('#ERR!');
return { k: 'range', a: ref, b: ref2 };
}
return { k: 'ref', ref: ref };
}
throw new FormulaError('#ERR!');
}
const ast = parseExpr();
if (pos !== tokens.length) throw new FormulaError('#ERR!');
return ast;
}
/** statically collect referenced cell keys from an AST */
function collectRefs(node, out) {
if (!node) return;
switch (node.k) {
case 'ref': {
const r = node.ref;
if (r.row >= 0 && r.row < ROWS && r.col >= 0 && r.col < COLS) out.add(K(r.row, r.col));
break;
}
case 'range': {
const r1 = Math.min(node.a.row, node.b.row), r2 = Math.max(node.a.row, node.b.row);
const c1 = Math.min(node.a.col, node.b.col), c2 = Math.max(node.a.col, node.b.col);
for (let r = Math.max(0, r1); r <= Math.min(ROWS - 1, r2); r++)
for (let c = Math.max(0, c1); c <= Math.min(COLS - 1, c2); c++) out.add(K(r, c));
break;
}
case 'bin': collectRefs(node.l, out); collectRefs(node.r, out); break;
case 'un': collectRefs(node.x, out); break;
case 'fn': node.args.forEach(a => collectRefs(a, out)); break;
}
}
/* ==========================================================================
5. FORMULA ENGINE — evaluator
========================================================================== */
/** coerce a value to a number ('' -> 0). Returns null when impossible. */
function toNum(v) {
if (isErr(v)) return null;
if (typeof v === 'number') return v;
if (v === BLANK || v === undefined || v === null) return 0;
const s = String(v).trim();
if (s === '') return 0;
if (!/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(s)) return null;
const n = parseFloat(s);
return isFinite(n) ? n : null;
}
function evalNode(node, getVal) {
switch (node.k) {
case 'num': return node.v;
case 'str': return node.v;
case 'ref': {
const r = node.ref;
if (r.row < 0 || r.row >= ROWS || r.col < 0 || r.col >= COLS) return ERR_REF;
return getVal(K(r.row, r.col));
}
case 'range': return ERR_PARSE; // ranges only valid as function args
case 'un': {
const v = evalNode(node.x, getVal);
if (isErr(v)) return v;
const n = toNum(v);
if (n === null) return ERR_PARSE;
return node.op === '-' ? -n : n;
}
case 'bin': {
const l = evalNode(node.l, getVal);
if (isErr(l)) return l;
const r = evalNode(node.r, getVal);
if (isErr(r)) return r;
const a = toNum(l), b = toNum(r);
if (a === null || b === null) return ERR_PARSE;
switch (node.op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return b === 0 ? ERR_DIV : a / b;
case '^': return Math.pow(a, b);
}
return ERR_PARSE;
}
case 'fn': return evalFn(node, getVal);
}
return ERR_PARSE;
}
function evalFn(node, getVal) {
const nums = [];
let err = null;
const push = v => {
if (isErr(v)) { if (!err) err = v; return; }
if (typeof v === 'number') { nums.push(v); return; }
if (typeof v === 'string' && v.trim() !== '') {
const n = Number(v);
if (!isNaN(n) && isFinite(n)) nums.push(n);
}
};
for (const arg of node.args) {
if (arg.k === 'range') {
const r1 = Math.min(arg.a.row, arg.b.row), r2 = Math.max(arg.a.row, arg.b.row);
const c1 = Math.min(arg.a.col, arg.b.col), c2 = Math.max(arg.a.col, arg.b.col);
if (r1 < 0 || c1 < 0 || r2 >= ROWS || c2 >= COLS) { if (!err) err = ERR_REF; continue; }
for (let r = r1; r <= r2; r++) for (let c = c1; c <= c2; c++) push(getVal(K(r, c)));
} else {
push(evalNode(arg, getVal));
}
}
if (err) return err;
const sum = nums.reduce((s, x) => s + x, 0);
switch (node.name) {
case 'SUM': return sum;
case 'AVG': case 'AVERAGE': return nums.length ? sum / nums.length : ERR_DIV;
case 'MIN': return nums.length ? nums.reduce((m, x) => x < m ? x : m, nums[0]) : 0;
case 'MAX': return nums.length ? nums.reduce((m, x) => x > m ? x : m, nums[0]) : 0;
case 'COUNT': return nums.length;
case 'ABS': return nums.length ? Math.abs(nums[0]) : ERR_PARSE;
case 'SQRT': return nums.length ? (nums[0] < 0 ? { err: '#NUM!' } : Math.sqrt(nums[0])) : ERR_PARSE;
case 'ROUND': return nums.length ? Number(nums[0].toFixed(Math.max(0, Math.min(15, nums[1] | 0)))) : ERR_PARSE;
}
return ERR_PARSE;
}
/** value of a non-formula cell */
function literal(raw) {
if (raw === undefined || raw === null || raw === '') return BLANK;
if (raw.charAt(0) === "'") return raw.slice(1); // forced text
const s = raw.trim();
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(s)) {
const n = parseFloat(s);
if (isFinite(n)) return n;
}
return raw;
}
function formatNumber(n) {
if (!isFinite(n)) return '#NUM!';
if (Number.isInteger(n) && Math.abs(n) < 1e15) return String(n);
const r = Number(n.toPrecision(CONFIG.NUM_PRECISION));
if (r !== 0 && (Math.abs(r) >= 1e15 || Math.abs(r) < 1e-9)) return r.toExponential(6).replace(/e([+-])(\d)$/, 'e$10$2');
return String(r);
}
/* ==========================================================================
6. DEPENDENCY GRAPH + RECALCULATION
========================================================================== */
let data = []; // data[r][c] = raw string
let values = new Map(); // key -> computed value
let precedents = new Map(); // key -> Set(key) what a cell reads
let dependents = new Map(); // key -> Set(key) who reads a cell
let parseCache = new Map(); // key -> {ast|err}
const emptyGrid = () => Array.from({ length: ROWS }, () => new Array(COLS).fill(''));
const rawOf = k => data[KR(k)][KC(k)] || '';
function compile(raw) {
try { return { ast: parseFormula(raw.slice(1)) }; }
catch (e) { return { err: (e instanceof FormulaError ? e.code : '#ERR!') }; }
}
/** re-register the precedents/dependents of one cell */
function updateDeps(k, raw) {
const old = precedents.get(k);
if (old) for (const p of old) { const s = dependents.get(p); if (s) s.delete(k); }
const set = new Set();
if (raw && raw.charAt(0) === '=') {
const comp = compile(raw);
parseCache.set(k, comp);
if (comp.ast) collectRefs(comp.ast, set);
} else {
parseCache.delete(k);
}
precedents.set(k, set);
for (const p of set) {
let s = dependents.get(p);
if (!s) { s = new Set(); dependents.set(p, s); }
s.add(k);
}
}
/**
* Recompute the transitive dependent-closure of `changed`, in topological order
* via memoised DFS. Grey-node hit => circular reference (#CYCLE!).
*/
function recompute(changed) {
const closure = new Set();
const stack = changed.slice();
while (stack.length) {
const k = stack.pop();
if (closure.has(k)) continue;
closure.add(k);
const d = dependents.get(k);
if (d) for (const x of d) if (!closure.has(x)) stack.push(x);
}
const memo = new Map(), state = new Map(); // state: 1 = in-progress
function evaluate(k) {
if (memo.has(k)) return memo.get(k);
if (!closure.has(k)) {
const cached = values.get(k);
return cached === undefined ? BLANK : cached;
}
if (state.get(k) === 1) return ERR_CYCLE;
state.set(k, 1);
let v;
try {
const raw = rawOf(k);
if (raw.charAt(0) === '=') {
let comp = parseCache.get(k);
if (!comp) { comp = compile(raw); parseCache.set(k, comp); }
v = comp.err ? (ERRS[comp.err] || ERR_PARSE) : evalNode(comp.ast, evaluate);
} else {
v = literal(raw);
}
} catch (e) { v = ERR_PARSE; }
state.set(k, 2);
if (v === undefined) v = BLANK;
memo.set(k, v);
return v;
}
for (const k of closure) evaluate(k);
for (const k of closure) {
const v = memo.get(k);
values.set(k, v === undefined ? BLANK : v);
paintKey(k);
}
}
/** full rebuild of graph + values (used after structural edits and on load) */
function rebuildAll() {
precedents.clear(); dependents.clear(); parseCache.clear(); values.clear();
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
const raw = data[r][c];
if (raw) updateDeps(K(r, c), raw);
}
const all = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) all.push(K(r, c));
recompute(all);
}
/** public write path */
function setCell(r, c, raw) {
if (raw === undefined || raw === null) raw = '';
const k = K(r, c);
if (data[r][c] === raw) return;
data[r][c] = raw;
updateDeps(k, raw);
recompute([k]);
refreshStatus();
scheduleSave();
}
/* ==========================================================================
7. STRUCTURAL EDITS — insert / delete rows & columns with ref rewriting
========================================================================== */
/** re-serialize a formula, mapping each reference through rowT/colT (-1 => #REF!) */
function rewriteFormula(raw, rowT, colT) {
if (!raw || raw.charAt(0) !== '=') return raw;
if (raw.indexOf('#REF!') >= 0) return raw;
let toks;
try { toks = tokenize(raw.slice(1)); } catch (e) { return raw; }
let out = '=';
for (let i = 0; i < toks.length; i++) {
const tk = toks[i];
if (tk.t === 'id') {
const up = tk.v.toUpperCase();
const nx = toks[i + 1];
const isFunc = FUNCS[up] && nx && nx.t === 'op' && nx.v === '(';
if (!isFunc) {
const ref = parseRef(tk.v);
if (ref) {
const nr = rowT(ref.row), nc = colT(ref.col);
if (nr < 0 || nc < 0) { out += '#REF!'; continue; }
out += refText(tk.raw, nc, nr);
continue;
}
}
}
out += tk.raw;
}
return out;
}
function mapAllFormulas(rowT, colT) {
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
const raw = data[r][c];
if (raw && raw.charAt(0) === '=') data[r][c] = rewriteFormula(raw, rowT, colT);
}
}
function insertRow(at) {
if (at < 0) at = 0; if (at > ROWS) at = ROWS;
mapAllFormulas(row => row >= at ? row + 1 : row, col => col);
for (let r = ROWS - 1; r > at; r--) data[r] = data[r - 1].slice();
data[at] = new Array(COLS).fill('');
data.length = ROWS;
rebuildAll(); scheduleSave(); refreshStatus();
}
function deleteRow(at) {
if (at < 0 || at >= ROWS) return;
mapAllFormulas(row => row === at ? -1 : (row > at ? row - 1 : row), col => col);
data.splice(at, 1);
data.push(new Array(COLS).fill(''));
rebuildAll(); scheduleSave(); refreshStatus();
}
function insertCol(at) {
if (at < 0) at = 0; if (at > COLS) at = COLS;
mapAllFormulas(row => row, col => col >= at ? col + 1 : col);
for (let r = 0; r < ROWS; r++) {
const row = data[r];
for (let c = COLS - 1; c > at; c--) row[c] = row[c - 1];
row[at] = '';
row.length = COLS;
}
rebuildAll(); scheduleSave(); refreshStatus();
}
function deleteCol(at) {
if (at < 0 || at >= COLS) return;
mapAllFormulas(row => row, col => col === at ? -1 : (col > at ? col - 1 : col));
for (let r = 0; r < ROWS; r++) {
const row = data[r];
row.splice(at, 1);
row.push('');
}
rebuildAll(); scheduleSave(); refreshStatus();
}
function clearCell(r, c) { if (editing) endEdit(true); setCell(r, c, ''); }
/* ==========================================================================
8. PERSISTENCE
========================================================================== */
let saveTimer = null;
function scheduleSave() {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(saveNow, CONFIG.SAVE_DEBOUNCE);
}
function saveNow() {
saveTimer = null;
try {
const cells = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++)
if (data[r][c]) cells.push([r, c, data[r][c]]);
localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify({ v: 1, rows: ROWS, cols: COLS, cells: cells }));
} catch (e) { /* storage unavailable — ignore */ }
}
function loadSaved() {
try {
const txt = localStorage.getItem(CONFIG.STORAGE_KEY);
if (!txt) return false;
const obj = JSON.parse(txt);
if (!obj || !Array.isArray(obj.cells)) return false;
for (const entry of obj.cells) {
const r = entry[0], c = entry[1], v = entry[2];
if (r >= 0 && r < ROWS && c >= 0 && c < COLS) data[r][c] = String(v);
}
return true;
} catch (e) { return false; }
}
/* ==========================================================================
9. DOM — grid construction & painting
========================================================================== */
const gridWrap = document.getElementById('gridWrap');
const table = document.getElementById('grid');
const editor = document.getElementById('editor');
const finput = document.getElementById('finput');
const cellRefEl = document.getElementById('cellRef');
const statSel = document.getElementById('statSel');
const statVal = document.getElementById('statVal');
const ctxMenu = document.getElementById('ctx');
let tds = []; // tds[r][c]
let colHeads = [], rowHeads = [];
function buildGrid() {
let html = '<thead><tr><th class="corner"></th>';
for (let c = 0; c < COLS; c++) html += '<th class="colh" data-c="' + c + '">' + colName(c) + '</th>';
html += '</tr></thead><tbody>';
for (let r = 0; r < ROWS; r++) {
html += '<tr><th class="rowh" data-r="' + r + '">' + (r + 1) + '</th>';
for (let c = 0; c < COLS; c++) html += '<td class="cell" data-r="' + r + '" data-c="' + c + '"></td>';
html += '</tr>';
}
html += '</tbody>';
table.innerHTML = html;
tds = [];
for (let r = 0; r < ROWS; r++) {
const tr = table.tBodies[0].rows[r];
tds.push([]);
for (let c = 0; c < COLS; c++) tds[r].push(tr.cells[c + 1]);
}
colHeads = Array.from(table.tHead.rows[0].cells).slice(1);
rowHeads = Array.from(table.tBodies[0].rows).map(tr => tr.cells[0]);
}
function paintKey(k) { paint(KR(k), KC(k)); }
function paint(r, c) {
const td = tds[r][c];
if (!td) return;
const v = values.get(K(r, c));
let text = '', cls = 'cell';
if (isErr(v)) { text = v.err; cls += ' err'; }
else if (typeof v === 'number') { text = formatNumber(v); cls += ' num'; }
else if (v === BLANK || v === undefined) { text = ''; }
else { text = String(v); }
if (td.textContent !== text) td.textContent = text;
let extra = '';
if (r === sel.r && c === sel.c) { extra += ' sel'; if (editing) extra += ' editing'; }
if (td.className !== cls + extra) td.className = cls + extra;
}
function paintAll() {
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) paint(r, c);
}
/* ==========================================================================
10. SELECTION
========================================================================== */
let sel = { r: 0, c: 0 };
let prevSel = { r: 0, c: 0 };
let editing = false, editBackup = '';
function clampSel() {
if (sel.r < 0) sel.r = 0; if (sel.r > ROWS - 1) sel.r = ROWS - 1;
if (sel.c < 0) sel.c = 0; if (sel.c > COLS - 1) sel.c = COLS - 1;
}
function select(r, c, opts) {
if (editing && !(opts && opts.keepEdit)) endEdit();
prevSel = { r: sel.r, c: sel.c };
sel.r = r; sel.c = c;
clampSel();
refreshSelUI();
if (!opts || !opts.noScroll) ensureVisible();
}
function refreshSelUI() {
for (let c = 0; c < COLS; c++) colHeads[c].classList.toggle('hl', c === sel.c);
for (let r = 0; r < ROWS; r++) rowHeads[r].classList.toggle('hl', r === sel.r);
cellRefEl.textContent = colName(sel.c) + (sel.r + 1);
finput.value = data[sel.r][sel.c] || '';
paint(prevSel.r, prevSel.c);
paint(sel.r, sel.c);
refreshStatus();
if (document.activeElement === finput) finput.blur();
}
function refreshStatus() {
statSel.textContent = colName(sel.c) + (sel.r + 1);
const raw = data[sel.r][sel.c];
const v = values.get(K(sel.r, sel.c));
if (raw && raw.charAt(0) === '=') {
statVal.textContent = isErr(v) ? v.err : '= ' + (typeof v === 'number' ? formatNumber(v) : v);
} else {
statVal.textContent = '';
}
}
function ensureVisible() {
const td = tds[sel.r][sel.c];
const r = td.getBoundingClientRect(), w = gridWrap.getBoundingClientRect();
const top = w.top + CONFIG.HEADER_HEIGHT, left = w.left + CONFIG.HEADER_WIDTH;
if (r.top < top) gridWrap.scrollTop -= (top - r.top);
else if (r.bottom > w.bottom) gridWrap.scrollTop += (r.bottom - w.bottom);
if (r.left < left) gridWrap.scrollLeft -= (left - r.left);
else if (r.right > w.right) gridWrap.scrollLeft += (r.right - w.right);
}
function move(dr, dc) {
select(sel.r + dr, sel.c + dc);
}
/* ==========================================================================
11. IN-CELL EDITING (floating input over the active cell)
========================================================================== */
function positionEditor() {
const td = tds[sel.r][sel.c];
if (!td) return;
const r = td.getBoundingClientRect();
editor.style.left = r.left + 'px';
editor.style.top = r.top + 'px';
editor.style.width = Math.max(24, r.width) + 'px';
editor.style.height = Math.max(18, r.height) + 'px';
editor.style.fontSize = getComputedStyle(td).fontSize;
}
function startEdit(initial) {
if (editing) return;
editing = true;
editBackup = data[sel.r][sel.c] || '';
editor.value = initial === undefined ? editBackup : initial;
editor.style.display = 'block';
positionEditor();
editor.focus();
if (initial === undefined) editor.select();
else { editor.setSelectionRange(editor.value.length, editor.value.length); setCell(sel.r, sel.c, editor.value); }
finput.value = editor.value;
paint(sel.r, sel.c);
}
function endEdit(cancel) {
if (!editing) return;
if (cancel) { setCell(sel.r, sel.c, editBackup); }
else { setCell(sel.r, sel.c, editor.value); }
editing = false;
editor.style.display = 'none';
editor.value = '';
try { editor.blur(); } catch (e) { }
if (document.activeElement === editor && document.activeElement.blur) document.activeElement.blur();
try { gridWrap.focus({ preventScroll: true }); } catch (e) { }
finput.value = data[sel.r][sel.c] || '';
paint(sel.r, sel.c);
refreshStatus();
}
function cancelEdit() { endEdit(true); }
editor.addEventListener('input', () => {
setCell(sel.r, sel.c, editor.value);
finput.value = editor.value;
});
editor.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); endEdit(); move(1, 0); }
else if (e.key === 'Tab') { e.preventDefault(); e.stopPropagation(); endEdit(); move(0, e.shiftKey ? -1 : 1); }
else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); endEdit(true); }
});
gridWrap.addEventListener('scroll', () => { if (editing) positionEditor(); });
window.addEventListener('resize', () => { if (editing) positionEditor(); });
/* ==========================================================================
12. FORMULA BAR
========================================================================== */
let fbarBackup = '';
finput.addEventListener('focus', () => { fbarBackup = data[sel.r][sel.c] || ''; });
finput.addEventListener('input', () => {
if (editing) {
editor.value = finput.value;
setCell(sel.r, sel.c, finput.value);
} else {
setCell(sel.r, sel.c, finput.value);
paint(sel.r, sel.c);
}
refreshStatus();
});
finput.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); endEdit(); move(1, 0); }
else if (e.key === 'Tab') { e.preventDefault(); e.stopPropagation(); endEdit(); move(0, e.shiftKey ? -1 : 1); }
else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); endEdit(); setCell(sel.r, sel.c, fbarBackup); finput.value = fbarBackup; finput.blur(); }
});
/* ==========================================================================
13. MOUSE — selection, double-click edit, header right-click menu
========================================================================== */
function cellFromEvent(e) {
const td = e.target.closest ? e.target.closest('td.cell') : null;
if (!td) return null;
return { r: +td.dataset.r, c: +td.dataset.c };
}
gridWrap.addEventListener('mousedown', e => {
if (e.target.closest('th')) return;
const pos = cellFromEvent(e);
if (!pos) return;
if (editing && (pos.r !== sel.r || pos.c !== sel.c)) endEdit();
if (pos.r === sel.r && pos.c === sel.c) { if (!editing) select(pos.r, pos.c, { keepEdit: true }); return; }
select(pos.r, pos.c, { keepEdit: true });
});
gridWrap.addEventListener('dblclick', e => {
const pos = cellFromEvent(e);
if (!pos) return;
select(pos.r, pos.c);
startEdit();
});
/* click on a row/column header selects the whole first cell of it + opens context */
gridWrap.addEventListener('contextmenu', e => {
const th = e.target.closest ? e.target.closest('th') : null;
if (!th) return;
e.preventDefault();
if (th.classList.contains('colh')) {
const c = +th.dataset.c;
select(sel.r, c);
openMenu(e.clientX, e.clientY, [
{ label: 'Insert column left', fn: () => { insertCol(c); select(sel.r, Math.min(c, COLS - 1)); } },
{ label: 'Insert column right', fn: () => { insertCol(c + 1); select(sel.r, Math.min(c + 1, COLS - 1)); } },
null,
{ label: 'Delete column ' + colName(c), fn: () => { deleteCol(c); select(sel.r, Math.min(c, COLS - 1)); } }
]);
} else if (th.classList.contains('rowh')) {
const r = +th.dataset.r;
select(r, sel.c);
openMenu(e.clientX, e.clientY, [
{ label: 'Insert row above', fn: () => { insertRow(r); select(Math.min(r, ROWS - 1), sel.c); } },
{ label: 'Insert row below', fn: () => { insertRow(r + 1); select(Math.min(r + 1, ROWS - 1), sel.c); } },
null,
{ label: 'Delete row ' + (r + 1), fn: () => { deleteRow(r); select(Math.min(r, ROWS - 1), sel.c); } }
]);
}
});
function openMenu(x, y, items) {
ctxMenu.innerHTML = '';
for (const it of items) {
if (!it) { ctxMenu.appendChild(document.createElement('hr')); continue; }
const b = document.createElement('button');
b.textContent = it.label;
b.addEventListener('click', () => { closeMenu(); it.fn(); });
ctxMenu.appendChild(b);
}
ctxMenu.style.display = 'block';
ctxMenu.style.left = Math.min(x, window.innerWidth - 200) + 'px';
ctxMenu.style.top = Math.min(y, window.innerHeight - 20 - items.length * 34) + 'px';
}
function closeMenu() { ctxMenu.style.display = 'none'; }
document.addEventListener('mousedown', e => { if (!e.target.closest('#ctx')) closeMenu(); }, true);
/* ==========================================================================
14. KEYBOARD NAVIGATION
========================================================================== */
/* capture phase at window: fires before any focus-related quirks can swallow grid keys */
window.addEventListener('keydown', e => {
const t = e.target;
if (editing || t === finput) return;
if (e.key === 'Escape') { closeMenu(); return; }
const nav = { ArrowUp: [-1, 0], ArrowDown: [1, 0], ArrowLeft: [0, -1], ArrowRight: [0, 1] };
if (nav[e.key]) {
e.preventDefault();
if (e.metaKey || e.ctrlKey) move(e.key === 'ArrowDown' ? ROWS : e.key === 'ArrowUp' ? -ROWS : 0,
e.key === 'ArrowRight' ? COLS : e.key === 'ArrowLeft' ? -COLS : 0);
else move(nav[e.key][0], nav[e.key][1]);
return;
}
if (e.key === 'Enter') {
e.preventDefault();
if (e.shiftKey) move(-1, 0); else move(1, 0);
return;
}
if (e.key === 'Tab') { e.preventDefault(); move(0, e.shiftKey ? -1 : 1); return; }
if (e.key === 'F2') { e.preventDefault(); startEdit(); return; }
if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); setCell(sel.r, sel.c, ''); return; }
if ((e.ctrlKey || e.metaKey) && (e.key === 'c' || e.key === 'v' || e.key === 'x')) return;
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
startEdit(e.key);
}
}, true);
/* ==========================================================================
15. TOOLBAR
========================================================================== */
function doAction(act) {
switch (act) {
case 'insRow': insertRow(sel.r); select(Math.min(sel.r, ROWS - 1), sel.c); break;
case 'delRow': deleteRow(sel.r); select(Math.min(sel.r, ROWS - 1), sel.c); break;
case 'insCol': insertCol(sel.c); select(sel.r, Math.min(sel.c, COLS - 1)); break;
case 'delCol': deleteCol(sel.c); select(sel.r, Math.min(sel.c, COLS - 1)); break;
case 'clear': setCell(sel.r, sel.c, ''); break;
}
}
document.querySelectorAll('[data-act]').forEach(b => {
b.addEventListener('click', () => { doAction(b.dataset.act); b.blur(); });
});
/* ==========================================================================
16. BOOT
========================================================================== */
function init() {
data = emptyGrid();
buildGrid();
loadSaved();
rebuildAll();
select(0, 0, { noScroll: true });
paintAll();
window.addEventListener('beforeunload', saveNow);
}
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4.1-flash","provider":"openrouter","persona":"full","sessionId":"b8c0bca5-571d-4e98-833b-f0758dfd1a69","tokensIn":4024705,"tokensOut":69860,"tokensTotal":4094565,"cost":0.06215640600000001,"turns":54,"toolCalls":53,"failedToolCalls":0,"timestamp":"2026-09-10T17:16:45.559Z"} -->