feat: paste overrides through a reviewable dialog, any origin (REQ-190)

navigator.clipboard.readText() only exists in a secure context, so on plain http
over a LAN address it is absent, not merely refused — the previous one-click
paste could never work there. A keyboard paste into a focused textarea is an
ordinary user gesture and is not gated, so paste now opens a dialog: the YAML
goes in a textarea, prefilled only when the browser allows the read.

Nothing is written until Apply, and Apply stays disabled while the text does not
parse, so a typo cannot be clicked through. A live line names the classes the
block matched, the container flags it would flip and the classes it would ignore.
Esc closes and returns focus to the button; Enter is left to the textarea,
because YAML needs newlines.

planPaste() produces that review line and applyPasted() does the writing, keeping
the selected-classes-only rule and the single batched container PATCH.
This commit is contained in:
asus committed 2026-10-05 14:41:22 +07:00
1 parent 4575c46f4c
commit 3eeb529e3d
6 files changed
+229 -84

No files matched your search

+16 -11
View File
@@ -160,22 +160,27 @@ changes.
review assist filter), surfaces as `MaxBox` in the params table, and is included in the
REQ-186 copy line. Size bounds are ordinary per-class tuning: the `container` flag
(REQ-184) stays purely the NMS containment carve-out and gains no size semantics.
- **REQ-190** — The same table has a **Paste YAML** button that reads the clipboard and fills
the per-class overrides from it, so a tuned configuration moves between projects without
retyping. It applies only the classes currently **selected in this modal**; a pasted class
that is not selected is reported as ignored rather than written, because `buildClassParams`
walks every entry and an unseen override would otherwise ride along in `class_params`.
Numbers merge per field (a pasted block overwrites the four numbers, leaves other state
alone); `container` goes out as one `PATCH /projects/{id} {containers: {classId: bool}}`
covering every flag that changed, reverted together with a message if the save is rejected.
- **REQ-190** — The same table has a **Paste YAML** button that opens a **dialog** holding the
YAML in a textarea, so a tuned configuration moves between projects without retyping. The
textarea is the real path and must work **anywhere the app is served** — no https, no
localhost, no permission prompt: `navigator.clipboard.readText()` only exists on a secure
origin, but a keyboard paste into a focused textarea is an ordinary user gesture and is not
gated. The dialog therefore *prefills* from the clipboard when the browser allows it and
otherwise just waits for Ctrl+V. Before anything is written, a live line reports what the
text would do — the classes it matched, the `container` flags it would flip, the classes it
would ignore — and any parse error, with **Apply disabled** while the text does not parse.
It applies only the classes currently **selected in this modal**; a pasted class that is not
selected is reported as ignored rather than written, because `buildClassParams` walks every
entry and an unseen override would otherwise ride along in `class_params`. Numbers merge per
field (only the fields the block mentions are overwritten, omitted ones keep their value);
`container` goes out as one `PATCH /projects/{id} {containers: {classId: bool}}` covering
every flag that changed, reverted together with a message if the save is rejected.
Parsing is strict on purpose — a subset reader for exactly what Copy emits (`#` comments,
`class:` at the left margin, `key: value` indented under it) rather than a general YAML
engine, and **any** malformed line throws with its line number and applies **nothing**, so a
half-understood paste cannot quietly set wrong parameters. A value must *look* like a number,
not merely start like one (`0.5abc` is rejected rather than read as `0.5`), and values
outside 0–1 are rejected the same way. Clipboard **read** has no legacy fallback, so a blocked or denied read (the app
served over plain http on a LAN address, or the browser prompt refused) is reported in the
status line instead of failing silently. Paste never touches the global sliders.
outside 0–1 are rejected the same way. Paste never touches the global sliders.
## E. Review & correction