// Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ButtonHTMLAttributes, CSSProperties, FC, ForwardedRef, HTMLAttributes, ReactNode, } from 'react'; import React, { forwardRef, memo, useId, useMemo } from 'react'; import type { Transition } from 'motion/react'; import { motion } from 'motion/react'; import type { TailwindStyles } from '../tw.dom.js'; import { tw } from '../tw.dom.js'; import { ExperimentalAxoBadge } from '../AxoBadge.dom.js'; import { createStrictContext, useStrictContext } from './StrictContext.dom.js'; const Namespace = 'AxoBaseSegmentedControl'; /** * Used to share styles/animations for SegmentedControls, Toolbar ToggleGroups, * and Tabs. * * @example Anatomy * ```tsx * * * * * * * * ``` */ export namespace ExperimentalAxoBaseSegmentedControl { export type Variant = 'track' | 'no-track'; export type RootWidth = 'fit' | 'full'; export type ItemWidth = 'fit' | 'equal'; export type RootValue = string | ReadonlyArray | null; type RootContextType = Readonly<{ id: string; value: RootValue; variant: Variant; rootWidth: RootWidth; itemWidth: ItemWidth; }>; const RootContext = createStrictContext(`${Namespace}.Root`); type VariantConfig = { rootStyles: TailwindStyles; indicatorStyles: TailwindStyles; }; const base: VariantConfig = { rootStyles: tw( 'flex min-w-min flex-row items-center justify-items-stretch', 'rounded-full', 'forced-colors:border', 'forced-colors:border-[ButtonBorder]' ), indicatorStyles: tw( 'pointer-events-none absolute inset-0 z-10 rounded-full', 'forced-colors:bg-[SelectedItem]' ), }; const Variants: Record = { track: { rootStyles: tw(base.rootStyles, 'bg-fill-secondary'), indicatorStyles: tw( base.indicatorStyles, 'bg-fill-primary', 'shadow-elevation-1' ), }, 'no-track': { rootStyles: tw(base.rootStyles), indicatorStyles: tw(base.indicatorStyles, 'bg-fill-selected'), }, }; const IndicatorTransition: Transition = { type: 'spring', stiffness: 422, damping: 37.3, mass: 1, }; /** * Component: * ----------------------------------------- */ const RootWidths: Record = { fit: tw('w-fit'), full: tw('w-full'), }; export type RootProps = HTMLAttributes & Readonly<{ value: RootValue; variant: Variant; width: RootWidth; itemWidth: ItemWidth; }>; export const Root: FC = memo( forwardRef((props, ref: ForwardedRef) => { const { value, variant, width, itemWidth, ...rest } = props; const id = useId(); const config = Variants[variant]; const widthStyles = RootWidths[width]; const context = useMemo(() => { return { id, value, variant, rootWidth: width, itemWidth }; }, [id, value, variant, width, itemWidth]); return (
); }) ); Root.displayName = `${Namespace}.Root`; /** * Component: * ----------------------------------------- */ const ItemWidths: Record = { fit: tw('min-w-0 shrink grow basis-auto'), equal: tw('flex-1'), }; export type ItemProps = ButtonHTMLAttributes & Readonly<{ value: string; }>; export const Item: FC = memo( forwardRef((props, ref: ForwardedRef) => { const { value, ...rest } = props; const context = useStrictContext(RootContext); const config = Variants[context.variant]; const itemWidthStyles = ItemWidths[context.itemWidth]; const isSelected = useMemo(() => { if (context.value == null) { return false; } if (Array.isArray(context.value)) { return context.value.includes(value); } return context.value === value; }, [value, context.value]); return ( ); }) ); Item.displayName = `${Namespace}.Item`; /** * Component: * --------------------------------------------- */ export type ItemMaxWidth = CSSProperties['maxWidth']; export type ItemTextProps = Readonly<{ maxWidth?: ItemMaxWidth; children: ReactNode; }>; export const ItemText: FC = memo(props => { return ( {props.children} ); }); ItemText.displayName = `${Namespace}.ItemText`; /** * Component: * ---------------------------------------------- */ export type ExperimentalItemBadgeProps = Omit< ExperimentalAxoBadge.RootProps, 'size' >; export const ExperimentalItemBadge = memo( (props: ExperimentalItemBadgeProps) => { return ( ); } ); ExperimentalItemBadge.displayName = `${Namespace}.ItemBadge`; }