diff --git a/package-lock.json b/package-lock.json index 9cef1c6644..e89438099b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51506,7 +51506,7 @@ }, "packages/client": { "name": "@librechat/client", - "version": "0.1.9", + "version": "0.2.0", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^25.0.2", diff --git a/packages/client/package.json b/packages/client/package.json index 5edd6505cf..f1775ed13d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/client", - "version": "0.1.9", + "version": "0.2.0", "description": "React components for LibreChat", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/client/src/components/Radio.tsx b/packages/client/src/components/Radio.tsx new file mode 100644 index 0000000000..b419f78e6d --- /dev/null +++ b/packages/client/src/components/Radio.tsx @@ -0,0 +1,99 @@ +import React, { useState, useRef, useLayoutEffect, useCallback, memo } from 'react'; +import { useLocalize } from '~/hooks'; + +interface Option { + value: string; + label: string; +} + +interface RadioProps { + options: Option[]; + value?: string; + onChange?: (value: string) => void; + disabled?: boolean; +} + +const Radio = memo(function Radio({ options, value, onChange, disabled = false }: RadioProps) { + const localize = useLocalize(); + const [currentValue, setCurrentValue] = useState(value ?? ''); + const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]); + const [backgroundStyle, setBackgroundStyle] = useState({}); + + const handleChange = (newValue: string) => { + setCurrentValue(newValue); + onChange?.(newValue); + }; + + const updateBackgroundStyle = useCallback(() => { + const selectedIndex = options.findIndex((opt) => opt.value === currentValue); + if (selectedIndex >= 0 && buttonRefs.current[selectedIndex]) { + const selectedButton = buttonRefs.current[selectedIndex]; + const container = selectedButton?.parentElement; + if (selectedButton && container) { + const containerRect = container.getBoundingClientRect(); + const buttonRect = selectedButton.getBoundingClientRect(); + const offsetLeft = buttonRect.left - containerRect.left - 4; + setBackgroundStyle({ + width: `${buttonRect.width}px`, + transform: `translateX(${offsetLeft}px)`, + }); + } + } + }, [currentValue, options]); + + useLayoutEffect(() => { + updateBackgroundStyle(); + }, [updateBackgroundStyle]); + + useLayoutEffect(() => { + if (value !== undefined) { + setCurrentValue(value); + } + }, [value]); + + if (options.length === 0) { + return ( +
+ + {localize('com_ui_no_options')} + +
+ ); + } + + const selectedIndex = options.findIndex((opt) => opt.value === currentValue); + + return ( +
+ {selectedIndex >= 0 && ( +
+ )} + {options.map((option, index) => ( + + ))} +
+ ); +}); + +export default Radio; diff --git a/packages/client/src/components/index.ts b/packages/client/src/components/index.ts index edaf240dc4..0269a04550 100644 --- a/packages/client/src/components/index.ts +++ b/packages/client/src/components/index.ts @@ -30,6 +30,7 @@ export * from './Progress'; export * from './InputOTP'; export * from './MultiSearch'; export * from './Resizable'; +export { default as Radio } from './Radio'; export { default as Badge } from './Badge'; export { default as Combobox } from './Combobox'; export { default as Dropdown } from './Dropdown'; diff --git a/packages/client/src/locales/en/translation.json b/packages/client/src/locales/en/translation.json index de39a30059..9913398ff6 100644 --- a/packages/client/src/locales/en/translation.json +++ b/packages/client/src/locales/en/translation.json @@ -1,3 +1,4 @@ { - "com_ui_cancel": "Cancel" + "com_ui_cancel": "Cancel", + "com_ui_no_options": "No options available" }