Files
xrpl-dev-portal/shared/components/Button/Button.scss

513 lines
13 KiB
SCSS

// BDS Button Component Styles
// Brand Design System - Scalable button component
//
// Naming Convention: BEM with 'bds' namespace
// .bds-btn - Base button (shared layout, typography, icon animation)
// .bds-btn--primary - Primary variant (solid background)
// .bds-btn--secondary - Secondary variant (outline style)
// .bds-btn--green - Green color theme (default)
// .bds-btn--black - Black color theme
// .bds-btn--tertiary - Tertiary variant (text-only style)
// .bds-btn__label - Label element
// .bds-btn__icon - Icon element (inherits color via currentColor)
// .bds-btn--disabled - Disabled state modifier
// .bds-btn--no-icon - No icon modifier
//
// Colors live on CSS custom properties (--bds-btn-fg, --bds-btn-fill, …).
// Variants never name a color; the palette table assigns them once.
// Dark mode remaps green and black onto the same palette.
//
// Note: This file is imported within xrpl.scss after Bootstrap and project
// variables are loaded, so $grid-breakpoints, colors, and mixins are available.
// Geometry/variants/mods stay layered so padding mods win without !important.
// Palette + paint are unlayered: custom properties in a layer can lose to
// unlayered `a { color }` (invalid var → white Bootstrap link color + underline).
@layer bds-btn.geometry, bds-btn.variants, bds-btn.mods;
// =============================================================================
// Design tokens
// =============================================================================
$bds-btn-neutral-black: #141414;
$bds-btn-neutral-black-15: rgba(20, 20, 20, 0.15);
$bds-btn-black-pressed: #434343;
$bds-btn-primary-black-bg-hover: #434343;
$bds-btn-border-radius: 100px;
$bds-btn-focus-border-width: 2px;
$bds-btn-pad-y: 8px;
$bds-btn-pad-x: 20px;
$bds-btn-gap: 16px;
$bds-btn-hover-shift: 6px;
$bds-btn-pad-x-sm: 16px; // $bds-btn-pad-x - 4px
$bds-btn-hover-shift-sm: 5px;
$bds-btn-transition-duration: 150ms;
$bds-btn-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
// =============================================================================
// Palette maps — assign once; variants consume via custom properties
// =============================================================================
@mixin bds-btn-vars($map) {
$defaults: (
fill: transparent,
fill-hover: transparent,
fill-disabled: transparent,
stroke: transparent,
stroke-hover: transparent,
stroke-active: transparent,
stroke-disabled: transparent,
);
@each $name, $value in map-merge($defaults, $map) {
--bds-btn-#{$name}: #{$value};
}
}
$bds-btn-primary-green: (
fg: $bds-btn-neutral-black,
fg-hover: $bds-btn-neutral-black,
fg-active: $bds-btn-neutral-black,
fg-disabled: $gray-500,
fill: $green-300,
fill-hover: $green-200,
fill-disabled: $gray-200,
);
$bds-btn-primary-black: (
fg: $white,
fg-hover: $white,
fg-active: $white,
fg-disabled: $gray-500,
fill: $bds-btn-neutral-black,
fill-hover: $bds-btn-primary-black-bg-hover,
fill-disabled: $gray-200,
);
$bds-btn-primary-dark: map-merge(
$bds-btn-primary-green,
(
fg-disabled: $gray-300,
fill-disabled: $gray-500,
)
);
$bds-btn-secondary-green: (
fg: $green-400,
fg-hover: $green-500,
fg-active: $green-400,
fg-disabled: $gray-400,
fill-hover: $green-100,
stroke: $green-400,
stroke-hover: $green-500,
stroke-active: $green-400,
stroke-disabled: $gray-400,
);
$bds-btn-secondary-black: (
fg: $bds-btn-neutral-black,
fg-hover: $bds-btn-neutral-black,
fg-active: $bds-btn-black-pressed,
fg-disabled: $gray-500,
fill-hover: $bds-btn-neutral-black-15,
stroke: $bds-btn-neutral-black,
stroke-hover: $bds-btn-neutral-black,
stroke-active: $bds-btn-black-pressed,
stroke-disabled: $gray-400,
);
$bds-btn-secondary-dark: (
fg: $green-300,
fg-hover: $green-200,
fg-active: $green-300,
fg-disabled: $gray-400,
fill-hover: $green-500,
stroke: $green-300,
stroke-hover: $green-200,
stroke-active: $green-300,
stroke-disabled: $gray-400,
);
$bds-btn-tertiary-green: (
fg: $green-400,
fg-hover: $green-500,
fg-active: $green-400,
fg-disabled: $gray-400,
);
$bds-btn-tertiary-black: (
fg: $bds-btn-neutral-black,
fg-hover: $bds-btn-neutral-black,
fg-active: $bds-btn-black-pressed,
fg-disabled: $gray-500,
);
$bds-btn-tertiary-dark: (
fg: $green-300,
fg-hover: $green-200,
fg-active: $green-300,
fg-disabled: $gray-400,
);
// =============================================================================
// Geometry mixin
// pad-x / pad-y are the border-box intent; border width is subtracted.
// On hover, $shift moves from padding-inline-end into gap (chevron travel).
// Primary/secondary rest end-padding is 1px tighter than start (optical).
// Secondary focus drops the 2px border and adds that width back to padding.
// =============================================================================
@mixin bds-btn-box-at($pad-y, $pad-x, $border, $end-inset, $shift) {
$y: $pad-y - $border;
$start: $pad-x - $border;
$end: $start - $end-inset;
padding: $y $end $y $start;
gap: $bds-btn-gap;
&.bds-btn--no-icon {
padding: $y $start;
}
&:hover:not(:disabled):not(.bds-btn--disabled):not(.bds-btn--no-icon),
&:focus-visible:not(:disabled):not(.bds-btn--disabled):not(
.bds-btn--no-icon
) {
padding: $y ($end - $shift) $y $start;
gap: $bds-btn-gap + $shift;
}
@if $border > 0 {
&:focus-visible:not(:disabled):not(.bds-btn--disabled):not(
.bds-btn--no-icon
) {
border: none;
padding: $y ($end - $shift + $border) $y ($start + $border);
gap: $bds-btn-gap + $shift;
}
&:focus-visible:not(:disabled):not(.bds-btn--disabled).bds-btn--no-icon {
border: none;
padding: $y ($start + $border);
}
}
}
@mixin bds-btn-box($border: 0px, $end-inset: 1px) {
@include bds-btn-box-at(
$bds-btn-pad-y,
$bds-btn-pad-x,
$border,
$end-inset,
$bds-btn-hover-shift
);
@include media-breakpoint-down(xl) {
@include bds-btn-box-at(
$bds-btn-pad-y,
$bds-btn-pad-x-sm,
$border,
$end-inset,
$bds-btn-hover-shift-sm
);
}
}
// =============================================================================
// Theme: palette table (unlayered so vars always apply)
// Dark mode: green and black share one palette (black is remapped to green).
// force-color restores the light black palette and keeps the dark focus ring.
// =============================================================================
.bds-btn {
--bds-btn-focus: #{$bds-btn-neutral-black};
@include bds-btn-vars($bds-btn-primary-green);
}
.bds-btn--primary.bds-btn--green {
@include bds-btn-vars($bds-btn-primary-green);
}
.bds-btn--primary.bds-btn--black {
@include bds-btn-vars($bds-btn-primary-black);
}
.bds-btn--secondary.bds-btn--green {
@include bds-btn-vars($bds-btn-secondary-green);
}
.bds-btn--secondary.bds-btn--black {
@include bds-btn-vars($bds-btn-secondary-black);
}
.bds-btn--tertiary.bds-btn--green {
@include bds-btn-vars($bds-btn-tertiary-green);
}
.bds-btn--tertiary.bds-btn--black {
@include bds-btn-vars($bds-btn-tertiary-black);
}
@include bds-theme-mode(dark, $at-root: true) {
.bds-btn {
--bds-btn-focus: #{$white};
}
.bds-btn--primary {
@include bds-btn-vars($bds-btn-primary-dark);
}
.bds-btn--secondary {
@include bds-btn-vars($bds-btn-secondary-dark);
}
.bds-btn--tertiary {
@include bds-btn-vars($bds-btn-tertiary-dark);
}
.bds-btn--force-color.bds-btn--black {
&.bds-btn--primary {
@include bds-btn-vars($bds-btn-primary-black);
}
&.bds-btn--secondary {
@include bds-btn-vars($bds-btn-secondary-black);
}
&.bds-btn--tertiary {
@include bds-btn-vars($bds-btn-tertiary-black);
}
--bds-btn-focus: #{$white};
}
}
// =============================================================================
// Geometry: layout, type, icon motion, padding/gap, state variable swaps
// =============================================================================
@layer bds-btn.geometry {
.bds-btn {
display: inline-flex;
align-items: center;
justify-content: center;
max-height: 40px;
@include type(label-r);
white-space: nowrap;
margin-bottom: 0;
border: none;
border-radius: $bds-btn-border-radius;
cursor: pointer;
position: relative;
overflow: hidden;
z-index: 1;
transition:
color $bds-btn-transition-duration $bds-btn-transition-timing,
border-color $bds-btn-transition-duration $bds-btn-transition-timing,
padding $bds-btn-transition-duration $bds-btn-transition-timing,
gap $bds-btn-transition-duration $bds-btn-transition-timing,
transform $bds-btn-transition-duration $bds-btn-transition-timing;
&::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
transform: scaleY(0);
transform-origin: bottom center;
transition: transform $bds-btn-transition-duration
$bds-btn-transition-timing;
}
&:hover:not(:disabled):not(.bds-btn--disabled),
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
--bds-btn-fg: var(--bds-btn-fg-hover);
--bds-btn-stroke: var(--bds-btn-stroke-hover);
&::before {
transform: scaleY(1);
}
.bds-btn__icon-line {
transform: scaleX(0);
}
}
&:active:not(:disabled):not(.bds-btn--disabled) {
--bds-btn-fg: var(--bds-btn-fg-active);
--bds-btn-stroke: var(--bds-btn-stroke-active);
&::before {
transform: scaleY(0);
}
}
&:disabled,
&.bds-btn--disabled {
--bds-btn-fg: var(--bds-btn-fg-disabled);
--bds-btn-fill: var(--bds-btn-fill-disabled);
--bds-btn-stroke: var(--bds-btn-stroke-disabled);
cursor: not-allowed;
pointer-events: none;
&::before {
display: none;
}
}
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
outline: $bds-btn-focus-border-width solid var(--bds-btn-focus);
outline-offset: 2px;
}
}
.bds-btn__label {
flex-shrink: 0;
position: relative;
z-index: 1;
}
.bds-btn__icon {
width: 15px;
height: 14px;
flex-shrink: 0;
transition: opacity $bds-btn-transition-duration $bds-btn-transition-timing;
color: currentColor;
overflow: visible;
position: relative;
z-index: 1;
}
.bds-btn__icon-line {
transform-box: fill-box;
transform-origin: right center;
transform: scaleX(1);
transition: transform $bds-btn-transition-duration
$bds-btn-transition-timing;
}
.bds-btn__icon-chevron {
transition: transform $bds-btn-transition-duration
$bds-btn-transition-timing;
}
}
// =============================================================================
// Variants: structure only (no named colors)
// =============================================================================
@layer bds-btn.variants {
.bds-btn--primary {
@include bds-btn-box;
}
.bds-btn--secondary {
border: 2px solid var(--bds-btn-stroke);
@include bds-btn-box($border: 2px);
}
.bds-btn--tertiary {
@include type(body-r);
margin-bottom: 0;
text-decoration: none;
@include bds-btn-box($end-inset: 0px);
&::before {
display: none;
}
&:hover:not(:disabled):not(.bds-btn--disabled),
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
text-decoration: underline;
}
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
border-radius: 0;
outline-offset: -2px;
}
&:disabled,
&.bds-btn--disabled {
text-decoration: none;
}
}
}
// =============================================================================
// Mods: later layer wins over geometry without !important
// =============================================================================
@layer bds-btn.mods {
.bds-btn--no-padding {
padding: 0;
justify-content: flex-start;
&:hover,
&:focus-visible,
&:active {
padding: 0;
}
}
}
// =============================================================================
// Unlayered paint — beats Bootstrap / Redocly / html.{light,dark} a rules.
// Double-class + theme wrapper matches the pre-refactor specificity.
// Hover fill lives on ::before, not the face fill. Icons follow currentColor.
// =============================================================================
@mixin bds-btn-anchor-paint {
color: var(--bds-btn-fg);
background-color: var(--bds-btn-fill);
border-color: var(--bds-btn-stroke);
text-decoration: none;
&:hover,
&:focus,
&:focus-visible,
&:active,
&:visited,
&:link {
color: var(--bds-btn-fg);
background-color: var(--bds-btn-fill);
border-color: var(--bds-btn-stroke);
text-decoration: none;
}
&.bds-btn--tertiary:hover:not(:disabled):not(.bds-btn--disabled),
&.bds-btn--tertiary:focus:not(:disabled):not(.bds-btn--disabled),
&.bds-btn--tertiary:focus-visible:not(:disabled):not(.bds-btn--disabled) {
text-decoration: underline;
}
}
.bds-btn {
color: var(--bds-btn-fg);
background-color: var(--bds-btn-fill);
border-color: var(--bds-btn-stroke);
text-decoration: none;
&::before {
background-color: var(--bds-btn-fill-hover);
}
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: inherit;
stroke: currentColor;
}
}
a.bds-btn.bds-btn {
@include bds-theme-mode(light) {
@include bds-btn-anchor-paint;
}
@include bds-theme-mode(dark) {
@include bds-btn-anchor-paint;
}
}