initial commit
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
/* ============================================================================
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user