-
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 | sf1 |
transparent |
fg1, hint fg3
|
| Hover | sf1 |
transparent | fg1 |
| Focus | sf1 |
colors.primary |
fg1 |
| Error | colors.danger.shade50 |
colors.danger |
fg1 |
| Disabled | sf1 |
transparent | fg4 |
| Read-only | sf1 |
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 (
r2, 8 logical pixels); 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 - 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)
-
actions (Set?, optional) — text selection toolbar actions (copy, cut, paste, select all). When null, all four built-in actions are offered. Pass
const {}to suppress the toolbar entirely. The set is further intersected with field state: an obscured field never offers copy or cut; a read-only field never offers cut or paste. Custom actions dedupe only by identity (soconstsets are impossible, see LayrzSelectableAction). -
padding (EdgeInsets?, optional) — custom padding; when null, defaults to
pd2(8 logical pixels on all sides). When provided, overrides this default entirely. - 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 (
r2, 8 logical pixels) 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
-
Text and icon sizing are token-based — text uses
bodytypography (16px, w400); icons are sized atbody.fontSize + sp1(20px), unless anIconThemeprovides a size -
Default padding is uniform — padding defaults to
pd2(8 logical pixels on all four sides); custom padding overrides this via thepaddingparameter - Selection overlay is invisible — caret, selection via keyboard, and Ctrl+A/Ctrl+C work; touch handles and toolbar are DESIGN-74 scope
The field supports touch and keyboard text selection via the following gestures:
- Single tap — places the caret at the tapped position
- Double tap — selects the word under the pointer
- Triple tap — selects the entire field content
- Long press — selects the word under the pointer; on touch, displays a magnifier above the pointer; dragging after long-press extends the selection
- Drag during long-press — extends the selection from the initial long-press position to the drag endpoint
- Keyboard selection — Shift+arrow keys extend the caret, Ctrl+A selects all, Ctrl+C/Ctrl+X/Ctrl+V perform copy/cut/paste
On platforms with a selection toolbar (iOS, macOS, Android), tapping a selected text area opens the toolbar with the available actions (see actions parameter above). The toolbar is dismissed by tapping outside the selection, or by deselecting via keyboard or tap.
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: MdiIcons.magnify,
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