ui(components): migrates to new toolbar ui components and Numeric for Input InputRange and InputDoubleRange (#4728)

Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
Dan Rukasandsedghi authored and GitHub committed 2025-01-29 13:36:52 -05:00
1 parent ac5ae4a29e
commit a8d84443a9
116 files changed
+2930 -1038

No files matched your search

@@ -0,0 +1,277 @@
// Numeric.tsx
import React, { createContext, useContext, useState, useCallback, PropsWithChildren } from 'react';
import { cn } from '../../lib/utils';
import { Input } from '../Input/Input';
import { Slider } from '../Slider/Slider';
import { DoubleSlider } from '../DoubleSlider/DoubleSlider';
interface NumericMetaContextValue {
mode: 'number' | 'singleRange' | 'doubleRange';
singleValue: number;
doubleValue: [number, number];
setSingleValue: (val: number) => void;
setDoubleValue: (vals: [number, number]) => void;
min: number;
max: number;
step: number;
}
const NumericMetaContext = createContext<NumericMetaContextValue | null>(null);
/* -------------------------------------------------------------------------
1) Container
---------------------------------------------------------------------------*/
interface NumericMetaContainerProps {
mode: 'number' | 'singleRange' | 'doubleRange';
value?: number; // for single-value usage
values?: [number, number]; // for double-range usage
onChange?: (val: number | [number, number]) => void;
min?: number;
max?: number;
step?: number;
className?: string;
}
function NumericMetaContainer({
mode,
value = 0,
values = [0, 100],
onChange,
min = 0,
max = 100,
step = 1,
className,
children,
}: PropsWithChildren<NumericMetaContainerProps>) {
// Initialize state with props but don't update automatically
const [internalSingleValue, setInternalSingleValue] = useState<number>(value);
const [internalDoubleValue, setInternalDoubleValue] = useState<[number, number]>(values);
const handleSingleChange = useCallback(
(newVal: number) => {
setInternalSingleValue(newVal);
onChange?.(newVal);
},
[onChange]
);
const handleDoubleChange = useCallback(
(newVals: [number, number]) => {
// Update internal state
setInternalDoubleValue(newVals);
// Notify parent if onChange is provided
onChange?.(newVals);
},
[onChange]
);
return (
<NumericMetaContext.Provider
value={{
mode,
singleValue: internalSingleValue,
doubleValue: internalDoubleValue,
setSingleValue: handleSingleChange,
setDoubleValue: handleDoubleChange,
min,
max,
step,
}}
>
<div className={cn('flex flex-col', className)}>{children}</div>
</NumericMetaContext.Provider>
);
}
/* -------------------------------------------------------------------------
2) Label sub-component
---------------------------------------------------------------------------*/
interface NumericMetaLabelProps {
showValue?: boolean; // optionally show the current numeric value(s)
className?: string;
children: React.ReactNode;
}
function NumericMetaLabel({ children, showValue, className }: NumericMetaLabelProps) {
const ctx = useContext(NumericMetaContext);
if (!ctx) {
throw new Error('NumericMetaLabel must be used inside <Numeric.Container>.');
}
const { mode, singleValue, doubleValue } = ctx;
let displayedValue = '';
let valueClasses = '';
if (mode === 'number' || mode === 'singleRange') {
displayedValue = singleValue.toString();
valueClasses = 'w-10';
} else if (mode === 'doubleRange') {
displayedValue = `[${doubleValue[0]} - ${doubleValue[1]}]`;
}
return (
<div className={cn('text-foreground flex text-base', className)}>
{children}
{showValue && (
<span className={cn('inline-block', valueClasses)}>{`: ${displayedValue}`}</span>
)}
</div>
);
}
/* -------------------------------------------------------------------------
3) SingleRange sub-component
---------------------------------------------------------------------------*/
interface SingleRangeProps {
showNumberInput?: boolean;
sliderClassName?: string;
numberInputClassName?: string;
}
function SingleRange({ showNumberInput, sliderClassName, numberInputClassName }: SingleRangeProps) {
const ctx = useContext(NumericMetaContext);
if (!ctx) {
throw new Error('SingleRange must be used inside <Numeric.Container>.');
}
const { mode, singleValue, setSingleValue, min, max, step } = ctx;
const handleSliderChange = useCallback(
(val: number[]) => {
setSingleValue(val[0]);
},
[setSingleValue]
);
const handleNumberChange = useCallback(
(evt: React.ChangeEvent<HTMLInputElement>) => {
const parsed = parseFloat(evt.target.value);
if (!isNaN(parsed)) {
setSingleValue(Math.max(min, Math.min(parsed, max)));
}
},
[min, max, setSingleValue]
);
if (mode !== 'singleRange') {
return null;
}
return (
<div className="flex flex-1 items-center space-x-2">
<Slider
className={cn('flex-1', sliderClassName)}
value={[singleValue]}
min={min}
max={max}
step={step}
onValueChange={handleSliderChange}
/>
{showNumberInput && (
<Input
type="number"
className={cn('w-[50px] shrink-0', numberInputClassName)}
value={singleValue}
step={step}
min={min}
max={max}
onChange={handleNumberChange}
/>
)}
</div>
);
}
/* -------------------------------------------------------------------------
4) DoubleRange sub-component
---------------------------------------------------------------------------*/
interface DoubleRangeProps {
showNumberInputs?: boolean;
className?: string;
}
function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) {
const ctx = useContext(NumericMetaContext);
if (!ctx) {
throw new Error('DoubleRange must be used inside <Numeric.Container>.');
}
const { mode, doubleValue, setDoubleValue, min, max, step } = ctx;
const handleSliderChange = useCallback(
(values: [number, number]) => {
setDoubleValue(values);
},
[setDoubleValue]
);
if (mode !== 'doubleRange') {
return null;
}
return (
<div className={cn('min-w-0 flex-1', className)}>
<DoubleSlider
min={min}
max={max}
step={step}
defaultValue={doubleValue}
onValueChange={handleSliderChange}
showNumberInputs={showNumberInputs}
/>
</div>
);
}
/* -------------------------------------------------------------------------
5) Basic NumberInput sub-component
---------------------------------------------------------------------------*/
interface NumberInputProps {
className?: string;
}
function NumberInput({ className }: NumberInputProps) {
const ctx = useContext(NumericMetaContext);
if (!ctx) {
throw new Error('NumberInput must be used inside <Numeric.Container>.');
}
const { mode, singleValue, setSingleValue, min, max, step } = ctx;
if (mode !== 'number') {
return null;
}
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
const val = parseFloat(evt.target.value);
if (!isNaN(val)) {
setSingleValue(Math.max(min, Math.min(val, max)));
}
};
// Calculate width based on max value's length, with a minimum of 3 characters
const maxLength = Math.max(3, max?.toString().length ?? 3);
const calculatedWidth = `${maxLength + 1.5}ch`;
return (
<Input
type="number"
value={singleValue}
step={step}
min={min}
max={max}
onChange={handleChange}
className={cn('min-w-[60px]', `w-[${calculatedWidth}]`, className)}
/>
);
}
export const Numeric = {
Container: NumericMetaContainer,
Label: NumericMetaLabel,
SingleRange,
DoubleRange,
NumberInput,
};
export default Numeric;