Skip to content

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

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

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

Conversation

@Yaraslaut

@Yaraslaut Yaraslaut commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Draft, work in progress. This branch holds the whole initiative: the gallery screens first, then the framework batches that come out of its gap log. It ends as one PR.

What this is

examples/lims_gallery is a separate example with no backend: no models, server or database. It implements ten LIMS UI designs as ui/1 declarative screens, to find out what the declarative UI can and cannot express. Sample data lives in screen state, and actions only change that state. Each screen has a compact and an expanded layout, a recording-backend test, and a Qt Quick render smoke test at both widths. Green is the main colour (#17784A), shipped as the bundle's ui-theme/1 theme.

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

To continue, start from examples/lims_gallery/design/next.md.

examples/lims_gallery/design/ holds the mockups, the gap log (gaps.md, G1–G56) and the batch plan (batches.md). Each framework batch has a design under docs/superpowers/specs/.

Status

Part State
Ten gallery designs (01–10) done
Batch A: defects and responsive layout landed; the gallery dropped its workarounds
Batch B: tables designed and reviewed; part 1 in progress (WIP commit)
Batch C: text, fields and small controls designed and reviewed
Batch D: layout, overlays and navigation not designed yet
Batch E: expressions and charts not started

Batch A

  • Width class: env.widthClass is set by every view backend, in units (ui::widthClassOf). The gallery reads it directly.
  • Interpreter:
    • exact decimals in progress and component numbers;
    • forEach keeps its common properties;
    • a splitter honours sizes;
    • close: self closes the enclosing dialog.
  • Keys: one key order on every backend: the widget's own chord, then its own key, then its ancestors. There are six new conformance cases, so the suite goes from 34 to 40.
  • Qt Quick:
    • the dialog and drawer follow the theme surface;
    • the progress bar follows the accent;
    • no keyValue polish loop;
    • read-only inputs look read-only;
    • table cells sized by kind, centred, with a horizontal scroll when wide;
    • touch-sized controls when compact;
    • the head row on its own line when it doesn't fit.

Closes #1046, #1047, #1048, #1049, #1050, #1051, #1052, #1054, #1055.

Filed during the batch designs, to be fixed here: #1058, #1059, #1060, #1061, #1062.

Verification (local, at 1e4c9cf34)

Suite Result
morph_tests 3164 cases; 1 "failed as expected" (known)
morph_qt_quick_tests 161 cases pass
morph_tui_tests 343 cases pass
morph_client_tests 33 cases pass
lims_gallery_tests 48 cases pass
lims_gallery_render_smoke passes
  • Every new test has a control: with the feature reverted, the test fails.
  • clang-tidy 22 was run on the changed library lines, with 0 findings. A seeded violation was reported, which shows the check was live.
  • Not run locally: Doxygen.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn

Yaraslaut and others added 7 commits October 24, 2026 13:47
A new example, examples/lims_gallery: laboratory screens from design mockups,
written as ui/1 documents to find what the declarative UI cannot express. It
has no backend; each screen keeps its records in its own state. The scaffold is
a Qt-free screens library whose bundle registers an index and every built
design with a green ui-theme/1 theme, recording-backend tests over a helper that
mounts a screen by id at a width class, a Qt Quick render smoke that mounts
every screen at both widths, and the lims-gallery app in the examples client.

Design 01, the e-signature: worksheet WS-2026-0412 signed to approve its
results. Expanded, a dialog over the worksheet with the record summary, the
refused-password banner, signer, meaning, reason and password; a wrong password
costs an attempt, the right one signs. Compact, the same fields as an in-page
sheet with Cancel at the top and a full-width sign button.

Gaps: G1, G2, G3, G4, G5, G6, G7, G8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Design 02, sample receipt at the bench. Expanded: a barcode scan field that
keeps its focus, a notice for a duplicate or unknown scan, the card of the
sample being checked (type, temperature with its 2-8 C range, received at,
storage, containers, courier, the four condition checks, a note) with Reject
and Receive-and-scan-next, and this session's outcomes beside it. The head
carries the session's start and the received and rejected counts. Receiving
or rejecting (with a reason, in a dialog) records the outcome and moves on to
the next registered sample; a temperature out of range blocks receiving.
Compact: one column with every field kept, the session folded into a
collapsible, and the two actions as the last row.

Adds a cellTag helper for status tags in table cells, and a submit helper to
the test harness.

Gaps: G1, G2, G5, G8, G9, G10, G11, G12, G13, G14, G15, G16, G17.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Design 03, the record of HPLC-02 (EQ-0142). The screen head carries the
breadcrumb, the service, qualification and calibration-due tags, and the Log
maintenance and Book actions; an overflow menu holds Record calibration, Print
equipment label, Export history and Take out of service. The record is in tabs:
Overview shows the details, recent events, qualification stages and the current
booking's elapsed bar; Bookings lists the bookings; the tabs the design does not
draw say so. Logging maintenance or a calibration adds a recent event (a
calibration clears the one due), taking the instrument out of service asks a
reason and changes its status, and booking adds a booking. Compact stacks the
details as label-over-value pairs, puts Book and Log maintenance in the page as
large buttons, and folds booking, qualification and events into collapsibles.

Gaps: G1, G2, G5, G7, G18, G19, G20, G21, G22, G23, G24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Design 04, the audit trail. Expanded, a facet rail beside a read-only table:
record type and action checkboxes with their counts, a user select and a
from-to date range narrow the trail, each active filter shows as a chip whose
"×" removes it, and Clear all empties them. The table shows when, who, the
record, the field, the old value in red, the new in green, and the reason; with
no entry left it shows an empty state. The head exports CSV and PDF. Compact
stacks each entry as a card, nothing shortened, and moves the facets into a
drawer opened by "Filters · n".

Gaps: G1, G2, G5, G13, G25, G26, G27, G28.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Design 05, the sample list. A dense table of open samples with their product and
lot, tests, priority, status, due time (an overdue one in red) and specification
outcome; a row with a result out of specification is tinted. Search, status and
priority filter it, and an empty state offers to clear the filters. A first
column of check boxes selects rows, three as the design opens; the bulk bar on
a green-tinted surface counts them and assigns them to a worksheet, changes
their priority, prints labels or clears them, and "Select all 312" widens the
selection. Compact turns each row into a card with the same selection, tinted
when out of specification, with the filters in a drawer and the bulk bar after
the list.

The bundle's theme gains two surfaces, selected and outOfSpec, for those tints.

Gaps: G1, G2, G5, G16, G18, G25, G26, G28, G29, G30, G31, G32, G33.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Design 06, step 2 of the stability study wizard: steps for the four stages,
the study start, pull window and containers per pull, the conditions ×
timepoints matrix of check boxes with each condition's pull count, and a
summary of pulls, first and last pull and containers needed against stock.
Too few containers in stock is a field error and a banner that keep Continue on
this step. Compact folds each condition into a collapsible of timepoints.

Missing: the screen is not registered in designs.cpp and neither the screen
nor its test is in CMakeLists.txt. Its unit tests passed when registered, but
the render smoke fails with a Qt Quick GridLayout polish loop in
MorphKeyValue.qml (G34), not isolated yet. Steps 1, 3 and 4 are placeholders.

Gaps: G1, G5, G9, G16, G25, G34, G35, G36, G37, G38, G39.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
The ten design mockups, the lanes' brief and the gap log lived in a
session scratch directory. Kept beside the gallery, the work can resume
from the branch alone: the brief opens with what is done, what is WIP
and the next steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Yaraslaut and others added 2 commits October 24, 2026 17:03
Design 06, a four-step wizard for a new stability study of Paracetamol
500 mg tablets. Step 1 picks the lot, its container closure and the
study's reason; the lot sets the stock the containers are checked
against. Step 2, the one the design draws, holds the study start, pull
window and containers per pull, the conditions x timepoints matrix of
check boxes with each condition's pull count, and a summary of pulls,
first and last pull and containers needed against stock. Too few
containers is a field error and a banner that keep Continue on this
step. Step 3 chooses the tests run at each pull, each at every pull or
at T0 and the last pull, with the test runs they add up to. Step 4
reviews the study and enables signing once the reviewer confirms it.
Compact folds each condition into a collapsible of timepoints and each
test into a card.

The summary and the review are rows of a caption beside a text, not a
keyValue: on Qt Quick a keyValue reports a GridLayout polish loop for
some caption and value widths, and the summary's reach them. The gap
log records the smallest reproduction found.

Gaps: G1, G5, G9, G13, G16, G25, G34, G35, G36, G37, G38, G39, G40.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Design 07, the layout of 96-well ELISA plate PL-0193 for worksheet
WS-2026-0415. Expanded shows the fourteen samples to place beside the
plate, a 13-column grid of row letters and 96 wells: the template's
standards, controls and blank on tinted surfaces, and sample wells
holding S1 to S67 or empty. A sample is dragged into a well, or the
selected wells are filled with the next samples in plate order; Clear
wells returns their samples, and Auto-fill fills every empty well.
Arrow keys move between wells, Shift with an arrow extends the
selection, and Enter opens Assign for the focused well. Compact lists
the wells row by row, each row a collapsible titled with its counts,
with Assign on every empty well.

The theme gains the standard, control and blank surfaces.

Gaps: G1, G2, G5, G23, G25, G26, G28, G32, G41, G42, G43, G44.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Yaraslaut and others added 4 commits October 24, 2026 17:19
Nine gap-log entries are defects in the library rather than vocabulary
the designs need. Each is now filed, and the entry names its issue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Design 08: worksheet WS-2026-0412's results for two samples, with the
system suitability checks above the grid. Each result is entered to the
decimal places of its specification and evaluated against it as it is
typed; the assay's mean and RSD and the total impurities are calculated,
read-only lines of the grid. Committing a changed result opens a
reason-for-change dialog (reason and comment required) that confirms or
undoes the change; the screen opens on the design's state, Impurity B
changed from 0.12 % to 0.21 % and out of specification. Compact shows one
sample at a time with Previous and Next, and asks the reason inline.

Gaps: adds G45-G48; extends G1, G2, G5, G6, G8, G9, G10, G16, G35, G40.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Lane 4 stopped on request before building designs 09 and 10. This keeps
the gaps found while reading 09 (G49-G51, all inferred from reading, not
reproduced) and updates the brief's status: 08 done, 09 and 10 not
started.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Design 09: the four worksheets awaiting review in a queue beside the one
under review, split by a splitter. The worksheet shows its submission,
its tags, and tabs for its results (flagged or all, each compared with
the previous lot), the previous lots, its audit trail and comments.
WS-2026-0409, which the reviewer entered, cannot be approved, and the
reason is shown in the queue, in a banner and beside the disabled
button. Returning and rejecting open a dialog that needs a reason and
take the worksheet out of the queue; "Approve and sign…" navigates to
design 01's e-signature with `as: dialog`. Compact shows the worksheet
as its own page, the queue behind a back button, actions in one row.

A cell tag now keeps its own height in a taller table row.

Gaps: G49 and G50 reproduced; G51 gets its workaround; adds G52-G54;
extends G1, G2, G5, G14, G16, G25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Yaraslaut and others added 2 commits October 24, 2026 21:27
Design 10, the most complex of the ten:
- a navigation sidebar, which is a menu on compact;
- the system suitability alert and the day's figures;
- an approximated Levey-Jennings chart;
- the instruments;
- the worklist beside a non-modal, resizable sample pane with
  Previous/Next. The pane says why the held sample cannot be submitted.

All ten designs are now built. The gap log gains G55 (no chart, and no
way for a document to bring one) and G56 (a screen's own navigation).
G49 is filed. The brief's status table marks 09 and 10 done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
The 56 gap-log entries become five coherent changes to the declarative
UI:
- A, defects and responsive layout;
- B, tables;
- C, text, fields and small controls;
- D, layout, overlays and navigation;
- E, expressions and charts.

Each batch is one design, one review and one implementation on this
branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Yaraslaut and others added 4 commits October 24, 2026 22:00
The cause of each batch A gap, re-checked in the code, with the fix,
the spec changes, tests with controls, and risks. G42 is reproduced on
all three backends; G34's cause differs from the gap log's inference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Enter inside a dialog follows one key order on every backend, width
classes are defined in units, the touch height token is a sibling of
control.height, and the table, head-line, close-self and intOf plans
cover what the review found missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
A progress bar, a custom component's number prop and every integer property
read a decimal through Rational::toDouble, which rounds to the value's own
places. div keeps its operands' places, so 29 / 100 reached the bar as 0, and
intOf turned 29/10 into 3 instead of truncating it to 2.

The interpreter now reads the unrounded quotient (detail::realOf) for progress
and component numbers, and intOf truncates in integer arithmetic. toDouble
stays a display reading.

No computed decimal in the examples or the corpus feeds an intOf key; the
gallery's progress(level / 10) now fills half the bar at level 5 instead of
all of it.

Closes #1051

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
lowerRows built a forEach without commonOf, and ui::forEach had no way to take
a Common, so testId, visible, sizing and every other common key on a forEach
was loaded, type-checked and dropped without a report. ForEach::common and the
mount already applied them.

Both ui::forEach overloads take a trailing Common, and the interpreter fills
it with commonOf as it does for a table.

Closes #1052

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Yaraslaut and others added 24 commits October 25, 2026 09:33
The loader accepted a splitter's sizes and the view tree, the mount and both
frontends applied them, but the lowering never read the key, so every
document splitter shared its space equally.

The lowering now binds sizes, reading each item as an integer property is. A
value that is not a list of numbers is reported once for its path and lowers
to an empty list. The document still has no onResize: a drag stays the
splitter's own.

The gallery's review test that pinned the dropped sizes now expects them.

Closes #1055

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Every close: self went to InterpreterHost::closeSelf, so a dialog's own
"close" asked the host to end the screen. The author API and document.md
define self as the dialog or drawer the command runs in, while host.md uses it
to end a screen opened by navigate.

The interpreter now walks the command's scope and its parents to the first one
a dialog or drawer owns and closes that overlay as close: <id> does. A walk
that reaches the screen's root scope still asks the host, so both meanings
hold. A walk that ends at a component's fallback scope, with no overlay of the
component's own, is reported and fails instead of closing the screen around
the component.

Closes #1048

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Enter on a focused button that declared its own Enter chord clicked it on Qt
Quick and the terminal, and ran the chord on RecordingBackend; the gallery's
plate test passed only because it runs on the latter. Which focused widgets let
Enter through to a dialog's default also differed: a terminal text input took
Enter whether or not it submitted, and a terminal check box toggled on it.

Every backend now routes a key the same way: first a chord the focused widget
declares itself, then the widget's own keys (a button takes Enter and Space, a
single-line text input takes Enter only when it submits, a check box takes
Space, a table takes Enter), then the ancestors' chords, innermost first.

- Terminal: dispatch, the field view and the list view try the widget's chords
  before its own handling; a field that does not submit commits and lets Enter
  bubble while it is still alive; a check box toggles on Space only.
- Qt Quick: MorphWidget gains takeOwnChord, which the button, the table row and
  every input's control call before their own handling; the text input stops
  Enter only while its widget has a submit handler (`submits`).
- RecordingBackend::press implements the three steps from the rule.
- The mount gives setOnSubmit an empty handler for a field without onSubmit,
  and the interpreter gives a text input one only when it has onSubmit or
  commits on Enter.

Six conformance cases pin the order on all three backends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
A dialog's Popup and a drawer's Drawer set no background, so the Basic
style's grey palette.window showed through, and their content took the
scheme of the node that declared them: a dialog declared under a sidebar
drew the sidebar's light text on that grey.

Both now draw as the theme's card surface (color.surface, its border,
radius.card on the dialog, elevation.card) with the page padding, and
OverlayImpl hands its content the card's scheme whatever surface
encloses the node. The drawer sets each side's padding itself, since the
style pads the edge it lies against for its own border line.

Closes #1047

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
TextInputImpl::setOnSubmit wrote `submits` on every text input, and a
multi-line one's MorphTextArea declares no such property, so each mount
of a text area with a submit handler reported a refused write. The
property is now written only where the field reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
MorphProgress used the Basic ProgressBar's own background and
contentItem, which paint the style's blue whatever the theme says. The
bar now draws its own: the scheme's accent on its neutral track, 4 px
tall with rounded ends. Replacing the contentItem loses the style's
indeterminate animation, so an indeterminate bar slides a third-wide
segment across the track itself, only while the bar is shown.

Closes #1050

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
MorphKeyValue capped each value at the grid's width less a caption
column width that the captions wrote back from onImplicitWidthChanged,
which runs during the grid's own polish. That is a binding cycle through
the layout, and for some text widths Qt reports it as a polish loop,
even in a widget far wider than its content.

The cap and the written-back width are gone. Captions keep their whole
width on one line (no wrap, a minimum of their implicit width) and
values fill what is left and wrap in it, so a long value still wraps in
a narrow widget.

Closes #1054

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
A read-only text input, text area, select, date input or file picker
drew the same input fill as an editable one, so nothing told the user
the field would refuse typing. MorphInputBackground takes `readOnly`,
which every input passes, and a read-only input draws on the page's
background colour instead of the fill, keeping its border and focus
ring since it still takes focus for selection.

The contract now says how a read-only field looks. The terminal needs
no change: no terminal field draws an input fill, so a read-only one is
already its text in place in the normal colour, distinct from a disabled
field's muted text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Three defects in how both frontends lay out a table's cells:

- A badge, a check box or a button in a cell stretched across its
  column on Qt Quick. MorphTableRow.place now gives a cell whose
  fillsAcross is false its natural width at the column's start, the
  rule stacks already follow.
- Every cell sat at the top of its row, so a text beside a taller
  select or check box sat above the control's centre line. A cell now
  keeps its own height and is centred in the row: in place() on Qt
  Quick, and through a table row stack's centre alignment on the
  terminal.
- A stretch column in an overfull table was given 0 width: its header
  vanished and a neighbour's text drew over its cells. A content or
  stretch column is now never narrower than its header (on Qt Quick the
  floor an empty content column has, the share it gives up taken from
  the other stretch columns), and a fixed column is never squeezed
  below its header on the terminal. On Qt Quick the header and the rows
  sit in a horizontal Flickable that scrolls sideways only while the
  columns are wider than the table; the virtualized vertical flick
  keeps the height the document gave the table, less the header. The
  terminal cuts the columns past its edge, and the contract says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Nothing ever wrote {"env": "widthClass"}: the Qt Quick backend computed
a class and handed it to a callback no shell set, and it did so before
the shell and its session existed. Under morph::client the class read
null on every frontend.

The width class is now the view backend's to report and the shell's to
copy into the session:

- IViewBackend gains widthClass() and setOnWidthClass(listener). The
  listener hears each later change on the owner, outside any layout
  pass; registering does not call it, so the initial value has one
  write path, before the first mount.
- ui::widthClassOf(units) classes a width in the vocabulary's own unit,
  the one fixed(n) counts: compact below 80, medium below 160. The
  terminal counts its columns (an 80-column terminal is medium), Qt
  Quick its window's width over Theme.unitWidth, so a document's
  breakpoints sit at the same number of units on both.
- Session::setEnvironmentValue sets one env value and keeps the others,
  so the class and a switch's backend do not drop each other.
- The client shell writes the class before the first mount and on each
  change. Qt Quick reports the host's width in units from MorphRoots,
  which also follows a theme with another font, and the frontend sizes
  the window's content item before the backend reads it. The terminal
  session re-classes after dispatching a resize.
- RecordingBackend reports expanded until setWidthClass; ScreenHarness
  takes the class to start with and can change it.

BackendOptions::onWidthClass, FrontendConfig::onWidthClass and
qt_quick::widthClassOf in pixels are gone.

Closes #1046

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Every control was Theme.controlHeight tall whatever the window, so a
compact layout drawn for touch got desktop-sized targets, and no node
or class could ask for more.

A theme token, control.touchHeight (44 px by default), is the least
control height while the window's width class is compact. It is a
sibling of control.height, since a token's object is never read for
tokens inside it. The backend sets Theme.touchTargets with the class,
and Theme.targetHeight is controlHeight, or at least touchHeight while
touchTargets holds: the button and input backgrounds, the text area
and the table row read it, and a check box and a tab button take it as
their least height while touch targets are on. The width class stands
in for touch input; it is not a density layer, which stays a choice the
theme index reserves. The terminal is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
ShellFrame put the title, the head slot, the principal and the sign-out
button in one row. A head row of a subtitle, a few badges and two
actions is wider than its share of that row, and a Qt Quick row cannot
shrink a child below its minimum, so the title wrapped and the sign-out
button was drawn over the head's actions, at 1280 px already.

The top bar is now a column of two lines: the title, the principal and
sign-out, and below them a line that holds the head slot in a
horizontal scroll, so a head wider than the bar scrolls sideways
instead of overlapping. The frame cannot see whether the mounted head
shows anything, so the line starts hidden and the client says:
ShellFrame::setHeadShown, and Workspace::activeHeadShown, a tracked read
sharing one rule with the head row's own visibility. The client shell
keeps an effect beside the head mount that shows the line while the
active tab's head has content.

A Qt Quick horizontal scroll's content is now at least its viewport's
width, as on the terminal, so a head row narrower than the bar fills it
and its actions sit at the trailing edge.

Closes #1049

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
The framework now does what the gallery's screens worked around, so the
screens use it directly:

- Width class: the screens lay out for {"env": "widthClass"} alone; the
  optional width param, kWidthParam, and the param forwarded by the
  index, the console and the review's navigate are gone. GalleryScreen
  sets the class on its RecordingBackend through ScreenHarness, and the
  render smoke resizes the real window to 60 or 180 units before each
  mount and checks the class the backend reported as the screen mounted.
- forEach carries its own test id: the wrapper columns around it go.
- Dialogs and drawers close with close(self) instead of naming their
  element.
- The booking's progress is the computed quotient 29 / 100, not a
  literal decimal at two places.
- Design 06 draws its summary, product and review pairs with keyValue;
  the containers line, which takes a tone, stays a row.
- A status tag in a table cell is a bare badge; layout::cellTag goes.

gaps.md marks G1, G5, G7, G12, G15, G19, G21, G22, G25, G34, G40, G42,
G48, G49 and G54 fixed in batch A, and batches.md marks batch A landed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Covers the batch C gaps from the gallery's gap log: spans for inline
emphasis, a description and tone on every input and on dialogs, the
required mark beside the caption, a checkbox group, a unit on a text
input, a prominent scanner field, one application date pattern with a
UTC-offset token, a destructive outline button, an outlined badge, a
segmented select, stacked and toned key-value pairs, and display-only
content in an item button. Plain-text rendering of document and data
text on Qt Quick lands first. The monospace role belongs to batch B.

Reviewed once; the review's findings and the decisions on its open
questions are folded in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Covers the batch B gaps from the gallery's gap log: a table's selection,
sort and page held in screen state and bound the same way a select's
choice is; one selection model on every backend (click, Ctrl-click,
Shift-click, keyboard, Ctrl+A) with Replace/Extend reports; a
check-box column with a select-all header; in-table sorting, paging and
group heading rows over in-memory rows; column alignment; the monospace
text role and font token; a selectable push button for plate wells; and
the sort, slice and runs expression forms. It lands in two parts:
columns and the selection, then the row pipeline.

Reviewed once; the review's findings and the decisions on its open
questions are folded in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Sample, lot and worksheet ids are set in a fixed-pitch face in every
LIMS design, and a table column's role is a text role, so the role
belongs to every text rather than to a column: `mono` joins the role
set and serves a `text` node and a column alike. It draws at the body
size, in the text colour, in the theme's new `font.mono` family, or in
the platform's fixed-pitch font while the theme names none.

`font.mono` is a sibling of `font.family`, not `font.family.mono`: a
token's object is never read for tokens inside it. On Qt Quick the
backend writes the platform's fixed-pitch family into the theme, since
QML cannot ask the font database, and a `font.mono` Qt cannot resolve
is reported as `font.family`'s is. A terminal draws every cell in one
fixed-pitch font, so mono draws as normal text there.

A column's literal role was never checked and an unknown name drew as
body text silently. The loader now checks a column's role as it checks
a text's, and a computed role that names none is reported once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Covers the batch D gaps from the gallery's gap log: tab labels that
follow the data and a chosen tab bound to state; surface as an
expression; focus that reaches into switch cases; the pinned bottom bar
idiom and a stepper helper; grid tracks of different widths; an
overflow menu and a head menu; removable badges; search and filter
tools in the head; a dialog that becomes a sheet on compact; navigate
as a dialog that returns a result; and navigator counts with a compact
shell. It proposes landing in four parts.

Not reviewed yet.

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
A column part gains `align` (start, center, end) and `valign` (start,
center, end), and the table a `valign` for every column that gives
none. A number column needs its digits and its header at the end, and
results and review each wrapped their numbers in a `justify: end` row
to get there: the same hand-rolled guarantee twice is a missing seam.
The audit trail draws every cell at the top of its row, where some
cells are two lines tall.

`align` places the text of a text or text input cell, a label-width
cell (badge, check box, push button) and the header label; a
container cell fills its column and places its own content. On Qt
Quick an end column also draws tabular figures. A terminal input keeps
its text at the start, since `InputField` has no alignment. The view
tree's `TableColumn` carries both places through `setColumns`, and
RecordingBackend records a place that is not the default.

A document table's first cell held the row's scope by being wrapped in
a column, which its table column would then have placed instead of the
cell. The first cell's node now owns the scope instead, for the same
lifetime, so every column places its own cell.

Layout conformance cases 8 and 9 check a badge at an end column's end
and cells at their row's top and bottom, on Qt Quick and the terminal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
A table listed a `selection` key that the lowering never read, had no
event for a user's change, and `set` could not name it, so a screen
could neither clear a table's selection nor share it with another
view of the same rows. The author API could not even write the key:
its `selection` option was the mode.

`selection` is now an expression for a list of row keys, controlled as
a select's `selected` is: bound, the table shows exactly the bound keys,
a user's change runs the new `onSelectionChange` with `event` the new
keys, and the mount's re-assertion snaps the widget back unless the
document writes the change back. Without the key the table keeps the
user's selection as before and still reports it. The element values
`selection` and `selectedCount` read the bound value when there is one.

The loader refuses `selection` and `onSelectionChange` on a table that
selects nothing or is model-backed, and its refusal of `set` on a table
says to bind the selection to state instead. The author option that
held the mode is renamed `selectionMode`, the key it writes, and
`selection` and `onSelectionChange` join it.

Closes #1053

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
The backends disagreed on what a click on a row does: Qt Quick
selected the row alone and toggled it on Ctrl, the terminal toggled it
on a plain click, and neither had a range. A Qt Quick table could not
be reached from the keyboard at all: no part of it was a Tab stop and
no arrow key moved anything, so a table of text cells had no keyboard
selection and Enter reached no row.

Every backend now follows one model in Multiple mode: a click selects
only its row and makes it the anchor, Ctrl+click toggles it, Shift+click
selects the range from the anchor; Space toggles the current row,
Shift+Up and Shift+Down extend a range, and Ctrl+A selects every row or
clears them. A Single table selects one row for each and lets Ctrl+A go
on to the ancestors' chords. Only a plain click is half of a double
click, so two quick Ctrl-clicks toggle twice.

A report now carries a SelectionChange, Replace or Extend, with the
keys: a backend that holds one page cannot report the keys it does not
hold, so only the kind says whether those stay selected. The handler of
TableWidget, Table and TableOptions takes it; setExistingKeys passes
the keys in row order, so a range covers rows that are not realized.

On Qt Quick the table is a Tab stop that hands the focus to its current
row, the TapHandler reports Ctrl and Shift, and the table's keys run
while it or a row holds the focus itself, so the arrows in a result
field do not move the current row. On the terminal a click reaches the
table with its modifiers through clickWith, and a plain click no longer
toggles. RecordingBackend gains clickRow and a Multiple table's Ctrl+A.

Conformance cases 41 to 43 check the click, the range and the keys on
every backend; case 43 runs the keyboard on a Qt Quick table of text
cells. The recording probe's NoModifiers fault fails cases 41 and 42.

Closes #1059

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
Design 05 needs a column of row check boxes under a "select all on
this page" box, and the gallery drew both by hand: two controls over
one state, where a click beside a row's box did something else. A
general control in a column header would turn the header, which both
frontends draw as labels, into a container the mount fills, with its
own scope and a rule for living beside a sort click; no design asks
for anything but select-all there.

So the table draws its own selection another way. `selectColumn`, a
literal flag on a `multiple` table, gives it a leading column of check
boxes: a row's box is checked while the row is selected and toggling
it is a Ctrl-click on the row; the header box is checked, partly
checked or clear by how many shown rows are selected, is disabled
while there are none, and toggling it is Ctrl+A. Neither box changes
its own state, so a selection the document refuses leaves both as the
selection is. `selectAllLabel` names the header box, and `rowLabel`,
read in each row's scope, names a row and its box; without it a row is
named by its key.

TableWidget gains setSelectColumn, setSelectAllLabel and setRowLabel,
pure so no backend silently draws no boxes. Qt Quick reserves a
touch-target width before the first column for CheckBoxes whose
nextCheckState keeps their state; the terminal widens its gutter to
five cells for `[x]`, `[ ]` and the header's `[-]`. The loader refuses
`selectColumn` without a literal `multiple` and `selectAllLabel`
without `selectColumn`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BmVLNhUvbWZjv85Hx2saxn
The sample list draws its selection with the table's own check-box
column over the `selected` state, its header box labelled "Select all
on this page", in place of a hand-made column of check boxes and a box
in the toolbar; the compact cards keep a check box over the same state.
Results and review align their number columns, and the result field,
to the end instead of wrapping each number in an end-justified row.
The audit trail places every cell at the top of its row. The console's
worklist marks the sample in the pane as its single selection rather
than with an info tone. Sample and record ids in those tables are mono.

The plate's well labels stay proportional: they are button labels, and
a button takes no text role. gaps.md marks G2, G29, G30 and G47 fixed
in batch B, and G43 fixed for tables, a button's modifiers staying out
of ui/1; next.md records that part 1 has landed.

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

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