elements: number-stepper kind + sheet detents/sizing props - #72
Conversation
Adds a general-purpose numeric stepper element (kind number-stepper, props value/min/max/step/text/enabled, emits ValueChanged) so apps can express bounded numeric input with a step increment, and two presentation props on sheet: detents (comma-separated medium|large|fraction) and sizing (form|fitted|page). Apple backend: LUINumberStepperView renders a SwiftUI Stepper over a clamped range; sheets apply .presentationDetents and .presentationSizing (iOS 18+/macOS 15+ gated) via LUIModalPresentationPolicy on the shared modal surface, so navigation-form sheets compose. Integral wire floats (e.g. value:60.0) decode as Int through Foundation's JSONDecoder, so float-typed props now normalize int -> double like grow/sourceX already did. Web/melange renders an <input type=number>; qt, flutter, and winui gain schema support and minimal renderers; winui coerces integral JSON numbers for float props the same way.
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e0e0e68737
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| case NodeKind::RadioGroup: return "LuiRadioGroup.qml"; | ||
| case NodeKind::Radio: return "LuiRadio.qml"; | ||
| case NodeKind::Slider: return "LuiSlider.qml"; | ||
| case NodeKind::NumberStepper: return "LuiNumberStepper.qml"; |
There was a problem hiding this comment.
Ship the Qt number-stepper QML component
When Qt renders a number-stepper, LuiNodeView.qml derives this component filename, but a repo-wide search of platform/qt/qml and the QML_FILES list in platform/qt/lib/CMakeLists.txt finds no LuiNumberStepper.qml. The QML loader therefore cannot instantiate the newly accepted node kind, leaving every Qt number-stepper blank with a component-load error.
Useful? React with 👍 / 👎.
| attach_toggle_event renderer node kind dom_node | ||
| | Radio -> attach_radio_event renderer node dom_node | ||
| | Slider -> attach_slider_event renderer node dom_node | ||
| | Slider | NumberStepper -> attach_slider_event renderer node dom_node |
There was a problem hiding this comment.
Sanitize web number-stepper values before dispatch
For the new number input, the reused slider listener emits valueAsNumber directly, without the finite-value check or min/max clamping implemented by every native backend. Clearing the field produces NaN, and decrementing a default stepper below zero produces a negative value because the generated input has no default min; an application that stores either event can then fail finite property validation or receive an out-of-range value. Validate and clamp number-stepper events against its retained bounds before emitting them.
Useful? React with 👍 / 👎.
| void SyncNumberStepper(LUINodeState state, LUISyncContext context) | ||
| { | ||
| if (Control is not NumberBox box) return; |
There was a problem hiding this comment.
Populate the WinUI NumberBox header from text
A number-stepper is valid when it has text but no accessibility-label, yet this sync routine only applies its numeric properties and never copies TextValue into the NumberBox.Header or another label. Consequently the common text-only usage renders an unlabeled numeric control on WinUI, unlike the Apple, web, and Flutter implementations.
Useful? React with 👍 / 👎.
| Flexible(child: Text(stepperState.properties['text'] as String? ?? '')), | ||
| IconButton( | ||
| icon: const Icon(Icons.remove), |
There was a problem hiding this comment.
Honor the Flutter stepper accessibility label
The validator explicitly allows a number-stepper whose accessible name comes solely from accessibility-label, but this view reads only text. With text omitted, it renders an empty label plus two icon-only buttons, and the surrounding generic wrapper applies only the accessibility identifier, so assistive technology receives no purpose for the decrement and increment controls. Apply the supplied accessibility label and meaningful button semantics here.
Useful? React with 👍 / 👎.
| (if Store.direct_toggle kind || kind = NumberStepper then | ||
| Util.child_element dom_node 0 |
There was a problem hiding this comment.
Remove the web stepper label from the same element
This new special case writes aria-label to the number-stepper's child input, but remove_property still treats it as an ordinary node and removes the attribute from the outer label element. If reconciliation drops an optional accessibility label while retaining nonempty text, the old input label remains and continues overriding the visible text for screen readers.
Useful? React with 👍 / 👎.
| Option.iter (Lui_ui.string_property context node Detents) detents; | ||
| Option.iter (Lui_ui.string_property_signal context node Detents) detents_signal; | ||
| Option.iter (Lui_ui.string_property context node Sizing) sizing; | ||
| Option.iter (Lui_ui.string_property_signal context node Sizing) sizing_signal; |
There was a problem hiding this comment.
Implement sheet presentation props outside Apple
When a standard sheet using either new prop runs on Web, Flutter, WinUI, or Qt, the backends accept the property but never apply its semantics: the Flutter modal route, WinUI LUIModalPresenter, and Qt LuiSheet.qml do not read either property, while Web only stores them in otherwise-unused data attributes. Applications therefore silently receive the default sheet on four supported backends; implement platform equivalents or explicitly scope/reject these props instead of exposing them as accepted no-ops.
Useful? React with 👍 / 👎.
| match | ||
| ( Property_map.find_opt MinValue properties, | ||
| Property_map.find_opt MaxValue properties ) | ||
| with | ||
| | Some (FloatValue minimum), Some (FloatValue maximum) -> | ||
| minimum <= maximum | ||
| | _ -> true |
There was a problem hiding this comment.
Validate max against the default minimum
When only max is supplied, this fallback accepts any finite value even though every backend uses an omitted min as 0.0. Thus a valid-looking stepper such as ~max:(-1.0) has an impossible effective range: Apple, Flutter, and Qt collapse it to zero, while WinUI assigns a NumberBox.Maximum below its default minimum and may reject or coerce the control. Compare the optional endpoints after applying their defaults rather than validating only the both-present case.
Useful? React with 👍 / 👎.
|
E2E-tested the new Web (melange): renders iOS (LUIComponents.app on simulator): native SwiftUI |
…re detents/sizing on all backends - add platform/qt/qml/LuiNumberStepper.qml and register it in QML_FILES - web: clamp number-stepper value to retained min/max and drop NaN before dispatching ValueChanged; route aria-label removal to the child input - apply sheet detents (height fraction) and sizing (fitted width) on the web, Qt, Flutter, and WinUI surfaces instead of accepting them as no-ops - winui: copy text to NumberBox.Header; flutter: wrap the stepper in a Semantics label and give the step buttons tooltips - compare stepper min/max after applying defaults so a lone negative max is rejected (ocaml + all backend mirrors)
Summary
Adds two general-purpose capabilities so app code (e.g. logseq_journal's asset-settings extension) can be expressed entirely with standard elements:
number-stepper— new leaf kind for bounded numeric input (distinct fromstepper/step, which is the wizard step indicator). Props:value(existingprogressValuefloat), newmin/max/stepfloats,textlabel,enabled. Emits the existingValueChanged(node, value)event.Sheet presentation props on the existing
sheetkind:detents: comma-separatedmedium,large, or a fractional height in (0,1] ("medium,large","0.4"); unknown tokens ignored, absent/all-unrecognized falls back to[.large]→.presentationDetentssizing:form | fitted | page→.presentationSizing, applied under#available(iOS 18.0, macOS 15.0, *)(package floor is iOS 17/macOS 14) and ignored on older OSesBoth apply inside
LUIModalSurfaceContent, so they compose with thenavigation-formstyle-class path (which produces SwiftUIForm+ navigation title + toolbar actions).Notable changes
LUIModalPresentationPolicygaineddetents(for:)/sizing(for:)(previously onlyshowsDragIndicator) and is now@MainActorsince it reads node props; the previously-unusedLUIModalPresentationStylemodifier now takes the node model and applies both modifiers.performValueChangeacceptsnumberStepperand clamps tomin...maxinstead of0...1on all backends.JSONDecoderdecodes integral JSON doubles (60.0) asInt, soLUIWireValue.normalized(for:)now coerces int → double forprogressValue,minValue,maxValue,stepValue(alongside the existinggrow/sourceXset). Without this, a legal wirevalue:1.0was rejected. WinUI's backend got the same coercion (TryGetInt64has the same habit); Qt/Flutter already treat all JSON numbers as doubles.mapsProgresstest's rejection case changed fromvalue:1(now legitimately coerced) tovalue:"x".number-stepperrequirestextorlabelplus a finitevalue, withmin <= maxwhen both present;sizingis restricted to the three wire tokens. Emit-time errors throw rather than producing partial ops.<input type="number">; Qt/Flutter/WinUI get schema support plus minimal renderers (QML spinner hookup, a Row of -/+ buttons in Flutter, WinUINumberBox).Verified:
dune build,dune runtest -j 4(32/32),make test-apple(135/135), Qt cmake build +tst_lui_qml_backend(14/14),flutter analyzeclean. WinUI not compiled here (no dotnet on this machine).Link to Devin session: https://app.devin.ai/sessions/14641ee59e124d8e853db24a05173c8b
Open in Devin Desktop: https://app.devin.ai/desktop/session/14641ee59e124d8e853db24a05173c8b?variant=devin
Requested by: @RCmerci