> ## Oymyakon DS index
> Fetch the full index at: https://super-dollop-pzmo65r.pages.github.io/llms.txt
> HTML preview: https://super-dollop-pzmo65r.pages.github.io/base-text-line.html
> Source: specs/components/base-text-line/base-text-line.md, specs/components/base-text-line/base-text-line-a11y.md, specs/components/base-text-line/capabilities.json

---

# BaseTextLine · Oymyakon DS 3

> The basic single-line text input, according to which the other inputs are assembled. A
> developer-facing core: designers work with the assembled fields (TextField, PhoneField,
> PriceField), never with BaseTextLine directly.

**Version:** 3.0.0 · **Status:** Draft · **Figma:** `[BaseTextLine] 3.0` (Specification frame `3110:4175` — the page ships **no master component**, by design)

---

## 1. Description

BaseTextLine is one editable line: an optional prefix, the input (with its placeholder and
cursor), and an optional suffix, laid out in a row. It owns text entry, masking, and the five
colour slots — and nothing else: no label, no helper, no border, no error state. Those belong to
the text-field components assembled on top of it (the `[TextField] 3.0` line, PhoneField,
PriceField).

Because designers never place it, the Figma page `[BaseTextLine] 3.0` carries only a developer
Specification frame (`3110:4175`); this file and the platform sources are the component's
contract.

### Platform implementations

| Platform | Entry point | Version | Notes |
|---|---|---|---|
| iOS | `Modules/DS3/DS3/Sources/BaseTextLine/DsBaseTextLine.swift` | 3.0 | SwiftUI wrapper over `DsPortedTextField` (a UIKit `UITextField` port); settings object `DsBaseTextLineSettings` |
| Android | `core/compose/…/ds/v3/textfield/base/textline/DsBaseTextLine.kt` | 3.0 (`@DsComponentInfo`) | Compose `BasicTextField` + `BaseTextLineContent` decorator; tagged `dsComponentTag("DsBaseTextLine")` |
| Flutter | — | — | Not shipped |
| Web | `src/shared/shared.css` (`.base-text-line`) | 3.0.0 | Reference implementation, this repo; preview page `src/base-text-line.njk` |

---

## 2. Anatomy

```
BaseTextLine (.base-text-line)          row · gap s4 · fills the host's width
├── prefix (.base-text-line__prefix)      optional — text or icon, before the input
├── input (.base-text-line__input)        the editable line — fills the remaining width
│     ├── placeholder                       shown while the input is empty
│     └── cursor                            1 × 20, the input's caret
└── suffix (.base-text-line__suffix)       optional — text or icon, after the input
```

| Part | Required | Description |
|---|---|---|
| **container** | Required | The row; centres the items vertically, hugs their height (20, or 24 with an icon affix) |
| **prefix** | Off | Clarifies context before the entry — a currency symbol before a sum, a search icon before a query |
| **input** | Required | Single line only; yields width to the affixes, never the other way round |
| **placeholder** | Off | Guides the expected format while the input is empty (`example@mail.com`, `Enter name`) |
| **cursor** | — | The caret; rendered by the platform, coloured by the `cursor` slot |
| **suffix** | Off | Units, currency, or extra context after the entry — `kg`, `m²`, `%`, a currency code |

**Affixes never truncate.** A currency symbol or unit must never wrap or shrink — when the row
runs out of space the input gives way, not the affix (iOS `keepingIntrinsicWidth`: single line +
intrinsic size + raised layout priority; web: `flex: 0 0 auto` + `white-space: nowrap`).

**Affix visibility follows input activity.** The Figma behaviour contract: an empty, unfocused
line shows only its placeholder; the affixes appear with input activity and stay once the line is
filled. Platform split: iOS shows the prefix on focus **or** content (`isPrefixAlwaysVisible`
opts out of hiding); Android's base renders whatever affixes the caller passes — the show/hide
policy lives in the assembled field. The web reference follows Android: visibility is the host's
call.

### RTL layout

Off by default; `dir="rtl"` on the host mirrors the row (prefix stays logically first) and flips
the default alignment from left to right. Two things do **not** mirror:

- **Input masks keep their LTR direction** — only the alignment changes (Figma RTL block).
- **Amounts are pinned left-to-right** in every locale: iOS `isInputForcedLeftToRight` keeps
  prefix–input–suffix LTR while the surrounding chrome mirrors; the web equivalent is the
  `.base-text-line--ltr` modifier. Inside RTL chrome the forced-LTR line still sits at the
  reading edge (right-aligned) — only the character order stays LTR.

---

## 3. Variants and sizes

One size; the axes are the affixes, the alignment, and the mask.

| Axis | Values | Default | Web mechanism |
|---|---|---|---|
| **Prefix** | `none` · `icon` · `text` | `none` | The `__prefix` element and its content |
| **Suffix** | `none` · `text` · `icon` | `none` | The `__suffix` element (icon: Android `Affix.Icon`; iOS ships text/attributed only) |
| **Alignment** | `left` · `center` · `right` | `left` | `.base-text-line--center` / `.base-text-line--right` on the root — the hugged content group shifts as one |
| **Mask** | off · pattern | off | Input filtering per § 5; the pattern is content, not a variant |
| **Colour slots** | any DS semantic token | see § 6 | `--base-text-line-{text,placeholder,prefix,suffix,cursor}` re-pointed on an instance |

- The Figma Specification declares the alignment axis; **neither mobile platform exposes an
  alignment knob today** — a recorded gap. The web modifier exists.
- Geometry never varies: the container fills the host's width, hugs its content height, and
  carries no padding of its own (§ 8).

---

## 4. States

Two independent axes — interaction × content — as staged in the Figma States block:

| State | Figma name | What shows |
|---|---|---|
| **Standard empty** | Rest empty | The placeholder alone |
| **Standard filled** | Rest filled | The entered text (with its affixes and mask formatting) |
| **Active empty** | Active empty | Focus in: the cursor appears; the placeholder stays until the first character |
| **Active filled** | Active filled | Focus in with content: cursor in the text |

- **Disabled is a platform state, not a Figma one.** The Figma set stages four states; both
  platforms additionally ship `enabled = false`, painting text, placeholder and affixes in
  `TextAndIcon/Disabled`. The web mirrors it with `.base-text-line--disabled` + the input's
  `disabled` attribute.
- **The Active affordance belongs to the host.** BaseTextLine's only own Active signal is the
  cursor; the visible focus treatment (underline, border, label move) is the assembled field's
  duty. The web reference suppresses the input's default outline for that reason — a bare
  BaseTextLine used standalone must restore one (see the a11y spec).
- On focus, Android places the cursor at the end of the text (`placeCursorAtEnd`); unfocused it
  renders static text (ellipsized) instead of the live field.

---

## 5. Animation and behavior

BaseTextLine declares no motion tokens — the only moving part is the platform caret's native
blink. State transitions (label float, border colour) belong to the assembled fields; see
[`motion-rules.md`](https://super-dollop-pzmo65r.pages.github.io/motion.md).

| Event | Token | Notes |
|---|---|---|
| Caret blink | — | Native platform behaviour; never re-implemented |
| Focus / state change | — | The host field animates its own chrome (`Transforming/State/*` there, not here) |

### Input mask

A mask restricts and formats the entry against a template of two reserved characters plus
literals — identical on both platforms and staged in the Figma Masks block:

- **`X`** — any letter or digit
- **`9`** — a digit only
- any other character is a literal, inserted automatically and not editable

Behaviour: in Standard/Active empty the placeholder shows the target format (mask `XXX9999` ↔
placeholder `ABC0123`); typing consumes the mask left to right, restricted symbols are filtered
out, literals self-insert. Any combination of `X`, `9` and literals is allowed; the keyboard can
additionally be restricted by type (numeric-only, etc.).

⚠️ **Phone numbers and dates use a separate mask implementation** with its own rules — the
`X`/`9` reserved characters do not apply there (the PhoneField line).

- iOS: `DsTextLineMaskHandler` (atoms parsed from the pattern, progressive application);
  additionally `DsTextLineFormatting` — live reformatting such as grouping separators, with the
  round-trip law `raw(display(value)) == value` and keystroke normalisation.
- Android: `InputTextMask` → `InputTransformation`; the default mask is a no-op; an out-of-range
  caret from a custom mask falls back to the end instead of crashing.
- Reference mask catalogue (LatAm document formats — passports, RUC/CUIT/NIT, CLABE, SWIFT/BIC,
  RUN, RFC/CUIL, postal and municipal codes) lives in the Figma Masks block (`3946:124`).

### Text handling

- Single line, always (`TextFieldLineLimits.SingleLine` / UITextField) — the multi-line form is
  a different component (Textarea).
- Android suffix **rides the text advance**: it is offset to the end of the current text, clamped
  to the field width minus its own width. iOS renders an inline suffix statically after the
  hugging input (`isSuffixRenderedInline`), or appends it inside the `UITextField` when not
  inline (with cursor clamping in `DsPortedTextField`). The web reference hugs the input to its
  content (`field-sizing: content`), so the suffix rides right after the text like the Figma
  TextContainer; a browser without `field-sizing` falls back to a filling input with the suffix
  at the line end — a recorded degradation.
- iOS corrects affix baselines when an affix wears a different font than the input (the price
  field's big amount): the row centres, so mismatched fonts get a computed baseline offset.
  Same-font affixes need no correction — the web reference relies on same-font centring.
- Secure entry (passwords) and keyboard/content-type/autocapitalization settings are iOS input
  settings (`isSecure`, `InputSettings`); Android configures the equivalents on the wrapper.

---

## 6. Color tokens

Five colour slots, all re-pointable per instance ("any color style from the design system can be
applied" — the Figma Styles block). The custom properties are the whole web colour contract:

| Slot | Custom property | Default |
|---|---|---|
| Filled text | `--base-text-line-text` | `var(--text-and-icon-primary)` |
| Placeholder | `--base-text-line-placeholder` | `var(--text-and-icon-secondary)` |
| Prefix | `--base-text-line-prefix` | `var(--text-and-icon-primary)` |
| Suffix | `--base-text-line-suffix` | `var(--text-and-icon-primary)` |
| Cursor | `--base-text-line-cursor` | `var(--text-and-icon-primary)` |

- **Disabled** re-points text, placeholder and affixes to `var(--text-and-icon-disabled)` (both
  platforms tint everything, including affix icons).
- **Selection**: Android paints the selection highlight with its **legacy** `backgroundTertiary`
  and the handles with the cursor colour; no DS3 semantic token names the selection surface on
  any platform — flagged to the owner. The web reference leaves `::selection` at the browser
  default until a token exists.

---

## 7. Typography

Main Body everywhere, by default — one style for input, placeholder, prefix and suffix
(`DsFont.mainBody16` / `DsTheme.typography.mainBody`):

| Element | Style | Tokens |
|---|---|---|
| Input / placeholder / prefix / suffix | Body/Main Body | `var(--text-body-main-body-*)` — set together |

An assembled field may hand an affix its own style (Android `Affix.Text(style:)`, iOS
`Fonts`/`customText` — the price field's big amount); the baseline correction in § 5 exists for
exactly that case. The base contract stays Main Body.

---

## 8. Spacing

| Property | Token | Value @100% | Notes |
|---|---|---|---|
| Width | — | fills the host | The row fills (`Flexible` iOS / `FillMax` Android / `width: 100%` web); inside it the input hugs its content (`field-sizing: content`) and shrinks at overflow |
| Height | — | hugs content | 20 (Main Body line) · 24 with an icon affix |
| Horizontal padding | `var(--sp-s0)` | 0 | The host field owns the field paddings |
| Vertical padding | `var(--sp-s0)` | 0 | Same |
| Item spacing | `var(--sp-s4)` | 4 | Container gap and the affix↔input padding (Android `BaseLineTextSize` = `4.su` per side; iOS `HStack(spacing: .s4)`) |
| Affix icon | `var(--sp-s24)` | 24 | Full ×150 scaling (`SP/150% ratio` binding). **iOS deviation: draws affix icons at `s20`** — flagged |
| Cursor | — | 1 × 20 | One device pixel wide, the text line tall; colour from the `cursor` slot |

The affix spacing is an SP unit on both platforms (`4.su`, `.s4`), so it scales with the SP mode
like every other DS gap.

---

## 9. Usage context

### When to use BaseTextLine

- As the input core of an assembled field: TextField, PhoneField, PriceField, search fields,
  code inputs — anything that edits one line of text.
- In a prototype that needs a bare line of entry before the full TextField ships on the web.

### When not to use

- Standalone in product UI — it has no label, border, helper or error affordance; users get an
  unnamed, unframed line. Assemble a field.
- For multi-line entry — that is Textarea's contract.
- For phone numbers and dates with their dedicated formats — the PhoneField line carries the
  separate mask implementation.

### Related components

- `[TextField] 3.0` (Figma line; web not shipped yet) — label, helper, border, error on top of
  this core; the Figma Masks block stages BaseTextLine masks inside TextField.
- Textarea (planned) — the multi-line sibling.
- IconContainer — the affix icon carrier in the Figma drawings.

---

## 10. Accessibility

Brief summary. Full spec: [`base-text-line-a11y.md`](https://super-dollop-pzmo65r.pages.github.io/base-text-line.md).

- One focus area: the input itself. Android role `EditText`, iOS trait `Textfield`, web
  `<input type="text">`.
- What is announced follows the state: placeholder while empty, the value when filled.
- Affixes are decorative for the screen reader — iOS hides every affix kind, Android hides
  **icon** affixes (`contentDescription = null`; a text affix has no explicit exclusion today —
  flagged), the web marks them `aria-hidden="true"`. Their meaning must live in the field's
  accessible name or hint.
- The accessible **name comes from the host** (the assembled field's label). A standalone
  BaseTextLine needs an explicit `aria-label`. ⚠ iOS today force-assigns
  `accessibilityLabel = placeholder`, making the placeholder the persistent VoiceOver name —
  contrary to this contract, flagged to the iOS owners.
- No `disabled` masking games: the platform-native disabled state is used, and the visible
  Active affordance is the host's duty (the web base suppresses the default outline — a
  standalone use must restore a focus indicator).

---

## 11. Analytics and coverage contract

Contract per [`prototype-analytics-and-coverage.md`](https://github.com/inDriver/oymyakon-ds/blob/main/docs/prototype-analytics-and-coverage.md).

| Field | Value |
|---|---|
| `data-ds-component` | `base-text-line` |
| Coverage unit | No when inside an assembled field (the field is the unit); yes when standalone in a prototype |
| Tap target model | The input element — focusing it is the interaction |
| Actions | none — the assembled field owns actions |
| Internal targets | none |
| Emits value | **Never the entered text.** Field contents are user data; emit at most empty/filled flags |

Attribute model: `data-ds-variant` = the affix shape (`plain` \| `prefix` \| `suffix` \|
`prefix-suffix`), `data-ds-state` = `standard` \| `active` \| `disabled` (per spec-conventions;
Figma's `Rest` maps to `standard`).

```html
<label class="base-text-line" data-ds-component="base-text-line"
       data-ds-component-id="trip-price" data-ds-variant="prefix-suffix"
       data-ds-state="standard" aria-label="Price">
  <span class="base-text-line__prefix" aria-hidden="true">$</span>
  <input class="base-text-line__input" type="text" inputmode="decimal" placeholder="0">
  <span class="base-text-line__suffix" aria-hidden="true">USD</span>
</label>
```

---

## Changelog

| Version | Date | Change |
|---|---|---|
| 3.0.0 | 2026-09-22 | Initial spec, ingested from the Figma `[BaseTextLine] 3.0` Specification frame (`3110:4175` — a spec-only page, no master component) and verified against both shipped implementations (iOS `DsBaseTextLine` 3.0, Android `DsBaseTextLine` 3.0). Contract: prefix / input+placeholder+cursor / suffix row at `s4` gap and `s0` paddings, Main Body throughout, five re-pointable colour slots, X/9 input masks (phone/date excluded — separate implementation), states Standard/Active × empty/filled + platform Disabled, affixes never truncate, RTL mirrors except masks and forced-LTR amounts. Web reference shipped in `shared.css` + preview page `src/base-text-line.njk` (added on the owner's call the same day — the component stays developer-facing, the page documents the contract). Recorded platform gaps: iOS affix icon `s20` vs the spec's `s24`; iOS force-assigns `accessibilityLabel = placeholder` (the placeholder becomes the persistent VoiceOver name); Android text affixes carry no explicit screen-reader exclusion; no alignment knob on either mobile platform; Android selection colour still on a legacy token; `color-rules.md` §14.2's large-text-only scoping of Secondary-on-Background/Primary is stale against measured ≈ 5.25:1 Day. |

---

# BaseTextLine — Accessibility

One editable line, one focus area: the input itself. How the component is announced follows its
state — the placeholder while empty, the value when filled. BaseTextLine has **no accessible name
of its own**: the name comes from the assembled field's label, and a standalone instance must be
given one explicitly. Affixes are visual context, never separate accessibility elements.

Label rule: name the field's purpose, never the decoration — ✅ "Price", ❌ "Input with a dollar
sign".

## Android · TalkBack

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Input | From the host field (or an explicit `contentDescription` when standalone) | The entered text; the placeholder while empty | `EditText` (edit box) | The keyboard/format guidance the host provides |
| Prefix / suffix **icon** | — | — | Not a separate element — `contentDescription = null` | — |
| Prefix / suffix **text** | — | — | ⚠ The shipped base renders a plain `DsText` with **no explicit exclusion** — a text affix may surface as static text on Android; the exclusion (or the affix's meaning joining the field's name) is the wrapper's duty today. Flagged to the Android owners | — |
| Cursor / selection | — | — | Native editing behaviour; selection handles take the cursor colour | — |

- TalkBack announces the edit box with its current value, the "edit box" role, and the
  double-tap-to-edit hint; while empty it reads the placeholder as the value stand-in.
- Affix meaning (currency, units) that matters to the user belongs in the field's label or hint
  text — a suffix `kg` invisible to TalkBack must not be the only carrier of "kilograms".
- The mask formats as the user types; TalkBack reads the formatted value. Literal characters
  arriving on their own is expected behaviour, not an announcement event.

### Edge states

- **Disabled (`enabled = false`):** the native disabled semantics — the platform reports the
  state; all content (text, placeholder, affix icons) tints `TextAndIcon/Disabled`. No custom
  masking of the state.
- **Empty vs filled:** no role change — only the announced value differs.
- **Active:** focus lands on the input; Android places the cursor at the end of the text.
- **RTL:** the row mirrors, masks keep LTR direction, forced-LTR amounts stay LTR — none of it
  changes what is announced.
- **Reduced motion:** nothing to reduce — the only motion is the native caret blink.

---

## iOS · VoiceOver

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Input | From the host field (or an explicit `accessibilityLabel` when standalone). ⚠ The shipped `DsPortedTextField` force-assigns `accessibilityLabel = placeholder`, so today the placeholder **is** the VoiceOver name and persists once the field is filled ("[placeholder], [value], text field") — contrary to this contract; flagged to the iOS owners | The entered text | `Textfield` (+ secure text field when `isSecure`) | "Double tap to edit" (system) |
| Prefix | — | — | `accessibilityHidden(true)` — the component hides it | — |
| Suffix | — | — | `accessibilityHidden(true)` — the component hides it | — |
| Cursor | — | — | Native editing behaviour | — |

- `DsBaseTextLine` hides both affixes from VoiceOver explicitly — icon, text and custom text
  alike (`accessibilityHidden(true)`); the affix's meaning joins the field's label or hint.
  Android matches this only for **icon** affixes today (see the TalkBack table).
- `isSecure` entry gets the secure-text-field treatment: the value is not read back
  character-by-character to bystanders.
- Keyboard type / content type / autocapitalization (`InputSettings`) shape the input experience
  for everyone and double as format hints for autofill.

### Edge states

- **Dimmed / disabled:** the native disabled state; content tints to the disabled colour, the
  trait reports it. No `accessibilityValue` games.
- **Active:** the focus and the caret follow the system text-editing behaviour, including rotor
  text navigation.
- **RTL / forced LTR / masks:** visual direction only — announcements unchanged.

---

## Web preview (reference)

The web reference in `shared.css` marks the line up so the same contract holds:

- **The input is the one focusable, named element**:

  ```html
  <label class="base-text-line" data-ds-component="base-text-line" aria-label="Price">
    <span class="base-text-line__prefix" aria-hidden="true">$</span>
    <input class="base-text-line__input" type="text" inputmode="decimal" placeholder="0">
    <span class="base-text-line__suffix" aria-hidden="true">USD</span>
  </label>
  ```

  Inside an assembled field the `aria-label` gives way to the field's real `<label>` /
  `aria-labelledby`; the `<label>` wrapper here makes the affixes click-through to the input.
- **Affixes are `aria-hidden="true"`**, matching both platforms. Meaning carried by an affix
  (currency, units) is repeated in the accessible name or description — `aria-label="Price in
  US dollars"`, not a bare "Price" next to a hidden "USD".
- **Placeholder is not a name.** `placeholder` guides format; the name lives in the label. A
  placeholder-only field is unnamed and fails the contract.
- **Focus visibility is the host's duty — and it must exist.** The base suppresses the input's
  default outline because the assembled field draws the Active state (border, underline, label
  move). Any standalone use restores an indicator, e.g. a `:focus-visible` outline of
  `var(--sp-s2)` in `var(--text-and-icon-primary)` on the wrapper. Never ship a bare line with
  no visible focus state.
- **Touch target is the host's duty too.** The bare line hugs 20–24 of height at `var(--sp-s0)`
  padding — far below the `var(--sp-s44)` interactive floor. The assembled field's paddings
  provide the target; the same standalone/prototype use that restores the focus ring also pads
  the tappable area to the floor.
- **Disabled** uses the real `disabled` attribute plus `.base-text-line--disabled` for the
  colour re-point — never `aria-disabled` on a live input, and never a readonly masquerade.
- **Masks filter input, they don't trap it.** Literal characters self-insert; the caret never
  gets stuck; an invalid keystroke is dropped, not error-announced by the base (validation and
  its announcements are the assembled field's job).
- **Keyboard**: standard text-editing keys only; the component adds no shortcuts and no
  `tabindex` beyond the input's natural one.
- **Reduced motion:** nothing moves but the native caret; no token, no override.

### Colour and contrast

| Pairing | Tokens | Requirement |
|---|---|---|
| Filled text on the host's surface | `--text-and-icon-primary` | ≥ 4.5:1 (text) — holds on `--background-primary` (≈ 15.7:1 Day) |
| Placeholder on the host's surface | `--text-and-icon-secondary` | ≥ 4.5:1 — placeholder is instructional text, not decoration. Measured: ≈ 5.25:1 Day / ≈ 7.9:1 Night on `--background-primary`, ≈ 4.6:1 Day on `--surface-on-white` — passes, with little margin on OnWhite; verify on the host's fill. (`color-rules.md` §14.2 still scopes this pair to large text only — stale against the measured values, flagged to the owner) |
| Prefix / suffix text | `--text-and-icon-primary` | ≥ 4.5:1 |
| Affix icons | `--text-and-icon-primary` | ≥ 3:1 (non-text UI) |
| Cursor | `--text-and-icon-primary` | Perceivable against the host fill; follows the text pairing |
| Disabled content | `--text-and-icon-disabled` | None — disabled and reported as such |

BaseTextLine draws no background of its own, so every pairing above is against the **host
field's** fill — the assembled field verifies its combination. An instance re-pointing the
`--base-text-line-*` slots carries the same duty for its new pairing.

---

## Machine contract — `specs/components/base-text-line/capabilities.json`

Axes, allowed values, defaults, constraints and CSS/Figma bindings. Read this instead of guessing what the component can do.

```json
{
  "$schema": "../component-capabilities.schema.json",
  "id": "base-text-line",
  "name": "BaseTextLine",
  "version": "3.0.0",
  "description": "The core single-line text input the text-field family is assembled on: optional prefix, the input (placeholder + caret), optional suffix in a row at s4 gap, Main Body throughout, five re-pointable colour slots. Developer-only — the Figma page ships a Specification frame and no master component.",
  "files": {
    "spec": "specs/components/base-text-line/base-text-line.md",
    "a11y": "specs/components/base-text-line/base-text-line-a11y.md",
    "css": "src/shared/shared.css",
    "preview": "src/base-text-line.njk"
  },
  "figma": {
    "library": "🕹️ Oymyakon 3.32.3 (components)",
    "fileKey": "7vdl5YkZFDWvh9QvSmydsH",
    "componentSets": {},
    "capturedAt": "2026-09-22"
  },
  "root": {
    "class": "base-text-line",
    "dataDsComponent": "base-text-line"
  },
  "anatomy": {
    "prefix": {
      "class": "base-text-line__prefix",
      "optional": true,
      "notes": "Text or an s24 icon before the input; never wraps or shrinks (flex: 0 0 auto + nowrap) — the input yields. aria-hidden; its meaning joins the field's accessible name. iOS draws affix icons at s20 — recorded deviation."
    },
    "input": {
      "class": "base-text-line__input",
      "notes": "The one editable, focusable element — a single-line <input> hugging its content (field-sizing: content; non-Chromium falls back to a filling input) and shrinking at overflow (min-width s0). Carries the placeholder and the caret (caret-color from the cursor slot). Default outline suppressed: the assembled field draws the Active state."
    },
    "suffix": {
      "class": "base-text-line__suffix",
      "optional": true,
      "notes": "Units / currency / context after the input; same never-truncate rule. On Android it rides the text advance (clamped offset); the web matches via the hugged input — the suffix sits right after the text."
    }
  },
  "axes": {
    "prefix": {
      "title": "Prefix",
      "type": "enum",
      "values": [
        "none",
        "icon",
        "text"
      ],
      "default": "none",
      "css": {
        "mechanism": "presence and content of .base-text-line__prefix; an icon is an inline s24 svg"
      },
      "figma": {
        "kind": "none",
        "notes": "No master component — the Specification frame stages the forms (node 3110:4175)."
      }
    },
    "suffix": {
      "title": "Suffix",
      "type": "enum",
      "values": [
        "none",
        "text",
        "icon"
      ],
      "default": "none",
      "css": {
        "mechanism": "presence and content of .base-text-line__suffix"
      },
      "figma": {
        "kind": "none"
      },
      "notes": "iOS ships text/attributed suffixes only; the icon value is Android (Affix.Icon) and web."
    },
    "alignment": {
      "title": "Text alignment",
      "type": "enum",
      "values": [
        "left",
        "center",
        "right"
      ],
      "default": "left",
      "css": {
        "mechanism": "left is the bare root; .base-text-line--center / .base-text-line--right shift the hugged content group via justify-content (plus text-align for fallback browsers)"
      },
      "figma": {
        "kind": "none",
        "notes": "Declared in the Specification frame's Description block."
      },
      "notes": "Neither mobile platform exposes an alignment knob today — a recorded gap; the web modifier exists."
    },
    "mask": {
      "title": "Input mask",
      "type": "boolean",
      "default": false,
      "css": {
        "mechanism": "Runtime input filtering, not CSS: X = any letter/digit, 9 = digit, anything else a self-inserting literal. Phone/date use a separate implementation (X/9 do not apply)."
      },
      "figma": {
        "kind": "none",
        "notes": "Mask catalogue and behaviour staged in the Masks block (node 3946:124)."
      }
    },
    "state": {
      "title": "State",
      "type": "enum",
      "values": [
        "standard",
        "active",
        "disabled"
      ],
      "default": "standard",
      "css": {
        "mechanism": "standard = the bare root; active = the input's :focus (the visible affordance is the host field's duty); disabled = .base-text-line--disabled + the input's disabled attribute"
      },
      "figma": {
        "kind": "none",
        "notes": "Figma stages Rest/Active × empty/filled; Rest maps to standard per spec-conventions. Disabled is a platform state, absent from the Figma set."
      }
    },
    "forcedLtr": {
      "title": "Forced LTR",
      "type": "boolean",
      "default": false,
      "css": {
        "modifier": ".base-text-line--ltr"
      },
      "figma": {
        "kind": "none"
      },
      "notes": "Amounts read left-to-right in every locale — only the surrounding chrome mirrors (iOS isInputForcedLeftToRight)."
    },
    "colorSlots": {
      "title": "Colour slots",
      "type": "token",
      "customizable": true,
      "tokens": [
        "--text-and-icon-primary",
        "--text-and-icon-secondary",
        "--text-and-icon-disabled"
      ],
      "css": {
        "mechanism": "Five custom properties re-pointed per instance: --base-text-line-text / -placeholder / -prefix / -suffix / -cursor; defaults Primary ×4 + Secondary placeholder"
      },
      "figma": {
        "kind": "none",
        "notes": "The Styles block: any DS colour style can be applied; the listed defaults are the contract."
      }
    }
  },
  "analytics": {
    "dataDsComponent": "base-text-line",
    "action": "none",
    "valueAttr": "data-ds-state",
    "notes": "Coverage unit only when standalone in a prototype (inside an assembled field the field is the unit). The input is the tap target; no internal targets, no actions of its own. Never emit the entered text — at most empty/filled flags; data-ds-state carries standard | active | disabled."
  },
  "rtl": {
    "supported": true,
    "notes": "dir=\"rtl\" on the host mirrors the row (prefix stays logically first) and flips the default alignment. Input masks keep LTR direction; amounts pin LTR via .base-text-line--ltr (iOS isInputForcedLeftToRight)."
  },
  "constraints": [
    "Single line only — the multi-line form is a different component (Textarea).",
    "Affixes never wrap, shrink or truncate; when the row runs out of space the input gives way.",
    "No label, border, helper or error affordance of its own — assembled fields own those; standalone use must provide an accessible name and a visible focus indicator.",
    "Container carries s0 paddings and the s4 item gap; the host field owns the field paddings.",
    "Input masks keep LTR direction under RTL; phone numbers and dates use a separate mask implementation.",
    "Never emit the entered text through analytics — at most empty/filled flags."
  ]
}
```
