feat(ui): link the editor's guided form and JSON view — live sync, cross-highlight, type-honest edits - #573
Merged
Merged
Conversation
…riptions The guided form led with the element's short description and demoted the name to small mono type — backwards for scanning. The name is the label now; the description rides second, muted and ellipsized, full text in the tooltip. The two panes also point at each other. Every JSON line now carries the dotted document path of its node (the same spelling the rows key on), so hovering or focusing a form row highlights that node's lines in the JSON, hovering a JSON line highlights the row that edits it, and clicking a JSON line jumps to the row and focuses its input. Delegated document-level script (editor-sync.js), so the standalone page and the Resources modal both get it; without JS the editor is simply un-linked.
While "Edit raw" is open the JSON pane is one textarea, so there are no lines to cross-highlight — instead the caret drives the form. A structure-only scan of the text up to the caret (string- and escape-aware, so a half-typed document still resolves) yields the dotted path of the node the caret sits in; that row lights up and is revealed in the tree. A caret inside a key string binds the key by looking ahead for the colon, so typing a new field name lands on that entry's row, not the container's.
Typing valid JSON in the raw textarea now re-renders the guided form as you go: the document round-trips to /ui/editor/render and only the form pane, the hidden document field, and the binding datalists are swapped — the textarea and its caret are never touched. Invalid JSON just waits; the form always shows the last parseable state. The other direction fell out of raw mode surviving re-renders: a guided edit already round-trips the textarea's document with the mutation applied, and capture/restore in both editor wirings now keeps the raw pane open, so the refreshed textarea shows the change instead of snapping back to the fold view.
With raw mode open there are no fold-view lines to light, so hovering or focusing a guided-form row found nothing to point at. A mirror <pre> now sits behind the textarea with identical metrics; its text is invisible and only the <mark> around the hovered node's character range shows through (the textarea's own background goes transparent while mirrored). The range comes from the same structure-only scan as the caret follow, entering and leaving the target's subtree — string-aware, tolerant of half-typed documents, and careful not to drag an ancestor's closing bracket into the range. The mark also pulls the textarea's scroll to keep the node in view, and every keystroke clears the mirror so stale offsets never mark the wrong text.
…rough The mirror positioned itself against the viewport — a full-width input-colored slab over the page as soon as a guided row got focus with raw mode open. The #543 dead-class sweep had removed class="editor-json__raw" from the raw pane (it matched no rule at the time), so the new .editor-json__raw { position: relative } anchor matched nothing and the absolute mirror escaped to the body. The class is back — it is load-bearing now — and the mirror is also pointer-events: none, so an aria-hidden overlay can never eat clicks again, wherever it ends up.
Name, badges, and the input stay on the head line — the input now keeps a 180px floor and the head wraps instead of squeezing it invisible — and the element's short description moves below, muted, free to wrap. Long descriptions used to eat the row and push the field out of sight.
…r shape The editor's set operation guessed the JSON type from the text: anything that parsed as an integer became a JSON number. A year-precision birthDate of 1974 — legal FHIR — round-tripped as a number and failed validation with "expected date, got number"; an identifier value of 12345 was silently retyped the same way. set_value now asks the schema: booleans and the numeric primitives (integer, positiveInt, unsignedInt, decimal — not integer64, which FHIR serializes as a string) leave string-land, everything else stays a string no matter how numeric it looks. Elements the schema does not know keep the old shape-based guess so unknown keys still round-trip their JSON types. Decimal entry also works now — it previously fell through the integer-only parse and was stored as a string.
…data Saving in the Resources modal left the table underneath showing the world as it was before the save; only a manual page refresh caught it up (and delete did a full location.reload()). The two scripts now meet over one event: resources.js announces hfs:data-changed after a successful save or delete (naming the affected type), and saved-queries.js re-runs the last search — without recording a new recent — and re-hydrates that one type's rail count rather than stampeding all 145. Delete loses its full-page reload and keeps the rail selection and scroll where the user left them.
editor-sync.spec.ts drives the whole contract in a real browser: row hover lights the JSON lines and back, clicking a line focuses the row's input, valid raw JSON re-renders the form without leaving raw mode and a guided edit refreshes the textarea in place, the caret lights the row of the node it sits in, the mirror marks the hovered node's text without ever taking a click, the element name leads the row with the description under it, and a save in the Resources modal refreshes the results table behind it. The suite's first run caught a real regression: with raw mode surviving re-renders, the exit toggle could no longer close it — the state capture ran while the pane was still open and faithfully restored it. Both editor wirings now close the pane before that round trip, so the one re-render that means "the user left raw mode" reads that way.
smunini
approved these changes
Aug 19, 2026
smunini
added a commit
that referenced
this pull request
Aug 19, 2026
Three `set_value` calls in the editor tests exceed the line limit and `cargo fmt --check` rejects them, failing the Linting job. Arrived on main with #573; purely mechanical reformat, no behavior change. Claude-Session: https://claude.ai/code/session_01ArLi5r8NFZ2vJJ7uJvwZWA
smunini
added a commit
that referenced
this pull request
Aug 19, 2026
Two call sites exceeded rustfmt's default fn_call_width (60) and were left on one line, failing `cargo fmt --all -- --check`: * crates/fhir-validator/src/editor.rs — arrived on main with #573 and is already breaking main's Linting job at 9636888; the merge inherited it. * crates/ui/tests/subscriptions_http.rs — predates the merge on this branch. Pure formatting; no behavior change. Claude-Session: https://claude.ai/code/session_0197k7pHRnRPd2TaDjibNSDJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Manual-review feedback on the resource editor, turned into a feature: the guided form and the JSON view now behave as two views of one document instead of two documents that meet on save.
Stacked on #572 (base is
fix/543-stylesheet-unification— it touches the same templates and stylesheet; GitHub will retarget tomainwhen #572 merges).The link
data-jpath) — the same spelling the rows key on, so the match is string equality.<pre>(pointer-events: none, invisible except the<mark>). And a guided edit no longer kicks the user back to the fold view: raw mode survives the re-render with the refreshed document in the textarea.editor-sync.js) drives the standalone page and the Resources modal alike; without JS the editor is simply un-linked.Fixes the same review shook out
set_value): the editor guessed JSON types from the shape of the text, so a year-precisionbirthDateof1974— legal FHIR — round-tripped as a JSON number and failed validation with "expected date, got number", and anidentifier.valueof12345was silently retyped. Coercion now follows the declared type: only booleans and the numeric primitives leave string-land (integer64stays a string, as FHIR serializes it); unknown keys keep the old shape-based guess. Decimal entry also works now — it previously fell through the integer-only parse.hfs:data-changed; the table re-runs the last search (without recording a new recent) and re-hydrates the affected type's rail count — not all 145. Delete loses itslocation.reload().Tests
editor-sync.spec.ts: seven browser-level specs — hover both directions, click-to-focus, live raw→form with raw surviving a guided edit, caret follow, the mirror marking without ever taking a click, the row hierarchy, and the modal save refreshing the table behind it.