···6677## [Unreleased]
8899-### Added
1010-- Both: accessibility, mobile responsive, sharing/permissions (#39)
1111-- Both: right-click context menus, print layout (#38)
1212-- Docs: outline sidebar, table improvements, link previews, zen mode (#37)
1313-- Sheets: drag-to-fill, paste special, format painter, virtual scrolling (#36)
1414-- Sheets: cross-sheet refs, named ranges, formula tracer (#35)
1515-- Sheets: status bar, formula auto-complete, cell notes (#34)
1616-- Collaboration: version history, suggesting mode, offline support (#33)
1717-- Landing: sort, star/favorite, soft-delete/trash, folders, search, username prompt (#32)
1818-- Sheets: charts via Chart.js, multi-column filter/sort (#31)
1919-- Sheets: conditional formatting, data validation, cell borders, wrap text, striped rows (#30)
2020-- Import/Export: PDF export, .docx import, .xlsx import (#29)
2121-- Docs: line spacing, paragraph spacing, page breaks, spell check (#28)
2222-- Add markdown autoformatting input rules (#27)
2323-- Toolbar polish: make more similar to Google Docs (#26)
2424-- Add dark mode support to docs and sheets (#23)
2525-- Sheets: column auto-fit width based on content (#19)
2626-- Sheets: improved drag-to-select range visual feedback (#18)
2727-- Docs+Sheets: autosave indicator with last saved timestamp (#17)
2828-- Docs: word and character count in footer (#16)
2929-- Docs: keyboard shortcut cheatsheet modal (#15)
3030-- Docs: indent/outdent for lists and paragraphs (#14)
3131-- Sheets: auto-detect CSV headers on import (#13)
3232-- Sheets: implement VLOOKUP and HLOOKUP formulas (#12)
3333-- Sheets: cell merging support (#11)
3434-- Docs: font size control beyond heading levels (#10)
3535-- Docs+Sheets: inline comments and annotations (#8)
3636-- Sheets: frozen panes (lock header rows/columns) (#7)
3737-- Sheets: resizable column widths (#6)
3838-- Docs: find and replace (Cmd+F / Cmd+H) (#5)
3939-- Toolbar: responsive collapse on narrow windows (#4)
4040-- Toolbar: add hover tooltips to all buttons (#3)
4141-- Toolbar: grouped dropdowns for alignment and list buttons (#2)
4242-- Toolbar: collapsible overflow menu for less-used items (#1)
99+### Documents
1010+- Outline sidebar with navigable H1/H2/H3 heading tree (#37)
1111+- Floating table toolbar: row/column manipulation, cell merge/split, header toggle, cell color (#37)
1212+- Link preview tooltips on hover: URL display, Open/Edit/Remove actions (#37)
1313+- Zen mode (Cmd+Shift+F): distraction-free editing with hidden toolbar (#37)
1414+- Slash commands (`/`): Notion-style command palette with 15 block types (#37)
1515+- Block handles: Notion-style drag handle with context menu (Turn into, Delete, Duplicate, Move) (#37)
1616+- Markdown source toggle (Cmd+Shift+M): switch between WYSIWYG and raw markdown editing
1717+- Markdown export via Turndown with GFM support (tables, task lists, strikethrough, code languages)
1818+- Markdown import via markdown-it with GFM extensions (tables, task lists, strikethrough)
1919+- Markdown autoformatting input rules: `#`, `##`, `-`, `1.`, `>`, `[]`, `---`, backticks, `**`, `~~`, `[text](url)` (#27)
2020+- Line spacing presets (1, 1.15, 1.5, 2, 2.5, 3) and paragraph spacing controls (#28)
2121+- Page breaks with print-friendly rendering (#28)
2222+- Inline comments with author, timestamp, and text (#8)
2323+- Suggesting mode (track changes): insert/delete marks with session grouping, accept/reject per suggestion or bulk (#33)
2424+- Find and replace (Cmd+F / Cmd+H) with match highlighting and active match indicator (#5)
2525+- Font size control beyond heading levels (#10)
2626+- Font family selection, text color, highlight color
2727+- Text alignment (left, center, right, justify)
2828+- Subscript, superscript support
2929+- Task lists with checkboxes (nestable)
3030+- Indent/outdent for paragraphs, headings, and lists (Cmd+]/Cmd+[) (#14)
3131+- PDF export via html2pdf.js with light-mode rendering (#29)
3232+- .docx import via mammoth.js with heading style mapping (#29)
3333+- Word and character count in footer (#16)
3434+- Keyboard shortcut cheatsheet modal (#15)
3535+3636+### Spreadsheets
3737+- Custom grid engine with 100x26 default grid, cell editing, formula bar
3838+- Formula engine: recursive descent parser with 50+ functions across math, text, date, lookup, conditional categories
3939+- Functions: SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, MEDIAN, STDEV, ABS, ROUND, ROUNDUP, ROUNDDOWN, INT, MOD, POWER, SQRT, LOG, LN, EXP, PI, RAND, IF, AND, OR, NOT, IFERROR, CONCATENATE, LEN, LEFT, RIGHT, MID, UPPER, LOWER, TRIM, SUBSTITUTE, FIND, SEARCH, TEXT, VALUE, NOW, TODAY, DATE, YEAR, MONTH, DAY, VLOOKUP, HLOOKUP, INDEX, MATCH, SUMIF, COUNTIF, AVERAGEIF (#12)
4040+- Cross-sheet references: `Sheet2!A1`, `'Sheet Name'!A1:B5` (#35)
4141+- Named ranges: human-friendly aliases for cell ranges in formulas (#35)
4242+- Formula autocomplete dropdown with function signatures (#34)
4343+- Formula tracer: trace precedents (inputs) and dependents (outputs) of any cell (#35)
4444+- Charts via Chart.js: bar, line, pie, scatter with auto-detected headers and axis labels (#31)
4545+- Multi-column filter: per-column value checkboxes (#31)
4646+- Multi-column sort: up to 3 sort levels, stable sort (#31)
4747+- Conditional formatting: 7 rule types (greaterThan, lessThan, equalTo, between, textContains, isEmpty, isNotEmpty) with custom colors (#30)
4848+- Data validation: list (dropdown), numberBetween, textLength with visual indicators (#30)
4949+- Cell borders: per-side and preset (all, outline, none) (#30)
5050+- Wrap text toggle per cell (#30)
5151+- Striped rows (alternating background) (#30)
5252+- Cell notes: plain text annotations with triangle indicator and hover tooltips (#34)
5353+- Status bar: SUM, AVERAGE, COUNT, MIN, MAX for multi-cell selections (#34)
5454+- Resizable column widths with drag handles (#6)
5555+- Column auto-fit on double-click (measure content width) (#19)
5656+- Frozen panes: lock header rows and columns (#7)
5757+- Cell merging and unmerging with colspan/rowspan (#11)
5858+- Drag-to-fill: pattern detection (number sequences, date sequences, formula adjustment, text repeat) (#36)
5959+- Paste special: values only, formulas only, formatting only, transpose (#36)
6060+- Format painter: copy cell formatting and apply to other cells (#36)
6161+- Multi-sheet tabs with add/rename/switch (#36)
6262+- .xlsx import via SheetJS with values, formulas, bold, number formats (#29)
6363+- CSV paste with auto-detect headers (#13)
6464+- Improved drag-to-select range visual feedback with border indicators (#18)
6565+- Virtual scrolling module for large sheet performance (pure logic ready) (#36)
6666+6767+### Collaboration
6868+- Real-time editing via Yjs CRDT with encrypted WebSocket provider
6969+- Awareness protocol: colored cursors with usernames
7070+- Encrypted sync: all Yjs messages AES-256-GCM encrypted before transmission
7171+- Automatic encrypted snapshot persistence (debounced saves + periodic saves)
7272+- Reconnection with exponential backoff and jitter
7373+- Version history: automatic capture (edit threshold or time threshold), FIFO pruning (max 50), word count deltas, restore (#33)
7474+- Suggesting mode: track changes with insert/delete marks, session grouping, accept/reject (#33)
7575+- Offline support: online/offline detection, change queue, cache strategy (#33)
7676+7777+### Sharing & Permissions
7878+- Share dialog with URL builder, mode selector (edit/view), copy to clipboard (#39)
7979+- View-only mode: disables toolbar and editing, shows badge (#39)
8080+- Link expiry: 1h, 1d, 7d, 30d options with server-side enforcement (HTTP 410) (#39)
8181+- E2EE key in URL fragment: encryption key never sent to server
8282+8383+### Landing Page
8484+- Document list with type icons, decrypted names, last updated timestamps
8585+- Sort: by last updated, created, name, or type; starred items sort first (#32)
8686+- Search: filter by decrypted document name (#32)
8787+- Folders: create, rename, delete; move documents between folders; breadcrumbs (#32)
8888+- Favorites/Stars: star/unstar with priority sorting (#32)
8989+- Trash: soft delete with 30-day auto-purge; restore or permanently delete (#32)
9090+- Username prompt on first visit; click badge to change (#32)
9191+9292+### Platform & UX
9393+- Dark mode: automatic (prefers-color-scheme), manual toggle, persisted (#23)
9494+- Right-click context menus for docs (text, link, image, table) and sheets (cell, column header, row header) with keyboard navigation (#38)
9595+- Print layout: page size presets, margin presets, headers/footers, pagination for docs; grid lines, header repeat, scaling for sheets (#38)
9696+- Responsive toolbar: collapse overflow items on narrow viewports (#4)
9797+- Hover tooltips on all toolbar buttons (#3)
9898+- Grouped dropdowns for alignment and list buttons (#2)
9999+- Collapsible overflow menu for less-used toolbar items (#1)
100100+- Autosave indicator with last saved timestamp (#17)
101101+- Skip links and ARIA roles for accessibility (#39)
102102+- Mobile-responsive layout with touch-friendly targets (#39)
103103+- System fonts only (Charter, system-ui, ui-monospace) -- no external font dependencies
104104+- OkLCH color system for perceptual uniformity across themes
105105+- Legacy localStorage key migration (crypt-* to tools-*)
4310644107### Fixed
45108- Fix Docker image reference in homelab-nix crypt-tools service (#24)
46109- Docs+Sheets: proper Tab key support (indent in docs, cell navigation in sheets) (#20)
4747-- Sheets: wire up Ctrl+Z/Cmd+Z undo keyboard binding (#9)
110110+- Sheets: wire up Ctrl+Z/Cmd+Z undo keyboard binding via Yjs UndoManager (#9)
4811149112### Changed
5050-- Add comprehensive tests for all new features (#25)
51113- Rename all crypt references to tools across the codebase (#22)
114114+- Add comprehensive test suite: 60+ test files covering all pure logic modules (#25)
115115+- Toolbar redesign to match Google Docs visual style (#26)
+460
CONTRIBUTING.md
···11+# Contributing to Tools
22+33+## Development Environment Setup
44+55+### Prerequisites
66+77+- Node.js 22+ (LTS recommended)
88+- npm 10+
99+1010+### Getting Started
1111+1212+```bash
1313+git clone <repo-url> tools
1414+cd tools
1515+npm install
1616+npm run dev
1717+```
1818+1919+This starts two servers concurrently:
2020+- **Express** on `http://localhost:3000` -- API, WebSocket relay, and production static files
2121+- **Vite** on `http://localhost:5173` -- Frontend with hot module replacement
2222+2323+Vite proxies `/api` and `/ws` requests to Express (configured in `vite.config.js`).
2424+2525+Open `http://localhost:5173` to start developing.
2626+2727+### HTTPS for Local Development
2828+2929+The Web Crypto API (`crypto.subtle`) requires a secure context. `localhost` qualifies, so HTTPS is not needed for local development. If you need HTTPS (e.g., testing on a LAN IP), generate a self-signed certificate:
3030+3131+```bash
3232+openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
3333+```
3434+3535+Place `cert.pem` and `key.pem` in the project root (or `DATA_DIR`). The server will detect them and start HTTPS on port 3443.
3636+3737+## Project Structure
3838+3939+```
4040+tools/
4141+ server.js # Express server, WebSocket relay, SQLite, REST API
4242+ vite.config.js # Vite build configuration (multi-page app)
4343+ package.json # Dependencies and scripts
4444+ CLAUDE.md # AI assistant context (architecture, key files)
4545+ PRODUCT.md # Product vision, features, roadmap
4646+ CHANGELOG.md # Release notes
4747+ Dockerfile # Multi-stage Docker build
4848+4949+ src/ # Frontend source code
5050+ index.html # Landing page
5151+ landing.js # Landing page DOM logic
5252+ landing-utils.js # Pure utility functions (sort, filter, folders, trash, search)
5353+5454+ docs/ # Document editor
5555+ index.html # Docs editor page
5656+ main.js # TipTap editor setup, toolbar, all feature integration
5757+ extensions/ # Custom TipTap extensions
5858+ font-size.js # Font size attribute on TextStyle marks
5959+ indent.js # Paragraph indentation (Cmd+]/[)
6060+ comment.js # Inline comment marks
6161+ line-spacing.js # Line height attribute on paragraphs/headings
6262+ paragraph-spacing.js # Margin-bottom attribute on paragraphs/headings
6363+ page-break.js # Page break node
6464+ suggestion-insert.js # Suggesting mode: inserted text mark
6565+ suggestion-delete.js # Suggesting mode: deleted text mark
6666+ markdown-autoformat.js # Markdown input rules ([text](url))
6767+ slash-commands.js # Notion-style "/" command palette (TipTap integration)
6868+ slash-menu.js # Slash command items, categories, filtering (pure logic)
6969+ block-handle.js # Block drag handle and context menu (pure logic)
7070+ outline.js # Document outline extraction and tree building
7171+ zen-mode.js # Distraction-free editing state
7272+ link-preview.js # Link hover tooltip positioning
7373+ table-toolbar.js # Floating table manipulation toolbar
7474+ search-replace.js # Find & replace TipTap extension
7575+ search-state.js # Find & replace state management
7676+ tab-support.js # Tab key behavior in docs
7777+ tab-handler.js # Tab key handler logic
7878+ autoformat-rules.js # Markdown autoformat rule inventory
7979+ pdf-export.js # PDF generation via html2pdf.js
8080+ docx-import.js # .docx import via mammoth.js
8181+ markdown-export.js # HTML-to-Markdown via Turndown
8282+ markdown-parser.js # Markdown-to-HTML via markdown-it
8383+ markdown-toggle.js # WYSIWYG/Markdown source toggle
8484+8585+ sheets/ # Spreadsheet editor
8686+ index.html # Sheets editor page
8787+ main.js # Grid engine, cell editing, toolbar, all feature integration
8888+ formulas.js # Formula tokenizer, parser, evaluator (50+ functions)
8989+ charts.js # Chart.js integration (config, data extraction, transforms)
9090+ filter.js # Multi-column filter logic
9191+ sort.js # Multi-column sort logic
9292+ conditional-format.js # Conditional formatting rule evaluation
9393+ data-validation.js # Cell validation (list, numberBetween, textLength)
9494+ cell-styles.js # Border, wrap text, and striped row CSS generation
9595+ cell-notes.js # Cell annotation CRUD
9696+ status-bar.js # Selection stats (SUM, AVERAGE, COUNT, MIN, MAX)
9797+ formula-autocomplete.js # Function name suggestions and keyboard navigation
9898+ formula-tracer.js # Precedent/dependent cell tracing
9999+ cross-sheet.js # Cross-sheet reference parsing and resolution
100100+ named-ranges.js # Named range validation, CRUD, and resolution
101101+ drag-fill.js # Drag-to-fill pattern detection and value generation
102102+ format-painter.js # Format extraction and application
103103+ paste-special.js # Values-only, formulas-only, formatting-only, transpose
104104+ virtual-scroll.js # Visible row range calculation for large sheets
105105+ xlsx-import.js # .xlsx import via SheetJS
106106+ tab-handler.js # Tab key handler for cell navigation
107107+108108+ lib/ # Shared modules
109109+ crypto.js # AES-256-GCM: generateKey, importKey, exportKey, encrypt, decrypt
110110+ provider.js # Encrypted Yjs WebSocket provider with persistence
111111+ version-history.js # Version capture triggers, FIFO pruning, word count deltas
112112+ offline.js # Online/offline detection, change queue, cache strategy
113113+ suggesting.js # Suggesting mode: sessions, accept/reject logic
114114+ share-dialog.js # Share URL building, view-only mode, expiry computation
115115+ print-layout.js # Print HTML generation (docs pages, sheets tables)
116116+ context-menu.js # Right-click context menu component with keyboard nav
117117+118118+ css/
119119+ app.css # All styles (~4000 lines): tokens, components, responsive, dark mode
120120+121121+ tests/ # Test suite (Vitest)
122122+ *.test.js # 60+ test files covering all pure logic modules
123123+```
124124+125125+## Architecture Principles
126126+127127+### Pure Logic Modules
128128+129129+Every non-trivial feature is split into two layers:
130130+131131+1. **Pure logic module** -- no DOM, no browser APIs, fully testable. Lives in a `.js` file that exports pure functions and/or classes.
132132+2. **DOM integration** -- lives in `main.js` (or the HTML file). Imports the pure module and wires it to the DOM.
133133+134134+This pattern is used throughout: `formulas.js`, `filter.js`, `sort.js`, `conditional-format.js`, `suggesting.js`, `version-history.js`, `offline.js`, `slash-menu.js`, `block-handle.js`, etc.
135135+136136+**When adding a new feature, always start with the pure logic module and its tests.**
137137+138138+### Yjs Integration
139139+140140+All collaborative data flows through Yjs shared types:
141141+142142+- **Docs:** `ydoc` is passed to TipTap via `@tiptap/extension-collaboration`. The editor reads/writes ProseMirror state via the Yjs XML fragment.
143143+- **Sheets:** `ydoc.getMap('sheets')` contains per-sheet Y.Maps, each with `cells` (Y.Map of cell data), `colWidths`, `freezeRows`, `freezeCols`, `cfRules`, `validations`, `merges`, `notes`, etc.
144144+145145+When adding a new synced feature to sheets:
146146+1. Add a Yjs shared type (usually a Y.Map or Y.Array) inside the sheet map
147147+2. Add getter/setter helper functions
148148+3. Read the Yjs data in `renderGrid()` or `refreshVisibleCells()`
149149+4. Write to Yjs in toolbar handlers or cell edit commits
150150+5. Changes automatically sync to all connected peers
151151+152152+### Encryption
153153+154154+All data that leaves the browser is encrypted:
155155+- **Snapshots:** `Y.encodeStateAsUpdate(doc)` -> `encrypt(bytes, key)` -> PUT to server
156156+- **WebSocket messages:** Yjs sync/update messages -> `encrypt(plain, key)` -> `ws.send(encrypted)`
157157+- **Document names:** `encryptString(name, key)` -> base64 -> stored in DB
158158+159159+When adding features that store data on the server, always encrypt first.
160160+161161+## How to Add a New Formula Function
162162+163163+Formula functions are defined in `src/sheets/formulas.js` in the `callFunction()` switch statement.
164164+165165+### Step 1: Add the implementation
166166+167167+```javascript
168168+// In callFunction() switch statement:
169169+case 'MYFUNCTION': {
170170+ const n = nums(args);
171171+ // Your implementation here
172172+ return result;
173173+}
174174+```
175175+176176+The `args` parameter is an array where each element is either:
177177+- A scalar value (number, string, boolean)
178178+- A flat array (from a range like `A1:A10`) with `_rangeRows` and `_rangeCols` metadata
179179+180180+Helper functions available:
181181+- `flat(args)` -- flatten all range arrays, filter out empty values
182182+- `nums(args)` -- flatten, convert to numbers, filter out NaN
183183+- `toNum(v)` -- convert a single value to a number (empty/null -> 0)
184184+185185+### Step 2: Add to the autocomplete list
186186+187187+In `src/sheets/formula-autocomplete.js`, add an entry to `FORMULA_FUNCTIONS`:
188188+189189+```javascript
190190+{ name: 'MYFUNCTION', signature: 'MYFUNCTION(arg1, [arg2])' },
191191+```
192192+193193+### Step 3: Write tests
194194+195195+In `tests/formulas.test.js` or `tests/formulas-extended.test.js`:
196196+197197+```javascript
198198+import { evaluate } from '../src/sheets/formulas.js';
199199+200200+describe('MYFUNCTION', () => {
201201+ const get = (ref) => {
202202+ const cells = { A1: 10, A2: 20, A3: 30 };
203203+ return cells[ref] ?? '';
204204+ };
205205+206206+ it('returns expected result', () => {
207207+ expect(evaluate('MYFUNCTION(A1, A2)', get)).toBe(expectedValue);
208208+ });
209209+210210+ it('handles empty arguments', () => {
211211+ expect(evaluate('MYFUNCTION()', get)).toBe(expectedDefault);
212212+ });
213213+});
214214+```
215215+216216+### Step 4: Run tests
217217+218218+```bash
219219+npm test
220220+```
221221+222222+## How to Add a New TipTap Extension
223223+224224+TipTap extensions live in `src/docs/extensions/`. Each extension is a self-contained module.
225225+226226+### Step 1: Create the extension file
227227+228228+```javascript
229229+// src/docs/extensions/my-feature.js
230230+import { Extension } from '@tiptap/core';
231231+// Or: import { Mark } from '@tiptap/core'; for inline marks
232232+// Or: import { Node } from '@tiptap/core'; for block nodes
233233+234234+export const MyFeature = Extension.create({
235235+ name: 'myFeature',
236236+237237+ addOptions() {
238238+ return { /* default options */ };
239239+ },
240240+241241+ // For marks/nodes: addAttributes(), parseHTML(), renderHTML()
242242+243243+ addCommands() {
244244+ return {
245245+ doMyThing: (param) => ({ commands }) => {
246246+ // Editor command implementation
247247+ },
248248+ };
249249+ },
250250+251251+ addKeyboardShortcuts() {
252252+ return {
253253+ 'Mod-Shift-X': () => this.editor.commands.doMyThing(),
254254+ };
255255+ },
256256+});
257257+```
258258+259259+### Step 2: Register the extension
260260+261261+In `src/docs/main.js`, import and add to the editor's extensions array:
262262+263263+```javascript
264264+import { MyFeature } from './extensions/my-feature.js';
265265+266266+const editor = new Editor({
267267+ extensions: [
268268+ // ... existing extensions
269269+ MyFeature,
270270+ ],
271271+});
272272+```
273273+274274+### Step 3: Add toolbar UI (if needed)
275275+276276+In `src/docs/main.js`, add a toolbar button handler:
277277+278278+```javascript
279279+document.getElementById('btn-my-feature').addEventListener('click', () => {
280280+ editor.chain().focus().doMyThing().run();
281281+});
282282+```
283283+284284+And in `src/docs/index.html`, add the toolbar button.
285285+286286+### Step 4: Add to slash commands (if applicable)
287287+288288+In `src/docs/slash-menu.js`, add to `SLASH_COMMAND_ITEMS`:
289289+290290+```javascript
291291+{
292292+ id: 'myFeature',
293293+ name: 'My Feature',
294294+ description: 'Description for the command palette',
295295+ category: 'advanced',
296296+ icon: 'icon',
297297+ shortcut: 'Mod+Shift+X',
298298+},
299299+```
300300+301301+And in `src/docs/extensions/slash-commands.js`, add to `getCommandExecutor()`.
302302+303303+## How to Add a New Sheet Feature
304304+305305+### Step 1: Create the pure logic module
306306+307307+```javascript
308308+// src/sheets/my-feature.js
309309+310310+/**
311311+ * My Feature - brief description.
312312+ * Pure functions for [what it does].
313313+ */
314314+315315+export function computeMyThing(input) {
316316+ // Pure logic, no DOM
317317+ return result;
318318+}
319319+```
320320+321321+### Step 2: Write tests
322322+323323+```javascript
324324+// tests/my-feature.test.js
325325+import { describe, it, expect } from 'vitest';
326326+import { computeMyThing } from '../src/sheets/my-feature.js';
327327+328328+describe('computeMyThing', () => {
329329+ it('handles basic case', () => {
330330+ expect(computeMyThing(input)).toBe(expected);
331331+ });
332332+});
333333+```
334334+335335+### Step 3: Add Yjs storage (if the feature has persistent state)
336336+337337+In `src/sheets/main.js`, add getter/setter functions near the top of the file:
338338+339339+```javascript
340340+function getMyFeatureState() {
341341+ const sheet = getActiveSheet();
342342+ if (!sheet.has('myFeature')) sheet.set('myFeature', new Y.Map());
343343+ return sheet.get('myFeature');
344344+}
345345+```
346346+347347+### Step 4: Integrate with rendering
348348+349349+If the feature affects cell display, modify `renderGrid()` and/or `refreshVisibleCells()`.
350350+351351+### Step 5: Add toolbar / keyboard handlers
352352+353353+Wire up buttons or keyboard shortcuts in the toolbar section of `main.js`.
354354+355355+## Testing Conventions
356356+357357+### Framework: Vitest
358358+359359+Tests live in `tests/` and use the `.test.js` extension. Run with:
360360+361361+```bash
362362+npm test # Run all tests once
363363+npx vitest watch # Watch mode
364364+npx vitest run tests/formulas.test.js # Run a specific file
365365+```
366366+367367+### What to Test
368368+369369+- **All pure logic modules** -- formulas, filter, sort, conditional-format, data-validation, etc.
370370+- **State management classes** -- VersionManager, SuggestionManager, OfflineManager, etc.
371371+- **Import/export functions** -- docx-import, xlsx-import, pdf-export, markdown conversion
372372+- **Edge cases** -- empty inputs, large numbers, special characters, Unicode
373373+- **Error handling** -- invalid formulas, corrupt files, missing data
374374+375375+### What NOT to Test (in unit tests)
376376+377377+- DOM manipulation (covered by integration tests or manual testing)
378378+- TipTap editor behavior (tested via TipTap's own test suite)
379379+- Network requests (mocked in integration tests)
380380+381381+### Test Style
382382+383383+```javascript
384384+import { describe, it, expect } from 'vitest';
385385+import { myFunction } from '../src/path/to/module.js';
386386+387387+describe('myFunction', () => {
388388+ it('returns correct result for basic input', () => {
389389+ expect(myFunction('input')).toBe('expected');
390390+ });
391391+392392+ it('handles edge case: empty input', () => {
393393+ expect(myFunction('')).toBe(defaultValue);
394394+ });
395395+396396+ it('handles edge case: null', () => {
397397+ expect(myFunction(null)).toBe(defaultValue);
398398+ });
399399+});
400400+```
401401+402402+## CSS Conventions
403403+404404+### Design Tokens
405405+406406+All colors, spacing, shadows, and fonts are defined as CSS custom properties in `:root`:
407407+408408+```css
409409+:root {
410410+ --color-bg: oklch(0.965 0.005 75);
411411+ --color-text: oklch(0.22 0.02 55);
412412+ --color-accent: oklch(0.52 0.14 25);
413413+ /* ... */
414414+}
415415+```
416416+417417+### OkLCH Color System
418418+419419+Colors use the OkLCH color space (`oklch(lightness chroma hue)`) for perceptual uniformity:
420420+- **Lightness**: 0 (black) to 1 (white)
421421+- **Chroma**: 0 (gray) to ~0.4 (saturated)
422422+- **Hue**: 0-360 degrees (25 = terracotta/accent, 75 = warm neutral, 155 = success green, 195 = teal/encrypted)
423423+424424+### Dark Mode
425425+426426+Dark mode overrides are in `[data-theme="dark"]` and `@media (prefers-color-scheme: dark)`:
427427+428428+```css
429429+[data-theme="dark"] {
430430+ --color-bg: oklch(0.16 0.005 75);
431431+ --color-text: oklch(0.88 0.01 75);
432432+ /* ... */
433433+}
434434+```
435435+436436+When adding new UI, always define colors using custom properties so dark mode works automatically.
437437+438438+### Fonts
439439+440440+Three font stacks, no external font dependencies:
441441+- `--font-display`: Charter (serif, for headings)
442442+- `--font-body`: system-ui (sans-serif, for UI and body text)
443443+- `--font-mono`: ui-monospace (for code and sheet cells)
444444+445445+### Responsive Breakpoints
446446+447447+- `768px`: Hide non-essential toolbar items (`.toolbar-mobile-hide`), show mobile "More" button
448448+- `640px`: Stack layouts, larger touch targets
449449+- `480px`: Minimal layout, even larger touch targets
450450+451451+## PR Process
452452+453453+1. **Branch** from `main`: `feat/<description>`, `fix/<description>`, `chore/<description>`
454454+2. **Write tests first** (red), then implementation (green), then refactor
455455+3. **Run tests**: `npm test`
456456+4. **Build check**: `npm run build` (ensure no build errors)
457457+5. **Commit** with conventional commit messages: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`
458458+6. **Push** to feature branch, create PR with clear description
459459+7. **CI must pass** before merge
460460+8. **Squash merge** for multi-commit PRs, regular merge for single-commit
+855
PRODUCT.md
···11+# Tools — Product Document
22+33+**Tools** is a privacy-first, end-to-end encrypted collaborative office suite built for individuals and small teams who refuse to trade privacy for productivity.
44+55+---
66+77+## 1. Vision
88+99+### What is Tools?
1010+1111+Tools is a self-hosted, E2EE office suite that provides Google Docs-caliber collaborative editing without exposing a single byte of document content to the server. It currently ships two surfaces — **Documents** and **Spreadsheets** — with forms, slides, and diagrams on the roadmap.
1212+1313+### Who is it for?
1414+1515+- Privacy-conscious individuals who want a real office suite, not just a notes app
1616+- Small teams and families on a shared Tailscale network who need collaborative editing
1717+- Power users who want keyboard-driven workflows with zero vendor lock-in
1818+- Developers who value self-hosting and minimal dependencies over SaaS subscriptions
1919+2020+### What makes it different?
2121+2222+| | Google Docs / Notion | Cryptee / Standard Notes | Tools |
2323+|---|---|---|---|
2424+| **Full office suite** (docs, sheets, charts) | Yes | No (notes only) | Yes |
2525+| **Real-time collaboration** | Yes | No | Yes |
2626+| **End-to-end encrypted** | No | Yes | Yes |
2727+| **Self-hosted** | No | Partial | Yes |
2828+| **Zero dependencies on cloud services** | No | No | Yes |
2929+| **Offline-capable** | Partial | Yes | Yes (with queued sync) |
3030+3131+The key insight: encryption and collaboration are not mutually exclusive. Tools uses Yjs CRDTs for conflict-free merging and AES-256-GCM encryption on every byte that leaves the browser. The server is a dumb relay — it never sees plaintext.
3232+3333+---
3434+3535+## 2. Architecture Overview
3636+3737+### Stack
3838+3939+```
4040+Browser (vanilla JS)
4141+ ├─ TipTap (ProseMirror) → rich text docs
4242+ ├─ Custom grid engine → spreadsheets
4343+ ├─ Chart.js → data visualization
4444+ ├─ Yjs CRDT → conflict-free collaboration
4545+ └─ Web Crypto API → AES-256-GCM E2EE
4646+4747+Server (Node.js)
4848+ ├─ Express → REST API + static serving
4949+ ├─ WebSocket relay → encrypted message relay
5050+ ├─ SQLite (WAL mode) → encrypted document storage
5151+ └─ Optional HTTPS → self-signed for crypto.subtle secure context
5252+```
5353+5454+### Key Architectural Decisions
5555+5656+**Yjs CRDT for collaboration.** Every document is a Yjs Doc. Edits merge deterministically without a central authority. The server never resolves conflicts — it relays encrypted binary messages between connected peers.
5757+5858+**Encryption key in the URL fragment.** The `#` fragment is never sent to the server per the HTTP spec. When a user opens `tools.example.com/docs/abc123#keybase64`, the browser extracts the key from the hash, imports it via `crypto.subtle.importKey()`, and uses it for all encrypt/decrypt operations. The server only knows the document ID.
5959+6060+**Encrypted snapshots.** The full Yjs document state is periodically encrypted and stored server-side. This allows new clients to bootstrap without waiting for a peer — the server sends the encrypted snapshot, the client decrypts it locally, then connects to the WebSocket room for live updates.
6161+6262+**Room-based WebSocket relay.** The server groups connections by room ID (document ID). Binary messages are relayed to all other peers in the room unchanged. JSON control messages (peer-count, peer-joined, peer-left) are the only unencrypted traffic and contain no sensitive data.
6363+6464+**Vanilla JS — no framework.** No React, Vue, or Svelte. The docs editor uses TipTap (ProseMirror wrapper). The sheets editor is a custom `<table>` renderer. This keeps the bundle small, eliminates framework churn, and gives full control over the DOM.
6565+6666+**Pure logic modules.** Every feature is split into a pure logic module (no DOM, fully testable) and a thin DOM integration layer. `formulas.js`, `filter.js`, `sort.js`, `suggesting.js`, `version-history.js`, `offline.js`, etc. are all DOM-free.
6767+6868+---
6969+7070+## 3. Current Features
7171+7272+### 3.1 Documents
7373+7474+#### Rich Text Editing
7575+- Full TipTap/ProseMirror editor with headings (H1-H6), paragraphs, blockquotes, horizontal rules
7676+- **Bold**, *italic*, ~~strikethrough~~, underline, subscript, superscript
7777+- Font family selection, font size control (beyond heading levels)
7878+- Text color and highlight color
7979+- Text alignment (left, center, right, justify)
8080+- Line spacing presets (1, 1.15, 1.5, 2, 2.5, 3) and paragraph spacing controls
8181+- Indent/outdent for paragraphs and headings (Cmd+]/Cmd+[)
8282+8383+#### Block Types
8484+- Bullet lists, numbered lists, task lists (with checkboxes)
8585+- Code blocks (fenced, with language hints)
8686+- Inline code
8787+- Blockquotes and callout boxes
8888+- Tables (with header rows, cell merge/split, column/row add/delete, cell background color)
8989+- Images (URL-based embedding)
9090+- Page breaks
9191+9292+#### Notion-Style Features
9393+- **Slash commands** (`/` to open command palette) with categorized menu: Text, Lists, Media, Code, Quote, Advanced
9494+- **Block handles** — Notion-style drag handle on hover with context menu: Turn into, Delete, Duplicate, Move up/down
9595+- **Markdown autoformatting** — type `# `, `## `, `- `, `1. `, `>`, `[]`, `---`, `` ` ``, `**`, `~~`, `[text](url)` and the editor auto-converts
9696+9797+#### Document Navigation
9898+- **Outline sidebar** — auto-extracted H1/H2/H3 headings in a navigable tree, toggleable panel
9999+- **Find and replace** (Cmd+F / Cmd+H) with match highlighting and active match indicator
100100+- **Zen mode** (Cmd+Shift+F) — hides toolbar and topbar for distraction-free writing, persists preference
101101+102102+#### Markdown Support
103103+- **Markdown source toggle** (Cmd+Shift+M) — switch between WYSIWYG and raw markdown editing
104104+- HTML-to-Markdown export via Turndown with GFM support (tables, task lists, strikethrough, code block languages)
105105+- Markdown-to-HTML import via markdown-it with GFM extensions
106106+107107+#### Collaboration Features (Docs)
108108+- **Inline comments** — mark text ranges with author, timestamp, and comment text; rendered as highlighted spans
109109+- **Suggesting mode** — track-changes-style editing with insert/delete marks, session grouping (consecutive keystrokes share one suggestion ID), accept/reject per suggestion or bulk
110110+- **Link preview tooltips** — hover over links to see URL, Open, Edit, Remove actions
111111+- **Floating table toolbar** — context-sensitive toolbar when cursor is in a table
112112+113113+#### Import / Export
114114+- **.docx import** via mammoth.js with heading style mapping
115115+- **PDF export** via html2pdf.js with light-mode rendering
116116+- **Markdown import/export** (toggle between WYSIWYG and source view)
117117+118118+#### Footer & Metadata
119119+- Word and character count in footer
120120+- Autosave indicator with last saved timestamp
121121+- Keyboard shortcut cheatsheet modal
122122+123123+### 3.2 Spreadsheets
124124+125125+#### Grid Engine
126126+- Custom `<table>`-based grid with 100 rows x 26 columns default (extensible)
127127+- **Cell selection** — click, Shift+click for range, drag-to-select with visual feedback
128128+- **Cell editing** — double-click or type to enter edit mode, formula bar for complex input
129129+- **Multi-sheet tabs** — multiple sheets per workbook with tab navigation
130130+- **Virtual scrolling** — only visible rows + buffer are rendered for performance
131131+132132+#### Formula Engine
133133+- **50+ functions** across categories:
134134+ - **Math**: SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, MEDIAN, STDEV, ABS, ROUND, ROUNDUP, ROUNDDOWN, INT, MOD, POWER, SQRT, LOG, LN, EXP, PI, RAND
135135+ - **Logical**: IF, AND, OR, NOT, IFERROR
136136+ - **Text**: CONCATENATE, LEN, LEFT, RIGHT, MID, UPPER, LOWER, TRIM, SUBSTITUTE, FIND, SEARCH, TEXT, VALUE
137137+ - **Date**: NOW, TODAY, DATE, YEAR, MONTH, DAY
138138+ - **Lookup**: VLOOKUP, HLOOKUP, INDEX, MATCH
139139+ - **Conditional**: SUMIF, COUNTIF, AVERAGEIF
140140+- **Recursive descent parser** with full operator precedence (comparison, concat, add/sub, mul/div, power, unary)
141141+- **Cell references**: A1, $A$1 (absolute), ranges (A1:B5)
142142+- **Cross-sheet references**: `Sheet2!A1`, `'Sheet Name'!A1:B5`
143143+- **Named ranges**: human-friendly aliases (e.g., `=SUM(Sales)` instead of `=SUM(A2:A100)`)
144144+- **Formula autocomplete** — dropdown suggestions when typing function names, with signatures
145145+- **Formula tracer** — trace precedents (inputs) and dependents (outputs) of any cell
146146+- **Number formatting**: auto, number, currency ($), percent, date, text
147147+- **Formula evaluation cache** for performance
148148+149149+#### Data Features
150150+- **Charts** via Chart.js — bar, line, pie, scatter with auto-detected headers, multi-series, axis labels, title
151151+- **Multi-column filter** — per-column value checkboxes to show/hide rows
152152+- **Multi-column sort** — up to 3 sort levels with ascending/descending, stable sort, numbers before strings
153153+- **Conditional formatting** — rules engine: greaterThan, lessThan, equalTo, between, textContains, isEmpty, isNotEmpty; custom bgColor/textColor per rule; first-match-wins
154154+- **Data validation** — list (dropdown), numberBetween, textLength; visual indicators for invalid cells; dropdown arrows for list validation
155155+156156+#### Grid Features
157157+- **Resizable columns** — drag column borders, double-click to auto-fit width based on content
158158+- **Frozen panes** — lock header rows and columns (synced via Yjs)
159159+- **Cell merging** — merge/unmerge selected ranges with colspan/rowspan
160160+- **Cell borders** — per-side (top/bottom/left/right) or presets (all, outline, none)
161161+- **Wrap text** — toggle word wrapping per cell
162162+- **Striped rows** — alternating row background for readability
163163+- **Cell notes** — plain text annotations with triangle indicator, hover tooltips
164164+- **Cell styles** — bold, italic, text color, background color, text alignment
165165+166166+#### Advanced Operations
167167+- **Drag-to-fill** — pattern detection (number sequences, date sequences, formula adjustment, text repeat) with smart auto-fill
168168+- **Paste special** — values only, formulas only, formatting only, transpose
169169+- **Format painter** — copy cell formatting and apply to other cells
170170+- **Status bar** — SUM, AVERAGE, COUNT, MIN, MAX aggregates for multi-cell selections
171171+- **Undo/redo** via Yjs UndoManager (Cmd+Z / Cmd+Shift+Z)
172172+173173+#### Import / Export
174174+- **.xlsx import** via SheetJS — reads values, formulas, bold formatting, number formats
175175+- **CSV import** with auto-detected headers
176176+177177+### 3.3 Collaboration
178178+179179+#### Real-Time Editing
180180+- **Yjs CRDT** — conflict-free collaborative editing, no central server needed for conflict resolution
181181+- **Encrypted WebSocket provider** — all Yjs sync messages (state vectors, updates, awareness) are AES-256-GCM encrypted before transmission
182182+- **Awareness protocol** — see other users' cursors, selections, and names with colored labels
183183+- **Peer management** — automatic sync when peers join/leave, reconnection with exponential backoff + jitter
184184+185185+#### Version History
186186+- **Automatic version capture** — triggers after N edits or M minutes (whichever first)
187187+- **FIFO pruning** — max 50 versions per document, oldest pruned automatically
188188+- **Word count delta** — each version shows +/- word count compared to previous
189189+- **Server-side storage** — versions stored as encrypted snapshots, retrievable via API
190190+- **Version restore** — load any previous version's state
191191+192192+#### Suggesting Mode
193193+- **Track changes** — insert and delete marks with author, timestamp, session grouping
194194+- **Accept/reject** — per-suggestion or bulk accept/reject all
195195+- **Session grouping** — consecutive keystrokes by the same author at adjacent positions share one suggestion ID (like Google Docs)
196196+197197+#### Offline Support
198198+- **Online/offline detection** with status indicator
199199+- **Change queue** — offline edits queued and synced when reconnecting
200200+- **Cache strategy** — static assets (cache-first), HTML (network-first), API/WS (network-only)
201201+- **Debounced snapshot saves** — 500ms debounce after each edit, 10s periodic saves
202202+203203+### 3.4 Sharing & Permissions
204204+205205+- **Share dialog** — modal with share link, mode selector (edit/view), expiry options
206206+- **View-only mode** — `?mode=view` disables toolbar and editing; view-only badge shown
207207+- **Link expiry** — 1 hour, 1 day, 7 days, 30 days, or no expiry; server rejects access after expiry (HTTP 410)
208208+- **E2EE key in URL** — the encryption key is part of the share link; anyone with the link can decrypt
209209+- **Server-side share settings** — share_mode and expires_at stored in SQLite
210210+211211+### 3.5 Landing Page (Document Browser)
212212+213213+- **Document list** with type icons, decrypted names, last updated timestamps
214214+- **Sort** — by last updated, created, name, or type; starred documents float to top
215215+- **Search** — filter by decrypted document name (client-side)
216216+- **Folders** — create, rename, delete folders; move documents between folders; breadcrumb navigation
217217+- **Favorites/Stars** — star/unstar documents; starred sort first
218218+- **Trash** — soft delete with 30-day auto-purge; restore or permanently delete; collapsible trash section
219219+- **Username prompt** — first-visit modal for display name; click badge to change; random fallback (User NNNN)
220220+- **Create actions** — large cards for New Document and New Spreadsheet
221221+- **Legacy key migration** — auto-migrates `crypt-*` localStorage keys to `tools-*`
222222+223223+### 3.6 Security Model
224224+225225+#### Encryption
226226+- **AES-256-GCM** via Web Crypto API (`crypto.subtle`)
227227+- **Random IV** (96-bit / 12 bytes) per encryption; prepended to ciphertext
228228+- **Key format**: raw AES key exported as URL-safe base64 (no padding)
229229+- **Key lifecycle**: generated on document creation, stored in localStorage keyed by document ID, shared via URL fragment
230230+231231+#### What the server sees
232232+- Document IDs (UUIDs)
233233+- Encrypted document names (AES-256-GCM ciphertext in base64)
234234+- Encrypted Yjs snapshots (binary blobs)
235235+- Encrypted WebSocket messages (relayed blindly)
236236+- Share mode (edit/view) and expiry timestamps
237237+- Created/updated timestamps
238238+239239+#### What the server CANNOT see
240240+- Document content (any form — text, formulas, cell data, formatting)
241241+- Document names (plaintext)
242242+- User identities beyond what they choose to share (display name in awareness)
243243+- Encryption keys (never sent — URL fragment is excluded from HTTP requests)
244244+245245+#### HTTPS
246246+- Optional self-signed HTTPS for `crypto.subtle` secure context requirement
247247+- Tailscale already encrypts transport; HTTPS is defense-in-depth for the Web Crypto API restriction
248248+249249+### 3.7 UI & Accessibility
250250+251251+- **Dark mode** — automatic via `prefers-color-scheme`, manual toggle, persisted in localStorage
252252+- **Skip links** — "Skip to content" link for screen readers
253253+- **Keyboard focus management** — focus rings only on Tab navigation (hidden on mouse click)
254254+- **ARIA roles** — menus, dialogs, and modals use proper ARIA attributes
255255+- **Context menus** — keyboard navigable (arrow keys, Enter, Escape)
256256+- **Responsive toolbar** — collapses overflow items on narrow viewports
257257+- **Mobile-responsive layout** — touch-friendly create cards, responsive grid
258258+259259+---
260260+261261+## 4. Planned Features
262262+263263+### Wave 1: Formula Engine Hardening + UX (Critical/High)
264264+265265+**Goal**: Make the formula engine production-grade.
266266+267267+- **Topological recalculation engine** — evaluate formulas in dependency order instead of ad-hoc (#89)
268268+- **Circular reference detection** with clear error messages (#90)
269269+- **Array formula support and spill behavior** (#91)
270270+- **Volatile function handling** (NOW, TODAY, RAND recalculate appropriately) (#92)
271271+- **Rich formula tooltips** with parameter highlighting (#93)
272272+- **Formula bar syntax highlighting** (#94)
273273+- **Range highlighting** while editing formulas (#95)
274274+- **Contextual error tooltips** for #REF!, #VALUE!, #NAME? etc. (#96)
275275+- **Function help panel** with examples (#97)
276276+277277+### Wave 2: Power Functions (Medium)
278278+279279+**Goal**: Match Google Sheets function library for power users.
280280+281281+- **XLOOKUP** — modern replacement for VLOOKUP/HLOOKUP (#98)
282282+- **SUMIFS/COUNTIFS/AVERAGEIFS** — multi-criteria aggregation (#99)
283283+- **LET()** — named sub-expressions within formulas (#100)
284284+- **QUERY()** — SQL-like data querying (#85)
285285+- **Dynamic array functions** — FILTER, SORT, UNIQUE, SEQUENCE (#86)
286286+- **SPARKLINE()** — inline mini-charts in cells (#87)
287287+- **LAMBDA()** — user-defined functions (#88)
288288+289289+### Wave 3: Forms (High/Medium)
290290+291291+**Goal**: E2EE form builder that feeds responses into Sheets.
292292+293293+- **Form builder** with question types: text, number, date, select, multi-select, file upload, rating, matrix (#77)
294294+- **Conditional logic** — show/hide questions based on previous answers (#78)
295295+- **Responses pipeline** — form submissions encrypt and write to a linked Sheet (#79)
296296+297297+### Wave 4: AI Integration (Medium)
298298+299299+**Goal**: Client-side AI assistance that respects E2EE.
300300+301301+- **AI formula assistant** via Aperture — explain formulas, suggest corrections, generate from natural language (#101)
302302+- **AI assistant for docs** — summarize, rewrite, translate, continue writing (#63)
303303+304304+### Wave 5: Platform Infrastructure (High/Medium)
305305+306306+**Goal**: The foundational capabilities that unlock many surface-level features.
307307+308308+- **E2EE image and file storage** — encrypted blob upload/download API (#80)
309309+- **Image cells in docs** — drag-drop, paste, and insert images stored encrypted (#81)
310310+- **Image cells in sheets** (#82)
311311+- **Service worker and asset caching** for PWA (#83)
312312+- **IndexedDB document cache** for true offline-first editing (#84)
313313+- **Full-text search** across all documents (client-side index) (#55)
314314+- **Document templates** — pre-built starting points for common use cases (#56)
315315+- **Command palette** (Cmd+K) — global search and action launcher (#52)
316316+317317+### Wave 6: Advanced Sheets (Medium)
318318+319319+**Goal**: Enterprise-grade sheet features.
320320+321321+- **Pivot tables** (#44)
322322+- **Database views** — kanban, gallery, calendar (#45, #73, #74, #75)
323323+- **Timeline/Gantt view** (#76)
324324+- **Rich cell types** — checkboxes, ratings, progress bars, links, tags (#46)
325325+326326+### Wave 7: More Surfaces (Medium/Low)
327327+328328+**Goal**: Complete the office suite.
329329+330330+- **Slides** — canvas rendering engine, master layouts/themes, presenter mode, transitions, PPTX import/PDF export (#40, #65, #66, #67, #68, #69)
331331+- **Diagrams/Whiteboard** — E2EE freeform canvas for diagrams, flowcharts, mind maps (#42)
332332+333333+### Wave 8: Deep Collaboration (Medium)
334334+335335+**Goal**: Team-grade collaboration features.
336336+337337+- **Threaded comments** — reply chains on comment marks, resolve/unresolve (#48)
338338+- **Named versions and version labels** — bookmark specific versions with names (#49)
339339+- **Follow mode** — click a collaborator's avatar to follow their cursor/viewport (#50)
340340+- **Granular permissions** — per-section or per-sheet access control (#51)
341341+342342+### Wave 9: Integration (Medium/Low)
343343+344344+**Goal**: Connect Tools to the broader ecosystem.
345345+346346+- **Cross-tool integration** — embed live Sheet ranges in Docs, embed Charts from Sheets in Docs, wiki-style cross-document links (#70, #71, #72, #43)
347347+- **Matrix/Owl integration** — share notifications, link previews in chat (#64)
348348+- **REST API** for automation and scripting (#57)
349349+350350+### Wave 10: UX Polish (Medium/Low)
351351+352352+**Goal**: Refinement and delight.
353353+354354+- **Landing page overhaul** — grid/list toggle, recent files, pinned folders (#58)
355355+- **Split view** — side-by-side editing of two documents (#59)
356356+- **Minimap** for long documents (#60)
357357+- **Focus mode enhancements** — typewriter scrolling, paragraph highlighting (#61)
358358+- **Expanded theming** — custom accent colors, font preferences (#62)
359359+360360+---
361361+362362+## 5. Design Principles
363363+364364+### Privacy by Default
365365+Encryption is not an opt-in feature. Every document is encrypted from the moment of creation. The server architecture makes it impossible for the operator to read document content, even with full database access.
366366+367367+### System Fonts
368368+No external font dependencies. The font stack uses Charter for display headings (a beautiful serif with wide availability), system-ui for body text, and ui-monospace for code and spreadsheet cells. This eliminates font-loading latency and external requests.
369369+370370+### OkLCH Color System
371371+All colors are defined in OkLCH (a perceptually uniform color space). This ensures consistent perceived contrast in both light and dark themes without manual per-color tuning. The palette is warm-neutral with a terracotta accent and teal for collaborative/encrypted indicators.
372372+373373+### Minimal Dependencies
374374+The entire backend is Express + ws + better-sqlite3 + compression. The frontend adds TipTap, Yjs, Chart.js, and a handful of import/export libraries. No ORM, no database server, no build-time CSS framework. Total `dependencies` in package.json: 20 packages.
375375+376376+### Keyboard First
377377+Every feature is accessible via keyboard. Slash commands, Cmd+K shortcuts, keyboard-navigable context menus, Tab/Shift+Tab cell navigation in sheets, Cmd+]/[ for indentation, Cmd+Shift+F for zen mode, Cmd+Shift+M for markdown toggle.
378378+379379+### Pure Logic Modules
380380+Every non-trivial feature separates pure logic (testable, no DOM) from DOM integration. This makes the codebase easier to test, reason about, and refactor. Examples: `formulas.js`, `filter.js`, `sort.js`, `conditional-format.js`, `data-validation.js`, `suggesting.js`, `version-history.js`, `offline.js`, `print-layout.js`, `context-menu.js`.
381381+382382+### Self-Hostable
383383+Single binary (Node.js) with SQLite. No external services required. Deployable as a single Nomad job, Docker container, or systemd service. Data directory is configurable via `DATA_DIR` env var.
384384+385385+---
386386+387387+## 6. Technical Decisions
388388+389389+### Why Vanilla JS (No React/Vue/Svelte)?
390390+391391+TipTap already provides a reactive document model via ProseMirror. The sheets grid is a straightforward `<table>` render that benefits from direct DOM manipulation over virtual DOM diffing. Adding a framework would increase bundle size, add a layer of abstraction over already-abstracted libraries, and introduce framework-specific patterns for marginal benefit. The pure logic module pattern gives us testability without a framework's component model.
392392+393393+### Why TipTap?
394394+395395+TipTap wraps ProseMirror (the gold standard for collaborative rich text editing) with a clean extension API. It has first-class Yjs integration via `@tiptap/extension-collaboration` and `@tiptap/extension-collaboration-cursor`. The extension system made it easy to add custom marks (comments, suggestions), nodes (page breaks), and behaviors (indent, line spacing, slash commands) without forking the core.
396396+397397+### Why Yjs?
398398+399399+Yjs is the most mature CRDT library for JavaScript. It handles:
400400+- Conflict-free merging of concurrent edits
401401+- Awareness protocol (cursors, user presence)
402402+- Binary encoding (compact wire format)
403403+- Undo/redo with `Y.UndoManager`
404404+- Snapshot encoding for persistence
405405+406406+The alternative (OT via ShareDB/Firepad) requires a central server to resolve conflicts, which conflicts with our E2EE relay architecture.
407407+408408+### Why SQLite?
409409+410410+Single-file database, zero configuration, WAL mode for concurrent reads during writes. Perfect for a self-hosted app that stores encrypted blobs. The data model is simple (documents table + versions table), so there is no need for a full RDBMS.
411411+412412+### Why Self-Signed HTTPS?
413413+414414+The Web Crypto API (`crypto.subtle`) is only available in secure contexts (HTTPS or localhost). Since Tools is deployed on Tailscale (which already provides WireGuard-level encryption), the self-signed cert exists solely to satisfy the browser's secure context requirement. Tailscale certs are used when available.
415415+416416+### Why AES-256-GCM?
417417+418418+GCM provides authenticated encryption (confidentiality + integrity). The Web Crypto API supports it natively with excellent performance. 256-bit key length provides a comfortable security margin. The 96-bit random IV is prepended to the ciphertext for self-describing messages.
419419+420420+### Why Vite?
421421+422422+Fast dev server with HMR, zero-config for vanilla JS, handles multi-page apps (landing, docs, sheets) cleanly. The production build is straightforward.
423423+424424+---
425425+426426+## 7. Competitive Landscape & Inspiration
427427+428428+### Google Sheets — What Power Users Love
429429+430430+**What to steal:**
431431+432432+- **Explore / Data insights panel.** Google Sheets auto-suggests charts, pivot summaries, and trends based on selected data. We could do this client-side with heuristics (detect date columns, numeric ranges, categorical data) without sending data anywhere. The E2EE angle: "AI-powered insights that never leave your browser."
433433+- **Apps Script / Macros.** The ability to write custom functions and automate workflows is Google Sheets' biggest power-user lock-in. We should plan for a lightweight scripting layer — possibly a formula-level LAMBDA() first (#88), then a proper JavaScript sandbox that can read/write cell data. E2EE-safe because scripts run client-side.
434434+- **Data connectors and IMPORTDATA/IMPORTRANGE.** Google Sheets can pull live data from URLs, other sheets, and external databases. For Tools, IMPORTDATA could fetch from URLs client-side (respecting CORS). Cross-document ranges are already partly addressed by cross-sheet refs; extending this to cross-document would be powerful.
435435+- **Pivot table UX.** Google's pivot table builder is drag-and-drop with instant preview. Our planned pivot tables (#44) should prioritize this same discoverability — a sidebar where you drag field pills into Rows, Columns, Values, and Filters.
436436+- **Conditional formatting presets.** Google offers one-click color scales, data bars, and icon sets. Our conditional formatting (#120) should match this ease of use — not just rule-based but visual preset palettes.
437437+- **Named ranges manager.** A sidebar panel listing all named ranges with edit/delete/navigate. We have the backend (#35) but the management UI is minimal.
438438+- **Go-to range (Cmd+G or name box click).** Click the cell address box, type "A100" or a named range, and jump there. This exists (cellAddressInput) but navigation-on-Enter should be verified.
439439+440440+**What to skip:**
441441+442442+- Google's server-side compute (ArrayFormula spilling across 10M rows). We are client-side and should optimize for thousands of rows, not millions. Focus on UX rather than scale.
443443+444444+### Notion — What Makes It Sticky
445445+446446+**What to steal:**
447447+448448+- **Databases with views.** Notion's killer feature: one data source, multiple views (table, board, calendar, gallery, timeline). Our database views (#45, #73-#76) should follow this model exactly — one Sheet, multiple view tabs that share the underlying Yjs data.
449449+- **Relations and rollups.** Link rows in one database to rows in another, then compute summaries (count, sum, average) across the relation. This is basically a JOIN + GROUP BY. For Tools, this could mean linking two Sheets and having formulas that reference the linked rows. Extremely powerful for project management and CRM use cases.
450450+- **Synced blocks.** A block in one document that mirrors content from another document. Changes propagate bidirectionally. For E2EE, this is tricky (both docs need the same key, or a key-sharing mechanism), but the concept of shared/embedded content blocks is compelling.
451451+- **Toggles (collapsible sections).** Simple but beloved. A heading or paragraph that can expand/collapse its children. TipTap can support this with a custom node — the data model is a wrapper node with an `open` boolean attribute.
452452+- **Templates with prefilled structure.** Notion's template buttons create new pages with predefined content. Our templates (#56) should include both document templates (meeting notes, project brief, 1-on-1) and sheet templates (budget, inventory, CRM).
453453+- **Breadcrumb navigation.** Notion shows the page hierarchy at the top. Our landing page has breadcrumbs for folders; extending this to show the document path when editing would aid navigation in multi-folder setups.
454454+- **Inline databases.** Embed a mini-spreadsheet/table inside a document. Our planned "embed live Sheet ranges in Docs" (#70) is the right approach — keep sheets as the data layer, docs as the presentation layer.
455455+456456+**What to skip:**
457457+458458+- Notion's workspace/team model (too complex for a self-hosted tool). Multi-tenant is a non-goal.
459459+460460+### Airtable — The Spreadsheet-Database Hybrid
461461+462462+**What to steal:**
463463+464464+- **Field types as first-class citizens.** Airtable columns have explicit types: single select, multi-select, date, checkbox, URL, email, phone, attachment, linked record, lookup, rollup, formula, count, barcode. Our "rich cell types" (#46) should start with the highest-value types: checkbox, single-select (dropdown), date picker, URL (clickable), rating (stars).
465465+- **Views as filtered/sorted/grouped perspectives.** Airtable views don't copy data; they are saved filter+sort+group configurations on the same table. We should implement this — each view stores a filter state, sort keys, grouping column, and hidden columns. Switching views is instant because the data is shared.
466466+- **Grouping.** Collapse rows by a column value (e.g., group tasks by Status). This is a natural extension of our existing sort/filter system.
467467+- **Record detail expansion.** Click a row to see all fields in a modal/sidebar card view. For sheets with many columns, this is much easier to read than horizontal scrolling. Implement as a "Row detail" panel.
468468+- **Automations (triggers + actions).** "When a record matches conditions, do X." For Tools, the E2EE-safe version is client-side automations that run in the browser: "When column Status changes to Done, set column Completed Date to TODAY()." Store automation rules in the Yjs document.
469469+- **Form view.** Airtable forms feed directly into tables. This is exactly our Forms plan (#41) — the form is a view of a Sheet, and responses become rows.
470470+471471+### Obsidian — Knowledge Management Stickiness
472472+473473+**What to steal:**
474474+475475+- **Backlinks panel.** Show all documents that link to the current document. Our wiki-style links (#72) should include a backlinks sidebar. Since we decrypt document content client-side, building a backlinks index is feasible (scan all doc content for `[[docName]]` patterns).
476476+- **Graph view.** Visualize the relationship between documents as an interactive node graph. Stunning for discovering clusters and orphans. Could use a lightweight library like d3-force or vis.js. The graph data is derived from cross-document links.
477477+- **Daily notes / journal.** Quick-create a document for today's date. A small feature with big daily-driver appeal. One button on the landing page: "Today's Note" that creates (or navigates to) a doc named with today's date.
478478+- **Local-first as a feature.** Obsidian's biggest selling point is that your data is local Markdown files. Tools' E2EE + offline support tells a similar story: "Your data is encrypted at rest, in transit, and even we can't read it." We should lean into this messaging harder.
479479+- **Command palette.** Obsidian's Cmd+P is the gateway to everything. Our Cmd+K command palette (#52) should be as comprehensive: navigate to any document, run any action, search content, switch themes, open settings.
480480+481481+**What to skip:**
482482+483483+- Plugin ecosystem. We should make the core great rather than building an extension API prematurely.
484484+485485+### Coda — Packs and Cross-Doc
486486+487487+**What to steal:**
488488+489489+- **Buttons that trigger actions.** A button in a doc or sheet that runs a formula or automation. "Click to mark all tasks complete," "Click to calculate totals." This is powerful for building interactive dashboards. For Tools, buttons could execute a predefined sequence of cell mutations.
490490+- **Cross-doc references.** Reference data from one doc in another. For Tools with E2EE, this requires shared keys. The simplest version: if you have the key for both documents, you can reference data across them.
491491+- **Canvas columns (rich content in cells).** Coda allows rich text, images, and embeds inside table cells. Our rich cell types (#46) could include a "rich text" type that opens a mini-editor.
492492+493493+### Linear — UX Lessons
494494+495495+**What to steal:**
496496+497497+- **Speed above all.** Linear feels instant because of aggressive optimistic updates, local-first data, and minimal re-renders. Our sheets grid should aim for <16ms render on cell edit (requestAnimationFrame batching is already in place).
498498+- **Keyboard-first everything.** Linear can be used entirely without a mouse. Every action has a shortcut, and the command palette (Cmd+K) is the universal entry point. Our command palette should support fuzzy matching and show shortcuts inline.
499499+- **Opinionated defaults.** Linear doesn't have 50 settings; it has great defaults. Tools should resist adding settings and instead pick the right defaults (e.g., auto-save always on, dark mode follows OS).
500500+- **Contextual actions.** Linear shows exactly the actions relevant to your current context. Our context menus and toolbar should adapt similarly — show chart options when in a data range, show table tools when in a table.
501501+- **Cycles and project scoping.** For project management use cases in sheets, having a built-in concept of "sprint" or "cycle" (date-bounded grouping) would differentiate from plain Airtable-style tables.
502502+503503+---
504504+505505+## 8. User Journeys
506506+507507+### 8.1 Personal Budget Tracking
508508+509509+**Scenario:** Scott creates a monthly budget spreadsheet to track income, expenses, and savings goals.
510510+511511+**What works great today:**
512512+- Create a new sheet, set up columns (Category, Description, Amount, Date, Type)
513513+- Formulas: `=SUM(A2:A50)`, `=SUMIF(E2:E50,"expense",C2:C50)`, `=AVERAGEIF(E2:E50,"expense",C2:C50)`
514514+- Conditional formatting: highlight expenses > $100 in red, savings contributions in green
515515+- Charts: pie chart of spending by category, bar chart of monthly trends
516516+- Dark mode for evening budget reviews
517517+518518+**Friction / missing:**
519519+- No date picker — dates must be typed manually, error-prone
520520+- No dropdown for Category column (data validation list exists but discoverability is low)
521521+- No currency formatting that auto-applies to new rows
522522+- No "freeze row 1" button in the toolbar (feature exists but UI is buried)
523523+- Cannot duplicate the sheet as a template for next month
524524+- No row grouping to collapse categories
525525+526526+**Features that would make it smooth:**
527527+- Cell date picker widget (#123)
528528+- Document duplication (#106)
529529+- Column type presets (set "Amount" column to currency format for all new rows)
530530+- Monthly template with pre-built formulas
531531+- Grouping by Category with subtotals
532532+533533+### 8.2 Meeting Notes with Action Items
534534+535535+**Scenario:** A team lead takes notes during a weekly sync, tags action items, and shares with the team.
536536+537537+**What works great today:**
538538+- Rich text editor with headings, bullet lists, task lists (checkboxes)
539539+- Slash commands to quickly insert headings, task lists, horizontal rules
540540+- Markdown autoformatting: type `- [ ]` for a task item
541541+- Share via link with E2EE key — recipients can view or edit
542542+- Suggesting mode for team members to propose changes
543543+- View-only mode for read-only sharing
544544+545545+**Friction / missing:**
546546+- No way to assign task items to people (no @mentions or assignee field)
547547+- No due dates on task items
548548+- No way to filter/view only incomplete tasks across multiple meeting docs
549549+- Cannot link to a previous meeting's notes (no cross-document linking)
550550+- Comments exist but are not threaded — no reply chains
551551+552552+**Features that would make it smooth:**
553553+- @mention support in docs (references a username from the collaboration awareness)
554554+- Task item metadata (assignee, due date) as inline properties
555555+- Cross-document wiki links (#72) to link meeting series
556556+- Threaded comments (#48)
557557+- Template: "Meeting Notes" with pre-built sections (Attendees, Agenda, Notes, Action Items, Next Steps)
558558+559559+### 8.3 Project Planning with Timeline
560560+561561+**Scenario:** A freelancer plans a client project with tasks, dependencies, milestones, and deadlines.
562562+563563+**What works great today:**
564564+- Sheet with columns: Task, Owner, Start Date, End Date, Status, Priority, Notes
565565+- Data validation: Status dropdown (Not Started, In Progress, Done, Blocked)
566566+- Conditional formatting: overdue tasks highlighted red, completed in green
567567+- Multi-column sort by Priority then Due Date
568568+- Filter to show only "In Progress" tasks
569569+570570+**Friction / missing:**
571571+- No Gantt/timeline visualization — must mentally map dates to durations
572572+- No dependency tracking between tasks (Task B starts after Task A)
573573+- No progress bar or % complete column type
574574+- Cannot group tasks by project phase
575575+- Status dropdown works but requires manual configuration per sheet
576576+577577+**Features that would make it smooth:**
578578+- Timeline/Gantt view (#76) computed from Start Date and End Date columns
579579+- Rich cell types: progress bar, checkbox for done/not-done (#46)
580580+- Database views (#45): table view for editing, Gantt for planning, kanban for execution
581581+- Row grouping by Phase column with collapsible sections
582582+583583+### 8.4 Collecting RSVPs or Feedback
584584+585585+**Scenario:** Organizing an event and collecting RSVPs with dietary preferences and plus-one info.
586586+587587+**What works great today:**
588588+- (Not much — no forms exist yet)
589589+- Could manually share a sheet in edit mode, but that exposes all other responses
590590+591591+**Friction / missing:**
592592+- No form builder — must share the raw sheet or use an external tool
593593+- No way to restrict a collaborator to append-only (they can edit others' responses)
594594+- No confirmation/thank-you page after submission
595595+596596+**Features that would make it smooth:**
597597+- Forms (#41): build a form with fields (Name, Email, Attending?, Dietary Needs, Plus One)
598598+- Form responses pipeline (#79): each submission becomes an encrypted row in a Sheet
599599+- Form share link: separate from the sheet link — respondents see only the form, not the data
600600+- View-only summary: share chart of "Attending: Yes/No/Maybe" breakdown
601601+602602+### 8.5 Writing a Blog Post or Long Document
603603+604604+**Scenario:** An author writes a 5,000-word article with headings, images, code blocks, and footnotes.
605605+606606+**What works great today:**
607607+- TipTap editor with full formatting: headings, blockquotes, code blocks, images, horizontal rules
608608+- Outline sidebar for navigating between sections
609609+- Zen mode for distraction-free writing
610610+- Word count in footer for tracking progress
611611+- Markdown toggle for source editing
612612+- PDF export for sharing final version
613613+- Version history for tracking drafts
614614+615615+**Friction / missing:**
616616+- No table of contents that can be inserted into the document itself
617617+- No footnotes/endnotes for citations
618618+- No syntax highlighting in code blocks (just monospace)
619619+- Images can only be inserted via URL — no drag-and-drop upload
620620+- No reading time estimate
621621+- No split view to reference source material while writing
622622+- Export to .docx would be useful for publisher submission
623623+624624+**Features that would make it smooth:**
625625+- Table of contents auto-generation (#104)
626626+- Footnotes and endnotes (#122)
627627+- Syntax-highlighted code blocks (#110)
628628+- E2EE image upload (#80, #81)
629629+- .docx export (#103)
630630+- Split view (#59)
631631+- Reading time estimate (based on word count / 250 wpm)
632632+633633+### 8.6 Team Knowledge Base
634634+635635+**Scenario:** A small team maintains a shared knowledge base: onboarding docs, process guides, decision logs, meeting archives.
636636+637637+**What works great today:**
638638+- Folders for organizational structure (Onboarding, Processes, Decisions, Meetings)
639639+- Search by document name
640640+- Share links with E2EE for controlled access
641641+- Favorites for frequently referenced docs
642642+- Dark mode for comfortable reading
643643+644644+**Friction / missing:**
645645+- No cross-document linking (cannot link from "Deployment Process" to "Server Architecture")
646646+- No backlinks (no way to see what other docs reference the current one)
647647+- Search only matches document titles, not content
648648+- No tags or labels on documents
649649+- No "recently viewed" list for quick access
650650+- No way to embed a sheet (e.g., team contact list) in a doc
651651+652652+**Features that would make it smooth:**
653653+- Wiki-style cross-document links (#72) with `[[Document Name]]` syntax
654654+- Backlinks panel showing all documents that reference the current one
655655+- Full-text content search (#55, #119)
656656+- Document tags/labels for cross-cutting organization
657657+- Recent documents list (#116)
658658+- Embed live sheet ranges in docs (#70)
659659+660660+### 8.7 Data Analysis Workflow
661661+662662+**Scenario:** A data analyst imports a CSV dataset, cleans it, runs calculations, builds charts, and writes up findings.
663663+664664+**What works great today:**
665665+- Import .xlsx with formulas and formatting preserved
666666+- CSV paste (tab-separated values paste into grid)
667667+- 50+ formula functions for calculations
668668+- Filter and multi-column sort for exploration
669669+- Charts (bar, line, pie, scatter) for visualization
670670+- Named ranges for cleaner formulas
671671+672672+**Friction / missing:**
673673+- No CSV file import (only .xlsx and manual paste)
674674+- Cannot export results as CSV for use in other tools
675675+- No QUERY() function for SQL-like filtering/aggregation
676676+- Charts cannot be embedded in a doc alongside written analysis
677677+- No pivot table for quick summarization
678678+- No dynamic arrays (FILTER, SORT, UNIQUE) for intermediate transformations
679679+- Formula errors (#VALUE!, #REF!) lack explanatory tooltips
680680+681681+**Features that would make it smooth:**
682682+- CSV/TSV import and export (#102)
683683+- QUERY() function (#85) and dynamic arrays (#86)
684684+- Pivot tables (#44)
685685+- Embed charts in docs (#71)
686686+- Contextual error tooltips (#96)
687687+- .xlsx export (#109) for sharing results externally
688688+689689+### 8.8 Inventory or CRM Tracking
690690+691691+**Scenario:** A small business tracks inventory items or customer contacts in a sheet used as a lightweight database.
692692+693693+**What works great today:**
694694+- Sheet columns: Name, Category, Quantity, Price, Last Updated, Notes
695695+- Data validation: Category dropdown, quantity range validation
696696+- Conditional formatting: low stock highlighted yellow, out-of-stock red
697697+- Cell notes for additional context
698698+- Sort and filter by any column
699699+700700+**Friction / missing:**
701701+- No rich cell types (checkbox for "active", URL for website, email for contact)
702702+- No kanban/gallery view for visual browsing
703703+- No record detail panel (hard to see all fields for wide tables)
704704+- No row insert/delete via UI (must add data at the end)
705705+- No way to attach images to records (product photos, headshots)
706706+- No form for external data entry (suppliers submitting inventory updates)
707707+708708+**Features that would make it smooth:**
709709+- Rich cell types: checkbox, URL, email, image (#46)
710710+- Database views: table + gallery (#74) + kanban (#73)
711711+- Row insert/delete operations (#113)
712712+- Image cells (#82) with encrypted storage (#80)
713713+- Forms (#41) for external data entry
714714+- Record detail sidebar (click a row to expand)
715715+716716+### 8.9 Collaborative Editing of a Proposal
717717+718718+**Scenario:** Two co-founders collaborate on a client proposal, one writing content and the other reviewing.
719719+720720+**What works great today:**
721721+- Real-time collaboration with colored cursors and usernames
722722+- Suggesting mode: reviewer's changes shown as tracked insertions/deletions
723723+- Accept/reject individual suggestions or bulk
724724+- Version history to see evolution of the document
725725+- Share link with E2EE — only people with the link can access
726726+- Inline comments for discussion
727727+- Save indicator confirms both collaborators' changes are persisted
728728+729729+**Friction / missing:**
730730+- Comments are inline marks, not threaded conversations — no reply/resolve flow
731731+- No way to see who wrote what (no author attribution per paragraph)
732732+- No notification when collaborator leaves a comment or suggestion
733733+- Cannot restrict one collaborator to view-only within specific sections
734734+- No way to export the final version as .docx for the client
735735+736736+**Features that would make it smooth:**
737737+- Threaded comments with resolve (#48)
738738+- Activity log showing who edited when (#124)
739739+- .docx export (#103)
740740+- Granular permissions (#51) — section-level or comment-only access
741741+- Follow mode (#50) to track collaborator's cursor
742742+743743+### 8.10 Encrypted Financial Document Sharing
744744+745745+**Scenario:** An accountant shares tax documents with a client, requiring confidentiality.
746746+747747+**What works great today:**
748748+- E2EE by default — the server operator cannot read the documents
749749+- Share link includes encryption key in URL fragment (never sent to server)
750750+- View-only mode prevents accidental edits
751751+- Link expiry (1h, 1d, 7d, 30d) for time-limited access
752752+- Offline access works after initial load (no server needed to read cached content)
753753+754754+**Friction / missing:**
755755+- No way to revoke access after sharing (key is in the URL — anyone who saved it can still decrypt)
756756+- No audit trail of who accessed the document
757757+- No password protection on top of the encryption key (defense-in-depth)
758758+- No way to watermark view-only documents
759759+- Cannot prove to a regulator that the server never had access to content
760760+761761+**Features that would make it smooth:**
762762+- Key rotation / re-encryption (generate new key, re-encrypt, invalidate old links)
763763+- Access audit log (encrypted, only visible to document owner)
764764+- Optional password layer on share links (key derivation from password + URL key)
765765+- Compliance documentation / zero-knowledge proof architecture doc
766766+- Encrypted document expiry (document self-destructs after expiry, not just the link)
767767+768768+---
769769+770770+## 9. E2EE as Differentiator — Features Only We Can Build
771771+772772+The E2EE architecture is not just a security feature — it enables a category of features that cloud-hosted tools fundamentally cannot offer.
773773+774774+### 9.1 Zero-Knowledge Compliance
775775+776776+**Concept:** Tools can provide a technical guarantee that the server operator cannot access document content, even under legal compulsion (subpoena, government request).
777777+778778+**Implementation:**
779779+- Architecture document proving zero-knowledge properties (server code is open, encryption is client-side, keys never traverse the network)
780780+- Compliance mode that logs server-side access attempts and proves no plaintext was ever stored
781781+- HIPAA, GDPR, SOC 2 alignment documentation ("we literally cannot be a data breach because we never have the data")
782782+783783+**Why only E2EE can do this:** Google/Notion/Airtable hold your data in plaintext (or with keys they control). They must comply with data requests. Tools physically cannot.
784784+785785+### 9.2 Key Rotation and Re-Encryption
786786+787787+**Concept:** When a team member leaves or a share link is compromised, rotate the document key: generate a new AES key, decrypt with the old key, re-encrypt with the new key, update the stored snapshot, and invalidate all old share links.
788788+789789+**Implementation:**
790790+- Client-side operation: decrypt snapshot with old key, re-encrypt with new key, PUT new snapshot
791791+- Update localStorage key for the document
792792+- Generate new share URLs with the new key
793793+- Old URLs with the old key will fail to decrypt (graceful "This link is no longer valid" error)
794794+795795+**Why only E2EE can do this:** In centralized systems, "revoking access" means changing a permission bit. In E2EE, revoking access means making the ciphertext undecryptable — a much stronger guarantee.
796796+797797+### 9.3 Encrypted Backup and Portability
798798+799799+**Concept:** Export your documents as encrypted archives that can be imported into any Tools instance. Your data is portable without ever being decrypted on a server.
800800+801801+**Implementation:**
802802+- Export: bundle Yjs snapshot + metadata + key into an encrypted .zip
803803+- Import: drag-and-drop the archive, extract, create new document with the data
804804+- Migration: move from one Tools instance to another without the servers exchanging any data
805805+- The archive is useless without the key (which is in the URL / localStorage, not in the archive)
806806+807807+**Why only E2EE can do this:** With centralized tools, "export" means the server decrypts, formats, and sends you plaintext. With Tools, the export can remain encrypted end-to-end.
808808+809809+### 9.4 Offline-First as a Feature (Not a Workaround)
810810+811811+**Concept:** Position offline support not as "it works when the internet is down" but as "your documents are always local, always yours, and optionally sync to a server."
812812+813813+**Implementation:**
814814+- IndexedDB stores encrypted Yjs snapshots locally
815815+- Documents load instantly from local cache, then sync changes in the background
816816+- Full editing capability offline — changes queue and merge when reconnecting
817817+- "Airplane mode" indicator that makes offline feel intentional, not broken
818818+819819+**Why only E2EE makes this better:** When you work offline with Google Docs, your unencrypted content sits in browser storage. With Tools, even the local cache is encrypted — a stolen laptop doesn't expose your documents.
820820+821821+### 9.5 Self-Hosting with One Command
822822+823823+**Concept:** Make self-hosting as easy as `docker run -p 3000:3000 tools`. No database server, no external services, no configuration. Data lives in a single SQLite file that can be backed up with `cp`.
824824+825825+**Implementation:**
826826+- Publish Docker image to a public registry
827827+- Single container with all dependencies (Node.js + SQLite)
828828+- `DATA_DIR` env var for persistent storage
829829+- Optional HTTPS with auto-generated self-signed cert
830830+- docker-compose.yml with volume mount for one-command deployment
831831+832832+**Why self-hosting matters for E2EE:** Self-hosting means the user controls the relay server. Even though the server is zero-knowledge, self-hosting eliminates the need to trust anyone. The user is both the operator and the user.
833833+834834+### 9.6 Secure Embeds
835835+836836+**Concept:** Embed a Tools spreadsheet or document in an external webpage (blog, internal wiki, documentation site) while maintaining E2EE. The embed loads in an iframe, the key is passed via postMessage from the parent frame.
837837+838838+**Implementation:**
839839+- `/embed/sheets/{docId}` route that renders a read-only view
840840+- Parent page passes the encryption key via `postMessage` to the iframe
841841+- Iframe decrypts and renders — the hosting page never has the key in its DOM
842842+- CSP headers to prevent key leakage
843843+844844+**Why only E2EE can do this:** Regular embeds expose content to the embedding server. Secure embeds keep the content encrypted — only the user's browser has the key.
845845+846846+### 9.7 Verifiable Encryption
847847+848848+**Concept:** Let users verify that their documents are actually encrypted, not just claimed to be. Open the browser dev tools, inspect the WebSocket traffic, and see nothing but encrypted binary blobs.
849849+850850+**Implementation:**
851851+- "Verify Encryption" panel in settings that shows: the encryption algorithm, the key fingerprint, a live view of encrypted vs decrypted traffic
852852+- Server health endpoint that shows it only has encrypted blobs
853853+- Browser extension or bookmarklet that verifies the encryption is real
854854+855855+**Why this matters:** Trust-but-verify. Users should not have to take our word for it. The architecture should be auditable by anyone with dev tools open.
+243
README.md
···11+# Tools
22+33+**End-to-end encrypted collaborative office suite.** Documents and spreadsheets with real-time collaboration, where the server never sees your data.
44+55+<!-- screenshot: landing page with document list, dark mode -->
66+77+## Features
88+99+**Documents**
1010+- Rich text editor (TipTap/ProseMirror) with full formatting, tables, images, task lists
1111+- Notion-style slash commands (`/`) and block handles
1212+- Markdown source toggle, markdown autoformatting, .docx import, PDF export
1313+- Outline sidebar, find & replace, zen mode, suggesting mode (track changes)
1414+- Inline comments, link previews, page breaks, line/paragraph spacing
1515+1616+**Spreadsheets**
1717+- Custom grid engine with 50+ formula functions (SUM, VLOOKUP, IF, COUNTIF, etc.)
1818+- Charts (bar, line, pie, scatter) via Chart.js
1919+- Conditional formatting, data validation, multi-column filter & sort
2020+- Cross-sheet references, named ranges, formula autocomplete with signatures
2121+- Cell merging, frozen panes, drag-to-fill, paste special, format painter
2222+2323+**Collaboration**
2424+- Real-time editing via Yjs CRDT with encrypted WebSocket sync
2525+- Colored cursors with usernames (awareness protocol)
2626+- Version history with word count deltas and restore
2727+- Suggesting mode with accept/reject workflow
2828+- Offline support with queued sync on reconnect
2929+3030+**Security**
3131+- AES-256-GCM encryption via Web Crypto API
3232+- Encryption key lives in the URL fragment (never sent to server)
3333+- Server is a zero-knowledge relay -- it stores and forwards encrypted blobs
3434+- Share links with view-only mode and configurable expiry
3535+3636+**Organization**
3737+- Landing page with folders, search, sort, favorites, and trash
3838+- Dark mode (auto or manual), responsive mobile layout
3939+- Right-click context menus, keyboard shortcuts, print layout
4040+4141+<!-- screenshot: docs editor with collaboration cursors -->
4242+<!-- screenshot: sheets editor with chart and conditional formatting -->
4343+4444+## Quick Start
4545+4646+```bash
4747+git clone <repo-url> tools
4848+cd tools
4949+npm install
5050+npm run dev # Starts Express server + Vite dev server
5151+```
5252+5353+Open `http://localhost:5173` in your browser.
5454+5555+### Scripts
5656+5757+| Command | Description |
5858+|---------|-------------|
5959+| `npm run dev` | Development mode (Express + Vite HMR) |
6060+| `npm run build` | Production build (outputs to `dist/`) |
6161+| `npm start` | Production server (serves built files) |
6262+| `npm run preview` | Build + start in one step |
6363+| `npm test` | Run test suite (Vitest) |
6464+6565+## Architecture
6666+6767+```
6868+Browser Server
6969++----------------------------------+ +-------------------------+
7070+| Landing Page (vanilla JS) | | Express + compression |
7171+| Docs Editor (TipTap/ProseMirror)| | REST API (CRUD) |
7272+| Sheets Editor (custom grid) | | WebSocket relay |
7373+| | | SQLite (WAL mode) |
7474+| +----------------------------+ | +----+----+----+----------+
7575+| | Web Crypto API | | | | |
7676+| | AES-256-GCM encrypt/decrypt| | | | |
7777+| +----------------------------+ | | | |
7878+| | | | |
7979+| +----------------------------+ | | | |
8080+| | Yjs CRDT | | encrypted | | |
8181+| | Encrypted WebSocket Provider+--+--bytes--->+ WS relay (room-based)
8282+| | Awareness (cursors/presence)| | | | |
8383+| +----------------------------+ | | | |
8484+| | encrypted| | |
8585+| encrypt(snapshot) +-----------------blobs--->+ SQLite
8686+| decrypt(snapshot) <-----------------blobs----+ |
8787++----------------------------------+ +-------------------------+
8888+```
8989+9090+**Key principle:** All content is encrypted in the browser before it touches the network. The server relays encrypted WebSocket messages between peers and stores encrypted snapshots. It never has access to plaintext.
9191+9292+### How E2EE Works
9393+9494+1. **Document creation:** Browser generates an AES-256-GCM key, creates a document ID on the server, and stores the key in the URL fragment (`#base64key`).
9595+9696+2. **Editing:** The TipTap editor (docs) or custom grid (sheets) writes to a Yjs CRDT document. The `EncryptedProvider` intercepts all Yjs sync messages, encrypts them with the document key, and sends encrypted binary over WebSocket.
9797+9898+3. **Relay:** The server receives opaque binary blobs and forwards them to all other clients in the same room. It cannot read or modify the content.
9999+100100+4. **Persistence:** Periodically, the browser encrypts the full Yjs document state and PUTs it to the server as an encrypted snapshot. New clients load this snapshot, decrypt it locally, and then connect to the WebSocket for live updates.
101101+102102+5. **Sharing:** The share link contains the encryption key in the URL fragment. Anyone with the link can decrypt. The `#` fragment is never sent to the server per HTTP spec.
103103+104104+### What the Server Sees
105105+106106+| Data | Visible to Server? |
107107+|------|-------------------|
108108+| Document content | No (encrypted) |
109109+| Document names | No (encrypted) |
110110+| Encryption keys | No (URL fragment) |
111111+| Document IDs | Yes (UUIDs) |
112112+| Created/updated timestamps | Yes |
113113+| Share mode (edit/view) | Yes |
114114+| Link expiry | Yes |
115115+| Connected peer count | Yes |
116116+117117+## Self-Hosting
118118+119119+### Docker
120120+121121+```bash
122122+docker run -d \
123123+ --name tools \
124124+ -p 3000:3000 \
125125+ -v tools-data:/data \
126126+ tools:latest
127127+```
128128+129129+Open `https://localhost:3000` (or behind your reverse proxy).
130130+131131+### Docker Compose
132132+133133+```yaml
134134+version: '3.8'
135135+services:
136136+ tools:
137137+ build: .
138138+ ports:
139139+ - "3000:3000"
140140+ volumes:
141141+ - tools-data:/data
142142+ environment:
143143+ - DATA_DIR=/data
144144+ - PORT=3000
145145+ restart: unless-stopped
146146+147147+volumes:
148148+ tools-data:
149149+```
150150+151151+### Environment Variables
152152+153153+| Variable | Default | Description |
154154+|----------|---------|-------------|
155155+| `PORT` | `3000` | HTTP server port |
156156+| `HTTPS_PORT` | `3443` | HTTPS server port (if TLS certs available) |
157157+| `DATA_DIR` | `.` (project root) | Directory for SQLite database and TLS certs |
158158+| `TLS_CERT` | Auto-detected | Path to TLS certificate (PEM) |
159159+| `TLS_KEY` | Auto-detected | Path to TLS private key (PEM) |
160160+161161+### HTTPS
162162+163163+Tools requires a secure context for `crypto.subtle` (the Web Crypto API). Options:
164164+165165+1. **localhost** -- works without HTTPS for local development
166166+2. **Self-signed cert** -- place `cert.pem` and `key.pem` in `DATA_DIR`
167167+3. **Tailscale certs** -- auto-detected from `/var/lib/tailscale/certs/`
168168+4. **Reverse proxy** -- terminate TLS at nginx/Caddy/Traefik
169169+170170+## Project Structure
171171+172172+```
173173+tools/
174174+ server.js Express + WebSocket relay + SQLite
175175+ src/
176176+ index.html Landing page HTML
177177+ landing.js Landing page logic (create, list, search, folders, trash)
178178+ landing-utils.js Pure functions for landing page (sort, filter, folders, trash)
179179+ docs/
180180+ index.html Docs editor HTML
181181+ main.js TipTap editor setup, toolbar, collaboration
182182+ extensions/ Custom TipTap extensions (font-size, indent, comments, etc.)
183183+ *.js Feature modules (outline, zen-mode, link-preview, etc.)
184184+ sheets/
185185+ index.html Sheets editor HTML
186186+ main.js Grid engine, cell editing, toolbar, collaboration
187187+ formulas.js Formula tokenizer, parser, evaluator (50+ functions)
188188+ *.js Feature modules (charts, filter, sort, conditional-format, etc.)
189189+ lib/
190190+ crypto.js AES-256-GCM encrypt/decrypt via Web Crypto API
191191+ provider.js Encrypted Yjs WebSocket provider
192192+ version-history.js Version capture and management
193193+ offline.js Online/offline detection and change queuing
194194+ suggesting.js Track changes (suggesting mode) logic
195195+ share-dialog.js Share URL building and dialog helpers
196196+ print-layout.js Print HTML generation for docs and sheets
197197+ context-menu.js Right-click context menu component
198198+ css/
199199+ app.css All styles (OkLCH tokens, dark mode, responsive)
200200+ tests/ Vitest test suite (60+ test files)
201201+ dist/ Production build output (generated)
202202+```
203203+204204+## Contributing
205205+206206+See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, coding conventions, and how to add new features.
207207+208208+### Quick Development Guide
209209+210210+```bash
211211+# Install dependencies
212212+npm install
213213+214214+# Start development server (Express + Vite with HMR)
215215+npm run dev
216216+217217+# Run tests
218218+npm test
219219+220220+# Build for production
221221+npm run build
222222+```
223223+224224+The development server runs Express on port 3000 (API + WebSocket) and Vite on port 5173 (frontend with HMR). Vite proxies `/api` and `/ws` to Express.
225225+226226+## Tech Stack
227227+228228+| Layer | Technology | Why |
229229+|-------|-----------|-----|
230230+| Rich text editor | TipTap (ProseMirror) | Best-in-class collaborative editing with extension API |
231231+| Spreadsheet grid | Custom `<table>` renderer | Full control, no framework overhead |
232232+| CRDT | Yjs | Mature, conflict-free collaboration without central authority |
233233+| Encryption | Web Crypto API (AES-256-GCM) | Native browser crypto, no JS dependencies |
234234+| Charts | Chart.js | Lightweight, good defaults, canvas rendering |
235235+| Server | Express + ws | Minimal, fast, WebSocket-native |
236236+| Database | SQLite (better-sqlite3) | Zero config, single file, WAL mode |
237237+| Build | Vite | Fast HMR, zero config for vanilla JS |
238238+| Tests | Vitest | Fast, ESM-native, compatible with Vite config |
239239+| Import/Export | mammoth (.docx), SheetJS (.xlsx), html2pdf.js (PDF), Turndown + markdown-it (Markdown) | Focused libraries for each format |
240240+241241+## License
242242+243243+Private. All rights reserved.