/** * Copyright (c) TonTech. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import { createContext, useContext, useMemo } from 'react'; import type { FC, ReactNode, ComponentProps, ChangeEvent } from 'react'; import clsx from 'clsx'; import { Skeleton } from '../skeleton'; import { useInputResize } from './use-input-resize'; import type { InputSize } from './use-input-resize'; import styles from './input.module.css'; type InputVariant = 'default' | 'unstyled'; interface InputContextProps { size: InputSize; variant: InputVariant; disabled?: boolean; error?: boolean; loading?: boolean; resizable?: boolean; } const InputContext = createContext(undefined); const useInputContext = () => { const context = useContext(InputContext); if (!context) { throw new Error('Input components must be used within an Input.Container'); } return context; }; export interface InputContainerProps extends ComponentProps<'div'> { size?: InputSize; variant?: InputVariant; disabled?: boolean; error?: boolean; loading?: boolean; resizable?: boolean; children: ReactNode; } const Container: FC = ({ size = 'm', variant = 'default', disabled, error, loading, resizable, className, children, ...props }) => { const contextValue = useMemo( () => ({ size, variant, disabled, error, loading, resizable }), [size, variant, disabled, error, loading, resizable], ); return (
{children}
); }; export interface InputHeaderProps extends ComponentProps<'div'> { children: ReactNode; } const Header: FC = ({ className, children, ...props }) => (
{children}
); const Title: FC> = ({ className, children, ...props }) => ( {children} ); export interface InputFieldProps extends ComponentProps<'div'> { children: ReactNode; } const Field: FC = ({ className, children, ...props }) => (
{children}
); export interface InputSlotProps extends ComponentProps<'div'> { side?: 'left' | 'right'; } const Slot: FC = ({ side, className, children, ...props }) => (
{children}
); export type InputControlProps = ComponentProps<'input'>; const InputControl: FC = ({ className, disabled: propsDisabled, onChange, ...props }) => { const { size: contextSize, disabled: contextDisabled, loading, resizable } = useInputContext(); const disabled = propsDisabled || contextDisabled; const { inputRef, measureMaxRef, measureMinRef, resizeStyle, adjustSize } = useInputResize({ resizable, contextSize, value: props.value, }); const handleChange = (e: ChangeEvent) => { onChange?.(e); adjustSize(); }; const text = String(props.value ?? props.defaultValue ?? ''); if (loading) { const skeletonClass = styles[`inputSkeleton_${contextSize}`]; return (
); } return ( <> {resizable && ( <> {/* Measures actual text width at max (contextSize) font — source of truth for scaling */} {text} {/* Empty span — only used to read minFontSize from CSS variable via computed style */} )} ); }; const Caption: FC> = ({ className, children, ...props }) => { const { error } = useInputContext(); return ( {children} ); }; export const Input = Object.assign(Container, { Container, Header, Title, Field, Slot, Input: InputControl, Caption, });