Skip to content

feat(form-engine): an opt-in calm appearance for the compact form, and a new form is not already wrong — 0.10.65 - #145

Draft
Foxhoundn wants to merge 13 commits into
developfrom
feature/144_form-engine-calm-appearance
Draft

Foxhoundn wants to merge 13 commits into
developfrom
feature/144_form-engine-calm-appearance

Conversation

@Foxhoundn

@Foxhoundn Foxhoundn commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Draft, for Filip to review — please don't merge yet.

Closes #144

Why

The Qorus IDE is moving to a calmer design: colour only where something needs a person, panels on one surface edged by one hairline, quiet headings and counts (instruction-files BRAND_DESIGN.md §1, decisions 001 / 004 / 005 / 006 / 017). Every IDE creator, editor and many dialogs render through FormEngine's compact mode, and it was the biggest surface still in the old look — green Ready and meter, green/amber tinted boxes with coloured count tags, a dot on every row, UPPERCASE group labels and "SELECT ONE:", red asterisks and a red "This field is required", Yes/No as green/red chips, and the outer panel painting a card around its own boxes once a theme gives panels a surface.

Other apps use Reqraft too, so the new look is opt-in: nothing changes for a consumer that does not ask for it.

The opt-in

<FormEngine compact appearance='calm' … />
  • appearance?: 'default' | 'calm' on FormEngine (default 'default').
  • FormEngine publishes it through FormAppearanceContext. Nested arg_schema sub-forms and every field editor the form mounts follow it; a form without its own appearance follows a FormAppearanceProvider above it, so a host can also set it once for its whole tree.
  • Every look choice is decided once, inside the engine (formAppearance.ts + the engine's styled parts). Nothing a row does reads it: bucketing, counts, validation, templates, search, the optional fields and read mode (fix(form-engine): read-only is a view, not a disabled editor #107) are identical in both looks.

What calm changes

Sections (Needs attention / Set / Optional)

  • Plain panels: Reqore paints the theme's panel surface and its one hairline edge — no green/amber tint, no accent border, no recessed black Optional box.
  • Header: the label and a quiet count number (no coloured tag, no icon). Needs attention carries one amber dot — one dot and a word, only where a person is needed.
  • Rows are label/value rows split by one full-width hairline, 16px inset, no rounded hover pill, no gap between them. The "N more fields" control is a quiet last row (no dashed box).
  • A pinned section header paints the panel surface.
  • The outer compact panel is transparent and un-raised, so it never draws a card around the section cards (and the readiness line, search and sections share one left edge).

Rows

  • Labels grey (the value is what the eye lands on); the same colour in the read row, the open row and the edit card.
  • No required asterisk. An empty required value reads Required · add a value in amber in the value column (read-only: Required · not set), instead of a dash plus a red "This field is required".
  • No status dot on a healthy row, nor on an empty required one (the amber words say it); a failing value keeps its red dot beside its reason.
  • Yes/No as plain words.
  • Done and Cancel in the open row are neutral buttons (no green / amber).
  • The one-of chip and the Draft chip are not upper-cased; a met one-of group's chip is not green.
  • The edit card is a tinted row of its section, not a second bordered card inside it.

Headings

  • Group sub-labels in sentence case ("Info", not "INFO"), 13px, primary text colour; the field labels below are grey.
  • "One of the below is required" is an amber sentence at the rows' size, not an upper-case eyebrow in a tinted box.
  • Choice headings ("Language:", "Select one:") in sentence case — one shared ChoiceGroupHeading for both choice editors. Radio-group option names and the schema preview's field names are no longer upper-cased.

Readiness line

  • One quiet line: the status word in the text colour, "N/M set" and the percentage as quiet figures, and only the outstanding part ("N need attention →") in amber.
  • A short (≤360px) 4px meter beside it, wrapping under the words on a phone: the done part grey, only the outstanding part amber.

Messages under an open field

  • Plain lines of text in their own colour, not tinted chips with a warning glyph.

Read mode

  • One list of hairline-split rows, flush with the host's edge, with no card of its own (a read view usually sits in a card the host drew), no dots.

A new form is not already wrong (both looks)

A brand-new form used to open with "2 need attention" and every required field in amber before anyone had typed, so the first thing a person met was a list of faults. Now:

  • An empty required field nobody has been to yet is pending: still in the first box, which reads To fill in in the quiet colour, with the requirement stated plainly ("Required · add a value" / "This field is required") and no intent.
  • It becomes a fault (todo, amber, under Needs attention with its dot) once the person opens it and leaves it empty, or when the host reports a failed submit with the new revealRequired prop. A value that fails validation is a fault from the start, as before. Open is not left: a field the form opens for typing on arrival (the first empty one) says its requirement under the editor in the same quiet colour (OptionFieldMessages takes requirementPending).
  • The readiness line says "N to fill in →" in the quiet colour; once something is a fault, "N need attention → · M to fill in", and the meter's amber run counts faults only.
  • A host's submit can say what is left, and a blocked submit is answered with where to type. onAttentionChange({ count, faults }) reports the readiness line's two numbers, so a submit button drawn from them can never disagree with the form. focusAttentionRequest (bump it per press) opens the first field that needs attention and puts the caret in it, though the submit button holds the caret at that moment; an already-open field gets the caret directly. The arrival opener and the request share one scan.
  • Found on the way, all with tests that fail without the fix: a field that opens as a card (long text, description, source code) never took the caret on opening, only the inline row did; the "need attention" filter hid the row being typed in after its first character; and "N to fill in →" was a clickable span, now a button with aria-pressed, Enter and Space.
  • compactBoxLabels.pending renames "To fill in". getReadFirstStatus takes visited (unset = visited, the old behaviour); getOptionFieldMessages takes requirementPending.
  • Tests: formEngineFreshForm.test.tsx, readFirst.test.ts; story FormEngine Appearance › Calm New Form (a new form, then a visit). Stories about which box a field lands in find the first box under either name; Calm Needs Attention now shows the state after a submit attempt.

Version

0.10.65: develop published 0.10.64 (the security dependency updates) while this PR was open, so the branch merged develop and bumped once more.

Not in this PR

  • An index of sections beside the form (the IDE concept shows one) — it needs a design decision about status boxes vs schema groups.
  • The expression builder's own argument labels (still upper-case micro labels).
  • Changing the default look.

Stories

Form/Engine/FormEngine Appearance — the same workflow-creator form in both looks:

  • Default Look — for comparison.
  • Calm — asserts: the sections' background and edge are grey with no tint and a 1px hairline; the count is not a tag; rows split by 1px hairlines; group labels not upper-cased; labels grey; no dot on filled rows; no asterisk; No is a word; the status word in the text colour; the meter's done part grey, ≤360px wide, 4px high.
  • Calm Needs Attention — asserts: one amber dot on the section; three amber, non-italic "Required · add a value" words and no "This field is required"; no dot beside them; only the attention link coloured; an open required field shows the words as plain text (not a tag).
  • Calm Choice Heading — the choice heading is not upper-cased.
  • Calm Read Only — no card, no line above the first row, hairlines between rows, no dots, "Required · not set".
  • Calm On A Phone — 390px: rows stay inside their section and nothing scrolls sideways.

Checks

Run locally on this branch (in a worktree outside .claude/worktrees, so the pre-push hook ran normally):

  • yarn test — 176 files, 1877 tests, all pass
  • yarn lint, yarn build:test, yarn build:test:prod — clean
  • yarn test:stories FormEngineAppearance — 6/6
  • yarn test:stories FormEngine.stories — 151/151 (the default look is untouched)
  • Looked at in Storybook at 1440 and 390: every section, an open inline row, an open edit card, the "N more fields" fold, a pinned section header after scrolling; nothing scrolls sideways.

Version: 0.10.63 — one patch bump, as the PR gate requires. The open #137 claims the same number; whichever merges second rebases and bumps again.

🤖 Generated with Claude Code

….10.64

The Qorus IDE is moving to a calmer design — colour only where something
needs a person, panels on one surface edged by one hairline, quiet headings
and counts — and every creator, editor and many dialogs there render through
FormEngine's compact mode. It was the biggest surface still in the old look:
a green Ready and meter, green/amber tinted boxes with coloured count tags, a
recessed black Optional box, a dot on every row, upper-case group labels and
"SELECT ONE:", red asterisks and a red "This field is required", Yes/No as
green/red chips, and the outer panel drawn as a card around its own boxes
once a theme gives panels a surface.

Other apps use Reqraft, so the new look is opt-in: `appearance="calm"` on
FormEngine, published through FormAppearanceContext so nested sub-forms and
the field editors a form mounts follow it, and a FormAppearanceProvider for a
host that wants to set it once. Every look choice is decided once inside the
engine; nothing a row does reads it, so bucketing, counts, validation,
templates, search, the optional fields and read mode are the same in both.

In the calm look the status boxes are plain panels on the theme's panel
surface with label/value rows split by full-width hairlines; labels are grey;
group headings, choice headings, radio options and preview labels keep their
case; an empty required value reads "Required · add a value" in amber where
the value would be (read-only: "Required · not set") instead of an asterisk
and a red message; counts are quiet numbers; only Needs attention carries a
dot, and a row keeps one only when its value fails; Yes/No are words; Done and
Cancel are neutral; the readiness line is one quiet line with a short meter
whose done part is grey and only the outstanding part amber; messages under
an open field are lines of text; read mode is one hairline-split list flush
with its host.

The default look is unchanged (the existing FormEngine stories pass as they
were). Stories: Form/Engine/FormEngine Appearance, with plays measuring the
surfaces, hairlines, case, dots, colours, meter and phone width.

Closes #144
@Foxhoundn Foxhoundn self-assigned this Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

📦 Prerelease published for this PR:

@qoretechnologies/reqraft@0.10.65-pr.145.gc151336

Install it in a consumer (updates package.json + lockfile):

yarn add @qoretechnologies/reqraft@0.10.65-pr.145.gc151336

Published under the pr dist-tag — latest/beta are untouched. Repoint consumers to the released version once this PR merges.

The branch skipped to 0.10.64 because the open PR #137 also claims 0.10.63,
but the gate counts bumps from the merge-base and allows exactly one; 0.10.63
is not published as a release. Whichever of the two PRs merges second rebases
and bumps again, as AGENTS.md says.

Refs #144
@Foxhoundn Foxhoundn changed the title feat(form-engine): an opt-in calm appearance for the compact form — 0.10.64 feat(form-engine): an opt-in calm appearance for the compact form — 0.10.63 Oct 19, 2026
@qlip-qore-technologies

qlip-qore-technologies Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

qlip visual regression

🔍 62 changed, 7 new of 604 snapshots.
⏳ Awaiting review.

Review build 20261022-091517 in the dashboard →

Seen in the Qorus IDE's column drawer: a field's schema note that only
explains ("The type decides which of the fields below apply") drew as a blue
info panel, while in the calm look blue says only "a filter is on" and green
is for a healthy figure. In the calm appearance an info or success note now
takes no intent; warning and danger notes keep theirs, because those are what
a person has to act on. The Calm story asserts the note's surface is grey.
The story frame also fills its width, so it renders the same in the Storybook
UI as in the story run.

Refs #144
TicketHeader passed uppercaseLabels to its description list, so the rail
read STATUS / OPENED BY in an app whose theme sets every label in sentence
case. Leaving the prop out lets the theme decide; a theme that says
nothing still gets uppercase, as before.
A brand-new form opened with "2 need attention" and every empty required
field in amber before anyone had typed, so the first thing a person met
was a list of faults (the Goal-gradient finding of the IDE's UX review).

An empty required field nobody has been to yet is now `pending`: still in
the first box, which reads "To fill in" in the quiet colour, with the
requirement stated plainly ("Required · add a value", "This field is
required") and no amber. It becomes a fault (`todo`, amber) once the
person opens it and leaves it empty, or when the host reports a submit
attempt through the new `revealRequired` prop. Invalid values are faults
from the start, as before.

- readFirst: the `pending` status; `getReadFirstStatus` takes `visited`
  (unset means visited, the old behaviour).
- FormEngine: `visitedOptions` (rows opened and left), `revealRequired`,
  a fault count for the toolbar and the first box; the box is "Needs
  attention" with its amber dot only while it holds a fault;
  `compactBoxLabels.pending` renames "To fill in".
- CompactRow / OptionFieldMessages: the requirement is stated with no
  intent until the field is visited (`requirementPending`).
- CompactToolbar: "N to fill in →" in the quiet colour; once something
  is a fault, "N need attention → · M to fill in", and the meter's amber
  run counts faults only.
- Stories about which box a field lands in find the first box under
  either name; "Calm Needs Attention" shows the state after a submit
  attempt, and "Calm New Form" shows a new form and the visit.
…ngine-calm-appearance

# Conflicts:
#	package.json
@Foxhoundn Foxhoundn changed the title feat(form-engine): an opt-in calm appearance for the compact form — 0.10.63 feat(form-engine): an opt-in calm appearance for the compact form, and a new form is not already wrong — 0.10.65 Oct 22, 2026
…ther

The quiet "to fill in" state reached the collapsed rows, the box and the
readiness line, but not the line under an open editor: OptionFieldMessages
took no requirementPending, so a host that opens the first empty field for
typing (the IDE's creators do) showed that one field in amber on a form
nobody had touched. The editor's message now follows the same rule as the
row (open is not left; a submit attempt reveals all), in the row's faint
pending colour.

CalmNewForm asserts the open field's message is not amber; it fails without
the fix.
…ubmit with where to type

Two seams for a host that draws its own submit control (the IDE's editor rail):

- onAttentionChange reports the readiness line's two numbers ({ count, faults })
  whenever they change, so a submit button drawn from them can never disagree
  with the form about what is left.
- focusAttentionRequest: each new value opens the first field that needs
  attention and puts the caret in it, the answer to a submit that could not go.
  The submit button holds the caret at that moment and an open row never takes
  a caret held outside it, so the engine lets go of it first; a field that is
  already open gets the caret directly.

The arrival opener and the new request share one scan (getAttentionFieldMeta,
openOption), so both pick the same field. The open row's editor cell carries
options-readfirst-row-editor so the caret lands in the control, not the label.
…d in

"N need attention →" shows only the fields that are still invalid. The first
character typed into an empty required field makes it valid, so the row was
filtered out from under the caret mid-word and the rest of the keystrokes went
nowhere. An open row now stays while the reader is in it, the same rule its
box already follows (stableBucketOf), and leaves the filtered list once it is
closed.

It mattered more now that a blocked submit turns the filter on and puts the
caret in the first field to fill in. The new test types the first letters,
waits for the form to take them, then types the rest; it fails without the fix.
…N to fill in →" is a real toggle

Two gaps a blocked submit made visible:

- A field that opens as a card (a long text, a description, source code) never
  took the caret on opening; only the inline row did. So clicking Description
  opened it and left the cursor nowhere, and a submit request opened it without
  the caret. The card now carries the same row and editor refs as the inline
  row, so the one tap-to-edit rule covers both shapes.
- "N need attention →" / "N to fill in →" was a clickable span: no role, no
  keyboard, no state. It is a toggle (show only those fields), so it is now a
  button with aria-pressed, Enter and Space. A host that turns it on for a
  person reads aria-pressed instead of guessing; the toolbar draws a different
  link once something becomes a fault, so a mark the host left on the old one
  was lost and a second press turned the filter back off.
… role="textbox" is text

Giving cards the row's caret rule exposed two gaps, found by typing into a
job's Source Code after a blocked submit:

- A code editor's edit area (Monaco) is a focusable div with role="textbox";
  its <textarea> is an aria-hidden IME helper. The text-entry scan knew
  input, textarea and contenteditable only, so it fell through to the first
  button: the editor's fullscreen toggle.
- A card is a form in its own right, so its first button is one of several
  and nobody asked for it; a caret there turned the next space typed into a
  press (the fullscreen editor opened). A card now takes the caret only where
  text goes (findRowFocusTarget textEntryOnly), and waits for a late editor to
  mount; an inline row still hands it to its control.
…hrome

A job's Source Code opens inline, and its code editor mounts after the row
does. Until then the only control in the row is the editor's fullscreen
toggle, so the row put the caret there and stopped watching; the next space
typed pressed it and the fullscreen editor opened.

A control that is not a text box now holds the caret provisionally: when a
text box arrives (Monaco's edit area, role="textbox") it takes the caret,
unless the reader has moved it since. A row that never grows one, a pick-one,
keeps its control, and a picker still opens as before.
…nded

Traced live on a job's Source Code: the row did move the caret from the
editor's fullscreen toggle to Monaco's edit area when it appeared, but
focus() on it did nothing (it is inserted before it can take focus), and the
row gave up after that one try. A focus() that did not land no longer
settles the caret; the row tries again on the editor's next change, and it
watches attribute changes too, since that is how a text box can become
focusable after it is inserted. It still stops the moment the reader moves
the caret themselves.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FormEngine: an opt-in calm appearance for the compact (read-first) form

1 participant