first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
import {RefObject, useEffect, useState} from 'react';
import {m} from 'framer-motion';
interface ActiveIndicatorProps {
selectedValue?: string;
labelsRef: RefObject<Record<string, HTMLLabelElement>>;
}
export function ActiveIndicator({
selectedValue,
labelsRef,
}: ActiveIndicatorProps) {
const [style, setStyle] = useState<{
width: number;
height: number;
left: number;
} | null>(null);
useEffect(() => {
if (selectedValue != null && labelsRef.current) {
const el = labelsRef.current[selectedValue];
if (!el) return;
setStyle({
width: el.offsetWidth,
height: el.offsetHeight,
left: el.offsetLeft,
});
}
}, [setStyle, selectedValue, labelsRef]);
if (!style) {
return null;
}
return (
<m.div
animate={style}
initial={false}
className="bg-paper shadow rounded absolute z-10 pointer-events-none"
/>
);
}

View File

@@ -0,0 +1,64 @@
import {
Children,
cloneElement,
forwardRef,
isValidElement,
useId,
useRef,
} from 'react';
import {RadioGroupProps} from '../radio-group/radio-group';
import {SegmentedRadioProps} from './segmented-radio';
import {ActiveIndicator} from './active-indicator';
import {useControlledState} from '@react-stately/utils';
import clsx from 'clsx';
export interface SegmentedRadioGroupProps
extends Omit<RadioGroupProps, 'orientation'> {
value?: string;
onChange?: (value: string) => void;
defaultValue?: string;
width?: string;
}
export const SegmentedRadioGroup = forwardRef<
HTMLFieldSetElement,
SegmentedRadioGroupProps
>((props, ref) => {
const {children, size, className} = props;
const id = useId();
const name = props.name || id;
const labelsRef = useRef<Record<string, HTMLLabelElement>>({});
const [selectedValue, setSelectedValue] = useControlledState(
props.value,
props.defaultValue,
props.onChange,
);
return (
<fieldset ref={ref} className={clsx(className, props.width ?? 'w-min')}>
<div className="relative isolate flex rounded bg-chip p-4">
<ActiveIndicator selectedValue={selectedValue} labelsRef={labelsRef} />
{Children.map(children, (child, index) => {
if (isValidElement<SegmentedRadioProps>(child)) {
return cloneElement<SegmentedRadioProps>(child, {
isFirst: index === 0,
name,
size,
onChange: e => {
setSelectedValue(e.target.value);
child.props.onChange?.(e);
},
labelRef: el => {
if (el) {
labelsRef.current[child.props.value] = el;
}
},
isSelected: selectedValue === child.props.value,
});
}
})}
</div>
</fieldset>
);
});

View File

@@ -0,0 +1,65 @@
import React, {forwardRef, Ref} from 'react';
import clsx from 'clsx';
import {useObjectRef} from '@react-aria/utils';
import {InputSize} from '../input-field/input-size';
import {useAutoFocus} from '../../focus/use-auto-focus';
import {RadioProps} from '../radio-group/radio';
export interface SegmentedRadioProps extends RadioProps {
labelRef?: Ref<HTMLLabelElement>;
isSelected?: boolean;
}
export const SegmentedRadio = forwardRef<HTMLInputElement, SegmentedRadioProps>(
(props, ref) => {
const {
children,
autoFocus,
size,
invalid,
isFirst,
labelRef,
isSelected,
...domProps
} = props;
const inputRef = useObjectRef(ref);
useAutoFocus({autoFocus}, inputRef);
const sizeClassNames = getSizeClassNames(size);
return (
<label
ref={labelRef}
className={clsx(
'relative z-20 inline-flex flex-auto cursor-pointer select-none items-center justify-center gap-8 whitespace-nowrap align-middle font-medium transition-colors hover:text-main',
isSelected ? 'text-main' : 'text-muted',
!isFirst && '',
sizeClassNames,
props.disabled && 'pointer-events-none text-disabled',
props.invalid && 'text-danger',
)}
>
<input
type="radio"
className="pointer-events-none absolute left-0 top-0 h-full w-full appearance-none rounded focus-visible:outline"
ref={inputRef}
{...domProps}
/>
{children && <span>{children}</span>}
</label>
);
},
);
function getSizeClassNames(size?: InputSize): string {
switch (size) {
case 'xs':
return 'px-6 py-3 text-xs';
case 'sm':
return 'px-10 py-5 text-sm';
case 'lg':
return 'px-16 py-6 text-lg';
default:
return 'px-16 py-8 text-sm';
}
}