import React from 'react';
import clsx from 'clsx';
import { Link } from '@redocly/theme/components/Link/Link';
import { LoaderIcon } from '../Icons';
/**
* Button — built from the Figma-derived specification.
*
* Source: github.com/samiamdesigns/pd-xrpl-developer-docs
* components/button.md the axes and the reasoning
* components/button.json every value resolved per mode
* components/button-examples.md the acceptance checklist
*
* See Button.md for how this differs from the current Button, and for
* the two places the spec is deliberately not followed.
*/
export type ButtonEmphasis = 'strong' | 'standard' | 'subtle';
export type ButtonIntention = 'brand' | 'neutral';
export type ButtonContext = 'on-theme' | 'on-inverse' | 'on-saturated';
/**
* The styling axes, as a union rather than three independent enums.
*
* `neutral` + `on-saturated` is absent because it has no tokens —
* `mode-color.neutral.on-saturated` is not in the set, since nothing on a solid
* brand block is neutral-coloured. Expressing the axes this way makes that
* combination fail to compile instead of failing at runtime, so you do not have
* to open Figma to find out which combinations are real.
*
* Note: button.md prints these members with `intention` required, which would
* reject `` — the zero-prop default
* its own examples show. button.json records `"default": "brand"`, so the
* examples and the JSON agree and the printed union is the outlier. `intention`
* is optional here. Reported upstream.
*/
export type ButtonSurface =
// brand on an ordinary page (the default)
| { intention?: 'brand'; context?: 'on-theme' }
// brand on an inverted block, or on solid brand green
| { intention?: 'brand'; context: 'on-inverse' | 'on-saturated' }
// neutral — no on-saturated group exists
| { intention: 'neutral'; context?: 'on-theme' | 'on-inverse' };
/**
* Emphasis is orthogonal: all three exist in every group, so it is intersected
* across the union rather than repeated in each member. Keeping the pair
* separable also lets a wrapper forward the surface as one value — spreading
* two independently-typed variables would lose the correlation the union
* exists to enforce.
*/
type ButtonVariant = ButtonSurface & { emphasis?: ButtonEmphasis };
/** The component's own props — the same whichever element ends up rendered. */
type ButtonOwnProps = ButtonVariant & {
/** Visible label. Always required. */
children: React.ReactNode;
/** Decorative, aria-hidden. Not used in current XRPL designs. */
iconStart?: React.ReactNode;
/** Decorative, aria-hidden. Replaced by the loader while loading. */
iconEnd?: React.ReactNode;
/** Action in flight: aria-busy, activation suppressed, indicator shown. */
loading?: boolean;
/** Non-interactive but still focusable and still in the accessibility tree. */
inactive?: boolean;
/** Native disabled. Leaves the tab order. */
disabled?: boolean;
className?: string;
};
/**
* Everything not declared above is forwarded to the rendered element, so which
* element that is decides which attributes are legal. `href` is the switch, and
* making it the discriminant lets the compiler carry that decision: the button
* member pins `href` to `never`, so a `formAction` and an `href` on the same
* call cannot type-check, and the pass-through narrows to one attribute set on
* an `href` check with nothing filtered at runtime.
*/
type ButtonAsButton = ButtonOwnProps &
Omit, keyof ButtonOwnProps> & {
href?: never;
download?: never;
};
type ButtonAsLink = ButtonOwnProps &
Omit, keyof ButtonOwnProps | 'href' | 'target' | 'download'> & {
href: string;
target?: '_self' | '_blank';
download?: boolean | string;
};
export type ButtonProps = ButtonAsButton | ButtonAsLink;
/**
* intention and context select ONE token group, because that is how the token
* set groups them — `brand-onInverse` is a group, not brand plus a modifier.
* kebab-cased here; the camelCase in the source files is a find-and-replace aid
* there and is deliberately not normalised upstream.
*/
const groupClass = (intention: ButtonIntention, context: ButtonContext) =>
context === 'on-theme' ? intention : `${intention}-${context}`;
export const Button: React.FC = ({
intention = 'brand',
context = 'on-theme',
emphasis = 'strong',
children,
iconStart,
iconEnd,
loading = false,
inactive = false,
disabled = false,
className,
...rest
}) => {
const classNames = clsx(
'bds-btn',
`bds-btn--${groupClass(intention, context)}`,
`bds-btn--${emphasis}`,
// The context class carries the focus-ring colour and the disabled group,
// both of which are chosen by context alone.
`bds-btn--${context}`,
{
'bds-btn--loading': loading,
'bds-btn--disabled': disabled,
},
className
);
/**
* Three non-interactive states, three code paths. They resolve identical
* colours in several combinations, so appearance will not tell you which one
* you built — what distinguishes them is behaviour.
*
* element ARIA tab order activates
* loading