// Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { CSSProperties, FC, Ref, ReactNode } from 'react'; import { memo, useId, useMemo } from 'react'; import type { Transition } from 'motion/react'; import { motion } from 'motion/react'; import { tw } from '../tw.dom.tsx'; import { ExperimentalAxoBadge } from '../AxoBadge.dom.tsx'; import { createStrictContext, useStrictContext } from './StrictContext.dom.tsx'; import { variants } from './variants.dom.tsx'; /** * Used to share styles/animations for SegmentedControls, Toolbar ToggleGroups, * and Tabs. * * @example Anatomy * ```tsx * * * * * * * * ``` */ export namespace ExperimentalAxoBaseSegmentedControl { /** * * -------------------------------------------------------------------------- */ /** * Visual style variant. */ export type Variant = 'track' | 'no-track'; /** * How the control sizes itself horizontally: * - `fit`: Shrinks to fit its content. * - `full`: Fills available width. */ export type RootWidth = 'fit' | 'full'; /** * How each item sizes itself within the control: * - `fit`: Items size to their content. * - `equal`: All items share equal width. */ export type ItemWidth = 'fit' | 'equal'; /** * The currently selected value(s). * A string for single-select, an array for multi-select, or `null` for nothing selected. */ export type RootValue = string | ReadonlyArray | null; /** @internal */ type RootContextType = Readonly<{ id: string; value: RootValue; variant: Variant; rootWidth: RootWidth; itemWidth: ItemWidth; }>; /** @internal */ const RootContext = createStrictContext( `AxoBaseSegmentedControl.Root` ); const baseRootStyles = tw( 'flex min-w-min flex-row items-center justify-items-stretch', 'rounded-full', 'forced-colors:border', 'forced-colors:border-[ButtonBorder]' ); const RootStyles = variants(`AxoBaseSegmentedControl.Variant`, { track: tw(baseRootStyles, 'bg-fill-secondary'), 'no-track': baseRootStyles, }); const baseIndicatorStyles = tw( 'pointer-events-none absolute inset-0 z-10 rounded-full', 'forced-colors:bg-[SelectedItem]' ); const IndicatorStyles = variants(`AxoBaseSegmentedControl.Variant`, { track: tw(baseIndicatorStyles, 'bg-fill-primary', 'shadow-elevation-1'), 'no-track': tw(baseIndicatorStyles, 'bg-fill-selected'), }); /** * * -------------------------------------------------------------------------- */ const RootWidths = variants(`AxoBaseSegmentedControl.RootWidth`, { fit: tw('w-fit'), full: tw('w-full'), }); export type RootProps = Readonly<{ /** Ref to the underlying `
` element. */ ref?: Ref; /** The currently selected value(s). */ value: RootValue; /** Visual style variant. */ variant: Variant; /** How the control sizes itself horizontally. */ width: RootWidth; /** How each item sizes itself within the control. */ itemWidth: ItemWidth; children: ReactNode; }>; export const Root: FC = memo(props => { const { value, variant, width, itemWidth, children, ...rest } = props; const id = useId(); const context = useMemo(() => { return { id, value, variant, rootWidth: width, itemWidth }; }, [id, value, variant, width, itemWidth]); return (
{children}
); }); Root.displayName = 'AxoBaseSegmentedControl.Root'; /** * * -------------------------------------------------------------------------- */ const ItemWidths = variants(`AxoBaseSegmentedControl.ItemWidth`, { fit: tw('min-w-0 shrink grow basis-auto'), equal: tw('flex-1'), }); const IndicatorTransition: Transition = { type: 'spring', stiffness: 422, damping: 37.3, mass: 1, }; export type ItemProps = Readonly<{ /** Ref to the underlying ` ); }); Item.displayName = 'AxoBaseSegmentedControl.Item'; /** * * -------------------------------------------------------------------------- */ /** CSS `max-width` value for the item label, used to prevent overflow. */ export type ItemMaxWidth = CSSProperties['maxWidth']; export type ItemTextProps = Readonly<{ /** Maximum width for the label before it truncates. */ maxWidth?: ItemMaxWidth; children: ReactNode; }>; /** Truncated label text inside a segmented control item. */ export const ItemText: FC = memo(props => { return ( {props.children} ); }); ItemText.displayName = 'AxoBaseSegmentedControl.ItemText'; /** * * -------------------------------------------------------------------------- */ export type ExperimentalItemBadgeProps = Omit< ExperimentalAxoBadge.RootProps, 'size' >; /** A badge rendered to the right of the item label. */ export const ExperimentalItemBadge = memo( (props: ExperimentalItemBadgeProps) => { return ( ); } ); ExperimentalItemBadge.displayName = 'AxoBaseSegmentedControl.ItemBadge'; }