-
Notifications
You must be signed in to change notification settings - Fork 0
Input Contract
This document defines the shared contract that every Layrz*Input component in layrz_ui conforms to. Rather than specifying this per-component, the contract is recorded here as the single source of truth for input field behavior, styling, and API, so the architectural decisions need to be made only once.
LayrzTextInput is the base of the entire input family. Every other Layrz*Input composes LayrzTextInput internally rather than reimplementing the field chrome. This is the same architectural pattern used in layrz_theme.
The picker-style inputs (date, time, color, emoji, etc.) render as a read-only LayrzTextInput that opens their selection surface on tap.
-
Shipping order:
LayrzTextInputmust ship first in M2 (shipped in 0.0.9). Every other input depends on it. -
Visual decisions are global: The chrome of
LayrzTextInput— label, prefix/suffix, help affordance, padding, error display, focus state, border decoration — is the chrome of every input in the system. Design decisions made forLayrzTextInputpropagate to all inputs. -
No separate input-container:
LayrzTextInputitself plays the role of the input container. There is no separate wrapper component; all input chrome is part ofLayrzTextInput.
Every form field in layrz_ui is named Layrz*Input, regardless of how it collects its value. The *Picker suffix from layrz_theme is retired entirely.
| layrz_theme Name | layrz_ui Name | Collection Method |
|---|---|---|
| ThemedDatePicker | LayrzDateInput | Calendar dialog |
| ThemedTimePicker | LayrzTimeInput | Time picker dialog |
| ThemedDateTimePicker | LayrzDateTimeInput | Calendar + time in tabbed dialog |
| ThemedMonthPicker | LayrzMonthInput | Month + year grid |
| ThemedEmojiPicker | LayrzEmojiInput | Emoji selection dialog |
| ThemedColorPicker | LayrzColorInput | Color wheel/palette dialog |
| ThemedFilePicker | LayrzFileInput | System file browser |
| ThemedIconPicker | LayrzIconInput | Icon selection dialog |
| ThemedAvatarPicker | LayrzAvatarInput | System image picker |
The *Picker suffix suggests a distinct interaction pattern from form fields. In layrz_ui, all of these components are inputs that participate in forms like any text field or select field. They are not separate pickers; they are inputs with selection surfaces. The unified naming clarifies this conceptual unity.
Every Layrz*Input exposes the following API, inherited from LayrzTextInput.
// Design sketch — illustrative only
class LayrzTextInput extends StatefulWidget {
/// The label text displayed above the input field (optional if hintText is provided).
///
/// This is the only label representation; a `label` Widget parameter is not supported.
/// At least one of [labelText] or [hintText] must be non-null.
final String? labelText;
/// Optional icon rendered beside the label text.
final IconData? labelIcon;
/// Hint text displayed inside the empty field (optional if labelText is provided).
///
/// Shown inside the field, disappears when the field has text.
/// At least one of [labelText] or [hintText] must be non-null.
final String? hintText;
// ...
}-
At least one of labelText or hintText is mandatory — the widget asserts
labelText != null || hintText != nullin debug mode - Both can be non-null — a field can have both a label and a hint
- labelIcon — optional icon rendered beside the label text
- No
labelWidget parameter; labels are text-only
Each input supports both a leading prefix and a trailing suffix. At most one of the three forms (icon, widget, or text) may be supplied per slot; providing multiple triggers a debug assertion.
// Design sketch — illustrative only
class LayrzTextInput extends StatefulWidget {
/// Icon displayed in the prefix slot.
///
/// Mutually exclusive with [prefix] and [prefixText].
final IconData? prefixIcon;
/// Custom widget displayed in the prefix slot.
///
/// Mutually exclusive with [prefixIcon] and [prefixText].
final Widget? prefix;
/// Text displayed in the prefix slot.
///
/// Mutually exclusive with [prefixIcon] and [prefix].
final String? prefixText;
/// Callback invoked when the user taps the prefix.
final VoidCallback? onPrefixTap;
/// Icon displayed in the suffix slot.
///
/// Mutually exclusive with [suffix] and [suffixText].
final IconData? suffixIcon;
/// Custom widget displayed in the suffix slot.
///
/// Mutually exclusive with [suffixIcon] and [suffixText].
final Widget? suffix;
/// Text displayed in the suffix slot.
///
/// Mutually exclusive with [suffixIcon] and [suffix].
final String? suffixText;
/// Callback invoked when the user taps the suffix.
final VoidCallback? onSuffixTap;
// ...
}-
Prefix slot: At most one of
prefixIcon,prefix, orprefixTextmay be non-null; supplying multiple triggers a debug assertion. -
Suffix slot: At most one of
suffixIcon,suffix, orsuffixTextmay be non-null; supplying multiple triggers a debug assertion. -
Error icons coexist: The error icon is independent and always appears in the suffix when
errorsis non-empty - Read-only lock icon coexists: In read-only mode, a lock icon also appears in the suffix alongside caller-supplied suffix widgets
An informational tooltip can be paired with the label, accessed via hover on desktop or long-press on mobile.
// Design sketch — illustrative only
class LayrzTextInput extends StatefulWidget {
/// Title text displayed in the help tooltip.
///
/// If supplied, a help icon appears and opens a tooltip on hover/long-press.
final String? helpTitleText;
/// Body text displayed in the help tooltip.
///
/// Shown below [helpTitleText] in the tooltip popup.
final String? helpContentText;
// ...
}- helpTitleText and helpContentText form a two-part help message.
- Rendered as a tooltip component (wrapping the SDK's
RawTooltip).
Errors are caller-owned. There is no built-in validator callback. Callers provide error messages directly as a list.
// Design sketch — illustrative only
class LayrzTextInput extends StatefulWidget {
/// List of error messages to display below the field.
///
/// Caller-owned; no automatic validation.
/// Multiple errors are joined with ", " (comma-space) into a single line.
/// When non-empty, an error icon appears in the field and the joined message
/// is rendered below the field in danger colour.
final List<String> errors;
/// Whether to hide the error message text below the field.
///
/// When true, error messages are not rendered (though the field's error visual state remains).
final bool hideDetails;
// ...
}-
errors is a
List<String>, not a validator callback. Callers manage validation and provide error messages. -
Errors are joined with
", "into a single line (e.g.,['required', 'too short']renders as'required, too short') -
Error visual state on field: When
errorsis non-empty, the field border becomesdangercolour, the background becomesdanger.shade50, and an error icon is displayed in the suffix. - hideDetails: When true, error messages below the field are hidden (but the field's error visual state remains).
// Design sketch — illustrative only
class LayrzTextInput extends StatefulWidget {
/// Callback invoked when the input value changes.
final ValueChanged<String>? onChanged;
/// Callback invoked when the user submits the input (e.g., via Enter on desktop).
final ValueChanged<String>? onSubmit;
/// Callback invoked when the input gains or loses focus.
final ValueChanged<bool>? onFocusChanged;
/// Callback invoked when the user taps anywhere on the input field.
///
/// Distinct from [onPrefixTap] and [onSuffixTap]. Useful for picker-style inputs
/// to open their selection surface. Fires even in read-only mode, but not in disabled mode.
final VoidCallback? onTap;
/// Whether the field is disabled (not editable, not tappable).
///
/// When true, keyboard input is blocked, the field does not respond to taps,
/// and no callbacks fire. Text is darkened to fg4 and border is dashed.
final bool disabled;
/// Whether the field is read-only (not editable, but tappable).
///
/// When true, the field displays a value but is not directly editable.
/// Callbacks like [onTap], [onPrefixTap], and [onSuffixTap] still fire.
/// A lock icon is displayed in the suffix to signal read-only mode.
/// Picker-style inputs use this to present a read-only display of the selected value.
final bool readOnly;
// ...
}- onChanged and onSubmit are independent callbacks.
-
onFocusChanged receives
truewhen focus is gained,falsewhen lost. - onTap fires in read-only mode (for pickers) but not in disabled mode.
-
disabled blocks all taps and input; text darkens to
fg4; border becomes dashed. -
readOnly allows
onTapto fire (for pickers) and displays a lock icon in the suffix; rest fill and solid border.
// Design sketch — illustrative only
class LayrzTextInput extends StatefulWidget {
/// Focus node for this input field.
///
/// If null, the widget constructs a node in [initState].
/// The widget disposes only nodes it creates. Caller-supplied nodes are left untouched.
final FocusNode? focusNode;
/// Text controller for this input field.
///
/// If null, the widget constructs a controller in [initState].
/// The widget disposes only controllers it creates. Caller-supplied controllers are left untouched.
final TextEditingController? controller;
// ...
}Disposal contract: The widget disposes ONLY the instances it created; caller-supplied instances are left untouched. This is the standard pattern and prevents controller leaks, but bears explicit documentation because it is a common source of bugs.
// Design sketch — illustrative only
class LayrzTextInput extends StatefulWidget {
/// Padding applied inside the input field around the text and slots.
///
/// Defaults to values from the M1 spacing tokens when null.
/// Can be overridden per-field to support custom layouts.
final EdgeInsets? padding;
// ...
}- padding uses spacing tokens from the design-tokens document by default.
- Customizable per-field.
Concrete inputs compose LayrzTextInput and supply display text through the controller. Any visual affordance (color swatch, avatar thumbnail, icon preview) is rendered through the existing prefix widget parameter.
Each concrete input specifies:
- The value type it holds (
Color,DateTime,List<String>, etc.). - How to format that value for display in the controller's text.
- The widget to render as a visual affordance in the prefix slot (if any).
Every concrete Layrz*Input specifies only its deltas from the base contract:
-
Value type — the data structure the input holds (e.g.,
Color,DateTime,Duration). - Selection surface — the dialog or interaction pattern used to set the value (e.g., calendar picker, color wheel, emoji grid).
- Input-specific parameters — validation rules, formatting options, or constraints particular to the input type.
| Input | Value Type | Selection Surface | Notes |
|---|---|---|---|
| LayrzNumberInput | double |
Stepper buttons + keyboard input | Min/max bounds, decimal formatting, step value |
| LayrzSelectInput |
String (or generic T) |
Searchable dropdown dialog | Single selection, custom item rendering |
| LayrzMultiSelectInput |
List<String> (or generic List<T>) |
Searchable dropdown with checkboxes | Multiple selection, checkboxes for visibility |
| LayrzCheckboxInput | bool |
Inline checkbox, switch, or dropdown field | Style variants: checkbox, switch, field (dropdown) |
| LayrzColorInput | Color |
Color wheel and/or palette picker | Hand-rolled to avoid Material dependency |
| LayrzFileInput | {base64: String, bytes: Uint8List} |
System file browser | Returns both base64 and raw bytes |
| LayrzIconInput | IconData |
Icon selection dialog | Picker over Solar icon set with search |
| LayrzAvatarInput |
String (base64) |
System image picker | Stores image as base64, displays thumbnail |
This table is illustrative of the pattern, not the complete catalogue. See Component Catalog for the full mapping of layrz_theme to layrz_ui inputs.
All default padding and inner spacing come from the M1 spacing token system. See the design-tokens document for token definitions (sp4, sp6, sp8, sp10, sp12, sp14, sp16, sp20, sp24, sp28, sp32, sp36, sp40, sp44, sp48).
Impact: Spacing is consistent across all inputs and customizable from the theme.
The help affordance is built on the tooltip component, which wraps the SDK's RawTooltip. This must be available for all inputs to render help text.
Dependency: LayrzTooltip (M2) shipped in 0.0.7.
The SDK's EditableText ships with selection-handle positioning, drag-selection via keyboard, and keyboard selection (Ctrl+A, Ctrl+C). Touch drag handles, a selection toolbar, and the magnifier are deferred to DESIGN-74. LayrzTextInput is configured with selectionControls: null and contextMenuBuilder: null, making the selection overlay invisible while keyboard-driven selection remains functional.
Current state: Text selection and keyboard shortcuts work. Touch affordances (drag handles, selection toolbar) are DESIGN-74 scope and may land after the initial M2 ship.
See the flutter-347-audit document for the full analysis of SDK dependencies and Material coupling.
LayrzTextInput renders in six visual states. This matrix is the shared chrome for the entire input family:
| State | Fill | Border (always 1.5px) | Text | Dashed |
|---|---|---|---|---|
| Rest | surface2 |
transparent |
fg1, hint fg3
|
false |
| Hover | surface3 |
transparent | fg1 |
false |
| Focus | surface2 |
colors.primary |
fg1 |
false |
| Error | colors.danger.shade50 |
colors.danger |
fg1 |
false |
| Disabled | surface2 |
divider |
fg4 |
true |
| Read-only | surface2 |
divider |
fg1 + lock icon |
false |
State precedence: disabled > read-only > error > pressed > hover/focused > default.
Key characteristics:
-
Focus preserves fill — focus only changes the border to primary; fill stays
surface2(not elevated tosurface) - Read-only is rest + lock icon — uses the rest state fill and solid divider border, with a lock icon in the suffix
- Disabled darkens text to fg4 and uses a dashed border to signal permanent non-editability
- Dashed border only on disabled — read-only uses solid borders
- Error state has danger colours on both fill and border
- Light mode only — dark theme variants are out of scope (decision D7)
Progress for input components and milestones is tracked in the Milestone Status tables in the repository documentation. See engineering/milestone-2.md for M2 work items and status.
Documentation files record specification and architecture; milestone tables record progress.
Last updated: 2026-08-18
Related documents: Component Catalog, design-tokens, architecture, roadmap, decisions, flutter-347-audit
Made with ❤️ by Golden M, Inc.