Skip to content

LIMS gallery: ten LIMS screens in ui/1, and the declarative UI gaps they expose - #1045

Open
Yaraslaut wants to merge 456 commits into
masterfrom
feat/lims-gallery
Open

Yaraslaut wants to merge 456 commits into
masterfrom
feat/lims-gallery

Conversation

@Yaraslaut

@Yaraslaut Yaraslaut commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

What this is

examples/lims_gallery implements ten real LIMS screens in ui/1 to find out what the declarative UI can and cannot express: a sample receipt, a sample list, a results worksheet, a review and sign-off, a plate layout, a stability study, an instrument console, an audit trail, an e-signature dialog and a scanner console. It has no backend: sample data lives in screen state. Each screen has a compact and an expanded layout, a recording-backend test and a Qt Quick render smoke at both widths. Green (#17784A) is the main colour.

Run it with morph_examples_client --app lims-gallery --ui=qt.

Every gap the screens hit is in examples/lims_gallery/design/gaps.md (G1–G56). The fixes landed in five framework batches, each designed and reviewed once (docs/superpowers/specs/2026-10-25-lims-gallery-batch-{b,c,d,e}-design.md). After each batch, the gallery dropped the workarounds that batch replaced.

The framework batches

A: defects and responsive layout.

  • env.widthClass is set by every backend, measured in units.
  • Exact decimals in progress and component numbers.
  • forEach keeps its common properties.
  • A splitter honours sizes, and close: self closes the enclosing dialog.
  • One key order on every backend.
  • Qt Quick:
    • the dialog, drawer and progress bar follow the theme;
    • read-only inputs look read-only;
    • table cells are sized by kind;
    • touch-sized controls when compact;
    • the head row wraps.

B: tables.

  • A mono text role and a font.mono token.
  • Column align and valign.
  • A bound selection with Replace and Extend changes, reachable from the keyboard.
  • A check-box select column.
  • A selected push button.
  • Sortable columns and the sort form.
  • pageSize and page, and the slice function.
  • The runs form.
  • group heading rows.
  • showHeader is read as the flag it is.

C: text, fields and small controls.

  • Qt Quick draws document text as plain characters.
  • Inline emphasis spans.
  • Label-over-value pairs and toned values.
  • The required mark after the caption.
  • Help text, and a value's evaluation, under an input.
  • A dialog and drawer description line.
  • A group of inputs with a legend.
  • A text input's unit, announced to screen readers.
  • An outlined destructive button, an outlined badge and a segmented select.
  • List entries of several lines.
  • One application-wide date pattern.
  • A focal scanner field.

D: layout, overlays and navigation.

  • The client mounts the app document.
  • Bound tab labels and choosing a tab from a command.
  • surface as an expression.
  • Focus resolves into switch cases and other rows.
  • Pinned bars and scrollable pages.
  • A stepper helper.
  • Grid column tracks.
  • A menu drawn as one button.
  • Removable badges.
  • The screen head's own menu, search and select.
  • A dialog shown as a sheet on compact.
  • navigate as a dialog that answers its opener.
  • Navigation counts and a compact shell.

E: expressions and charts.

  • Out-of-range list indexes read as null.
  • A get form.
  • One checked 128-bit rounding everywhere expr/1 rounds, so no saturated values.
  • Half-up rounding.
  • abs, sqrt, stdev and rsd, exact until one correct rounding.
  • dateAdd.
  • A chart node, drawn by every frontend, with colour-blind-safe series colours and a marker per series.

Closes #1046, #1047, #1048, #1049, #1050, #1051, #1052, #1053, #1054, #1055, #1058, #1059, #1060, #1061, #1062, #1063, #1064, #1065, #1066.

Not closed here: #1056 (env locale, platform and online are never set), #1067 (two default tones below 3:1 on the dark sidebar, weak evidence) and #1068 (a state declared any with an init takes the init's type; found by this PR's coverage tests and kept as a [!shouldfail] case).

Verification

Run on the branch's tip (linux-everything, clang, -Werror), Qt suites with QT_QPA_PLATFORM=offscreen:

  • The full build: no warnings.

  • morph_tests: 3360 cases, 3358 passed, 2 failed as expected (the replay-ledger negative control and the ui/1: a state declared "any" with an init takes the init's type #1068 case, both marked so).

  • morph_qt_quick_tests 189 cases (also on Qt 6.8.1, which the ladder job uses), morph_tui_tests 367, morph_client_tests 38, lims_gallery_tests 54, gallery_tests and all ten render smokes: all pass.

  • include/morph/ui branch coverage 92.57%, measured locally with clang 21.

  • qmllint (qt_quick_qmllint) passes.

  • Doxygen with warnings as errors passes.

  • clang-format 22 over every tracked C++ file: no differences.

  • 54 conformance cases and 16 layout cases, run by the recording backend, Qt Quick and the TUI.

  • Every new test has a control: the feature was broken, the test was seen to fail, and the feature was restored.

  • clang-tidy 22 was run on changed lines in include/ and src/ (Qt sources with a local clang-tidy, because the tidy container has no Qt).

  • Not run locally: MSVC, GCC 15 -O3. The new looks were checked through grabs and the render smoke only.

Known follow-ups

  • The plate's well grid uses stretch tracks, and on Qt Quick at 1280 × 900 "STD1" is cut to "S…". Whether stretch tracks should take their widest cell's width is a framework decision.
  • stepper cannot pin its footer, so the stability screen's Continue button scrolls on compact.
  • A check-box toggle and a select choice now mark their field touched, so a required group's message shows under the default policy.

The run's tracker and handoff note are in examples/lims_gallery/design/ (status/, next.md).

🤖 Generated with Claude Code

https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn

Yaraslaut and others added 29 commits November 2, 2026 16:14
The theme gains actions.gap (0 keeps spacing step 2), the space between the
actions of a row of actions. actionBar becomes a ui/1 kind, with children, align
and justify and no gap or wrap, lowering to ui::ActionBar. It mounts in the
footer of the overlay whose content holds it, as before, and now also in place
anywhere else, where the mount used to throw. StackWidget::setActionRow marks
the stack it makes: on Qt Quick its children are Theme.actionSpacing apart, as
MorphFooter's now are, and the terminal keeps its step 2 in cells. stepperParts'
footer is an actionBar.

StackWidget::setActionRow is implemented by the Qt Quick stack, the table row
view (a no-op), the terminal stack (a no-op) and the recording backend.
The gallery example screen gains an actionBar, since its test requires every
node kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
HeadTrailEnd (None, Unframed, Framed) replaces ShellFrame::setTrailEndInset(bool).
Compact, the line ends step 4 from the edge, the page's gutter, unless an
unframed button (a ghost or icon-only action, or the head menu's trigger) ends
it: that button carries its own padding. A line ending in text, a badge or a
select used to stand step 1 from the edge. The bar's vertical padding keeps its
rule (step 3 for a tall head or a framed end), and setHeadOneLine now ends in
showTrail() beside showBarInset().

Workspace::activeHeadTrailEnd reads the last shown element of the trail in its
order (tools, actions, menu) in place of activeHeadTrailEndsFramed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
A text input (not masked) and a date input's typed field that has neither the
focus nor a selection and holds text wider than its text room are drawn from the
start, cut with an ellipsis, as a one-line text is. Qt Quick draws the cut text
in a MorphFieldElision label over the editor's text box and makes the editor's
own text transparent while the label shows; the label's text is the editor's, the
elided form is never written into the value, and the widget's tooltip is the
whole text while the label is cut. The terminal draws it with paintElidedField,
in the room before the control's marks, so no mark is drawn over the ellipsis.

Closes #1082.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
A grid without tracks takes minColumnWidth (units, 1 to 64, a literal; refused
with tracks, without columns, with columns: 1 and when computed). It shows as
many of its columns as fit that width, at least one: floor((W + g) / (m * u + g)
+ 1e-6) capped at columns, the epsilon keeping a width that exactly holds n
columns at n whatever the unit. The natural width stays the full column count's;
the natural height follows the columns shown at the width the grid was last laid
out at, on Qt Quick through MorphGrid.place and on the terminal through the
drawn width, which paint records and re-lays-out on when it changes, as a
wrapping row does.

GridWidget::setMinColumnWidth is implemented by the Qt Quick grid, the terminal
grid and the recording backend. Five layout conformance cases cover it on both
frontends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
mountMinColumnGrid takes its width, least column width, gap and trailing text in
a struct rather than as adjacent ints, and the elision tests declare what they
only read const.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Tests for the branches of actionBar's author options and wrap refusal, a ghost
head action with its label, a grid with tracks that gives a least column width,
and the five least-column-width conformance cases run against the recording
probe's faults. include/morph/ui branch coverage is 92.03 % on the clang
coverage preset. The four default-arm allowlist hints in document.hpp follow the
lines the new kinds and checks moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
… column shows its whole name at 1215

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
… px wide at 1215 and end at one edge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
…ubtitle stays on one line

The label's rich-text implicit width is fractional (295.0156 for the plate's subtitle), and a layout that
lays the item out in whole pixels gives it 295, a fraction under the ideal width, so the last word wrapped
at every window width. The text now asks for the ceiling of that width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
…eir buttons sit 8 px apart

Instrument, audit and samples had a hand-set 4 px gap; results, review and plate wrote the 8 px
by hand and now take the theme's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
…e gives its Time column 6 units

The field grid shows 3 columns of at least 25 units and the check grid 2 of at least 32, so the
sample type and the longest check read whole at 1000 px. The session pane is 33 % of the row so the
Received badge stays whole beside the wider Time column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
…o temporary string is bound per iteration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
…referenced

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
…ch is its own share, while it computes the shares

Qt 6.8 reports the read as a binding loop on stretchShares: a content-sized or stretching child's preferred length is the stack's share of it, so reading it from inside the share computation re-enters that computation. Only a fixed length or a wanted percent share is read now; the value used is unchanged, as the others read -1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
…it still tells two columns from three

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
…ned and in progress

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
… so no temporary string is bound per iteration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01No1NznPS3foEqTUMjvBEts
…ir is copied per iteration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01No1NznPS3foEqTUMjvBEts
…e text measured alone, so no font can leave a fixed unit range

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01No1NznPS3foEqTUMjvBEts
A head's tools, actions and menu were a wrapping Row with gap 1 holding three
ForEach stacks of gap 1, so they sat 4 px apart where the mockups space them
8 px. The trail is now an ActionBar with wrap, so its parts are the theme's
actions.gap apart, and a workspace overlay's footer actions are an ActionBar
too, so the same actions are spaced alike in a head and in an overlay.

ActionBar gains wrap (the view tree, ActionBarOptions, and the ui/1 flag).
A horizontal forEach that is a direct child of an actionBar is part of its row:
the mount marks it as a row of actions and gives it step 2. A gap on such a
forEach is refused at load for a literal horizontal axis and reported at
lowering for a computed one. A wrapping action bar in an overlay's content is
refused at mount, because the footer sizes its row to one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
The stock header background drew only a fill, so a theme that fills a header
band could not close it with the 1 px line that separates the header from the
first row. The token color.table.headerRule (transparent by default, so the
stock look does not change) is a 1 px rule MorphTableHeaderBackground draws
over the header's last pixel row, inside its height so nothing moves.

The token follows color.table.header along every path that colour takes:
Theme.tableHeaderRule, each of Theme.qml's seven built-in schemes (the joined
toolbar from the bar scheme), the surfaces a theme declares, MorphScheme and
MorphButton's role copy. A table in a card draws with the card scheme, so
leaving that binding out would draw no rule in the common case. The terminal
reads no table colour and is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
…t 8 px apart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
…ch column

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
…t 8 px apart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
…one flag, so the ui branch figure clears its floor

The two cases checked 'the container exists, holds two children, the reference text exists' once for the
check and again for the guard, each as a short-circuit chain whose false arms only a failing backend takes.
They now evaluate the conditions together through allHold and test the flag, which leaves the include/morph/ui
branch figure at 92.11 % (it was 92.07 %).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01No1NznPS3foEqTUMjvBEts

This branch has not been deployed

No deployments
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.

ui: {"env": "widthClass"} is never set, so no screen can reach a compact layout

1 participant