Files
sharper.ly/apps/web/app/components/ui/Input/Input.css
T

142 lines
5.3 KiB
CSS
Raw Normal View History

2026-09-11 02:46:56 +02:00
/* ============================================================================
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;
}
}