Start organizational migration

This commit is contained in:
adilallo
2026-02-05 18:21:56 -07:00
parent 69074b23f3
commit db3c0274f6
161 changed files with 145 additions and 145 deletions
@@ -0,0 +1,231 @@
"use client";
import { memo, forwardRef, useState, useRef } from "react";
import { useComponentId, useFormField } from "../../../hooks";
import { TextInputView } from "./TextInput.view";
import type { TextInputProps } from "./TextInput.types";
import { normalizeInputState } from "../../../../lib/propNormalization";
const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
(
{
state: externalStateProp = "default",
disabled = false,
error = false,
label,
placeholder,
value,
onChange,
onFocus,
onBlur,
id,
name,
type = "text",
className = "",
showHelpIcon = true,
...props
},
ref,
) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const externalState = normalizeInputState(externalStateProp);
// Generate unique ID for accessibility if not provided
const { id: inputId, labelId } = useComponentId("text-input", id);
// Internal state management: track if focused and how (mouse vs keyboard)
const [isFocused, setIsFocused] = useState(false);
const [focusMethod, setFocusMethod] = useState<"mouse" | "keyboard" | null>(null);
const wasMouseDownRef = useRef(false);
// Determine if we should auto-manage focus (only when state is "default" or undefined)
// If state is "active", "hover", or "focus", respect it and don't override
const shouldAutoManageFocus = externalState === "default" || externalState === undefined;
// Determine actual state:
// - Active: when clicked (mouse focus)
// - Focus: when tabbed (keyboard focus)
// - Default: when not focused
const actualState = shouldAutoManageFocus
? isFocused
? focusMethod === "mouse"
? "active"
: "focus"
: "default"
: externalState;
// Determine if input is filled (has value)
const isFilled = Boolean(value && value.trim().length > 0);
// Fixed size styles (medium only per Figma designs)
const sizeStyles = {
input: "h-[40px] px-[12px] py-[8px] text-[16px]",
label: "text-[14px] leading-[20px] font-medium",
container: "gap-[8px]",
radius: "var(--measures-radius-200,8px)",
};
// State styles based on Figma designs
const getStateStyles = (): {
input: string;
label: string;
inputWrapper: string;
focusRing: string;
} => {
if (disabled) {
return {
input:
"bg-[var(--color-surface-default-secondary)] text-[var(--color-content-inverse-tertiary,#2d2d2d)] border border-solid border-[var(--color-border-default-primary)] cursor-not-allowed",
label: "text-[var(--color-content-default-secondary)]",
inputWrapper: "relative",
focusRing: "",
};
}
if (error) {
const filledStyles = isFilled
? "font-medium leading-[20px]"
: "font-normal leading-[24px]";
return {
input: `bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-2 border-solid border-[var(--color-border-default-utility-negative)] ${filledStyles}`,
label: "text-[var(--color-content-default-secondary)]",
inputWrapper: "relative",
focusRing: "",
};
}
switch (actualState) {
case "active": {
const filledStyles = isFilled
? "font-medium leading-[20px]"
: "font-normal leading-[24px]";
return {
input: `bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-2 border-solid border-[var(--color-border-default-tertiary)] ${filledStyles}`,
label: "text-[var(--color-content-default-secondary)]",
inputWrapper: "relative",
focusRing: "",
};
}
case "focus": {
const filledStyles = isFilled
? "font-medium leading-[20px]"
: "font-normal leading-[24px]";
return {
input: `bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] border border-solid border-[var(--color-border-default-tertiary)] ${filledStyles}`,
label: "text-[var(--color-content-default-secondary)]",
inputWrapper: "relative",
focusRing:
"absolute border-2 border-solid border-[var(--color-border-inverse-primary)] inset-0 rounded-[var(--measures-radius-200,8px)] shadow-[0px_0px_0px_2px_var(--color-border-default-primary)] pointer-events-none",
};
}
default: {
const filledStyles = isFilled
? "font-medium leading-[20px]"
: "font-normal leading-[24px]";
// Default state uses primary border (matches Figma - border color same as background, so border is subtle)
return {
input: `bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] border border-solid border-[var(--color-border-default-primary)] ${filledStyles}`,
label: "text-[var(--color-content-default-secondary)]",
inputWrapper: "relative",
focusRing: "",
};
}
}
};
const stateStyles = getStateStyles();
// Container classes (default label variant only)
const containerClasses = `flex flex-col ${sizeStyles.container}`;
const labelClasses = `${sizeStyles.label} font-inter`;
// Base classes without border (border is added in state styles)
const inputClasses = `
w-full transition-all duration-200 ease-in-out
focus:outline-none focus:ring-0
placeholder:text-[var(--color-content-default-tertiary,#b4b4b4)]
${sizeStyles.input}
${stateStyles.input}
${className}
`.trim();
// Text color for filled text (placeholder color is handled above)
const textColorClass = isFilled
? "text-[var(--color-content-default-primary)]"
: "text-[var(--color-content-default-tertiary,#b4b4b4)]";
// Form field handlers with disabled state handling
const { handleChange, handleBlur } = useFormField<HTMLInputElement>(disabled, {
onChange,
onBlur: (e) => {
if (shouldAutoManageFocus) {
setIsFocused(false);
setFocusMethod(null);
wasMouseDownRef.current = false;
}
onBlur?.(e);
},
});
// Handle mouse down to detect mouse clicks
const handleMouseDown = () => {
if (!disabled && shouldAutoManageFocus) {
wasMouseDownRef.current = true;
}
};
// Custom focus handler to detect mouse vs keyboard
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
if (disabled) return;
// Detect if focus came from keyboard (Tab) or mouse (click)
// If mouseDown was detected before focus, it's a mouse click (active)
// Otherwise, it's keyboard navigation (focus)
const method = wasMouseDownRef.current ? "mouse" : "keyboard";
if (shouldAutoManageFocus) {
setIsFocused(true);
setFocusMethod(method);
// Reset mouse down flag after focus is processed
wasMouseDownRef.current = false;
}
onFocus?.(e);
};
return (
<TextInputView
ref={ref}
inputId={inputId}
labelId={labelId}
state={actualState}
disabled={disabled}
error={error}
label={label}
placeholder={placeholder}
value={value}
name={name}
type={type}
className={className}
containerClasses={containerClasses}
labelClasses={labelClasses}
inputClasses={`${inputClasses} ${textColorClass}`}
borderRadius={sizeStyles.radius}
handleChange={handleChange}
handleFocus={handleFocus}
handleBlur={handleBlur}
handleMouseDown={handleMouseDown}
showHelpIcon={showHelpIcon}
isFilled={isFilled}
inputWrapperClasses={stateStyles.inputWrapper}
focusRingClasses={stateStyles.focusRing}
{...props}
/>
);
},
);
TextInputContainer.displayName = "TextInput";
export default memo(TextInputContainer);
@@ -0,0 +1,48 @@
import type { InputStateValue } from "../../../../lib/propNormalization";
export interface TextInputProps extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
"size" | "onChange" | "onFocus" | "onBlur"
> {
/**
* Visual state. Accepts "default"/"Default", "active"/"Active", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
state?: InputStateValue;
disabled?: boolean;
error?: boolean;
label?: string;
placeholder?: string;
value?: string;
onChange?: (_e: React.ChangeEvent<HTMLInputElement>) => void;
onFocus?: (_e: React.FocusEvent<HTMLInputElement>) => void;
onBlur?: (_e: React.FocusEvent<HTMLInputElement>) => void;
className?: string;
showHelpIcon?: boolean;
}
export interface TextInputViewProps {
inputId: string;
labelId: string;
state: "default" | "active" | "hover" | "focus";
disabled: boolean;
error: boolean;
label?: string;
placeholder?: string;
value?: string;
name?: string;
type: string;
className: string;
containerClasses: string;
labelClasses: string;
inputClasses: string;
borderRadius: string;
handleChange: (_e: React.ChangeEvent<HTMLInputElement>) => void;
handleFocus: (_e: React.FocusEvent<HTMLInputElement>) => void;
handleBlur: (_e: React.FocusEvent<HTMLInputElement>) => void;
handleMouseDown?: () => void;
showHelpIcon?: boolean;
isFilled?: boolean;
inputWrapperClasses?: string;
focusRingClasses?: string;
}
@@ -0,0 +1,83 @@
import { forwardRef } from "react";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import type { TextInputViewProps } from "./TextInput.types";
export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
(
{
inputId,
labelId,
label,
placeholder,
value,
name,
type,
disabled,
error: _error,
className: _className,
containerClasses,
labelClasses,
inputClasses,
borderRadius,
handleChange,
handleFocus,
handleBlur,
handleMouseDown,
showHelpIcon = true,
inputWrapperClasses = "relative",
focusRingClasses = "",
},
ref,
) => {
return (
<div className={containerClasses}>
{label && (
<div className="flex flex-wrap gap-[var(--measures-spacing-200,4px_8px)] items-baseline pr-[var(--measures-spacing-100,4px)] relative shrink-0 w-full">
<div className="flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0">
<label
id={labelId}
htmlFor={inputId}
className={`${labelClasses} font-inter font-medium text-[var(--color-content-default-primary)]`}
>
{label}
</label>
{showHelpIcon && (
<div className="relative shrink-0 size-[12px]">
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt="Help"
className="block max-w-none size-full"
/>
</div>
)}
</div>
</div>
)}
<div className={inputWrapperClasses}>
<div className={disabled ? "opacity-40" : ""}>
<input
ref={ref}
id={inputId}
name={name}
type={type}
value={value}
placeholder={placeholder}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
onMouseDown={handleMouseDown}
disabled={disabled}
className={inputClasses}
style={{ borderRadius }}
/>
</div>
{focusRingClasses && (
<div className={focusRingClasses} aria-hidden="true" />
)}
</div>
</div>
);
},
);
TextInputView.displayName = "TextInputView";
@@ -0,0 +1,2 @@
export { default } from "./TextInput.container";
export type { TextInputProps } from "./TextInput.types";