-
Notifications
You must be signed in to change notification settings - Fork 0
LayrzTextInput
A Material-free, single-line text input field with optional label and hint text, prefix/suffix slots, error display, help affordance, and keyboard shortcut badging.
Metadata
Mirrors: ThemedTextInput (layrz_theme)
Phase: M2 (Core primitives)
Domain: Inputs
Primitive: EditableText
Status: Shipped in 0.0.9.
LayrzTextInput is the foundational text input widget and the base of the entire M3+ input family. Every other Layrz*Input (e.g., LayrzNumberInput, LayrzSelectInput) composes LayrzTextInput internally rather than reimplementing the field chrome. Picker-style inputs (date, time, color, emoji, etc.) render as a read-only LayrzTextInput that opens their selection surface on tap.
- Filled visual style only — single-line, consistent with Material 3 conventions, with a filled background and border
-
Label or hint is mandatory — at least one of
labelTextorhintTextmust be non-null; both together are valid; neither alone is valid (debug assertion enforced) - Label is static — displayed above the field, never animated, never repositioned
- Hint text is display-only — shown inside the empty field when there is no label, disappears when the field has text
-
Optional label icon — an icon rendered beside the label text via
RichText, inheriting label styling -
Prefix and suffix are mutually exclusive per slot — at most one of
prefixIcon,prefix, orprefixTextper slot; the same rule applies to the suffix trio -
Errors are caller-owned — no built-in
validatorcallback; callers provide aList<String>of error messages joined with", "into a single line -
Disabled and read-only are distinct — disabled blocks all taps and shows muted text; read-only fires
onTap(used by pickers) and displays a lock icon in the suffix - Selection handles and toolbar are deferred — text selection and keyboard selection work, but touch drag handles, selection toolbar, and magnifier are DESIGN-74 scope
The six interaction states of the field, their visual treatment, and border rendering:
| State | Fill | Border (always 1.5px) | Text |
|---|---|---|---|
| Rest | surface2 |
transparent |
fg1, hint fg3
|
| Hover | surface3 |
transparent | fg1 |
| Focus | surface2 |
colors.primary |
fg1 |
| Error | colors.danger.shade50 |
colors.danger |
fg1 |
| Disabled | surface2 |
transparent | fg4 |
| Read-only | surface2 |
transparent |
fg1 + lock icon |
State precedence: disabled > read-only > error > pressed > hover/focused > default.
Key differences:
- Focus preserves the rest fill (surface2) and only changes the border to primary
- Read-only is the rest state plus a lock icon in the suffix (transparent border, full-contrast text)
- Disabled darkens text to fg4 and uses transparent border to signal modal state
- All borders are solid with constant width (1.5px) and radius (r10); transparency renders invisible borders where needed
- Error state uses danger-coloured fill and border
- Geometry is byte-identical across all states; content height is fixed via internal token-based calculation
The constructor requires at least one of labelText or hintText:
class LayrzTextInput extends StatefulWidget {
/// The label text displayed above the input field (optional if hintText is provided).
///
/// When provided, rendered in `label` typography, `fg2` colour, static above the field.
/// At least one of [labelText] or [hintText] must be non-null.
final String? labelText;
/// Hint text displayed inside the empty field when there is no label (optional if labelText is provided).
///
/// Rendered in the field's text style but with `fg3` (muted) colour.
/// Disappears when the field has text.
/// At least one of [labelText] or [hintText] must be non-null.
final String? hintText;
// ... rest of parameters
}Assertion: labelText != null || hintText != null. Both can be non-null; neither alone is an error.
-
labelText (String?, optional) — label displayed above the field. At least one of this or
hintTextmust be non-null. -
hintText (String?, optional) — hint inside the empty field. At least one of this or
labelTextmust be non-null. -
isRequired (bool, default false) — when true, a red
*appears next to the label text - prefixIcon, prefix, prefixText (mutually exclusive) — prefix slot content
- suffixIcon, suffix, suffixText (mutually exclusive) — suffix slot content
-
errors (List, default []) — error messages, joined with
", "into a single line below the field - hideDetails (bool, default false) — when true, hides error text (field error state remains)
-
disabled (bool, default false) — blocks all input and taps, darkens text to
fg4, uses transparent border -
readOnly (bool, default false) — blocks editing but fires
onTap; displays lock icon in suffix -
dense (bool, default false) — when true, reduces vertical padding to
sp6; content height remains constant - onChanged, onSubmit, onFocusChanged, onTap — interaction callbacks
- controller, focusNode — text and focus management (caller-owned if supplied)
- keyboardType, textInputAction, inputFormatters, maxLength, autofocus, textCapitalization, autocorrect, enableSuggestions — standard EditableText passthroughs
- shortcut (Set?, optional) — keyboard shortcut badge (display-only, hidden on mobile)
-
padding (EdgeInsets?, optional) — custom padding; defaults to
sp10horizontal andsp10/sp6vertical based ondense - helpTitleText, helpContentText (optional) — help affordance tooltip
-
Label or hint is mandatory — at least one of
labelTextorhintTextmust be non-null (debug assertion) - Slot exclusivity — at most one of three forms per slot; assertion enforced in debug mode
-
Read-only lock icon — always appears in the suffix when
readOnly: true, coexisting with caller-supplied suffixes -
Error text is single-line on wide widths — multiple errors are joined with
", "(comma-space), not rendered as a bulleted list -
Errors move to tooltip on compact widths — below the
smbreakpoint (viewport width < 960px), validation errors leave the inline slot and display in a tooltip anchored to the error icon, positioned left, with one error per line -
Error icon coexists — the error
!icon is independent and always appears iferrorsis non-empty - Shortcut is display-only — keyboard shortcuts badge is rendered but does not bind key events
-
Single-line only —
maxLinesis always 1; multiline textarea is separate component (LayrzTextAreaInput) - Geometry is byte-identical — border width (1.5px), content height, and radius (r10) are identical in all six states; only colour and transparency vary (D15)
- Fixed content height — independent of icon or slot presence; icons fit inside the height, caller-supplied widgets are constrained to it
-
Dense mode affects padding only —
dense: truereduces vertical padding but does not change content height - Selection overlay is invisible — caret, selection via keyboard, and Ctrl+A/Ctrl+C work; touch handles and toolbar are DESIGN-74 scope
Validation errors are displayed inline beneath the field:
- All error messages joined with
", "into a single line - Character counter (if
maxLengthis set) appears to the right of the error line
Validation errors appear in a tooltip anchored to the error icon:
- The inline error slot beneath the field is hidden, giving the character counter full width (no truncation)
- Tapping the error icon opens a tooltip positioned to the left of the icon
- Each error appears on its own line within the tooltip (errors joined with newlines, not comma-space)
- The tooltip uses
LayrzTooltipTrigger.tap, so tapping the icon toggles the tooltip open/closed
Threshold: The swap occurs at the sm breakpoint (width < 960px). This includes:
- Phone-width browser windows
- Most tablets in portrait orientation
- Narrow desktop windows
This is a transparent, internal optimization. Consumers do not opt in; the field automatically adapts based on available viewport width.
-
M1 Theme System (
LayrzTheme,LayrzThemeData,LayrzTokens) -
M2 Tooltips (
LayrzTooltip) — for help affordances - layrz_icons (^1.1.1) — for error and lock icons
-
Flutter 3.47+ —
EditableTextand widget state API
LayrzTextInput(
labelText: 'Email',
hintText: 'user@example.com',
keyboardType: TextInputType.emailAddress,
onChanged: (value) => setState(() => email = value),
)LayrzTextInput(
hintText: 'Search...',
prefixIcon: LayrzIcons.solarOutlineSearch,
onChanged: (value) => setState(() => query = value),
)LayrzTextInput(
labelText: 'Username',
isRequired: true,
errors: username.isEmpty ? ['Username is required'] : [],
onChanged: (value) => setState(() => username = value),
)LayrzTextInput(
labelText: 'Date',
readOnly: true,
controller: TextEditingController(text: selectedDate?.toString() ?? ''),
onTap: () => _showDatePicker(), // Opens date picker
)LayrzTextInput(
labelText: 'Locked Field',
disabled: true,
controller: TextEditingController(text: 'This field is disabled'),
)LayrzTextInput(
labelText: 'Password',
isRequired: true,
obscureText: true,
errors: _validatePassword(password), // Returns ['too short', 'no capitals'] → joins as 'too short, no capitals'
onChanged: (value) => setState(() => password = value),
)LayrzTextInput is the foundational input component. All concrete input types compose it, ensuring visual and behavioral consistency across the entire input family.
Last updated: 2026-08-18
Related documents: Input Contract, Milestone 2, Design Tokens, Decisions (D32, D33, D34), Architecture
Made with ❤️ by Golden M, Inc.
- LayrzTextInput
- LayrzSelectableAction
- LayrzSelectionToolbar
- LayrzTextSelectionControls
- LayrzSelectionMagnifier
- LayrzSelectionHandlePainter
- LayrzTextAreaInput
- LayrzComboBoxInput
- LayrzNumberInput
- LayrzPasswordInput
- LayrzCheckboxInput
- LayrzRadioInput
- LayrzSelectInput
- LayrzMultiSelectInput
- LayrzSearchInput
- LayrzDualListInput
- LayrzDurationInput
- LayrzSlider
- LayrzStepper