import * as stylex from "@stylexjs/stylex"; import { type MouseEvent, type PropsWithChildren, type ReactElement, memo, } from "react"; import { Link, type To } from "react-router"; import { controlColor } from "./theme.stylex"; import { color, font, fontSize, radius, size, timingFunction, transition, } from "./tokens.stylex"; import { a11y, interaction, reset } from "./mixins"; import useRippleEffect from "./useRippleEffect"; const styles = stylex.create({ button: { position: "relative", // needed for ripple effect + badge fontFamily: font.main, fontSize: fontSize.body, textAlign: "center", borderStyle: "none", cursor: { default: "pointer", ":disabled": "not-allowed", }, display: "flex", gap: size.rem3, justifyContent: "center", alignItems: "center", transition: `${transition.a11yOutline}, ${transition.themeBackground}, ${transition.themeColor}`, // used for badge "::after": { opacity: 0, transition: `opacity ${timingFunction.fast} ease-out`, }, }, // Neuer Stil für lowercase Text lowercase: { textTransform: "lowercase", }, // Stile für verschiedene Schriftgewichte fontWeightNormal: { fontWeight: "normal", }, fontWeightBold: { fontWeight: "bold", }, fontWeightLight: { fontWeight: 300, }, fontWeightMedium: { fontWeight: 500, }, inline: { display: "inline-flex", }, icon: { fontSize: "120%", // TODO: ASK-UX proportional icon size? display: "flex", }, hasBadge: { "::after": { position: "absolute", content: "''", aspectRatio: "1/1", opacity: 1, // not using ds-unit because the border could not be made with that borderWidth: "3px", borderStyle: "solid", borderColor: color.white900, backgroundColor: controlColor.buttonPrimaryBackground, borderRadius: radius.circle, top: size.n_px2, right: size.n_px2, width: size.px4, height: size.px4, }, }, noPadding: { padding: 0, }, }); const buttonSizes = stylex.create({ tiny: { gap: size.rem1, padding: `${size.rem1} ${size.rem2}`, fontSize: fontSize.tiny, }, small: { padding: `${size.rem2} ${size.rem4}`, fontSize: fontSize.sub, }, medium: { padding: `${size.rem3} ${size.rem6}`, fontSize: fontSize.body, }, large: { padding: `${size.rem4} ${size.rem8}`, fontSize: fontSize.h4, }, huge: { flexDirection: "column", gap: size.rem2, padding: `${size.rem4} ${size.rem10}`, fontSize: fontSize.h4, }, }); const CAN_HOVER = "@media (hover: hover)"; const buttonVariants = stylex.create({ primary: { background: { default: controlColor.buttonPrimaryBackground, [CAN_HOVER]: { ":hover": controlColor.buttonPrimaryHoverBackground, }, ":active": controlColor.buttonPrimaryActiveBackground, ":disabled": controlColor.buttonPrimaryDisabledBackground, }, color: { default: controlColor.buttonPrimaryColor, [CAN_HOVER]: { ":hover": controlColor.buttonPrimaryHoverColor, }, ":active": controlColor.buttonPrimaryActiveColor, ":disabled": controlColor.buttonPrimaryDisabledColor, }, }, secondary: { background: { default: controlColor.buttonPrimaryColor, [CAN_HOVER]: { ":hover": controlColor.buttonPrimaryBackground, }, ":active": controlColor.buttonPrimaryActiveBackground, ":disabled": controlColor.buttonPrimaryDisabledBackground, }, color: { default: controlColor.buttonPrimaryBackground, [CAN_HOVER]: { ":hover": controlColor.buttonPrimaryColor, }, ":active": controlColor.buttonPrimaryActiveColor, ":disabled": controlColor.buttonPrimaryDisabledColor, }, }, subscription: { background: { default: color.mustard150, [CAN_HOVER]: { ":hover": controlColor.buttonPrimaryHoverBackground, }, ":active": controlColor.buttonPrimaryActiveBackground, ":disabled": controlColor.buttonPrimaryDisabledBackground, }, color: { default: controlColor.buttonPrimaryColor, [CAN_HOVER]: { ":hover": controlColor.buttonPrimaryHoverColor, }, ":active": controlColor.buttonPrimaryActiveColor, ":disabled": controlColor.buttonPrimaryDisabledColor, }, }, tertiary: { background: { default: controlColor.buttonTertiaryBackground, [CAN_HOVER]: { ":hover": controlColor.buttonTertiaryHoverBackground, }, // TODO: ASK-UX Active style? ":active": controlColor.buttonTertiaryActiveBackground, ":disabled": controlColor.buttonTertiaryDisabledBackground, }, color: { default: controlColor.buttonTertiaryColor, [CAN_HOVER]: { ":hover": controlColor.buttonTertiaryHoverColor, }, ":active": controlColor.buttonTertiaryActiveColor, ":disabled": controlColor.buttonTertiaryDisabledColor, }, }, // Quaternary variant: Inverted colors of tertiary quaternary: { background: { default: controlColor.buttonTertiaryHoverBackground, [CAN_HOVER]: { ":hover": controlColor.buttonTertiaryBackground, }, ":active": controlColor.buttonTertiaryActiveBackground, ":disabled": controlColor.buttonTertiaryDisabledBackground, }, color: { default: controlColor.buttonTertiaryHoverColor, [CAN_HOVER]: { ":hover": controlColor.buttonTertiaryColor, }, ":active": controlColor.buttonTertiaryActiveColor, ":disabled": controlColor.buttonTertiaryDisabledColor, }, }, link: { color: { default: controlColor.linkColor, [CAN_HOVER]: { ":hover": controlColor.linkHoverColor, }, ":active": controlColor.linkActiveColor, // Nothing here yet? // ":disabled": , }, fontWeight: "normal", fontSize: "unset", background: "none", padding: 0, margin: 0, }, }); type NativeButtonProps = React.DetailedHTMLProps< React.ButtonHTMLAttributes, HTMLButtonElement >; export interface ButtonBaseProps extends PropsWithChildren { /** * See aria roles: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles */ role?: NativeButtonProps["role"]; title?: string; form?: NativeButtonProps["form"]; variant?: | "primary" | "tertiary" | "link" | "subscription" | "secondary" | "quaternary"; size?: "tiny" | "small" | "medium" | "large" | "huge"; icon?: ReactElement; noPadding?: boolean; inline?: boolean; /** * Wenn `true`, wird der Text in Kleinbuchstaben angezeigt. * @default false */ lowercase?: boolean; /** * Legt das Schriftgewicht des Buttons fest. * @default "normal" */ fontWeight?: "normal" | "bold" | "light" | "medium"; /** * If `true`, shows a dot indicating that there is something new. * * Refs: https://setproduct.com/blog/badge-ui-design */ badge?: boolean; } // Properties and some behavior of this button is based on the Button of MUI: // https://mui.com/material-ui/react-button/ export interface ButtonButtonProps extends ButtonBaseProps { component?: "button" | undefined; // Commonly used button props onClick?: (event: MouseEvent) => void; /** Mandatory because the default type is "submit", which is not ideal. */ type: NativeButtonProps["type"]; disabled?: boolean; } export interface LinkButtonProps extends ButtonBaseProps { component: "link"; disabled?: boolean; to: To; /** * Also available on the link, so we can implement a share button that links to a URL and can trigger a share functionality. * @remarks If you want to use this, really think it through. Maybe you want to use a `component="button"` instead. */ onClick?: () => void; } export type ButtonProps = ButtonButtonProps | LinkButtonProps; // TODO: Refactor this to a link button export default memo(function Button(props: ButtonProps) { // Bestimme den fontWeight-Stil basierend auf der Prop oder verwende den Standard "normal" const getFontWeightStyle = () => { switch (props.fontWeight) { case "bold": return styles.fontWeightBold; case "light": return styles.fontWeightLight; case "medium": return styles.fontWeightMedium; default: return styles.fontWeightNormal; } }; const elementProps = stylex.props( interaction.disableTapHighlight, reset.buttonStyle, a11y.defaultOutline, styles.button, buttonSizes[props.size ?? "small"], buttonVariants[props.variant ?? "primary"], props.inline && styles.inline, props.noPadding && styles.noPadding, props.badge === true && styles.hasBadge, props.lowercase && styles.lowercase, getFontWeightStyle(), ); const rippleRef = useRippleEffect(props.disabled); const refForAnimatedButtons = props.noPadding || props.variant === "link" || props.variant === "subscription" ? undefined : rippleRef; const onClick = props.onClick; if (props.component === "link") { return ( { e.preventDefault(); (onClick as () => void)(); } : undefined } > {props.icon && ( {props.icon} )} {props.children} ); } return ( ); });