Single-file, local-first office tools: fully offline, zero dependencies, built on one rule: AI advises, deterministic math decides.
A family of tools that all read and write one JSON format (localoffice/v1); the
shared core is built once, and each tool is a thin layer on top. Every tool has a
JSON button that shows the document's raw localoffice/v1 envelope: view it,
edit it, copy or download it, or apply changes back (invalid JSON is rejected, so
a bad edit never clobbers your document).
Conventions: one self-contained HTML file per tool, no dependencies, no build step, fully offline. The only optional network call is to a local Ollama. MIT.
LocalOffice.html The Hub. Pick a workspace folder, browse every file, open the right tool.
src/core.js Shared loader/saver, the foundation every tool builds on.
src/test-core.js Node unit tests for the core (no dependencies).
src/verify.js Shared verification kernel (deterministic exact + coverage + tolerance).
src/test-verify.js Node unit tests for the verify kernel.
src/render.js Shared static renderer (LocalRender): any object → HTML/SVG for embeds + export.
src/test-render.js Node unit tests for the render module.
src/csv.js Shared RFC-4180 CSV adapter (LocalCSV): parse + serialize, inlined per tool.
src/test-csv.js Node unit tests for the CSV adapter.
core-harness.html Browser dev harness for the IO paths Node can't test.
run-all-tests.js Runs every suite across the whole project.
fs-mock.js In-memory File System Access API mock for headless save/open tests.
embed-harness.html Test fixture that drives the real Hub iframe + postMessage handoff.
verify-hub.js Hub dashboard, embed protocol, and Hub-mediated save tests.
verify-embed.js Embedded smoke test: each tool rendered in a real iframe.
localDeck/ Deck tool (body type: slides). Single file plus headless tests.
localCards/ Flashcards tool (body type: flashcards). Single file plus tests.
localSheets/ Spreadsheet (body type: sheet), refactored onto the envelope.
localPlan/ Planner (body type: plan, tracks/sections/items) on the envelope.
localMindMap/ Mindmap (body type: mindmap). Single-file canvas plus headless tests.
localMark/ Image sanitizer (body type: image). Scrub/redact/annotate plus headless tests.
localCheck/ QA runbook (body type: runbook). Kernel-gated sign-off plus headless tests.
localDoc/ Block doc writer (body type: doc). Coverage compliance linter plus headless tests.
localValidate/ Spec conformance checker: paste an envelope, get pass/fail + which rule failed.
templates/ localoffice/v1 sheet and plan presets (unit pack, net-worth, PM plan).
SPEC.md The canonical localoffice/v1 envelope specification.
- LLM authoring (
✦ Prompt). Every tool's JSON panel has a✦ Prompttoggle showing a copy-ready prompt to build that document with any LLM (local, on-prem, or cloud); paste the returned JSON back and Apply. The AI-panel tools also have a one-click Generate via a local Ollama. The deterministic kernel always decides correctness; the model only proposes. - Embedded objects. A tool can embed another tool's object as a live, editable
child: a sheet's table or a mind map on a deck slide, an object inline in a doc,
or attached via each tool's
◲ Embedsdrawer. Objects are stored as a nestedlocaloffice/v1envelope (they round-trip on save/load) and drawn by the shared static renderersrc/render.js(LocalRender), so they render the same in the editor and in exports, fully offline. You edit by opening the real tool. - CSV import/export. The tabular tools speak CSV through one shared,
RFC-4180-correct adapter (
src/csv.js,LocalCSV): LocalSheets (cells), localCards (Front/Back/Tags), and localPlan (Track/Section/Item). Commas, quotes, and embedded newlines are handled in one tested place, so a round-trip never mangles data. - Mind-map diagramming. Node shapes (rounded, rectangle, circle, diamond, triangle, logic gates), a full colour palette (named + custom hex), and connections you can click to label / style (line, arrow, double-arrow, dashed) or drag to re-connect. Delete is Visio-style: node-only (re-parenting its children), with Shift+Del to delete the whole branch.
The cohesion layer: one file that makes the tools feel like a single workspace. Single file, no deps, offline.
- Pick a workspace folder (File System Access on Chrome/Edge, a "Files…"
picker fallback on Firefox/Safari). The Hub scans recursively for
*.localoffice.jsonand reads only each file'smeta(title, type, tags). Nothing else is parsed, and nothing leaves the machine. - Dashboard: a card per file with a type badge, tags, and path, plus search, filter by type/tag, and sort (recent / title / type). Unreadable files are still listed and flagged.
- Open in place: clicking a file loads its owning tool in an iframe and hands
it the file over a small embed protocol (
localoffice:open/:ready/:opened/:save/:saved). Each tool still works standalone; the receiver only activates when embedded. - Save back to the same file: file pickers are blocked in a cross-origin
subframe, so an embedded tool delegates the write to the Hub (the top
frame), which holds the directory handle and writes in place, even over
file://. Save reuses the handle; Save As always opens the picker for a new file. If the Hub has no handle (the "Files…" fallback), it shows the picker itself, so Save works whether you launch fromfile://or a host. - No localStorage shadow when embedded: the tools that autosave to
localStoragestandalone (LocalPlan, localMindMap) turn that off in the Hub. The file is the single source of truth, so there is no stale per-tool copy to drift or leak across the many files you open.
Verify headlessly: node verify-hub.js (48 checks): indexing/filter/sort/render,
the open-message split (text vs. handle), the Hub-mediated save handler, and the
real iframe + postMessage handoff for every tool via embed-harness.html. Each
tool's suite also asserts the embed handshake loads a file. The folder picker
needs a real browser gesture.
Copy and paste carries data between tools, not just within one. On copy, a
tool writes plain TSV/text (for any app) and a localoffice/clip-v1 fragment
smuggled in text/html (a [data-localoffice] span, the rich type the Clipboard
API allows). On paste, a tool reads that fragment, or falls back to the plain
text, and ingests it natively:
- LocalSheets copy emits a grid fragment of the selection.
- localMindMap paste turns a grid/TSV into a cluster of nodes (one label per row) under the selected node.
- localDeck paste turns a grid/TSV into a table slide; plain lines become a bullets slide.
Paste is always explicit (Ctrl/Cmd-V); nothing is auto-generated. Apps that
only see text/plain still interoperate (TSV in, TSV/lines out). The transforms
are covered by unit tests in each tool's suite.
Structured slide authoring: type content, pick a layout, reorder slides. It is
not a freeform canvas. Single file, no deps, offline. Reads/writes
localoffice/v1 (type: "slides").
- Layouts: Title, Title+Bullets, Two columns, Section, Quote, Image,
Image+caption, Text+image, Code, Table, Embedded object, Blank. The embedded-
object layout puts another tool's object on the slide (a sheet table, a mind map,
…), drawn by
LocalRenderand editable in a modal; A− / A+ scale it. - Pictures: insert by file picker, drag-and-drop, or paste (Ctrl/Cmd-V).
Every image is re-encoded through a
<canvas>, which strips all source metadata (EXIF/GPS/camera/author) and downscales it (longest side 1600px), so exports stay anonymous even with photos on the slides. - Code blocks: monospace, preserved indentation, language label, line numbers, S/M/L sizing. No syntax-highlight dependency.
- Editing: undo/redo (Ctrl+Z / Ctrl+Y), speaker notes (in the file and presenter view, never in the HTML export), optional footer and slide numbers.
- Templates: Engineering design review, Incident postmortem (RCA), Sprint demo, Research/field report. They are embedded in-file, since offline can't fetch.
- Export: standalone single-file HTML (arrow-key/click nav) and PDF (browser print, "Save as PDF"). The firewall holds: exports carry zero identifying metadata and zero speaker notes, only the deck title, slide content, and theme.
- Local AI (optional): the ✦ AI panel talks only to a local Ollama at
localhost:11434. It is the only network call, fully optional, and the AI never changes the deck on its own (it proposes, you apply). Assist mode puts freeform text into a field or notes; Draft-slide mode returns a reviewed JSON patch for the current slide. Setup:localDeck/OLLAMA_SETUP.md. - Light / dark editor: a ☾/☀ toggle switches the editor chrome (it follows the system setting until you choose, then persists). This is separate from the deck's own theme presets (midnight/slate/paper/warm), which style the slides.
Verify headlessly: node localDeck/verify-deck.js (138 checks, headless Chromium
via Playwright). The AI path runs against a mocked Ollama stream, so it works with
or without Ollama installed. The PDF print dialog, a live Ollama, and the
Firefox/Safari download fallback are the only paths that need a manual click.
Spaced-repetition flashcards (body type flashcards). Single file, no deps,
offline. Reads/writes localoffice/v1.
- Deterministic auto-grading via the shared verification kernel
(
exact/coverageonly), never an LLM. Cloze deletion auto-builds its own check from the hidden words. - Review: FSRS-lite scheduling, self-grade plus typed-answer auto-grade, reverse/both-way cards, study-card zoom, leech/stats panel.
- Authoring: card-type presets, sample decks (feature demo plus science, geography, language, all neutral examples), images on cards (canvas EXIF-scrub), TSV/JSON import and export.
- Optional local AI (
✦ AI, Ollama) is authoring-only and fenced from grading. It drafts cards and proposes keyword checks; the kernel always decides. The AI writes the test, the math grades it. - Theming: app light/dark mode plus per-card visual themes, with overlays that stay legible in every combination.
Verify headlessly: node localCards/verify-cards.js (104 checks).
Single-file spreadsheet (body type sheet). Vanilla JS, no deps, offline.
Refactored onto the shared envelope. See
localSheets/README.md.
- Engine: a broad formula function library, absolute refs (
$A$1), cross-sheet refs, insert/delete row and column with formula rewriting, fill-handle propagation. - Charts and sparklines: bar / line / pie as inline SVG;
=SPARKLINE(). - Back-compat: reads both the legacy
localsheetsformat and thelocaloffice/v1envelope, migrates old files transparently on open, and writes the envelope going forward, so existing users' files never break. - Clipboard: copy emits a
localofficegrid fragment (paste into localMindMap or localDeck). Optional local AI (✦ AI, Ollama) for formulas and JSON-patch edits. Opens in the Hub with in-place save.
Verify headlessly: node localSheets/src/test-engine.js (143),
test-store.js (58), test-envelope.js (23), and
node localSheets/verify-localsheets.js (52).
Single-file planner (body type plan) built on a tracks, sections, items
model, not a task or Gantt list. Vanilla JS, no deps, offline. See
localPlan/README.md.
- Structure: tracks (with icons), each holding horizon sections (Now / Soon /
Later / Someday) of items. Star items into a Today view, add recurring
items, drag to reorder, search with
/, and import via brain-dump or starter packs. Full undo / redo (Ctrl+Z/Ctrl+Y, plus toolbar buttons). - Optional local AI (
✦ AI, Ollama): Distill messy notes into plan items (through the existing braindump parser), Decompose a task into sub-steps, and Suggest priorities. Advisory, validated, never auto-applied. - Envelope save/open, zoom, light/dark. Opens in the Hub with in-place save.
Verify headlessly: node localPlan/verify-plan.js (55 checks, including
mocked-Ollama AI and a mocked-FS round-trip).
Single-file mindmap (body type mindmap). Vanilla JS canvas, no deps, offline.
Reads/writes localoffice/v1 (type: "mindmap").
- Canvas: drag the background to pan, scroll to zoom, Fit to frame
everything. Node positions are stored (
x/y) and edited by dragging. There is no automatic layout (deliberately out of v1 scope); new children land at simple deterministic offsets you can then move. - Structure: a parent/child tree (
node.parent) drawn as curved links, plus optional labeled cross-links (theedgesarray) via the Link tool. - Editing:
Tabadds a child,Entera sibling,F2/double-click edits in place,Delremoves a node and its subtree,Spacecollapses/expands (a collapsed node shows a+countbadge). - Categorize: tag nodes with a category color (
node.color) from the toolbar swatches, which matters for systems and architecture maps. Per-node text size (node.size, A-/A+) lets you emphasize headings. - Templates: three neutral starters (System architecture, Incident/RCA, Product roadmap), laid out cleanly so you skip the blank canvas.
- Tidy and align: AI-distilled maps and templates use a tidy-tree layout (depth to x, leaves stacked) so branches never overlap, and an optional Snap toggle rounds nodes to a grid as you drag. Tidy (selected branch) and Tidy all re-flow placement on demand, with Ctrl+Z undo. Both are explicit, user-invoked actions, never automatic, so manual layouts only change when you ask.
- Images on nodes: paste a screenshot (Ctrl+V) or pick a file onto the
selected node. Every image is re-encoded through a
<canvas>(the same scrub localDeck and localCards use), which strips EXIF/GPS/source metadata and downscales it, so a saved map is a safe offline lab notebook. - Touch: one-finger drag pans or moves nodes, two-finger pinch zooms, so it is usable on a tablet in the field.
- Files: New / Open / Save / Save As (
Ctrl+S) and Print. File System Access on Chrome/Edge, download/upload fallback elsewhere, withlocalStorageautosave. Unknown body fields are preserved on round-trip (forward-compat). - Theming: light/dark, following the system setting until you choose.
- Schematic affordances: tag a node with a logic-gate shape (AND/OR/XOR/NOT/MUX) from the toolbar; links between two gates port-anchor out to in for a signal-flow look. It is purely a diagram affordance, with no netlist and no simulation.
- Optional local AI (
✦ AI, Ollama): the same advisory, fenced pattern as the other tools. Expand node breaks the selected node into validated sub-children, Distill to map turns pasted text into a previewed nested map, and State machine from code plots an FSM (states to nodes, transitions to labeled signal-flow edges) from pasted code. It talks only to a local Ollama atlocalhost:11434(off by default) and never changes the map without your approval.
Verify headlessly: node localMindMap/verify-mindmap.js (122 checks, including
mocked-Ollama AI, color/image/touch, tidy-layout/snap/templates, tidy plus undo,
the Hub embed handshake, and a mocked-FS save and open round-trip).
An IP sanitizer, not a photo editor (body type image). Single file, no
deps, offline. Reads/writes localoffice/v1 (type: "image").
- Scrub on import: drop, pick, or paste an image and it is re-encoded through
a
<canvas>(EXIF/GPS/camera metadata structurally stripped, downscaled to 1600px). - Redaction is the moat, and it is destructive: confirming a blackout box flattens black pixels into the raster immediately, so the originals are erased from the file, not merely covered. A redaction marker is recorded for audit.
- Annotations: watermark text (tiled) and arrow callouts with labels are non-destructive vector overlays you can undo.
- Dimensional calibration: ⊢ Calibrate from a known feature (drag a line,
type its real length, e.g.
1.0 mm/5 cm/0.4 in) setsbody.scale, and 📏 Measure then labels distances in real-world units (3.20 mm). Pure arithmetic, no AI, no network. - QR overlay: stamp an asset tag or calibration URL as a QR code, using a dependency-free byte-mode encoder (EC level M, versions 1 to 6, about 106 chars). The encoder's math is verified offline (GF(256), Reed-Solomon zero-syndrome, data round-trip); confirm scannability once with your phone.
- Export: a flat, metadata-free PNG (base plus overlays composited).
- Save/Open as
localoffice/v1, light/dark, opens in the Hub with in-place save.
Verify headlessly: node localMark/verify-mark.js (32 checks), including a
pixel-level assertion that redaction erases the underlying pixels, the calibration
px-to-mm math, and the QR encoder's GF and Reed-Solomon math plus a data
round-trip. Physical QR scannability is a one-time manual check.
An interactive SOP / QA runbook (body type runbook), the verification kernel
turned into a product. Single file, no deps, offline.
- Steps: Check (tick to confirm), Measure (a typed value), Note.
- The gate: each Measure step is graded by the shared kernel,
exact/coverage/tolerance(for example "ambient temperature between 18 and 24", or "100 ± 2"), deterministically, never an LLM. A live PASS/FAIL badge updates as you type. - Sign-off is gated: you cannot sign off the runbook until every gating step passes. An out-of-bounds reading blocks it; the math decides, not a click.
- Integrity seal: signing records a SHA-256 of the content (
body.seal, via WebCrypto), and reopening re-verifies it. An edited reading turns the bar red (Integrity check FAILED). Honest framing: the hash is keyless, so it is tamper-evident, not unforgeable (and deterministic, so the no-LLM rule holds). - Conditional branching: mark a step diagnostic to hide it until a Measure step's on fail target branches to it. A failing reading unfolds the diagnostic block, a passing one folds it away: a state-machine runbook, not a linear PDF. Hidden diagnostics never gate, and the reveal is deterministic.
- Web Serial read: a Measure step's Read USB button pulls a value from a connected instrument and feeds it to the same kernel gate (Chrome/Edge, user-gesture, secure context; the parse logic is tested, the device read is a manual path).
- Neutral templates (Equipment readiness, Deployment checklist). Save/Open envelope, light/dark, opens in the Hub with in-place save.
This is where the kernel gained tolerance (still deterministic, see
src/verify.js). Verify headlessly: node localCheck/verify-check.js (42
checks), including the headline that out-of-bounds blocks sign-off and in-bounds
allows it, plus the sign, reopen, tamper-detected seal path, the fail-to-diagnostic
branch, and the Web Serial parse logic.
A block-based engineering-doc writer (body type doc) for design docs, RCAs,
and technical proposals, not a Word clone. Single file, no deps, offline.
- Sections, not a page: add headings and prose blocks, then reorder or remove.
- Compliance linter: give a section a required-keywords rule and the shared
coveragekernel gates it. Markdown export is blocked until every required section contains its keywords (for example, an RCA Mitigation must mentionpatch,deploy). Matching is whole-word (sodispatchedcan't satisfypatch) and a malformed rule fails closed. Deterministic, never an LLM. A live badge shows each section's status. - Templates: Incident postmortem (RCA), Engineering design doc, Technical proposal (RFC), each pre-wired with compliance rules.
- Optional local AI (
✦ AI): route pasted raw text (a chat thread, notes) into the document's sections as a previewed draft you approve. The AI fills prose; the compliance verdict stays the kernel's, never the AI's. - Export Markdown when compliant. Save/Open envelope, light/dark, Hub embed.
Verify headlessly: node localDoc/verify-doc.js (104 checks), including the
headline that export is blocked until mandatory keywords are present, the
keyword-matching truth-table, the malformed-rule fail-closed class, WYSIWYG round-
trip fidelity, and a mocked-Ollama distill.
One dependency-free module, usable from the browser (window.LocalOffice) or
Node (require). Responsibilities:
- Envelope:
createEnvelope(type, opts),validate(obj),normalize(obj). - Parse/serialize:
parse(text, {coerce}),stringify(env),serialize(env). Round-trips preserve unknown fields (forward-compat).coerceis the seam for reading older tool formats (used by the LocalSheets back-compat step). - Dispatch on type:
dispatch(env, handlers), falling back tohandlers.defaultfor unknown types (graceful degradation: show the title, offer hand-off). - File IO:
openFile,saveFile,saveFileAs,download. File System Access API on Chrome/Edge, manual download/upload fallback on Firefox/Safari.
Exports never stamp identifying metadata (author/user/machine), only
id/title/created/modified/app/tags.
LocalOffice is benchmarked on the floor, on purpose. The reference machine is a roughly 6-year-old (2019-class) thin-and-light: a 15 W mobile CPU (4 cores, 8 threads), 16 GB RAM, integrated-class graphics plus a small 2 GB entry discrete GPU too weak to accelerate local models, so AI inference runs on the CPU. The numbers describe a power class, not a brand.
Measured there with verify-perf.js (node verify-perf.js):
- ~1,113 KB (≈1.09 MB) for the entire 10-file suite, 0 dependencies, 0 build
step. No framework, no DOM-diffing runtime, no bundler. The source is the
app. (The shared modules (core, verify kernel, the
LocalRenderrenderer, and theLocalCSVadapter) are inlined into each tool rather than loaded, which keeps every file standalone at the cost of some duplication.) - Cold start (parse to interactive) of about 20 to 330 ms for nine of the ten tools on that CPU; the spreadsheet/formula engine, the heavy one, takes about 0.6 s. Roughly 10 MB JS heap per tool (browser-reported, coarse).
- No idle work: zero background polling, zero cloud sync, zero telemetry, zero DNS lookups. The only interval timers in the whole suite are sub-second AI-progress counters that exist only while a request you started is in flight, and they are always cleared, so the CPU can drop into deep sleep between interactions.
- Verification is effectively free: the deterministic kernel runs 300,000 checks in about 0.24 s (roughly 0.8 µs each) on that CPU. Because grading is math, not an LLM, it never re-enters the inference path, so the slow part (optional local AI) stays off the critical path entirely.
- Local AI is patient, and that is the point. On this CPU (no accelerator) a
small quantized model decodes at about 5 tok/s (a 3B model) down to 2.5
tok/s (a 7B model), measured with
node verify-perf.js --ai. You would never put that on a critical path. Here it is only ever advisory (draft a slide, route prose), so the speed is a non-issue and the deterministic kernel does the instant work.
This is a small but real software/hardware co-design stance: shape the workload to the silicon you actually have. The "AI advises, deterministic math decides" split is a scheduling decision (burst the accelerator for generation, keep grading on cheap low-power cores), and it is what makes a slow local model useful on weak hardware, because nothing time-critical waits on inference.
Who it's for:
- People who choose to stay local: privacy, data ownership, no cloud, no account, no telemetry.
- People who have to stay local: old hardware, low or no connectivity, no budget for subscriptions.
Both want the same thing, software that runs fully local on modest hardware and offline, so one tool serves both.
Headroom note: on current GPU/NPU silicon the optional local AI is far faster, but those numbers aren't the point and aren't quoted here. The floor is the claim; the ceiling takes care of itself.
Run every suite with one command. The LocalSheets tool lives here as a subdirectory, so a single clone of this repo has everything it needs:
node run-all-tests.js
It executes all suites as child processes and passes only if every suite passes
(exit code plus the parsed N passed, M failed). Current state: 1098 passed, 0
failed across 17 suites. The browser suites need Playwright (dev tooling only):
install it once with npm i -D playwright && npx playwright install chromium, or
point PLAYWRIGHT_DIR at an existing install. Every tool's File System Access path (showSaveFilePicker
to createWritable/write/close to showOpenFilePicker to getFile) is
covered headlessly via an in-memory FS mock (fs-mock.js). The embedded suite
drives each tool in a real iframe and reaches into the frame to confirm it renders
the opened file, delegates Save back to the Hub (Ctrl+S posts the serialized
envelope of the right type instead of calling a blocked picker), and (for the
autosaving tools) writes no localStorage shadow while embedded:
| Suite | Checks |
|---|---|
| LocalOffice Hub (dashboard + embed) | 48 |
| LocalOffice embedded (rendering layer + Save delegation) | 26 |
| LocalOffice core (loader/saver) | 33 |
| LocalOffice verify kernel | 33 |
| localDeck (headless browser) | 98 |
| localCards (headless browser) | 100 |
| LocalSheets engine | 143 |
| LocalSheets Store | 58 |
| LocalSheets envelope + interop | 23 |
| LocalSheets app (shipped html) | 46 |
| LocalPlan (envelope + advisory AI) | 51 |
| localMindMap (mindmap + advisory AI) | 88 |
| localMark (image sanitizer) | 32 |
| localCheck (QA runbook) | 36 |
| localDoc (compliance writer) | 82 |
| Templates (sheet/plan presets) | 16 |
Individual suites:
node src/test-core.js # shared core (no deps)
node src/test-verify.js # verification kernel (no deps)
node localDeck/verify-deck.js # localDeck, headless Chromium
node localCards/verify-cards.js # localCards, headless Chromium
node localSheets/src/test-envelope.js # envelope adapter + cross-tool interop
The browser IO layer (file pickers, downloads), the PDF print dialog, a live Ollama, and the Firefox/Safari fallback are the only paths checked by hand.
Separately, node verify-perf.js runs the footprint and cold-start benchmark
(28 checks plus a numbers table, see Hardware floor & accessibility).
It is kept out of run-all-tests.js on purpose: the timing numbers are
machine-dependent, so it is a report you run on a target machine, not a pass/fail
gate. Its assertions (self-contained files, no idle-timer leaks, clean boot,
near-zero kernel cost) are machine-independent. Add --ai
(node verify-perf.js --ai) to also measure local-AI decode throughput when a
local Ollama is running.
MIT. Use it, fork it, sell it, give it away. Each tool's folder has its own README with the details.
