feat: first release !

This commit is contained in:
Kharec committed 2026-09-28 14:21:06 +02:00
1 parent 77a333e8d9
commit 664527182d
66 files changed
+13812 -16

No files matched your search

+408
View File
@@ -0,0 +1,408 @@
# AGENTS.md
Everything you need to start working on **Plotzilla**, a Linux screenwriting app (an Arc Studio Pro /
Final Draft alternative) that produces scripts in the Hollywood standard format. Read this file
first; `README.md` is the user-facing documentation.
## 1. At a glance
| | |
|---|---|
| Backend | Go ≥ 1.23, [Wails v2](https://wails.io) (GTK 3 + WebKitGTK 4.1) |
| Frontend | TypeScript 7, [ProseMirror](https://prosemirror.net), Vite 8, plain CSS, no UI framework |
| Target | Any Linux desktop (GNOME, Plasma, Xfce…; Wayland and X11), GNOME look (Arch dev machine) |
| Native format | Fountain (`.fountain`); also FDX import/export and PDF export |
| License | GPL-3.0 (`LICENSE`); new dependencies must be GPL-3.0 compatible |
| Language | **Everything user-facing is English**: UI, messages, dialogs, README, docs, comments, commit messages |
## 2. Commands
```sh
make # npm install + vite build + go build → build/bin/plotzilla
make test # go test ./internal/...
make screenshots # regenerate the README images (needs Firefox and pdftoppm)
make dev # wails dev (live reload; needs `go install github.com/wailsapp/wails/v2/cmd/wails@latest`)
./build/bin/plotzilla path/to/script.fountain
cd frontend && npm run build # tsc --noEmit (type check) + vite build → frontend/dist
go vet -tags desktop,production,webkit2_41 ./...
gofmt -l . # must print nothing
```
- The Go build **requires the tag `webkit2_41`** (the machine only ships WebKitGTK 4.1). Plain
`go build .` fails; always go through `make` or pass `-tags desktop,production,webkit2_41`.
- `main.go` embeds `frontend/dist` (`//go:embed all:frontend/dist`). The frontend must be built
before the Go binary. `frontend/public/.gitkeep` keeps `dist/` non-empty on a fresh clone.
- System deps (Arch): `go nodejs npm webkit2gtk-4.1 gtk3`. Optional font: `ttf-courier-prime`.
Optional Hunspell dictionaries for spell checking: `hunspell-en_us`, `hunspell-fr`…
Definition of done for any change: `make` succeeds, `make test` passes, `gofmt -l .` is empty,
`npm run build` has no type errors, `npm audit` reports no vulnerabilities, README updated if
behaviour or shortcuts changed.
## 3. Repository map
```
main.go Wails bootstrap: frameless translucent window, LANGUAGE=en, CLI file argument
app.go App struct bound to JS: file I/O, dialogs, pagination, autosave recovery
desktop_linux.go cgo: desktop light/dark preference for GTK dialogs, window button layout, compositing
internal/screenplay the model: Document{TitlePage, Elements, Settings}, Element{Type, Runs, SceneNumber,
Dual, Level, Locked}, Run{Text, b/i/u, Note, Rev}, revision set names
internal/fountain Fountain parser (parse.go) and writer (write.go), incl. Plotzilla's boneyard data
internal/fdx Final Draft XML reader/writer
internal/layout line wrapping + pagination (paper sizes, locked pages); the single source of truth
for page geometry
internal/pdf hand-written PDF 1.4 writer, standard Courier fonts, zero dependencies
internal/spell Hunspell .aff/.dic reader, checker (compounds too), suggestions, word scan
frontend/index.html app shell: header bar, navigator, paper, HUD, <dialog>s
frontend/src/main.ts app controller: state, save/autosave, pagination loop, navigator, menus, shortcuts, window frame
frontend/src/schema.ts ProseMirror schema + conversion PM doc ⇄ ScriptDocument JSON
frontend/src/commands.ts screenplay keymap (Enter/Tab/Ctrl+1…9), retype, dual dialogue, auto-format
frontend/src/revisions.ts revision marking and locked-page anchors (plugins), lock/unlock/clear
frontend/src/autocomplete.ts suggestion popup (characters, locations, times, extensions, shots, transitions)
frontend/src/decorations.ts live page-break widgets + empty-element placeholder
frontend/src/search.ts find & replace: query, match highlights, next/previous, replace (all)
frontend/src/spellcheck.ts misspelling underlines (from Go), word under the cursor left alone
frontend/src/menu.ts popup menus with keyboard navigation (spelling corrections, languages)
frontend/src/backend.ts typed wrappers for window.go.main.App and the Wails runtime
frontend/src/icons.ts inline Lucide-style SVG icons
frontend/src/style.css the whole UI theme (dark, blue) and the on-screen page geometry
screenshots/ README images (2880×1800 UI captures + pdf-output.png)
tools/screenshots/ `make screenshots`: sample script, mock server (main.go), Firefox capture (capture.mjs)
VERSION current version (1.0)
CHANGELOG.md Keep a Changelog; sections Added, Changed, Fixed only
```
## 4. Architecture
### Data flow
```
ProseMirror doc ──toScript()──▶ ScriptDocument JSON ──Wails──▶ screenplay.Document (Go)
▲ │
└────────fromScript()◀──── OpenResult ◀── fountain/fdx ◀──────┘
├─▶ layout.Paginate ─▶ breaks → editor widgets
└─▶ layout.Paginate ─▶ pdf.Render
```
- The **Go model is canonical**. The frontend never parses or writes files itself; it sends the
whole `ScriptDocument` to Go (`Save`, `Paginate`, `ExportPDF`, …). Documents are small enough
for this to be cheap; pagination is debounced (300 ms).
- **Pagination lives only in Go** (`internal/layout`). The editor shows page breaks computed by
that same code, so the screen and the PDF always agree. Never reimplement wrapping or page
rules in TypeScript.
### The JSON contract (`backend.ts` ⇄ `internal/screenplay`)
`ScriptDocument = { titlePage, elements, settings? }`
`Element = { type, runs: [{text, b?, i?, u?, n?, rev?}] | null, sceneNumber?, dual?, level?, newPage?, locked? }`
`Settings = { paper?: '' | 'a4', revisions?: [{name, color, date}], revising?, locked? }`
- `runs` may be `null` from Go; always read it as `el.runs ?? []`. Every settings field may be
missing (Go omits zero values).
- `n: true` marks a note run (Fountain `[[…]]`): part of `Text()` and of every offset, never
printed (`Element.PrintedText()`, `printedText()` in the frontend). `rev: n` is the revision
set the text belongs to (`settings.revisions[n-1]`).
- `newPage: true` forces a page break before the element (Fountain `===`, FDX
`StartsNewPage`). On a dialogue part or a dual dialogue it breaks before the whole block;
on an element that prints nothing it moves to the next printed one. `Ctrl+Enter`
(`pageBreak` in commands.ts) sets it on the block's first element, never on a part.
- `level` is a section's depth (from 1). `locked: [{offset, page}]` lists the locked pages that
start in the element (rune offsets, like `Break`); in the editor they live in the element's
`locked` attribute as UTF-16 offsets (`runeOffset`/`utf16Offset` in schema.ts convert).
- A `"\n"` inside a run is a hard line break (ProseMirror `hard_break` node).
- `dual: true` on a **character** element means "this dialogue block sits to the right of the
previous one". In the editor this is a nested `dual > column column > element+` structure.
- `Break.label` / `ScenePage.label` is the printed page number (`12`, `12A`, `12-13`); `page` is
the physical page index. Show the label, compare with `page`.
- Element **indexes** used by `Break` and `ScenePage` count elements in flat document order,
including empty ones and elements inside dual dialogues. Use `flatElements(doc)` in the
frontend whenever you map an index to a position; never use `doc.child(i)`.
- `Break.offset` is a **rune** (code point) offset. JS strings are UTF-16: convert with
`utf16Offset()` (decorations.ts). `Misspelling` (`element`, `offset`, `length`) follows the
same rules.
- Any change to these structs must be made in both `internal/screenplay/screenplay.go` and
`frontend/src/backend.ts`, in the same change.
### Element types
`scene, action, character, parenthetical, dialogue, transition, shot, intertitle, centered,
section, synopsis` (Ctrl+1…9 for the first nine, in that order; the order of `ELEMENT_TYPES` in
schema.ts defines the shortcuts). Sections and synopses are outline elements: not printed
(`ElementType.Printed()` / `isPrinted()`), typed with `# ` / `= ` (auto-format).
**Checklist to add or change an element type** (the compiler catches the `Record<ElementType,…>`
maps, but not the rest):
1. `internal/screenplay`: constant, `Uppercase()` / `InDialogue()` / `Printed()` if relevant.
2. `internal/layout`: `specs` (left inch, width chars, blank lines before), `x()` alignment,
keep-with-next / splittable rules.
3. `internal/fountain`: parse rule and writer rule (Fountain has few types; document any lossy
mapping in README "Formats").
4. `internal/fdx`: `typeNames` and the reader switch.
5. Tests in `fountain_test.go`, `fdx_test.go`, `layout_test.go`.
6. `frontend/src/backend.ts` union, `schema.ts` `ELEMENT_TYPES` + `LABELS`.
7. `commands.ts`: `NEXT_ON_ENTER`, `RETYPE_ON_EMPTY_ENTER`, `TAB`, `SHIFT_TAB`, auto-detection.
8. `autocomplete.ts` if it has suggestions; `style.css` `.el-<type>` + `--t-<type>` color.
9. README shortcut table and format table; tooltip in `index.html` if the Ctrl+N range changes.
### Screenplay format rules (do not break)
US Letter, Courier 12 pt = 10 chars/inch, 6 lines/inch, 54 body lines per page, 1" top/bottom,
1.5" left margin. Indents (inches from page edge / width in chars): scene & action 1.5/60,
character 3.7/38, parenthetical 3.1/25 with 1-char hanging indent, dialogue 2.5/35, transition
right-aligned at 7.5", dual columns at 1.5" and 4.5". Pagination: scene headings and inserts are
kept with the next two lines; action and dialogue split only at sentence ends with ≥ 2 lines on
each side; never end a page on a parenthetical; split dialogue gets `(MORE)` / `NAME (CONT'D)`;
dual dialogue is never split; title page and page 1 are unnumbered; sections, synopses, notes
and lines holding only a note are not printed.
A4 (`layout.PaperGeometry`) keeps every margin and width: the page is 8.27" wide and holds 58
lines; right scene numbers move from 7.75" to 7.6". Revision asterisks go at 8.1" (A4: 7.95"), a
revised page's header (`Pink Revision - 09/28/26`) at 1.5" on the page-number line; a page is
starred only for its latest revision.
Locked pages (`settings.locked` + `Element.Locked`): each locked page starts where its anchor
is; overflow makes A pages (`12` → `12A` → `12B`), a page left empty is merged into the previous
one (`12-13`), an anchor inside a dialogue block cuts it with `(MORE)` / `(CONT'D)`, an anchor
inside a dual dialogue starts the page above the whole block.
The editor CSS reproduces the same geometry with `in` and `ch` units (1ch = 0.1in), the page
width through `--page-width` (`.paper[data-paper='a4']`): **if you change a spec in `layout.go`,
change `style.css` too** (and vice versa).
### Frontend state and saving
- `app` in `main.ts` holds `path`, `dirty`, `revision`, `saving`, `draftSaved`, `titlePage`,
`settings`, `layout`, preferences. The ProseMirror doc is the source of truth for the script
body; the title page lives in `app.titlePage` and the production settings (paper, revision
sets, marking, locking) in `app.settings`. Settings are not editor transactions: after
changing one, call `settingsChanged()` (marks the script changed, repaginates).
- Revisions (`revisions.ts`): `revisionPlugin` marks, in an appended transaction, the ranges
each change inserted (a deletion marks its neighbour); it skips undo/redo
(`isHistoryTransaction`) so undo removes text and mark together. `lockPlugin` maps every
locked-page anchor through each change and rewrites the `locked` attributes; lock, unlock and
clearing marks are built by `lockPages`, `unlockPages`, `clearRevisionMarks`. Their own
transactions carry the `INTERNAL` meta so the plugins never react to each other. Locking and
unlocking are not undoable (`addToHistory: false`), like the setting they go with.
- The `revision` mark is not inclusive (typing next to revised text is not revised unless
marking is on); the `note` mark is, like bold. Hard breaks carry note/revision marks only.
- **The frontend owns the dirty flag** and pushes it to Go with `SetDirty`. Go's `Save` must not
reset it: an autosave can finish after newer edits (`revision` guards this).
- Every change goes through `markChanged()` (revision++, dirty, schedule autosave).
- Autosave (1.2 s idle): writes the real file when there is a path, otherwise
`~/.local/share/plotzilla/recovery.fountain` (offered back at launch, removed on explicit discard
or first save).
- Spelling is checked in Go (`SpellCheck`, 500 ms after the last change, stale results
dropped like pagination); the frontend only draws underlines and menus. Dictionaries are the
system's Hunspell files, loaded lazily per language; the personal dictionary is
`~/.local/share/plotzilla/dictionary.txt`. Compound words are tried only when the word
itself is unknown (`Dictionary.accepts`): parts need COMPOUNDFLAG or the BEGIN/MIDDLE/LAST
flag of their position; the first part may take a prefix, the last a suffix, any other affix
needs COMPOUNDPERMITFLAG in its continuation classes. Suggestions do not build compounds.
- Find & replace is frontend-only (`search.ts`): matches are computed on `blockText`, where one
character is one document position. Replacements go through `replaceText()` (commands.ts),
which keeps the marks, and each one is its own undo step (`closeHistory`).
- Closing: `OnBeforeClose` in Go returns "prevent" while dirty and emits `confirm-close`; the
frontend asks with its own dialog and calls `runtime.Quit()` after `SetDirty(false)`.
### Window and desktop integration
- The window is **frameless and translucent**; the top bar is a GNOME-style header bar on every
desktop (`--wails-draggable: drag`, every interactive child `no-drag`, double-click toggles
maximise). `body` draws the 12 px rounded corners; `body.maximized` removes them, and so does
`body.opaque` (no compositor, hence no alpha channel: rounded corners would show black
wedges); `body.backdrop` dims the bar when unfocused.
- **Nothing may depend on GNOME being the desktop.** Read desktop settings through GTK or the
XDG portal, never with `gsettings`/`org.gnome.*` alone (they hold GNOME's defaults, not the
user's choice, on other desktops). `WindowFrame()` returns GTK's `gtk-decoration-layout`
(each desktop feeds it: GNOME/Plasma settings, XSettings on Xfce/MATE/Cinnamon,
`settings.ini`) and whether the window is really translucent; the frontend lays out the
buttons from it and ignores entries it does not draw (`menu`, `appmenu`, `icon`, `spacer`).
The light/dark preference for native dialogs comes from the portal's
`org.freedesktop.appearance color-scheme`, with GNOME's `color-scheme` as the fallback when
no portal answers; "no preference" leaves GTK's configured theme alone.
- **Never use Wails `runtime.MessageDialog` for questions**: on Linux it ignores custom buttons
and shows GTK stock Yes/No/OK translated into the system language. Use `messageBox()` in
`main.ts` (supports `destructive`).
- `main.go` sets `LANGUAGE=en` so GTK's own strings (file chooser) are English.
- `desktop_linux.go` must only touch GTK through `g_idle_add` (main loop); Wails callbacks run
on other goroutines. `query_window_frame` shows how to get an answer back synchronously
(refcounted request + `GCond` with a timeout); never call it from the main thread.
## 5. Testing
- Go: unit tests live next to each package (`make test`). Add a test for every parser,
writer or pagination change; round-trip tests (`Parse(Write(doc)) == doc`) protect formats.
- PDF visual check: `PLOTZILLA_PDF=/tmp/x.pdf go test ./internal/pdf`, then
`pdftoppm -png -r 60 /tmp/x.pdf /tmp/page` and look at the image, or
`pdftotext -layout /tmp/x.pdf -` to check columns.
- UI without the Go side: serve `frontend/dist` over HTTP with a small script defining
`window.go.main.App` (async stubs returning sample data) and `window.runtime`, injected before
the module script; screenshot with
`firefox --headless -no-remote -profile <tmpdir> --window-size=1440,900 --screenshot out.png URL`.
Disable animations in that page (`*{animation:none!important;transition:none!important}`)
or the capture happens mid-fade. Firefox is not WebKitGTK: confirm anything engine-specific
(zoom, drag, dialogs, fonts) in the real app with `make dev`.
- Screenshots (`screenshots/`): regenerate them with `make screenshots` whenever the UI changes
visibly, and keep the README references in sync (a new image: add its steps to
`tools/screenshots/capture.mjs`). The run is deterministic: unchanged UI gives identical
pixels. How it works:
- `tools/screenshots/main.go` serves `frontend/dist` with a `window.go.main.App` proxy posting
each call to `/api/<Method>`, answered by the real `fountain`, `layout` and `screenplay`
packages, so pagination, locked pages and revision sets are **real**. The sample script is
`tools/screenshots/the-last-reel.fountain`; `?v=typos` and `?v=outline` serve variants (the
misspellings `projecter`, `thraeds`, `wacth` for `spelling.png`, whose suggestions are fixed,
so no dictionary is needed; sections, synopses and a note for `outline.png` and
`revisions.png`). `Save` writes nothing; revision sets have fixed dates.
- `capture.mjs` drives Firefox through `puppeteer-core` (WebDriver BiDi) at 1440×900, device
scale 2, with animations, transitions and the caret disabled and `<html spellcheck="false">`
(WebKitGTK does not spell-check; Firefox does). It moves the pointer off the header before
each capture (tooltips).
- BiDi cannot omit the background, so `main.go` masks the 12 px window corners (radius 24 px
at scale 2), and renders `pdf-output.png`: the PDF export of the sample with scene numbers,
`pdftoppm -r 110`, pages side by side on the `--bg` color.
- `FIREFOX=/path/to/firefox` picks another browser binary; `SCREENSHOTS_FLAGS="-out dir"`
writes the images elsewhere (to compare before replacing).
- Behaviour that only exists in the real window (drag, resize, GTK dialogs, close flow,
autosave timing) must be checked by running `./build/bin/plotzilla`; say so when you could not.
## 6. Go best practices (this repo)
- `gofmt` everything; `go vet` clean with the build tags. Standard library first: the PDF writer
and all formats have **no third-party dependency**, keep it that way unless there is a strong reason.
- Business logic lives in `internal/…` packages that know nothing about Wails; `app.go` is a thin
adapter (dialogs, file system, JSON-friendly return types). Keep packages free of GUI imports
so they stay testable with plain `go test`.
- Bound methods: exported methods on `App` become JS APIs. Return `(T, error)` and let the
frontend handle the error; return `nil`/`""` (not an error) when the user cancels a dialog.
- Wrap errors with context (`fmt.Errorf("reading FDX: %w", err)`), lower-case, no trailing
punctuation. Never `panic` on user input; malformed files degrade to best effort.
- Write files atomically (`writeAtomic`: temp file in the same directory + rename).
- Text is Unicode: iterate with runes, use `screenplay.UpperRunes` when offsets must survive
upper-casing (`strings.ToUpper("ß")` changes length). Offsets exchanged with the UI are runes.
- Prefer small pure functions and value types; avoid package-level mutable state (the specs
tables are read-only). Use `min`/`max` builtins, `strings.Builder`, `bytes.Buffer`.
- Comments explain *why* (format rules, GTK quirks), not what; doc comments on exported identifiers
start with the name.
- cgo is confined to `desktop_linux.go` behind `//go:build linux`.
## 7. TypeScript best practices (this repo)
- `strict` mode, `noUnusedLocals`; `npm run build` must type-check cleanly. No `any`: use
`unknown` and narrow, or a precise type. Use `import type` for type-only imports.
- No framework: DOM APIs, small modules, one responsibility per file. Query elements once and keep
references; build DOM with `createElement`/`textContent`. Use `innerHTML` only for static
trusted markup (icons); **never** for script text, file names or error messages.
- ProseMirror rules:
- All document changes go through transactions (`view.dispatch(tr)`); never mutate DOM inside
the editor. Build one transaction per user action so undo is a single step; set
`addToHistory: false` for non-content metadata (page-break decorations).
- Commands follow the `(state, dispatch) => boolean` contract: return `false` when not
applicable, do nothing if `dispatch` is undefined.
- When positions change inside a transaction, map them with `tr.mapping.map()` instead of
recomputing from the old document.
- Positions are UTF-16 offsets; convert Go rune offsets explicitly.
- Plugin order matters: autocomplete before the screenplay keymap, keymaps before `baseKeymap`.
- Async calls to Go can resolve after the document changed: compare against a captured doc or
`revision` before applying results (see `paginate()` and `autosave()`).
- Handle every promise: `await` inside `try/catch`, or `.catch(reportError | console.error)`.
- `localStorage` only for per-user preferences (zoom, panel, autosave, export options), always
through the `load`/`store` helpers (they swallow storage errors).
- Keyboard shortcuts: global ones in the `shortcuts` map (capture phase, skipped while a dialog is
open); editing ones in `screenplayKeys()`. Update the README table and tooltips together.
## 8. CSS best practices (this repo)
- One stylesheet, organised in commented sections (topbar, menu, main, hud, paper, page break,
autocomplete, dialogs). Plain CSS, no preprocessor.
- **Design tokens only**: colors, radii, easing and fonts come from the custom properties in
`:root` (`--bg`, `--surface`, `--text`, `--accent`, `--t-<element>`…). Do not hard-code a new
color in a rule; add a token. The theme is intentionally always dark, blue-dominant.
- Screen geometry of the page uses physical units (`in`, `pt`, `ch`) that mirror
`internal/layout`; UI chrome uses `px`. Do not mix them.
- Class names are short, kebab-case, component-scoped (`.nav-head`, `.win-button`); state is a
class on `body` (`.focus-mode`, `.no-navigator`, `.maximized`, `.backdrop`) or a `data-*`
attribute (`[data-state='saved']`), never inline styles except computed positions.
- Animate only `opacity` and `transform`/`translate` with `var(--ease)`; keep durations
≤ 350 ms; every hover state has a matching transition. Respect focus visibility
(`:focus-visible` outlines on buttons).
- Custom properties inherit, which the page-break widget relies on (`--indent` per element type)
and so does window dragging (`--wails-draggable`). Remember both when nesting new elements in
`.topbar` or `.el`.
- Target WebKitGTK: `-webkit-` scrollbar pseudo-elements plus standard `scrollbar-*` properties;
`zoom` is used for editor zoom; `backdrop-filter` is fine. Avoid features WebKit lacks.
## 9. Gotchas already paid for
- `go:embed` fails if `frontend/dist` is missing → run `make`, not `go build`.
- GTK stock buttons follow the system locale (`fr_FR` here) → in-app dialogs + `LANGUAGE=en`.
- GTK 3 ignores the desktop's dark preference → `desktop_linux.go` (portal, then GNOME fallback).
- GTK reports GNOME's `appmenu` button as `menu`; both are ignored by the header bar.
- `gsettings` exists on every desktop that has GTK 3, but outside GNOME it returns GNOME's
defaults: it is not a way to read the user's desktop settings.
- Fountain cannot express insert/shot or intertitle types: shots round-trip only when they start
with a known prefix (`INSERT`, `CLOSE ON`, `POV`…); all-caps centred text is read as an
intertitle. Documented in README; keep the parse and write rules symmetric.
- Fountain has no revisions, locks or paper size: they are boneyard comments, which other apps
ignore. Inline markers travel with the text (`/*rev 2*/…/*/rev*/`, `/*page 12*/`), settings are
one `/* Plotzilla {json} */` block at the end (`*/` inside JSON is written `*\/`). The parser
first replaces notes and markers by tokens (U+E000 payload U+E001, no line break) so a note
spanning lines keeps its paragraph; line rules test `bare(line)` (tokens removed) and `inline`
turns tokens back into runs and locks. Writers must put Fountain's force prefixes (`.`, `@`,
`!`, `>`) **before** any marker. A script without these features must write byte-identical
output to before (`TestPlainScriptUnchanged`).
- The Fountain writer skips empty elements: their locked pages and forced page break are carried
to the next element written (layout does the same for elements that print nothing).
- Final Draft has no sections or synopses: the FDX writer stores them as `ScriptNote`s whose
text starts with `#` / `=`, attached (Range `0,0`) to the next paragraph, or to an empty
trailing Action paragraph at the end; the reader turns such notes back into elements. An
ordinary note starting with `#`, `=` or `\` is written with a leading `\`, removed on
reading. Revised text is `<Text RevisionID="n">`, the sets a `<Revisions>` block named
`Blue Revision - date`. The exact Final Draft schema for these (ScriptNote placement,
Revisions attributes) was written from memory, not checked against Final Draft itself.
- COMPOUNDRULE parts are bare dictionary words (no affixes), as in Hunspell; the first
COMPOUNDRULE line is the rule count.
- A literal BOM or other invisible character in Go source breaks `go vet`; write `""`.
- `npm` blocks install scripts (esbuild postinstall); the build still works with the bundled
binary.
- Vite empties `dist/` on each build; placeholders belong in `frontend/public/`.
- Floating layers over the script (menus, suggestions, HUD) use the opaque `--popover` token:
translucent backgrounds let the script text bleed through.
- A `position: fixed` popup anchored to the text must follow scrolling (see `reposition` in
autocomplete.ts).
- Hidden-but-hoverable UI (focus mode) must stay in place with `opacity: 0`; moving it off-screen
with a transform also moves its hover area.
- The dev shell is zsh: unquoted variables are not word-split and an unmatched glob is an error.
- `pkill -f <pattern>` also matches the shell running it when the pattern is in its command
line (it kills your own command, exit code 144); use `pkill -x <name>`.
- Firefox gives a synthetic `ClipboardEvent` an empty `clipboardData`: test paste with
`navigator.clipboard.writeText` + a real `Ctrl+V`.
## 10. Git and commits
- Commits are **atomic**: one logical change per commit, with the code, its tests and its
documentation together. Every commit must build (`make`) and pass `make test`, so any commit
can be checked out or reverted on its own. Do not mix a fix with a feature or a reformat.
- Messages are in **English** and follow this convention:
| Prefix | Use for |
|---|---|
| `feat:` | a new feature or a user-visible improvement |
| `fix:` | a bug fix |
| `chore:` | changelog and maintenance (dependency updates, build and project configuration) |
| `ci:` | anything that touches CI |
| `test:` | changes to tests only |
| `docs:` | README, AGENTS.md and other documentation |
- Summary line: `<prefix> <what the commit does>`, imperative mood, lower case, no final period,
72 characters at most (`feat: add dual dialogue`, `fix: keep autocomplete popup under the
caret when scrolling`). Leave a blank line, then explain *why* in the body when it is not
obvious.
- A feature commit includes its tests; use `test:` only for commits that change tests alone.
Screenshot updates go with `docs:`.
- Never commit build output (`build/bin`, `frontend/dist` except `.gitkeep`,
`frontend/node_modules`, `frontend/wailsjs`); `.gitignore` covers them.
## 11. Roadmap (not implemented)
Character/location reports, recent files, locked pages and paper size in FDX.
Keep new features consistent with the rules above: Go owns formats and layout, the frontend
owns interaction.