{
  "name": "ChatInput",
  "type": "pattern",
  "description": "Single-line chat composer: labelled text field plus send button, with a three-way border state for resting, focused-or-filled, and disabled, in two shapes: Figma's fully-round phone pill with a chipless tint-only send glyph (the default), and cr-web-demo's 12px panel bar with a filled send chip",
  "export": "@gtmi/strix-react/atoms/chat-input",
  "path": "@gtmi/strix-react/atoms/chat-input",
  "figma": {
    "file": "zT3vGm4QlFEI2Jp9nxyFKI",
    "fileName": "Demo components library",
    "nodes": [
      {
        "node": "22:5288",
        "name": ".cellphone-input-text",
        "section": "Atoms"
      }
    ]
  },
  "props": [
    {
      "name": "onSend",
      "type": "(message: string) => void",
      "description": "Called with the trimmed message. Never called with an empty string"
    },
    {
      "name": "label",
      "type": "string",
      "default": "Message",
      "description": "Accessible name for the field, exposed as aria-label"
    },
    {
      "name": "sendLabel",
      "type": "string",
      "default": "Send message",
      "description": "Accessible name for the icon-only send button"
    },
    {
      "name": "placeholder",
      "type": "string",
      "default": "Type here",
      "description": "Field placeholder"
    },
    {
      "name": "shape",
      "type": "'panel' | 'pill'",
      "default": "pill",
      "description": "pill is Figma's phone composer Input (1147:19104) — radius 50, a 12/8 asymmetric inset, and a chipless tint-only 16px send glyph. panel is cr-web-demo's 12px nested panel with a filled 32px send chip and has no Figma counterpart, retained as an opt-in. Behaviour is identical. pill is the default because Figma is the source of truth, including for which member is the default"
    },
    {
      "name": "disabled",
      "type": "boolean",
      "default": "false",
      "description": "Blocks typing and submission, and fades the whole bar"
    },
    {
      "name": "defaultValue",
      "type": "string",
      "description": "Initial message. The value is owned internally, like the source"
    },
    {
      "name": "className",
      "type": "string",
      "description": "Applied to the input"
    },
    {
      "name": "containerClassName",
      "type": "string",
      "description": "Applied to the form root"
    }
  ],
  "ai_tags": ["chat", "composer", "message-input", "send", "pattern"],
  "since": "0.2.0",
  "source": "cr-web-demo packages/ui/src/atoms/chat-input.tsx",
  "tokens_used": [
    "--color-bg-button-primary",
    "--color-border-chat-input",
    "--color-border-focus",
    "--color-border-neutral-soft",
    "--color-icon-accent",
    "--color-icon-accent-hover",
    "--color-icon-primary",
    "--color-surface-base",
    "--color-surface-neutral-soft",
    "--color-text-button-primary",
    "--color-text-disabled-on-surface",
    "--color-text-primary",
    "--color-text-secondary",
    "--color-text-tertiary",
    "--radius-2x-large",
    "--radius-med-sm",
    "--radius-sm",
    "--spacing-gap-200",
    "--spacing-padding-horizontal-md",
    "--spacing-padding-horizontal-med-sm",
    "--spacing-padding-horizontal-sm",
    "--spacing-padding-vertical-sm"
  ],
  "notes": "Ported from cr-web-demo, the primary visual authority. Compose-vs-build: the wrapper is built, the field's type treatment and the send button are composed. Input's addonContainer already has a focus-within and a has-value boundary, but three things ruled it out: (1) `:placeholder-shown` is not `value.trim()`, so an Input-driven wrapper would light its accent border for whitespace while the submit gate stayed closed — the border must be driven by the same trimmed boolean; (2) addonContainer is rounded-sm (8px, a control) where this bar is rounded-med-sm (12px, the middle step of cr-web-demo's 16/12/8 radius hierarchy) and there is no seam to change that at the right layer; (3) addonContainer draws with `outline` plus mx-0.5 and carries an error/success/warning axis a composer has no use for, and its disabled treatment needs `!important` overrides. What is composed: inputStyles({ variant: 'unstyled' }) for the field, so the type ramp, placeholder ink and disabled ink cannot drift from Input; Button for send, for its focus-visible ring, :disabled handling and render seam; SendIcon instead of the source's inline <svg>. Deviations from the source: (a) the root is a <form> and send is type=\"submit\", replacing the source's Enter keydown handler — a virtual keyboard's go key, requestSubmit(), and any other submit control all did nothing before, and there was no onSubmit seam; the cost is that this must not be nested inside another <form>, which HTML forbids; (b) both accessible names are new, and every string is a prop with an English default; (c) motion-reduce:transition-none on both 150ms colour transitions, per WCAG 2.3.3; (d) source py-2.5 (10px) became padding-vertical-sm (8px), the nearest token; (e) Button's disabled:opacity-50 is overridden to opacity-100 because the bar already fades to 40% and the two compound to 20%. Kept from the source: send is `disabled` only when the bar is, not when the field is empty — a control that leaves the tab order as you type is worse than one that is briefly inert, and pressing it empty is a no-op. Measured token decisions, all asserted in both themes from the token CSS: bar surface is --color-surface-base rather than --color-surface-secondary. The original reason was that --color-border-control measured 3.73:1 dark on base but only 2.83:1 on secondary, missing WCAG 1.4.11's 3:1; that constraint is gone, because the 2.83:1 was itself the defect and the dark token is now secondary-500 (4.20:1 on secondary, 5.55:1 on base). The choice is retained on its own merits -- a composer reads as a surface you type into rather than a field within one -- and is no longer forced; the resting border matches Input's --color-border-control (4.78:1 light / 3.73:1 dark) and the active border is --color-border-focus (6.98 / 6.11), replacing the source's #1866ee; the disabled border is --color-border-neutral-soft at 1.17:1, exempt under 1.4.11 and excluded by name in the test; the source's #3acefa (--color-highlight) is used nowhere, as it measures 1.84:1 on the light surface; the ready send button is --color-bg-button-primary + --color-text-button-primary (5.03:1 both themes) and the muted one is --color-surface-neutral-soft + --color-text-secondary (7.17 / 7.40), replacing #1e3a5f/60 + #9aa0b4 which would have been 2.07:1 dark on the nearest medium fill. Contrast assertions derive their tokens from the component's own cva output rather than naming them; the assertion was mutation-tested by swapping the send ink for text-highlight, which failed. The `shape` axis (GTM-990 Track E, E3) adds Figma's phone composer, `Input` (1147:19104) in the Multi product demo file, alongside cr-web-demo's bar rather than as a second component, because the two disagree on exactly two things and agree on behaviour, gap and vertical padding. `pill` is the default because Figma is the source of truth and that includes which member is the default; `panel` has no Figma counterpart at all and is retained as an opt-in for the desktop surfaces already on it. `pill` is `radius: 50` with `pl: 12` / `pr: 8` — asymmetric in Dev Mode where the REST extraction had flattened it to `px: 12`. Its `Send button` (1147:19632) is a bare 16px glyph with no chip, fill or padding. It renders `Button variant=\"ghost\"`, Figma's own borderless type; `bg-transparent` is still stated at all three steps so the states stay tint-only. Two tints are re-points rather than substitutions, which is why they are used: Figma's `Default` white is `--color-icon-primary`, exactly #ffffff in dark (19.37:1 / 17.85:1 on the bar), and its `Active` blue-500 is `--color-icon-accent`, exactly #1866ee in light (5.03:1 / 9.78:1). Two entries in docs/FIGMA-EXCEPTIONS.md: the 16px glyph is below WCAG 2.5.8's 24px on a fine pointer and ships anyway, which is what makes the bar 32px, Figma's exact height — touch is a gap rather than a conflict, so Button's 44px `pointer-coarse` floor still applies and audit-touch needs no exemption; and the `Hover` tint blue-200 #3acefa (`--color-highlight`, 1.84:1 on white) is a light-theme gap the dark-only file never drew, derived to `--color-icon-accent-hover` (6.98:1 / 6.11:1), with a test asserting no pill state names `highlight` at all. `pl-padding-horizontal-med-sm` is the first single-side spacing utility in the package, so build.storybook.test.ts asserts it compiles to a real `padding-left:var(...)` — that a namespace generates `px-` is not proof it generates `pl-`, and a silent miss would flatten the inset to zero while the class still sat on the element.\n\nTHE RESTING BORDER IS FIGMA'S: color/brand/white via --color-border-chat-input, 19.37:1 on the color/brand/ink fill, from .cellphone-input-text (22:5288) Default. It was --color-border-control (6.02:1) under a comment reading \"Input's resting control border, matching it exactly\" -- and that cross-reference had expired twice over: the value was never Figma's, and the .input-text port moved Input off --color-border-control entirely. A comment asserting parity with another component is a claim with an expiry date.\n\nWHY NO CHECK CAUGHT IT. This file's contrast tests derive their tokens from cva output and never name one, which is right for proving the component's pairings are measured -- and cannot prove it draws the value FIGMA draws, because any conformant substitute passes. 6.02:1 and 19.37:1 both clear 1.4.11's 3:1. One named assertion now pins the resolved hex, with that reasoning recorded beside it.\n\nDev Mode confirmed the rest of the atom exactly: three states, blue-500 on Hover, the placeholder-to-caret layer swap on Active, and the send glyph flipping white -> #1866EE on Active (read from the two exported SVGs rather than the variable table).\n\nTHE SEND GLYPH WAS RINGED IN A BORDER FIGMA DOES NOT DRAW. 2ea1813 deleted the old `ghost` variant and mechanically rewrote this call site to `secondary` -- which is `.button-main`'s OUTLINED type -- so the bare glyph gained a 1px `--color-border-button-outline` ring, white in dark, on every state. Dev Mode on `.cellphone-input-text` (22:5288) shows `Send button` (22:2932) as a flex container holding a 16px glyph: no stroke, no fill, no padding. The border belongs to the pill bar, not to the button inside it. Now `variant=\"ghost\"`, which since 0.6.x ports Figma's borderless `Link` (1520:37925). Measured after: border-width 0px, 16x16, transparent fill, and 44x44 under `pointer-coarse`.\n\nWHY NO CHECK CAUGHT IT. Every assertion here reads `chatInputSendStyles`, the cva this file owns -- and the border came from the `variant` prop, which that cva never names. A test that derives its tokens from one style function cannot see what a sibling prop adds. The pill assertions now also pin the variant.\n\n`--color-icon-accent` IS #9ec1fd IN DARK, not #1866ee. The claim above that it \"*is* #1866ee\" was true of the LIGHT value, and rule 3 removed the light theme, so the ready glyph resolves to `primary-300` #9ec1fd (10.61:1 on the bar). It predates this fix and is recorded rather than changed: re-pointing it is a separate decision about which token carries Figma's blue-500 in a dark-only system."
}
