left4me/docs/superpowers/specs/2026-05-16-textarea-code-editor-design.md
mwiegand b19b00e706
spec(textarea-editor): adopt dedicated editor.css, simplify vocab sourcing
CSS lives in a dedicated stylesheet loaded only by the editor-assets partial,
not folded into components.css — keeps the editor's footprint isolated from
the global widget styles. Drop the two-stage vocab sourcing in favor of a
single cvarlist/cmdlist dump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:52:50 +02:00

175 lines
8.5 KiB
Markdown

# Textarea Code Editor (Highlighting + Autocomplete) Design
**Goal:** Upgrade three plain-text `<textarea>` fields in left4me into a
lightweight code editor with syntax highlighting and identifier-as-you-type
autocomplete. The motivating case is the blueprint config field, where users
edit Source-engine `server.cfg`-style content but have to remember hundreds of
L4D2 cvar names from memory.
**Architecture:** One reusable vanilla-JS widget. Each `<textarea>` opts in via
a `data-editor-language` attribute. The textarea stays in the DOM as the value
carrier — the widget mounts a `contenteditable` sibling that mirrors content
back into the textarea on every input, so HTML form submission and existing
JS that reads `textarea.value` keep working unchanged. Library stack is two
single-file self-hosted dependencies (CodeJar + Prism) plus a small custom
grammar — no bundler, no build step.
---
## Call sites
| Template | Line | Language | Save path |
|---|---|---|---|
| `l4d2web/l4d2web/templates/blueprint_detail.html` | 52 | `srccfg` (preset) | HTML form POST → `blueprint_routes.update_blueprint_form` |
| `l4d2web/l4d2web/templates/overlay_detail.html` | 25 | `bash` (preset) | HTML form POST → overlay script update route |
| `l4d2web/l4d2web/templates/overlay_detail.html` | 178 | `auto` (filename-derived; dropdown override) | `fetch('/files/save', {path, content})` from `files-overlay.js:450-557` |
## Widget contract
Every textarea with `data-editor-language="<lang>"` gets upgraded on
`DOMContentLoaded`:
- Sibling `<div class="editor-shell">` holding `<code class="editor-code"
contenteditable="true">`, initialized with the textarea's value.
- Textarea set to `display: none` (kept in DOM as form/value carrier).
- CodeJar mounted on the `<code>`, with highlighter →
`Prism.highlightElement` for the active language.
- CodeJar `onUpdate(code)` writes back to `textarea.value` and dispatches an
`input` event on the textarea.
- Floating autocomplete `<ul>` positioned at the caret via
`getSelection().getRangeAt(0).getBoundingClientRect()`.
- Exposes `setLanguage(name)` for the files-editor language dropdown.
If JS fails to bootstrap, the textarea is shown (matching today's behavior) —
graceful no-JS fallback for free.
## Autocomplete behavior
- **Trigger:** word fragment before caret matches `[A-Za-z0-9_]{2,}` and has
≥1 hit in the active language's vocabulary.
- **Filter:** case-insensitive prefix match first, then substring match. Cap
50 candidates; render top 8 with scroll for the rest.
- **Keys (popup open):** ↑/↓ navigate · Tab/Enter accept · Esc close · any
other key continues editing and re-filters.
- **Mouse:** click to accept.
- **Display:** identifier line + muted description line if `desc` present.
## Languages
- **`srccfg`** — custom Prism grammar (~30 lines). Tokens: comment (`//…`),
string (`"…"` with escapes), number, keyword (`exec | alias | bind`),
identifier. Purely visual — no semantic validation.
- **`bash`** — Prism's stock `bash` grammar. No project-owned code.
- **`auto`** — sentinel resolved on mount from the filename: `.cfg → srccfg`,
`.sh → bash`, otherwise `plain`. Re-evaluated when the filename input
changes (only while the dropdown is in its initial auto state).
- **`plain`** — no highlighter, no autocomplete. The widget still mounts so
the language dropdown remains usable; setting language back to `srccfg`
or `bash` re-activates highlighting.
## Vocabulary
Lives at `l4d2web/l4d2web/static/data/srccfg-vocab.json`:
```json
{
"cvars": [{"name": "sv_cheats", "desc": "Allow cheat cvars (0/1)"}, ],
"commands": [{"name": "exec", "desc": "Execute a .cfg file"}, ]
}
```
**Sourcing:** dump `cvarlist` / `cmdlist` from a freshly-started L4D2
server with the project's common SourceMod plugins loaded. Hand-trim engine
internals nobody touches. Descriptions come from the `cvarlist` output's
trailing help text where present; otherwise omitted. Generated once,
committed verbatim. Document the regeneration procedure in the top-of-file
comment of `srccfg-vocab.json`.
**Loading:** lazy fetch on first `srccfg` editor mount; cached on
`window.__srccfgVocab` so multiple editors on the same page share the load.
## Asset layout (new)
```
l4d2web/l4d2web/static/
vendor/
prism.js # prismjs.com custom build: core + clike + bash, pinned
prism.css
codejar.js # github.com/antonmedv/codejar release, pinned
README.md # source URLs + versions + SHA256 per file
js/
editor.js # widget: mount, popup, sync, language switch
srccfg-grammar.js # Prism.languages.srccfg
data/
srccfg-vocab.json # curated cvars + commands
```
All vendored files are self-hosted; the existing CSP
(`l4d2web/l4d2web/app.py:99` — `default-src 'self'; script-src 'self'
'nonce-…'`) requires it. Template `<script>` tags use `nonce="{{ g.csp_nonce }}"`
per the established pattern (`app.py:86`).
## Files Touched
| File | Change |
|---|---|
| `l4d2web/l4d2web/templates/blueprint_detail.html` | Add `data-editor-language="srccfg"` to line 52; include editor asset block |
| `l4d2web/l4d2web/templates/overlay_detail.html` | Add `data-editor-language="bash"` to line 25; add `data-editor-language="auto"` + language `<select>` near line 178 filename field; include editor asset block |
| `l4d2web/l4d2web/templates/_editor_assets.html` | New Jinja partial: the five script/link tags with nonces |
| `l4d2web/l4d2web/static/vendor/prism.{js,css}` | New (vendored) |
| `l4d2web/l4d2web/static/vendor/codejar.js` | New (vendored) |
| `l4d2web/l4d2web/static/vendor/README.md` | New — versions + SHA256s |
| `l4d2web/l4d2web/static/js/editor.js` | New — ~250 LOC widget |
| `l4d2web/l4d2web/static/js/srccfg-grammar.js` | New — ~30 LOC Prism grammar |
| `l4d2web/l4d2web/static/data/srccfg-vocab.json` | New — curated cvars/commands |
| `l4d2web/l4d2web/static/css/editor.css` | New — dedicated stylesheet for `.editor-shell`, `.editor-code`, `.editor-popup`; reuses `tokens.css` color variables; preserves textarea visual to avoid layout shift. Loaded by the `_editor_assets.html` partial so it ships only on pages that mount an editor. |
| `l4d2web/pyproject.toml` | Add `playwright` dev dep |
| `l4d2web/tests/e2e/conftest.py` | New — boot Flask app under test, expose URL fixture |
| `l4d2web/tests/e2e/test_editor.py` | New — Playwright test: type `sv_che`, assert popup, accept, assert textarea value |
| `l4d2web/tests/test_blueprints.py` | Extend — assert editor attrs in GET, assert form contract on POST |
**Untouched by design:**
`l4d2web/l4d2web/blueprint_routes.py`, `l4d2web/l4d2web/static/js/files-overlay.js`,
`l4d2web/l4d2web/app.py` — the form contract and the JSON-fetch save path
remain identical, which is the central reason this design uses
"textarea-as-value-carrier" rather than replacing the textarea.
## Reusable patterns referenced
- `l4d2web/l4d2web/static/js/blueprint-overlay-picker.js:20-26,63`
progressive enhancement via DOM-manipulated form fields. This editor
follows the same pattern (no submit interception, native form serialization).
- `l4d2web/l4d2web/app.py:86``g.csp_nonce` accessor for template `<script>`
tags.
- `l4d2web/l4d2web/static/js/files-overlay.js:450-557` — JSON-fetch save
path that continues to read `textarea.value` unchanged.
## Verification
Manual (Chrome MCP during development):
1. `/blueprints/<id>` — textarea replaced by editor; existing content preserved.
2. Type `sv_che` → popup shows `sv_cheats`; Tab accepts; line highlights.
3. Submit form; reload; saved value matches editor content.
4. Script-type overlay → bash highlighting.
5. Files-type overlay → create `test.cfg`, srccfg highlighting; dropdown to
bash, re-highlights; save; persisted content matches.
6. Disable JS, reload blueprint → textarea visible, form submits.
7. View-source → no inline scripts, all assets nonce'd.
Automated:
- `cd l4d2web && uv run pytest tests/test_blueprints.py`
- `cd l4d2web && uv run pytest -m e2e` (Playwright)
## Open / Closed
- **Closed in v1:** line numbers, search-in-editor, multi-cursor, bracket
matching, theme switching, SourcePawn highlighting, bash buffer-context
autocomplete, per-server live `cvarlist` capture.
- **Open as additive future work:** line numbers (deferred per explicit
scope decision; cheap to add later as a non-wrapping mode for `srccfg` +
`bash`); SourcePawn grammar; per-server cvar augmentation.
- **No backend changes.** The design is intentionally constrained to static
assets + template attributes so the change is small to review and easy to
revert by deleting the asset block.