Skip to content

LayrzTextInput

Kenny Mochizuki Escalona edited this page Aug 19, 2026 · 12 revisions

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.


Overview

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.

Design Principles

  • 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 labelText or hintText must 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, or prefixText per slot; the same rule applies to the suffix trio
  • Errors are caller-owned — no built-in validator callback; callers provide a List<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

Interaction States

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

API Structure

Core Constructor (Simplified)

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;

  /// Optional icon rendered beside the label text via RichText.
  ///
  /// Inherits the label's colour (fg2) and size. Sits to the left of the label text.
  final IconData? labelIcon;

  /// 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.

Key Parameters

  • labelText (String?, optional) — label displayed above the field. At least one of this or hintText must be non-null.
  • labelIcon (IconData?, optional) — icon rendered beside label text
  • hintText (String?, optional) — hint inside the empty field. At least one of this or labelText must 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 sp10 horizontal and sp10/sp6 vertical based on dense
  • helpTitleText, helpContentText (optional) — help affordance tooltip

Key Invariants

  • Label or hint is mandatory — at least one of labelText or hintText must 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 — multiple errors are joined with ", " (comma-space), not rendered as a bulleted list
  • Error icon coexists — the error ! icon is independent and always appears if errors is non-empty
  • Shortcut is display-only — keyboard shortcuts badge is rendered but does not bind key events
  • Single-line onlymaxLines is 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 onlydense: true reduces 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

Dependencies

  • 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+EditableText and widget state API

Usage Examples

Basic with Label

LayrzTextInput(
  labelText: 'Email',
  hintText: 'user@example.com',
  keyboardType: TextInputType.emailAddress,
  onChanged: (value) => setState(() => email = value),
)

Search with Hint Only (No Label)

LayrzTextInput(
  hintText: 'Search...',
  prefixIcon: LayrzIcons.solarOutlineSearch,
  onChanged: (value) => setState(() => query = value),
)

With Label Icon and Required Flag

LayrzTextInput(
  labelText: 'Username',
  labelIcon: LayrzIcons.solarOutlinePerson,
  isRequired: true,
  errors: username.isEmpty ? ['Username is required'] : [],
  onChanged: (value) => setState(() => username = value),
)

Read-Only (Picker Style)

LayrzTextInput(
  labelText: 'Date',
  readOnly: true,
  controller: TextEditingController(text: selectedDate?.toString() ?? ''),
  onTap: () => _showDatePicker(),  // Opens date picker
)

Disabled Field

LayrzTextInput(
  labelText: 'Locked Field',
  disabled: true,
  controller: TextEditingController(text: 'This field is disabled'),
)

With Error Messages

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),
)

Architectural Role

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

Clone this wiki locally