Skip to content

LayrzDurationInput

Kenny Mochizuki Escalona edited this page Aug 27, 2026 · 6 revisions

LayrzDurationInput

Time span selection field with day, hour, minute, and second components.

Specification Status: LayrzDurationInput has shipped and is implemented at lib/src/inputs/src/duration/duration_input.dart (+ duration_picker_panel.dart). The section below marked Shipped behavior describes the actual, current implementation.

Everything below that, starting at "Metadata", is the original pre-implementation design sketch carried over from the layrz_theme migration plan. It predates the actual implementation and does not match the shipped API in several places (for example: the shipped surface is LayrzAnchoredPanel on desktop / LayrzBottomSheet on mobile, not a RawDialog; visibleUnits is a Set<LayrzDurationUnit>, not List<LDurationUnit>; the summary format is controlled by the LayrzDurationFormat enum (long/short), not a fixed "and"-joined humanization; there is no label widget parameter, suffixIcon, or prefixIcon — the field exposes no prefix/suffix parameters at all). Reconciling the rest of this legacy sketch with the shipped widget is out of scope for this unit and is left for a dedicated documentation pass.

Shipped behavior: external clock affordance

LayrzDurationInput renders a clock-style icon (MdiIcons.clockOutline) that identifies the field as a duration picker. Added because the field previously had no visual affordance at all distinguishing it from a plain read-only text field.

  • External sibling, not a slot. The icon is not placed in the chrome's prefixSlot or suffixSlot — both remain completely empty (LayrzInputPrefixSlot() / LayrzInputSuffixSlot(), no icon/widget/text) and free for a caller to use, the same way every other slot-bearing input in this package keeps its slots free of its own chrome. The icon instead sits beside the chrome, inside one outer bordered container — the same composition LayrzNumberInput uses for its increment/decrement step controls: a Row of [chrome, icon], with the inner LayrzInputChrome given showBorder: false and borderRadius: BorderRadius.zero so its own box never paints a competing border, and the outer Container drawing the single unified border and radius. LayrzInputChrome needed no change to support this — showBorder and borderRadius already existed on it for exactly this purpose.
  • Present on both bands. The icon renders identically whether the field opens the desktop anchored panel or the mobile bottom sheet — it is part of the shared anchor composition both bands render, not duplicated per band.
  • State-matched color, no separate tracking. The icon's color is read from the same LayrzInputStyleSpec the chrome itself resolves for its text color — fg1 at rest, fg4 when disabled, and the danger color when errors is non-empty — so it always agrees with the field's current state without the icon tracking hover/focus/disabled on its own. A left divider (the same error-aware color logic as NumberFieldControl's cap divider) separates it visually from the chrome.
  • Decorative — excluded from semantics. The icon carries no accessible name and is wrapped in ExcludeSemantics; the field's own Semantics node (label, button: true, enabled state) already describes the control, so the icon is never announced as a second, unlabeled element.

Shipped behavior: panel width, dismissal, and field values

The panel matches the anchor's width (BREAKING). LayrzDurationInput's desktop panel uses LayrzAnchoredPanelWidthPolicy.matchAnchor, the same "elevated field" illusion (DESIGN-145) LayrzSelectInput uses — the panel covers the field itself, at the field's own width, rather than sizing to its content within a fixed 280–480 logical-pixel band as an earlier version did. The unit fields inside the panel no longer depend on that band either: LayrzDurationPickerPanel reads its own measured width via a LayoutBuilder and packs as many fields as fit per row without any field dropping below a minimum usable width, wrapping the rest to additional rows instead of shrinking further.

Editing a field no longer closes the panel — only Reset does. Tapping a unit field's +/ control, or typing a value directly into one, keeps the panel open, so a duration can be composed across multiple fields (day, then hour, then minute) in one session. LayrzDurationPickerPanel exposes onChanged (fired on every field edit) and a separate onReset (fired once, after Reset zeroes every field) — only onReset closes the panel. Previously both were the same callback, which the field treated as "close", so the panel closed on every keystroke or step-button tap.

Unit fields are integers, not decimals. Day/hour/minute/second values render as whole numbers (e.g. 2, not 2.0) and reject a typed decimal separator at the formatter level.

The field now styles itself as errored. With one or more errors, the field renders the danger border and fill, matching every other input in the package. It previously could not: the field passed readOnly: true into its own style resolution, and the resolver's precedence ranks readOnly above error, so the danger styling was silently suppressed regardless of how many errors were supplied. LayrzDurationInput exposes no readOnly parameter of its own — the flag described an internal fact about the field (it never accepts typed input, see the read-only anchor note above), not a caller-supplied state, and fixing this did not add one.

Metadata

Property Value
Mirror ThemedDurationInput
Phase M3 Inputs
Domain Inputs
SDK Primitive Composes LayrzTextInput (read-only) + RawDialog with LayrzNumberInput controls

Conformance

LayrzDurationInput conforms to the Input Contract. Like all picker-style inputs, it composes LayrzTextInput internally in read-only mode and opens a selection surface on tap. The entire contract applies: labels, prefix/suffix, help affordance, focus management, padding, and validation error display.

Value Type and Selection Surface

  • Value type: Duration — represents a time span (days, hours, minutes, seconds)
  • Selection surface: Dialog with numeric input fields for each time unit
  • Summary display: The field displays the duration in human-readable format (e.g., "2 days 3 hours 15 minutes"); unselected state shows placeholder or empty
  • Formatting: Duration is formatted using human-readable language (e.g., "2 days, 3 hours, and 15 minutes")

Deltas from the Input Contract

Duration Composition

// Design sketch — illustrative only
class LayrzDurationInput extends StatefulWidget {
  /// The currently selected duration.
  final Duration? value;

  /// Callback invoked when user changes the duration.
  final void Function(Duration?)? onChanged;

  /// Which time units are visible in the picker.
  /// Supported: day, hour, minute, second.
  /// Defaults to all four.
  final List<LDurationUnit> visibleUnits;

  const LayrzDurationInput({
    required this.value,
    this.onChanged,
    this.visibleUnits = const [
      LDurationUnit.day,
      LDurationUnit.hour,
      LDurationUnit.minute,
      LDurationUnit.second,
    ],
    // ... shared contract parameters (label, placeholder, prefix, suffix, etc.)
  });
}

enum LDurationUnit {
  /// Day unit (24-hour increment)
  day,

  /// Hour unit (0–23 range; wraps when combined with days)
  hour,

  /// Minute unit (0–59 range; wraps when combined with hours)
  minute,

  /// Second unit (0–59 range; wraps when combined with minutes)
  second,
}

Dialog Layout

Inside the dialog:

  • Each visible unit is rendered as a numeric input field with a label and suffix (e.g., "Days: [2]")
  • Numeric inputs display step buttons (+/−) or spin controls
  • Minimum value for all units is 0; no maximum constraints

Formatting

The field displays the duration as human-readable text, e.g.:

  • "2 days, 3 hours, and 15 minutes" (with localized "and")
  • "1 hour 30 minutes" (short format if only two units)
  • "45 seconds" (single unit)

The format depends on which units are visible; hidden units are omitted from the summary.

Inherits from Input Contract

All of the following are inherited from LayrzTextInput:

  • labelText and label (mutually exclusive)
  • placeholder
  • prefixIcon, prefixWidget, onPrefixTap (mutually exclusive icon and widget)
  • suffixIcon, suffixWidget, onSuffixTap (mutually exclusive icon and widget)
  • helpTitleText, helpContentText (two-part help tooltip)
  • readOnly (always true for duration input)
  • onTap (opens the duration picker dialog)
  • focusNode, controller (focus and value management)
  • dense (bool, default false) — pd2/10px by default, pd1/6px when true, identically on every viewport (isCompact no longer varies this). No padding override (D66)
  • disabled and error display

Reference: Current layrz_theme API

ThemedDurationInput (source: lib/src/inputs/src/general/duration_input.dart):

Parameter Type Notes
value Duration? Currently selected duration
onChanged Function(Duration?)? Callback when duration changes
errors List<String> Error messages (default: [])
labelText String? Label text (or use label Widget instead)
label Widget? Label widget (mutually exclusive with labelText)
suffixIcon IconData? Icon in suffix slot
prefixIcon IconData? Icon in prefix slot
disabled bool Disable the field (default: false)
padding EdgeInsets? Field padding
visibleValues List<ThemedUnits> Which units to display (default: day, hour, minute, second)

ThemedUnits enum:

  • year (not supported in duration picker; mentioned for reference)
  • month (not supported)
  • week (not supported)
  • day
  • hour
  • minute
  • second
  • millisecond (not typically used)

Supported units for ThemedDurationInput:

const kThemedDurationSupported = [
  ThemedUnits.day,
  ThemedUnits.hour,
  ThemedUnits.minute,
  ThemedUnits.second,
];

Dependencies and Blockers

  • LayrzTextInput — must ship first; LayrzDurationInput composes it.
  • LayrzNumberInput — required for numeric controls inside the picker dialog.
  • LayrzTooltip — required for help affordances if using helpTitleText / helpContentText.
  • Material-free TextSelectionControls — if LayrzTextInput requires copy/paste in read-only mode, this blocker applies.
  • i18n Support — humanized duration formatting requires localization strings for unit names (days, hours, minutes, seconds).

Implementation Notes from layrz_theme

  • Step controls: The numeric input controls inside the dialog include +/− step buttons for easy adjustment.
  • Reset button: The dialog includes a "Reset" button to set all units to 0.
  • Field value update: When the dialog closes, the field's display text is updated with the newly formatted duration.
  • Humanized formatting: The duration is formatted using a humanization library; the format respects the visible units and the current locale.

Open Questions

  1. Additional time units: Should year, month, and week be added as optional visible units? Currently, only day/hour/minute/second are supported.

  2. Unit constraints: Should there be maximum values for each unit (e.g., hours capped at 23, minutes at 59)? Or should overflow be allowed and calculated into days?

  3. Formatting options: Should there be a valueFormatter callback to customize the human-readable summary display? Or is localized humanization the only option?

  4. Keyboard entry: Can users type values directly into the numeric input fields, or only use step buttons?

  5. Negative durations: Should negative durations be supported, or are all values constrained to ≥ 0?

  6. Dialog height/constraints: Is the dialog height fixed, or does it adapt based on the number of visible units?

  7. Millisecond precision: Should millisecond duration component be supported if needed (e.g., for timeout values)?


Last updated: 2026-08-26 (v0.0.13 — documented the panel's matchAnchor width, the onChanged/onReset split that keeps the panel open across field edits and closes it only on Reset, integer-only unit fields, and the field's error styling fix; duration-external-affordance's external clock affordance icon note from 2026-08-25 remains; the remaining pre-implementation sections are unchanged and still need a full documentation pass)
Related documents: Input Contract, Component Catalog, Design Tokens

Clone this wiki locally