List components #31

Open
stne3960 wants to merge 57 commits from list_item into main
Showing only changes of commit 8690658ac8 - Show all commits

View File

@ -0,0 +1,107 @@
import type { InputHTMLAttributes, ReactNode, CSSProperties } from 'react';
// isValidElement: checks if something is a React element (e.g., <svg>, <MyComponent />)
// cloneElement: creates a copy of a React element with modified/additional props
import { cloneElement, isValidElement } from 'react';
export type TextInputSize = 'sm' | 'md' | 'lg';
// Omit<... 'size'> removes the native 'size' attribute from input elements so we can use our own
export interface TextInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
size?: TextInputSize;
icon?: ReactNode;
error?: boolean;
fullWidth?: boolean;
customWidth?: string;
label?: string;
message?: string;
}
const heightClasses: Record<TextInputSize, string> = {
sm: 'h-(--control-height-sm)',
md: 'h-(--control-height-md)',
lg: 'h-(--control-height-lg)',
};
const widthClasses: Record<TextInputSize, string> = {
sm: 'w-(--text-input-default-width-md)',
md: 'w-(--text-input-default-width-md)',
lg: 'w-(--text-input-default-width-lg)',
};
const radiusClasses: Record<TextInputSize, string> = {
sm: 'rounded-(--border-radius-sm)',
md: 'rounded-(--border-radius-sm)',
lg: 'rounded-(--border-radius-md)',
};
const textClasses: Record<TextInputSize, string> = {
sm: 'body-normal-md',
md: 'body-normal-md',
lg: 'body-normal-lg',
};
const iconContainerStyles: Record<TextInputSize, CSSProperties> = {
sm: { width: 'var(--control-height-sm)', height: 'var(--control-height-sm)' },
md: { width: 'var(--control-height-md)', height: 'var(--control-height-md)' },
lg: { width: 'var(--control-height-lg)', height: 'var(--control-height-lg)' },
};
const iconStyles: Record<TextInputSize, CSSProperties> = {
sm: { width: 'var(--font-size-body-md)', height: 'var(--font-size-body-md)' },
md: { width: 'var(--font-size-body-md)', height: 'var(--font-size-body-md)' },
lg: { width: 'var(--font-size-body-lg)', height: 'var(--font-size-body-lg)' },
};
const baseClasses = 'bg-base-canvas border-(length:--border-width-sm) border-base-ink-medium placeholder:text-base-ink-medium';
// focus-within: applies styles when any child element (the input) has focus
const defaultStateClasses =
'hover:border-base-ink-placeholder focus-within:border-primary focus-within:outline focus-within:outline-sky-100 focus-within:outline-(length:--border-width-lg)';
const errorStateClasses = 'border-fire-100 outline outline-fire-100 outline-(length:--border-width-sm)';
export default function TextInput({
size = 'md',
icon,
error = false,
fullWidth = false,
customWidth,
label,
message,
className = '',
...props
}: TextInputProps) {
const widthClass = fullWidth ? 'w-full' : widthClasses[size];
const widthStyle = customWidth ? { width: customWidth } : undefined;
const stateClasses = error ? errorStateClasses : defaultStateClasses;
const inputPadding = icon ? 'pr-(--padding-md)' : 'px-(--padding-md)';
const inputField = (
<div
className={`flex items-center ${baseClasses} ${heightClasses[size]} ${widthClass} ${radiusClasses[size]} ${stateClasses} ${className}`}
style={widthStyle}
>
{icon && (
<div className="flex items-center justify-center shrink-0 text-base-ink-placeholder" style={iconContainerStyles[size]}>
{isValidElement<{ style?: CSSProperties }>(icon) ? cloneElement(icon, { style: iconStyles[size] }) : icon}
</div>
)}
<input
className={`flex-1 h-full bg-transparent border-none outline-none placeholder:text-base-ink-medium ${inputPadding} ${textClasses[size]}`}
{...props}
/>
</div>
);
if (!label && !message) {
return inputField;
}
return (
<div className="flex flex-col gap-(--spacing-sm)">
{label && <label className="body-bold-md text-base-ink-strong">{label}</label>}
{inputField}
{message && <span className="body-light-sm text-base-ink-strong">{message}</span>}
</div>
);
}