// 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 'framer-motion';
import { motion } from 'framer-motion';
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`;
}