Drop Cookies Settings from the footer, link the Home features that already have pages, and give leftover routes their own descriptions.
203 lines
13 KiB
TypeScript
203 lines
13 KiB
TypeScript
import { memo } from "react";
|
|
import type {
|
|
SizeValue,
|
|
ButtonTypeValue,
|
|
ButtonPaletteValue,
|
|
ButtonStateValue,
|
|
} from "../../../../lib/propNormalization";
|
|
|
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
children: React.ReactNode;
|
|
/**
|
|
* Button type (Figma prop).
|
|
* @default "filled"
|
|
*/
|
|
buttonType?: ButtonTypeValue;
|
|
/**
|
|
* Button palette (Figma prop).
|
|
* @default "default"
|
|
*/
|
|
palette?: ButtonPaletteValue;
|
|
/**
|
|
* Button size.
|
|
* @default "xsmall"
|
|
*/
|
|
size?: SizeValue;
|
|
/**
|
|
* Button state (Figma prop).
|
|
* @default "default"
|
|
*/
|
|
state?: ButtonStateValue;
|
|
/**
|
|
* Danger treatment beside Type. Renders the outline button in red.
|
|
* @default false
|
|
*/
|
|
danger?: boolean;
|
|
/**
|
|
* Whether to show a leading icon (Figma prop).
|
|
* @default false
|
|
*/
|
|
hasIconLeading?: boolean;
|
|
/**
|
|
* Whether to show a following icon (Figma prop).
|
|
* @default false
|
|
*/
|
|
hasIconFollowing?: boolean;
|
|
/**
|
|
* Whether to show text (Figma prop).
|
|
* @default true
|
|
*/
|
|
hasText?: boolean;
|
|
className?: string;
|
|
disabled?: boolean;
|
|
type?: "button" | "submit" | "reset";
|
|
onClick?: (
|
|
_e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
|
|
) => void;
|
|
href?: string;
|
|
target?: string;
|
|
rel?: string;
|
|
ariaLabel?: string;
|
|
}
|
|
|
|
const Button = memo<ButtonProps>(
|
|
({
|
|
children,
|
|
buttonType: typeProp,
|
|
palette: paletteProp,
|
|
size: sizeProp = "xsmall",
|
|
state: _stateProp,
|
|
danger = false,
|
|
hasIconLeading = false,
|
|
hasIconFollowing = false,
|
|
hasText = true,
|
|
className = "",
|
|
disabled = false,
|
|
type: htmlType = "button",
|
|
onClick,
|
|
href,
|
|
target,
|
|
rel,
|
|
ariaLabel,
|
|
...props
|
|
}) => {
|
|
const buttonType = typeProp ?? "filled";
|
|
const buttonPalette = paletteProp ?? "default";
|
|
const size = sizeProp;
|
|
|
|
// Map type + palette to variant string for styling (internal use only)
|
|
const getVariantFromTypeAndPalette = (
|
|
type: typeof buttonType,
|
|
palette: typeof buttonPalette,
|
|
): string => {
|
|
if (type === "filled" && palette === "default") return "filled";
|
|
if (type === "filled" && palette === "inverse") return "filled-inverse";
|
|
if (type === "outline" && palette === "default") return "outline";
|
|
if (type === "outline" && palette === "inverse") return "outline-inverse";
|
|
if (type === "ghost" && palette === "default") return "ghost";
|
|
return "ghost-inverse";
|
|
};
|
|
|
|
const variant = danger
|
|
? buttonPalette === "inverse"
|
|
? "danger-inverse"
|
|
: "danger"
|
|
: getVariantFromTypeAndPalette(buttonType, buttonPalette);
|
|
|
|
const sizeStyles: Record<string, string> = {
|
|
xsmall: "p-[var(--space-100)] gap-[var(--space-050)]",
|
|
small: "p-[var(--space-200)] gap-[var(--space-050)]",
|
|
medium: "p-[var(--space-250)] gap-[var(--space-100)]",
|
|
large: "p-[var(--space-300)] gap-[var(--space-150)]",
|
|
xlarge: "p-[var(--space-400)] gap-[var(--space-200)]",
|
|
};
|
|
|
|
const strokeStyles: Record<string, string> = {
|
|
xsmall: "border-[1px] active:border-[1.25px]",
|
|
small: "border-[1.25px] active:border-[1.5px]",
|
|
medium: "border-[1.5px] active:border-[1.75px]",
|
|
large: "border-[1.75px]",
|
|
xlarge: "border-[2px]",
|
|
};
|
|
|
|
const fontStyles: Record<string, string> = {
|
|
xsmall: "text-xx-small-label",
|
|
small: "text-x-small-label",
|
|
medium: "text-small-label",
|
|
large: "text-medium-label",
|
|
xlarge: "text-x-large-label",
|
|
};
|
|
|
|
const variantStyles: Record<string, string> = {
|
|
filled:
|
|
"bg-[var(--color-surface-invert-primary)] text-[var(--color-content-invert-primary)] border-transparent hover:bg-[var(--color-surface-invert-primary)] hover:text-[var(--color-content-invert-brand-primary)] hover:border-[var(--color-border-invert-brand-primary)] focus-visible:bg-[var(--color-surface-invert-primary)] focus-visible:text-[var(--color-content-invert-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[var(--shadow-focus-default)] active:bg-[var(--color-surface-invert-brand-primary)] active:text-[var(--color-content-invert-primary)] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
|
|
"filled-inverse":
|
|
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-transparent hover:bg-[var(--color-surface-default-primary)] hover:text-[var(--color-content-default-brand-primary)] hover:border-[var(--color-border-default-brand-primary)] focus-visible:bg-[var(--color-surface-default-primary)] focus-visible:text-[var(--color-content-default-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[var(--shadow-focus-invert)] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
|
|
outline:
|
|
"bg-transparent text-[var(--color-content-default-primary)] border-[var(--color-border-invert-primary)] hover:bg-transparent hover:text-[var(--color-content-default-brand-primary)] hover:border-[var(--color-border-default-brand-primary)] focus-visible:bg-transparent focus-visible:text-[var(--color-content-default-primary)] focus-visible:outline-none focus-visible:border-[var(--color-border-invert-primary)] focus-visible:shadow-[var(--shadow-focus-default)] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
|
|
"outline-inverse":
|
|
"bg-transparent text-[var(--color-content-invert-primary)] border-[var(--color-border-default-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-brand-primary)] hover:border-[var(--color-border-invert-brand-primary)] focus-visible:bg-transparent focus-visible:text-[var(--color-content-invert-primary)] focus-visible:outline-none focus-visible:border-[var(--color-border-default-primary)] focus-visible:shadow-[var(--shadow-focus-invert)] active:bg-transparent active:text-[var(--color-content-invert-primary)] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
|
|
ghost:
|
|
"bg-transparent text-[var(--color-content-default-brand-primary)] border-transparent hover:bg-transparent hover:text-[var(--color-content-default-primary)] hover:border-transparent focus-visible:bg-transparent focus-visible:text-[var(--color-content-default-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[var(--shadow-focus-default)] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
|
|
"ghost-inverse":
|
|
"bg-transparent text-[var(--color-content-invert-primary)] border-transparent hover:bg-transparent hover:text-[var(--color-content-invert-primary)] hover:border-transparent focus-visible:bg-transparent focus-visible:text-[var(--color-content-invert-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[var(--shadow-focus-invert)] active:bg-[var(--color-surface-invert-brand-primary)] active:text-[var(--color-content-invert-primary)] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
|
|
danger:
|
|
"bg-transparent text-[var(--color-content-default-negative-primary)] border-[var(--color-border-default-negative-primary)] hover:bg-[var(--color-surface-invert-negative-secondary)] hover:text-[var(--color-content-invert-negative-primary)] hover:border-[var(--color-border-default-negative-primary)] focus-visible:bg-transparent focus-visible:text-[var(--color-content-default-negative-primary)] focus-visible:outline-none focus-visible:border-[var(--color-border-default-negative-primary)] focus-visible:shadow-[var(--shadow-focus-default)] active:bg-[var(--color-surface-invert-negative-primary)] active:text-[var(--color-content-invert-primary)] active:border-[var(--color-border-default-negative-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
|
|
"danger-inverse":
|
|
"bg-transparent text-[var(--color-content-invert-negative-primary)] border-[var(--color-border-invert-negative-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-negative-primary)] hover:border-[var(--color-border-invert-negative-primary)] focus-visible:bg-transparent focus-visible:text-[var(--color-content-invert-negative-primary)] focus-visible:outline-none focus-visible:border-[var(--color-border-invert-negative-primary)] focus-visible:shadow-[var(--shadow-focus-invert)] active:bg-[var(--color-surface-default-negative-primary)] active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-negative-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
|
|
};
|
|
|
|
// Hover/focus: color + border only — no scale and no extra outline (those
|
|
// enlarge the label). Press: active:scale-[0.98]. Same rule for nav items
|
|
// and cards: color, opacity, or shadow on hover; scale only on press.
|
|
// State prop is informational for Figma alignment; CSS pseudo-classes own
|
|
// the actual interaction states.
|
|
const baseStyles = `inline-flex items-center justify-start box-border whitespace-nowrap shrink-0 touch-manipulation [-webkit-tap-highlight-color:transparent] ${sizeStyles[size]} rounded-[var(--radius-full)] ${fontStyles[size]} transition-[transform,color,background-color,border-color,box-shadow] duration-150 ease-out cursor-pointer ${variantStyles[variant]} ${strokeStyles[size]}`;
|
|
const combinedStyles = `${baseStyles} ${className}`;
|
|
|
|
const sharedA11y = {
|
|
...(ariaLabel && { "aria-label": ariaLabel }),
|
|
...(disabled && { "aria-disabled": true }),
|
|
tabIndex: disabled ? -1 : 0,
|
|
};
|
|
|
|
// Filter children based on hasIconLeading, hasIconFollowing, hasText props
|
|
// For now, we render all children but these props are available for future icon support
|
|
const renderContent = () => {
|
|
if (!hasText && !hasIconLeading && !hasIconFollowing) {
|
|
return children; // If all are false, render children as-is (backward compatibility)
|
|
}
|
|
// TODO: When icon support is added, filter children based on these props
|
|
return children;
|
|
};
|
|
|
|
if (href && !disabled) {
|
|
const anchorProps: React.AnchorHTMLAttributes<HTMLAnchorElement> = {
|
|
href,
|
|
className: combinedStyles,
|
|
onClick,
|
|
...sharedA11y,
|
|
...(target && { target }),
|
|
...(rel && { rel }),
|
|
};
|
|
|
|
return <a {...anchorProps}>{renderContent()}</a>;
|
|
}
|
|
|
|
const buttonProps: React.ButtonHTMLAttributes<HTMLButtonElement> = {
|
|
type: htmlType,
|
|
className: combinedStyles,
|
|
disabled,
|
|
onClick,
|
|
...sharedA11y,
|
|
...props,
|
|
};
|
|
|
|
return <button {...buttonProps}>{renderContent()}</button>;
|
|
},
|
|
);
|
|
|
|
Button.displayName = "Button";
|
|
|
|
export default Button;
|