248 lines
10 KiB
CSS
248 lines
10 KiB
CSS
/* ============================================================================
|
|
BUTTON - figma frame 14:487 (Variant x Size x State)
|
|
----------------------------------------------------------------------------
|
|
Only TIER-2 semantic tokens are referenced here. The `--sh-btn-*` custom
|
|
properties are component-local plumbing: each variant block binds them to a
|
|
tier-2 token once, and the state rules below read them, so the whole 5x3x3
|
|
matrix is expressed as five small blocks instead of forty-five selectors.
|
|
|
|
Everything lives in @layer components so a Tailwind utility passed through
|
|
`className` (`w-full`, `mt-lg`, `text-text-secondary`, ...) still wins.
|
|
The layer statement is a defensive no-op when app.css has already declared
|
|
the same order, and makes this file correct on its own if it ever loads first.
|
|
|
|
HEIGHTS. Figma draws the stroke *inside* the frame, so a Secondary button is
|
|
the same height as a Primary one. Borders are therefore subtracted from the
|
|
padding, which lands every size on its designed box:
|
|
sm 6.5*2 + 20 + 1.5*2 = 36 md 10.5*2 + 24 + 1.5*2 = 48
|
|
lg 10.5*2 + 28 + 1.5*2 = 52
|
|
The 20/24/28 line-heights come from the type role the component puts on the
|
|
element (body-sm-bold / body-md-bold / body-lg-bold).
|
|
|
|
DEVIATIONS FROM FIGMA, deliberate:
|
|
* Focus-visible is not drawn in Figma at all. WCAG 2.4.7 requires it, so the
|
|
semantic focus ring is applied here; filled variants switch to the inverse
|
|
ring colour because a green ring on a green fill is invisible.
|
|
* "Ghost / Large / Default" is the one cell in the matrix with no padding
|
|
while its own Hover and Disabled cells have it - an authoring slip that
|
|
would make the button jump on hover. Padding is kept on all ghost states.
|
|
* Disabled uses the opaque `-bg-disabled` / `-fg-disabled` tokens rather than
|
|
Figma's 40% opacity, so the page never shows through a disabled control.
|
|
`--control-opacity-disabled` still holds the 0.4 if that is ever wanted.
|
|
========================================================================= */
|
|
|
|
@layer theme, base, components, utilities;
|
|
|
|
@layer components {
|
|
/* ---------------------------------------------------------------------
|
|
Base - shared by every variant and size.
|
|
A one-cell grid stacks the label and the loading spinner without either
|
|
transforms or absolute offsets, so it stays correct under dir="rtl".
|
|
------------------------------------------------------------------ */
|
|
.sh-btn {
|
|
display: inline-grid;
|
|
grid-template-areas: "sh-btn-stack";
|
|
place-items: center;
|
|
border-style: solid;
|
|
border-width: var(--control-border-width);
|
|
border-radius: var(--radius-control);
|
|
background-color: var(--sh-btn-bg);
|
|
color: var(--sh-btn-fg);
|
|
border-color: var(--sh-btn-border);
|
|
text-align: center;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
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-btn--block {
|
|
display: grid;
|
|
inline-size: 100%;
|
|
}
|
|
|
|
.sh-btn__content {
|
|
grid-area: sh-btn-stack;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--control-gap);
|
|
transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
|
|
}
|
|
|
|
/* The 24px box the icon is drawn inside in Figma. Kept on the slot rather
|
|
than on the icon so a consumer's own glyph inherits the same geometry. */
|
|
.sh-btn__icon {
|
|
display: inline-flex;
|
|
flex: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
inline-size: var(--control-icon-size);
|
|
block-size: var(--control-icon-size);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Sizes - Figma: sm 16/8 (space.md/space.xs), md + lg 24/12 (space.lg/sm)
|
|
------------------------------------------------------------------ */
|
|
.sh-btn[data-size="sm"] {
|
|
padding-inline: calc(var(--control-padding-inline-sm) - var(--control-border-width));
|
|
padding-block: calc(var(--control-padding-block-sm) - var(--control-border-width));
|
|
}
|
|
|
|
.sh-btn[data-size="md"] {
|
|
padding-inline: calc(var(--control-padding-inline-md) - var(--control-border-width));
|
|
padding-block: calc(var(--control-padding-block-md) - var(--control-border-width));
|
|
}
|
|
|
|
.sh-btn[data-size="lg"] {
|
|
padding-inline: calc(var(--control-padding-inline-lg) - var(--control-border-width));
|
|
padding-block: calc(var(--control-padding-block-lg) - var(--control-border-width));
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Variants - the only place a token name appears twice.
|
|
------------------------------------------------------------------ */
|
|
.sh-btn[data-variant="primary"] {
|
|
--sh-btn-bg: var(--interactive-primary-bg);
|
|
--sh-btn-bg-hover: var(--interactive-primary-bg-hover);
|
|
--sh-btn-bg-active: var(--interactive-primary-bg-active);
|
|
--sh-btn-bg-disabled: var(--interactive-primary-bg-disabled);
|
|
--sh-btn-fg: var(--interactive-primary-fg);
|
|
--sh-btn-fg-hover: var(--interactive-primary-fg-hover);
|
|
--sh-btn-fg-disabled: var(--interactive-primary-fg-disabled);
|
|
--sh-btn-border: var(--interactive-primary-border);
|
|
--sh-btn-border-hover: var(--interactive-primary-border-hover);
|
|
--sh-btn-border-disabled: var(--interactive-primary-border-disabled);
|
|
}
|
|
|
|
.sh-btn[data-variant="secondary"] {
|
|
--sh-btn-bg: var(--interactive-secondary-bg);
|
|
--sh-btn-bg-hover: var(--interactive-secondary-bg-hover);
|
|
--sh-btn-bg-active: var(--interactive-secondary-bg-active);
|
|
--sh-btn-bg-disabled: var(--interactive-secondary-bg-disabled);
|
|
--sh-btn-fg: var(--interactive-secondary-fg);
|
|
--sh-btn-fg-hover: var(--interactive-secondary-fg-hover);
|
|
--sh-btn-fg-disabled: var(--interactive-secondary-fg-disabled);
|
|
--sh-btn-border: var(--interactive-secondary-border);
|
|
--sh-btn-border-hover: var(--interactive-secondary-border-hover);
|
|
--sh-btn-border-disabled: var(--interactive-secondary-border-disabled);
|
|
}
|
|
|
|
.sh-btn[data-variant="dark"] {
|
|
--sh-btn-bg: var(--interactive-dark-bg);
|
|
--sh-btn-bg-hover: var(--interactive-dark-bg-hover);
|
|
--sh-btn-bg-active: var(--interactive-dark-bg-active);
|
|
--sh-btn-bg-disabled: var(--interactive-dark-bg-disabled);
|
|
--sh-btn-fg: var(--interactive-dark-fg);
|
|
--sh-btn-fg-hover: var(--interactive-dark-fg-hover);
|
|
--sh-btn-fg-disabled: var(--interactive-dark-fg-disabled);
|
|
--sh-btn-border: var(--interactive-dark-border);
|
|
--sh-btn-border-hover: var(--interactive-dark-border-hover);
|
|
--sh-btn-border-disabled: var(--interactive-dark-border-disabled);
|
|
}
|
|
|
|
.sh-btn[data-variant="danger"] {
|
|
--sh-btn-bg: var(--interactive-danger-bg);
|
|
--sh-btn-bg-hover: var(--interactive-danger-bg-hover);
|
|
--sh-btn-bg-active: var(--interactive-danger-bg-active);
|
|
--sh-btn-bg-disabled: var(--interactive-danger-bg-disabled);
|
|
--sh-btn-fg: var(--interactive-danger-fg);
|
|
--sh-btn-fg-hover: var(--interactive-danger-fg-hover);
|
|
--sh-btn-fg-disabled: var(--interactive-danger-fg-disabled);
|
|
--sh-btn-border: var(--interactive-danger-border);
|
|
--sh-btn-border-hover: var(--interactive-danger-border-hover);
|
|
--sh-btn-border-disabled: var(--interactive-danger-border-disabled);
|
|
}
|
|
|
|
.sh-btn[data-variant="ghost"] {
|
|
--sh-btn-bg: var(--interactive-ghost-bg);
|
|
--sh-btn-bg-hover: var(--interactive-ghost-bg-hover);
|
|
--sh-btn-bg-active: var(--interactive-ghost-bg-active);
|
|
--sh-btn-bg-disabled: var(--interactive-ghost-bg-disabled);
|
|
--sh-btn-fg: var(--interactive-ghost-fg);
|
|
--sh-btn-fg-hover: var(--interactive-ghost-fg-hover);
|
|
--sh-btn-fg-disabled: var(--interactive-ghost-fg-disabled);
|
|
--sh-btn-border: var(--interactive-ghost-border);
|
|
--sh-btn-border-hover: var(--interactive-ghost-border-hover);
|
|
--sh-btn-border-disabled: var(--interactive-ghost-border-disabled);
|
|
}
|
|
|
|
/* Figma gives the ghost md/lg cells radius.md while every other cell uses
|
|
radius.sm - reproduced rather than normalised. */
|
|
.sh-btn[data-variant="ghost"][data-size="md"],
|
|
.sh-btn[data-variant="ghost"][data-size="lg"] {
|
|
border-radius: var(--radius-control-lg);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
States
|
|
------------------------------------------------------------------ */
|
|
@media (hover: hover) {
|
|
.sh-btn:hover:not(:disabled, [aria-disabled="true"]) {
|
|
background-color: var(--sh-btn-bg-hover);
|
|
color: var(--sh-btn-fg-hover);
|
|
border-color: var(--sh-btn-border-hover);
|
|
}
|
|
}
|
|
|
|
/* No `-fg-active` / `-border-active` exists in the token contract, so the
|
|
pressed state deepens the fill only. */
|
|
.sh-btn:active:not(:disabled, [aria-disabled="true"]) {
|
|
background-color: var(--sh-btn-bg-active);
|
|
}
|
|
|
|
.sh-btn:focus-visible {
|
|
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
outline-offset: var(--focus-ring-offset);
|
|
}
|
|
|
|
/* A green ring on a green (or red, or near-black) fill reads as nothing. */
|
|
.sh-btn[data-variant="primary"]:focus-visible,
|
|
.sh-btn[data-variant="danger"]:focus-visible,
|
|
.sh-btn[data-variant="dark"]:focus-visible {
|
|
outline-color: var(--focus-ring-color-inverse);
|
|
}
|
|
|
|
.sh-btn:disabled,
|
|
.sh-btn[aria-disabled="true"] {
|
|
background-color: var(--sh-btn-bg-disabled);
|
|
color: var(--sh-btn-fg-disabled);
|
|
border-color: var(--sh-btn-border-disabled);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Loading - the label keeps its box (no reflow) and stays in the
|
|
accessibility tree, which `visibility: hidden` would not allow.
|
|
------------------------------------------------------------------ */
|
|
.sh-btn[data-loading="true"] {
|
|
cursor: progress;
|
|
}
|
|
|
|
.sh-btn[data-loading="true"] .sh-btn__content {
|
|
opacity: 0;
|
|
}
|
|
|
|
.sh-btn__spinner {
|
|
grid-area: sh-btn-stack;
|
|
inline-size: 1em;
|
|
block-size: 1em;
|
|
border: var(--control-border-width) solid currentColor;
|
|
border-block-start-color: transparent;
|
|
border-radius: var(--radius-pill);
|
|
/* The contract publishes fast/base/slow only; two slows is the ~640ms a
|
|
spinner wants without inventing a duration token. */
|
|
animation: sh-btn-spin calc(var(--motion-duration-slow) * 2) linear infinite;
|
|
}
|
|
}
|
|
|
|
@keyframes sh-btn-spin {
|
|
to {
|
|
rotate: 1turn;
|
|
}
|
|
}
|