Design goals (v0.1)
- Expand without surgery — new formula functions, commands, and structure kinds register in one place.
- No bundler — classic
<script>tags; load order = dependency order. - Clear layers — model → formula → format → UI → boot.
- Hooks — tiny event bus for plugins and future integrations.
Repository layout
projects/metagrid/
app.html spreadsheet shell (DOM + overlays)
site.css marketing + shared
js/
core/namespace.js Metagrid root, hooks, registries
model/
structures.js factories, geometry, get/setCell, TYPE
state.js workbook / UI / cloud state
columns.js colStruct / rowStruct / lock templates
history.js clone, snapshot, undo stack
seed.js demo workbook
pivot.js pivot structure + fields pane
chart.js chart structure + source bind
access.js users, nested groups, see/edit/grant
push.js button click: snapshot source → write dest
formula/
engine.js tokenize, resolve, eval, special forms
functions/builtins.js Metagrid.formula.register(…)
functions/analysis.js FILTER, GROUPBY, stats
api/
core.js Agent API: JSON view, named paths, ops
format/
document.js metagrid-v2 export / migrate
number-format.js Excel-like number/date display codes
import-xlsx.js Excel/CSV → structures
import-json.js Plain JSON paste/import → list/table/grid
calc.js deps + invalidation
formula/
engine.js, builtins, analysis, slash.js (= pull · / push)
ui/
dom.js, ribbon.js, paint.js, selection.js, navigation.js,
edit.js, commands.js, access.js, dialogs.js, fn-library.js,
picker.js, keytips.js, keyboard.js, cloud.js, metasheet.js, menus.js,
chart.js, queries.js, files.js
boot.js restore local | seed → ribbon → render → cloud
The Metagrid namespace
Metagrid.version
Metagrid.hooks on / once / emit / clear
Metagrid.formula register / get / call / sections / all
Metagrid.commands register / run / get / all
Metagrid.structures register / get / typeMap / all
Metagrid.state live workbook + UI fields
Metagrid.model / .format / .ui
Metagrid.extend { function, command, structure, hook }
Classic scripts still use shared top-level bindings (root, evalFormula, …) for call-site compatibility. New code should prefer Metagrid.*.
How to extend
Add a formula function
// js/formula/functions/builtins.js (or a new file loaded after it)
Metagrid.formula.register({
name: "ABS",
category: "math", // math | stat | logic | lookup | lambda | text | info
sig: "ABS(number)",
args: ["number"],
desc: "Absolute value (maps over lists)",
map: true, // element-wise over lists / columns
minArgs: 1,
maxArgs: 1,
impl(args, ctx) {
return Math.abs(ctx.asNum(args[0]));
},
});
The Function library UI is derived from the registry — no second catalog. Special forms (IF, LAMBDA, LET, …) stay in engine.js (custom parse / deferred args) and are registered with special: true for the library list only.
Add a ribbon command
Metagrid.commands.register({
id: "mycommand",
run() { /* … */ },
});
// reference id from RIBBON config in ui/ribbon.js
Add a structure type
Metagrid.structures.register({
type: "grid",
name: "Grid",
icon: "⊞",
color: "var(--sel)",
mk: (rows, cols) => mkGrid(rows, cols),
});
Built-ins: grid, table, list, pivot, chart. Paint/edit still branch on k.type; the registry is the place to grow new kinds.
Listen for lifecycle
Metagrid.hooks.on("boot", () => { … });
Metagrid.hooks.on("documentLoaded", ({ root, source }) => { … });
Metagrid.hooks.on("afterCommitEdit", ({ k, r, c, val }) => { … });
Metagrid.hooks.on("afterInvalidate", () => { … });
Layers
- Core — namespace, registries, hooks,
ERR. - Model — structures, geometry, column templates, history, seed.
- Formula — tokenizer, path resolve, element-wise map, eval + function registry.
- Format —
metagrid-v2export/load, Excel import, calc invalidation. - UI — ribbon, virtualized paint, edit, commands, cloud chrome.
- Boot — local restore or demo seed → catalog → ribbon → render → cloud init → hooks.
Agent API
Language models talk to cloud sheets through POST /api/metagrid (api/metagrid.js). Document ops are DOM-free in js/api/core.js (JSON view + named paths). Auth is an owner API key hashed in subsheet_api_keys; RPCs in supabase/migrations/002_api_keys.sql. This is not the sharing/projection layer — keys see the canonical tree of sheets you own. Catalog: GET the same URL with no body. Docs: Agent API.
Formula evaluation path
displayVal(k,r,c)
→ effectiveFormula (cell "=" or table colFormula)
→ computeVal / cellValue
· noteCellRead → dependency edge into calcDeps
· visiting set → #CYCLE
· evalFormula(tokenize(src))
special forms: IF, LAMBDA, LET, …
else Metagrid.formula.call(name, args, fctx)
· map:true → element-wise over lists
operators ^ * / + - & cmp also map over lists
→ cache under cellKey = id|r|c
- While a formula cell evaluates,
_evalDepTargetrecords every cell read as a source → dependent edge. invalidateCellwalks dependents;invalidateAllclears cache + graphs.- Column refs materialize as temporary lists for exploration and path entry.
- Scalar ops on lists:
ABS(Data),Data^Data(same length),Data+1(broadcast).
Column structure subsystem
Tables store per-column maps instead of inventing a parallel type system:
| Field | Role |
|---|---|
| colFormula | Scalar formula string per column |
| colStruct | Nested type for every cell in the column |
| colStructLock | Boolean: template owns headers + formulas |
| colStructHeaders | Canonical header list for nested tables |
| colStructFormulas | Canonical colFormula map for nested tables |
| colValid | Validation rule per column |
applyLockedTemplateToTable is the single apply path for “parent template → child table.”
Rendering
- Logical dims vs visual dims (header/index chrome).
- Virtual window: fixed row height / col width constants, overscan, scroll-bound re-paint of visible cells only.
- Selection chrome via classes + edge box-shadows; formula pointer uses a second highlight color.
Persistence
- Local:
localStoragekeymetagrid:local-v2(workbook + names + cloud meta). - Cloud: dedicated Supabase project still named
subsheet(ghoybhnmlltyruudjeqe). Auth is a Metagrid account (profiles.id). Members areuser_id, not email strings. Linked emails/phones live insubsheet_identities(Settings → Sending addresses). Spell Chess is on the Requests project, not this one. - File: Export/import
metagrid-v2JSON; Excel/CSV import via SheetJS on demand.
Load order
Documented in app.html comments. Rule of thumb: core → model → formula → push → format → ui → boot. Never call UI from model. Formula may read model; push snapshots formula results then writes stored cells; format may use both; UI uses everything.