diff --git a/docs/superpowers/specs/2026-05-17-textarea-editor-v2-design.md b/docs/superpowers/specs/2026-05-17-textarea-editor-v2-design.md new file mode 100644 index 0000000..c66db2d --- /dev/null +++ b/docs/superpowers/specs/2026-05-17-textarea-editor-v2-design.md @@ -0,0 +1,396 @@ +# Textarea Code Editor v2 (CodeMirror 6) — Design + +**Status:** Approved 2026-05-17. Supersedes `2026-05-16-textarea-code-editor-design.md` +(historical reference only — its functional requirements remain authoritative; +its architectural choices, CodeJar + Prism over `contenteditable`, are stale). + +**History.** A 35-commit implementation of the v1 design was rolled back in +commit `f14d352` after four classes of bug surfaced (copy collapses +multi-line selections, Enter sometimes needs two presses, autocomplete +fails silently at end-of-line, manual caret save/restore is unreliable). +All four are characteristic of `contenteditable` + tokenized DOM and +proved resistant to incremental fixes. The full post-mortem lives in +[`2026-05-17-textarea-editor-handoff.md`](./2026-05-17-textarea-editor-handoff.md). + +## Goal + +Upgrade three plain-text ` │ │ + └──────────────────────────┬────────────────────────────┘ │ + │ hidden, courier-only │ + mounted next to│ ▲ on form `submit` (capture): │ + │ │ textarea.value = │ + │ │ view.state.doc.toString() │ + ▼ │ │ + ┌────────────────────────────┴──────────────────────┐ │ + │
◄── EditorView (cm6) ─┤ │ + │ cm-content (contenteditable, hardened by cm6) │ │ + │
│ │ + └───────────────────────────────────────────────────┘ │ + │ controller.getValue() │ + ▼ called at JSON-save time │ + fetch('/files/save', …) ────────────────┘ +``` + +Why submit-time copy and not a per-keystroke live mirror: live mirroring +keeps two sources of truth (cm6 doc + textarea) in sync, fires +`input` events on the hidden textarea on every keystroke (a semantic +muddle — those events historically mean "the user typed", here they +also mean "cm6 dispatched"), and requires guard code to prevent +mirror writes from re-entering cm6. Submit-time copy collapses to a +single source of truth (cm6 owns the doc), with the textarea written +once per submission. Cost: 5 read sites + 3 write sites in +`files-overlay.js` switch from `textarea.value` to `controller.getValue()` +/ `controller.setContent()` — bounded, mechanical, single file. + +### Subsystems + +| Unit | Purpose | Depends on | +|---|---|---| +| `editor.js` | Un-bundled. Bootstraps cm6 on every `textarea[data-editor-language]`, hides the textarea, installs the submit-capture handler, exposes per-textarea controllers. | bundled cm6 + `tokens.css` | +| `editor.bundle.js` | Pre-built cm6 + extensions + custom srccfg grammar + light/dark themes, exposed as `window.__editor` (façade). | esbuild + npm deps | +| `editor.css` | Skins cm6 classes (`.cm-keyword`, `.cm-string`, gutter, selection, caret) against `tokens.css` CSS variables. | `tokens.css` | +| `srccfg-vocab.json` | Autocomplete corpus (~2196 cvars/cmds). | `build-vocab.py` reading `cvar_list` | +| `_editor_assets.html` | Jinja partial injecting nonce-tagged `