// Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ButtonHTMLAttributes, FC, ForwardedRef } from 'react'; import React, { forwardRef, memo } from 'react'; import { AxoSymbol } from './AxoSymbol.dom.js'; import type { TailwindStyles } from './tw.dom.js'; import { tw } from './tw.dom.js'; import type { SpinnerVariant } from '../components/SpinnerV2.dom.js'; import { SpinnerV2 } from '../components/SpinnerV2.dom.js'; const Namespace = 'AxoIconButton'; type GenericButtonProps = ButtonHTMLAttributes; export namespace AxoIconButton { const baseStyles = tw( 'relative rounded-full select-none', 'outline-border-focused not-forced-colors:outline-0 not-forced-colors:focused:outline-[2.5px]', 'forced-colors:border forced-colors:border-[ButtonBorder] forced-colors:bg-[ButtonFace] forced-colors:text-[ButtonText]', 'forced-colors:disabled:text-[GrayText]', 'forced-colors:aria-pressed:bg-[SelectedItem] forced-colors:aria-pressed:text-[SelectedItemText]' ); const pressedStyles = { fillInverted: tw( 'aria-pressed:bg-fill-inverted aria-pressed:pressed:bg-fill-inverted-pressed', 'aria-pressed:text-label-primary-inverted aria-pressed:disabled:text-label-disabled-inverted' ), colorFillPrimary: tw( 'aria-pressed:bg-color-fill-primary aria-pressed:pressed:bg-color-fill-primary-pressed', 'aria-pressed:text-label-primary-on-color aria-pressed:disabled:text-label-disabled-on-color' ), }; const Variants: Record = { secondary: tw( 'bg-fill-secondary pressed:bg-fill-secondary-pressed', 'data-[axo-dropdownmenu-state=open]:bg-fill-secondary-pressed', 'text-label-primary disabled:text-label-disabled', pressedStyles.fillInverted ), primary: tw( 'bg-color-fill-primary pressed:bg-color-fill-primary-pressed', 'data-[axo-dropdownmenu-state=open]:bg-color-fill-primary-pressed', 'text-label-primary-on-color disabled:text-label-disabled-on-color', pressedStyles.fillInverted ), affirmative: tw( 'bg-color-fill-affirmative pressed:bg-color-fill-affirmative-pressed', 'data-[axo-dropdownmenu-state=open]:bg-color-fill-affirmative-pressed', 'text-label-primary-on-color disabled:text-label-disabled-on-color', pressedStyles.fillInverted ), destructive: tw( 'bg-color-fill-destructive pressed:bg-color-fill-destructive-pressed', 'data-[axo-dropdownmenu-state=open]:bg-color-fill-destructive-pressed', 'text-label-primary-on-color disabled:text-label-disabled-on-color', pressedStyles.fillInverted ), 'borderless-secondary': tw( 'hovered:bg-fill-secondary pressed:bg-fill-secondary-pressed', 'focus:bg-fill-secondary', 'data-[axo-dropdownmenu-state=open]:bg-fill-secondary-pressed', 'text-label-primary disabled:text-label-disabled', pressedStyles.colorFillPrimary ), 'floating-secondary': tw( 'bg-fill-floating pressed:bg-fill-floating-pressed', 'data-[axo-dropdownmenu-state=open]:bg-fill-floating-pressed', 'text-label-primary disabled:text-label-disabled', 'shadow-elevation-1', pressedStyles.fillInverted ), }; export function _getAllVariants(): ReadonlyArray { return Object.keys(Variants) as ReadonlyArray; } type SizeConfig = Readonly<{ buttonStyles: TailwindStyles; iconSize: AxoSymbol.IconSize; }>; const Sizes: Record = { sm: { buttonStyles: tw('p-[5px]'), iconSize: 18 }, md: { buttonStyles: tw('p-1.5'), iconSize: 20 }, lg: { buttonStyles: tw('p-2'), iconSize: 20 }, }; export function _getAllSizes(): ReadonlyArray { return Object.keys(Sizes) as ReadonlyArray; } export type Variant = | 'secondary' | 'primary' | 'affirmative' | 'destructive' | 'borderless-secondary' | 'floating-secondary'; export type Size = 'sm' | 'md' | 'lg'; export type RootProps = Readonly<{ // required: Should describe the purpose of the button, not the icon. 'aria-label': string; variant: Variant; size: Size; symbol: AxoSymbol.IconName; experimentalSpinner?: { 'aria-label': string } | null; disabled?: GenericButtonProps['disabled']; onClick?: GenericButtonProps['onClick']; 'aria-pressed'?: GenericButtonProps['aria-pressed']; // Note: Technically we forward all props for Radix, but we restrict the // props that the type accepts }>; export const Root: FC = memo( forwardRef((props, ref: ForwardedRef) => { const { variant, size, symbol, experimentalSpinner, ...rest } = props; return ( ); }) ); Root.displayName = `${Namespace}.Root`; const SpinnerVariants: Record = { primary: 'axo-button-spinner-on-color', secondary: 'axo-button-spinner-secondary', affirmative: 'axo-button-spinner-on-color', destructive: 'axo-button-spinner-on-color', 'floating-secondary': 'axo-button-spinner-secondary', 'borderless-secondary': 'axo-button-spinner-secondary', }; type SpinnerSizeConfig = { size: number; strokeWidth: number }; const SpinnerSizes: Record = { lg: { size: 20, strokeWidth: 2 }, md: { size: 20, strokeWidth: 2 }, sm: { size: 16, strokeWidth: 1.5 }, }; type SpinnerProps = Readonly<{ buttonVariant: Variant; buttonSize: Size; 'aria-label': string; }>; // eslint-disable-next-line no-inner-declarations function Spinner(props: SpinnerProps): JSX.Element { const variant = SpinnerVariants[props.buttonVariant]; const sizeConfig = SpinnerSizes[props.buttonSize]; return ( ); } }