/* ============================================================================ INPUT / TEXTAREA - figma frame 18:212 (Type x State) ---------------------------------------------------------------------------- Only TIER-2 semantic tokens. The label, hint and error colours are driven off two data attributes on the group (`data-invalid`, `data-disabled`) plus `:focus-within`, so the whole 2x5 matrix is a handful of rules. Layout (the column and its gaps) is plain Tailwind spacing utilities in Input.tsx, and they name a step of the spacing scale (`gap-xs` = 8, `ms-2xs` = 4) rather than the raw `gap-1.5` / `ms-0.5` multiples of --spacing, which are tier-1. Everything token-driven lives here. BOX. As with the button, Figma strokes inside the frame, so the 1.5px border is subtracted from the 16/12 padding and the outer edge stays where it was drawn. DEVIATIONS FROM FIGMA, deliberate: * Height. Figma hand-sets 44px, which does not agree with its own 12px padding and 24px Body/Medium line-height. Following the tokens gives 48px, which is also exactly the Medium button, so a field and a button sitting on one row line up. Textarea keeps Figma's 120px, reached honestly as rows={4}: 4*24 + 10.5*2 + 1.5*2. * Focus. Figma paints a 2px grey (`stroke/hover`) border, which both moves the box by half a pixel and is not a WCAG-visible focus indicator. The border instead turns `--field-border-focus` at an unchanged 1.5px and the semantic focus ring is drawn outside it. The green label on focus is kept. * Disabled. Figma dims the whole group to 50%. The `--field-*-disabled` tokens already encode that reading and stay opaque, so no group opacity is applied and text on a translucent dark surface cannot smear. * The error caption is Figma's "Caption" role (Regular 12), which the type scale does not publish. It uses `body-sm` (14/20), the smallest published body role - more legible for a message a user has to act on. ========================================================================= */ @layer theme, base, components, utilities; @layer components { /* --------------------------------------------------------------------- Label, hint, error ------------------------------------------------------------------ */ .sh-field__label { color: var(--text-primary); transition: color var(--motion-duration-fast) var(--motion-ease-standard); } .sh-field:focus-within .sh-field__label { color: var(--text-accent); } .sh-field[data-invalid="true"] .sh-field__label, .sh-field[data-invalid="true"]:focus-within .sh-field__label { color: var(--text-danger); } .sh-field[data-disabled="true"] .sh-field__label { color: var(--text-disabled); } .sh-field__required { color: var(--text-danger); } .sh-field__hint { color: var(--text-secondary); } .sh-field[data-disabled="true"] .sh-field__hint { color: var(--text-disabled); } .sh-field__error { color: var(--text-danger); } /* --------------------------------------------------------------------- The control itself ------------------------------------------------------------------ */ .sh-field__control { display: block; inline-size: 100%; appearance: none; background-color: var(--field-bg); color: var(--field-fg); border: var(--control-border-width) solid var(--field-border); border-radius: var(--radius-control-lg); padding-inline: calc(var(--control-padding-inline-md) - var(--control-border-width)); padding-block: calc(var(--control-padding-block-md) - var(--control-border-width)); text-align: start; transition: background-color var(--motion-duration-fast) var(--motion-ease-standard), border-color var(--motion-duration-fast) var(--motion-ease-standard), color var(--motion-duration-fast) var(--motion-ease-standard); } .sh-field__control::placeholder { color: var(--field-placeholder); opacity: 1; } @media (hover: hover) { .sh-field__control:hover:not(:disabled) { border-color: var(--field-border-hover); } } /* Matched to the hover rule's specificity and placed after it, so focusing a field the pointer happens to be over still shows the focus border. */ .sh-field .sh-field__control:focus-visible { border-color: var(--field-border-focus); outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: var(--focus-ring-offset); } .sh-field[data-invalid="true"] .sh-field__control { border-color: var(--field-border-error); } .sh-field[data-invalid="true"] .sh-field__control:focus-visible { border-color: var(--field-border-error); outline-color: var(--field-border-error); } /* Last, and specific enough to win over the invalid border - a disabled field reads as disabled even when it is still holding an error. */ .sh-field .sh-field__control:disabled { background-color: var(--field-bg-disabled); color: var(--field-fg-disabled); border-color: var(--border-subtle); cursor: not-allowed; } .sh-field .sh-field__control:disabled::placeholder { color: var(--field-fg-disabled); } /* Only the block axis resizes - growing a textarea sideways would break the column it sits in. */ .sh-field__control--textarea { resize: vertical; } }