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

+21 -9
View File
@@ -468,10 +468,10 @@ Two columns inside one dialog (920 px wide, max 96 vw / 90 vh, scrollable).
with revert on rejection — preview and batch job read the same stored flag.
- **Copy / Paste YAML (REQ-186, REQ-190)** — a small right-aligned toolbar above the overrides
block: two real buttons, `Copy YAML` and `Paste YAML`, each an inline SVG icon (Lucide copy /
clipboard-paste, no emoji) plus its label, `btn` styling at `0.76rem`, both disabled while a
paste is in flight. A `role="status"` line beside them carries the outcome — `Copied`,
`Pasted 3 classes`, `Pasted 2 classes — ignored crane`, or the failure in `--danger` — success
text clears itself after 2 s, a failure stays until the next action.
clipboard-paste, no emoji) plus its label, `btn` styling at `0.76rem`. A `role="status"` line
beside them carries the outcome — `Copied`, `Pasted 3 classes`, `Pasted 2 classes — ignored
crane`, or the failure in `--danger` — success text clears itself after 2 s, a failure stays
until the next action.
Copy puts one YAML block per selected class on the clipboard:
```yaml
@@ -487,8 +487,21 @@ Two columns inside one dialog (920 px wide, max 96 vw / 90 vh, scrollable).
Field names are the row labels as written on screen; a name is quoted only when YAML would
misread it (`"weird: name":`). An empty input copies the global value, so the block is a
complete configuration, not a diff.
Paste reads the clipboard and fills the overrides for the classes **selected in this modal**;
an unselected class is named in the status line and never written. Numbers merge per field.
Paste opens a dialog and never applies blind. A dark overlay card (`rgba(0,0,0,0.85)`, above
the auto-annotate modal in z-order, `role="dialog" aria-modal="true"`), the title *Paste
per-class overrides*, a monospace `textarea` focused on open, and a live review line under it
(`role="status"`, recomputed per keystroke, no debounce): the matched classes, the `container`
flags it would flip, the classes it would ignore — or the parse error in `--danger`.
**Apply stays disabled while the text does not parse**, so a typo cannot be clicked through;
*Cancel* is a ghost button and a backdrop click cancels. Keyboard paste works everywhere: the
dialog prefills from the clipboard only when the browser permits a read (localhost or https)
and otherwise says so and waits for Ctrl+V — no permission prompt and no https requirement.
`Esc` closes and returns focus to the *Paste YAML* button; **Enter is never bound to Apply**,
because YAML needs newlines. Focus moves into the textarea on open; no focus trap (nothing in
this app has one).
Apply fills the overrides for the classes **selected in this modal**; an unselected class is
named in the review line and the status line, and never written. Numbers merge per field — the
fields the block mentions are overwritten, the ones it omits keep their value.
`container` is saved to the server like ticking the box by hand (one PATCH for all changed
classes, reverted together on rejection, and the status line says the flags were not saved).
Parsing is strict and **atomic**: `#` comments (whole lines, or trailing on a value), `class:`
@@ -496,9 +509,7 @@ Two columns inside one dialog (920 px wide, max 96 vw / 90 vh, scrollable).
(`0.5abc` is a typo, not `0.5`), `container` `true|false|yes|no|1|0`; a list item, a nested
map, a top-level scalar, an unknown key, a malformed number, an out-of-range value or a
key before any class throws `line N: …` and applies **nothing**. A class repeated in the same
document merges rather than replacing. Where the browser blocks the clipboard read — the
app on plain http over a LAN address, or the permission prompt refused — the status line says
so rather than doing nothing.
document merges rather than replacing.
Both buttons live in the shared per-class table, so they exist in the engine-chooser modal
**and** the mass modal.
- `ClassPromptPanel`:
@@ -1161,6 +1172,7 @@ Handlers are attached in the **capture** phase on `document`, and:
| hold `S` | SAM3 assist mode while held | — |
| `Enter` | apply the exemplar preview (panel open) | — |
| `Esc` | discard the exemplar preview (panel open) | — |
| `Esc` | cancel the Paste YAML dialog, focus back on its trigger | — |
**In select mode the marquee owns `Delete` and the digits.** Otherwise a 40-box selection is
thrown away by one keystroke meant for it.