Completed template

This commit is contained in:
adilallo
2026-03-02 22:12:50 -07:00
parent d811b87b12
commit 3e3d2881f5
103 changed files with 1410 additions and 622 deletions
@@ -4,7 +4,10 @@ import { memo } from "react";
import { useComponentId } from "../../../hooks";
import { CheckboxView } from "./Checkbox.view";
import type { CheckboxProps } from "./Checkbox.types";
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
const CheckboxContainer = memo<CheckboxProps>(
({
@@ -24,7 +27,7 @@ const CheckboxContainer = memo<CheckboxProps>(
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp);
const state = normalizeState(stateProp);
const isInverse = mode === "inverse";
const isStandard = mode === "standard";
@@ -43,7 +46,9 @@ const CheckboxContainer = memo<CheckboxProps>(
transition-all
duration-200
ease-in-out
`.trim().replace(/\s+/g, " ");
`
.trim()
.replace(/\s+/g, " ");
// Get box styles based on state and checked status per Figma designs
const getBoxStyles = (): string => {
@@ -22,8 +22,10 @@ const CheckboxGroupContainer = ({
const groupId = name || `checkbox-group-${generatedId}`;
// Internal state to track checked values (only used if value prop is not provided)
const [internalCheckedValues, setInternalCheckedValues] = useState<string[]>([]);
const [internalCheckedValues, setInternalCheckedValues] = useState<string[]>(
[],
);
// Use controlled value if provided, otherwise use internal state
const checkedValues = value !== undefined ? value : internalCheckedValues;
@@ -23,10 +23,7 @@ export function CheckboxGroupView({
// If there's subtext, render checkbox without label and handle layout separately
if (option.subtext) {
return (
<div
key={option.value}
className="flex gap-[8px] items-start"
>
<div key={option.value} className="flex gap-[8px] items-start">
<Checkbox
checked={isChecked}
mode={mode}
@@ -41,7 +41,10 @@ const ChipContainer = memo<ChipProps>(
}
}, [isCustom]);
const handleCheck = (value: string, event: React.MouseEvent<HTMLButtonElement>) => {
const handleCheck = (
value: string,
event: React.MouseEvent<HTMLButtonElement>,
) => {
if (onCheck && value.trim()) {
onCheck(value.trim(), event);
// Reset input after successful check
@@ -63,7 +66,10 @@ const ChipContainer = memo<ChipProps>(
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter" && inputValue.trim() && onCheck) {
event.preventDefault();
handleCheck(inputValue.trim(), event as unknown as React.MouseEvent<HTMLButtonElement>);
handleCheck(
inputValue.trim(),
event as unknown as React.MouseEvent<HTMLButtonElement>,
);
} else if (event.key === "Escape" && onClose) {
event.preventDefault();
handleClose(event as unknown as React.MouseEvent<HTMLButtonElement>);
@@ -95,4 +101,3 @@ const ChipContainer = memo<ChipProps>(
ChipContainer.displayName = "Chip";
export default ChipContainer;
@@ -68,4 +68,3 @@ export interface ChipViewProps {
inputRef?: React.RefObject<HTMLInputElement>;
ariaLabel?: string;
}
+24 -31
View File
@@ -42,32 +42,26 @@ function ChipView({
// Palette + state styling based on Figma examples
// Use consistent border width to prevent layout shift
const borderWidth = isSmall ? "border-[1.25px]" : "border-2";
let background = "bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
let border =
`${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`;
let background =
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
let border = `${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`;
let textColor =
"text-[color:var(--color-content-default-brand-primary,#fefcc9)]";
if (isDefault) {
if (state === "custom") {
background =
"bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom
background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom
border = "border-none";
textColor =
"text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
} else if (state === "disabled") {
background =
"bg-[var(--color-surface-default-secondary,#141414)]"; // dark background
background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background
border = "border-none";
textColor =
"text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
} else if (isSelected) {
background =
"bg-[var(--color-surface-inverse-brandaccent,#fdfaa8)]"; // yellow selected
background = "bg-[var(--color-surface-inverse-brandaccent,#fdfaa8)]"; // yellow selected
border = `${borderWidth} border-[var(--color-border-default-brand-primary,#fdfaa8)]`;
textColor =
"text-[color:var(--color-content-inverse-primary,black)]";
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
} else {
// Unselected default
background =
@@ -78,24 +72,20 @@ function ChipView({
}
} else if (isInverse) {
if (state === "disabled") {
background =
"bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]";
background = "bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]";
border = "border-none";
textColor =
"text-[color:var(--color-content-inverse-primary,black)]";
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
} else if (isSelected) {
background =
"bg-[var(--color-surface-default-semi-opaque,rgba(0,0,0,0.05))]";
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
textColor =
"text-[color:var(--color-content-inverse-primary,black)]";
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
} else {
// Unselected / custom inverse
background =
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
textColor =
"text-[color:var(--color-content-inverse-primary,black)]";
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
}
}
@@ -134,7 +124,9 @@ function ChipView({
.filter(Boolean)
.join(" ");
const handleClick = (event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>) => {
const handleClick = (
event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>,
) => {
if (isDisabled) {
event.preventDefault();
return;
@@ -162,7 +154,9 @@ function ChipView({
}}
{...sharedA11y}
>
<div className={`flex items-center ${isSmall ? "gap-[8px]" : "gap-[12px]"}`}>
<div
className={`flex items-center ${isSmall ? "gap-[8px]" : "gap-[12px]"}`}
>
{/* Check button */}
{onCheck && (
<button
@@ -208,7 +202,9 @@ function ChipView({
placeholder="Type to add"
className="bg-transparent border-none outline-none flex-1 min-w-0 font-inter font-normal text-[color:var(--color-content-default-tertiary,#b4b4b4)] placeholder:text-[color:var(--color-content-default-tertiary,#b4b4b4)]"
style={{
fontSize: isSmall ? "var(--sizing-300,12px)" : "var(--sizing-400,16px)",
fontSize: isSmall
? "var(--sizing-300,12px)"
: "var(--sizing-400,16px)",
lineHeight: isSmall ? "16px" : "24px",
}}
onClick={(e) => e.stopPropagation()}
@@ -259,9 +255,7 @@ function ChipView({
onClick={handleClick}
{...sharedA11y}
>
<span className="flex items-center justify-center">
{label}
</span>
<span className="flex items-center justify-center">{label}</span>
{onRemove && !isDisabled && (
<button
type="button"
@@ -284,4 +278,3 @@ function ChipView({
ChipView.displayName = "ChipView";
export default memo(ChipView);
-1
View File
@@ -1,3 +1,2 @@
export { default } from "./Chip.container";
export type { ChipProps } from "./Chip.types";
@@ -3,7 +3,10 @@
import { memo } from "react";
import MultiSelectView from "./MultiSelect.view";
import type { MultiSelectProps } from "./MultiSelect.types";
import { normalizeMultiSelectSize, normalizeChipPalette } from "../../../../lib/propNormalization";
import {
normalizeMultiSelectSize,
normalizeChipPalette,
} from "../../../../lib/propNormalization";
const MultiSelectContainer = memo<MultiSelectProps>(
({
@@ -1,4 +1,7 @@
import type { ChipStateValue, ChipPaletteValue } from "../../../../lib/propNormalization";
import type {
ChipStateValue,
ChipPaletteValue,
} from "../../../../lib/propNormalization";
export interface ChipOption {
id: string;
@@ -31,7 +31,9 @@ function MultiSelectView({
const chipSize = isSmall ? "S" : "M";
return (
<div className={`flex flex-col ${isSmall ? "gap-[var(--measures-spacing-200,8px)]" : "gap-[var(--measures-spacing-300,12px)]"} items-start relative w-full ${className}`}>
<div
className={`flex flex-col ${isSmall ? "gap-[var(--measures-spacing-200,8px)]" : "gap-[var(--measures-spacing-300,12px)]"} items-start relative w-full ${className}`}
>
{/* Label using InputLabel component */}
{formHeader && label && (
<InputLabel
@@ -45,7 +47,9 @@ function MultiSelectView({
)}
{/* Chips container */}
<div className={`flex flex-wrap ${gapClass} items-center relative shrink-0 w-full`}>
<div
className={`flex flex-wrap ${gapClass} items-center relative shrink-0 w-full`}
>
{options.map((option) => (
<Chip
key={option.id}
@@ -110,7 +114,9 @@ function MultiSelectView({
</svg>
{/* Text - only show if addButtonText is provided */}
{addButtonText && (
<span className={`font-inter font-medium ${isSmall ? "text-[length:var(--sizing-300,12px)] leading-[14px]" : "text-[length:var(--sizing-400,16px)] leading-[20px]"} ${isInverse ? "text-[color:var(--color-content-inverse-primary,black)]" : "text-[color:var(--color-content-default-brand-primary,#fefcc9)]"}`}>
<span
className={`font-inter font-medium ${isSmall ? "text-[length:var(--sizing-300,12px)] leading-[14px]" : "text-[length:var(--sizing-400,16px)] leading-[20px]"} ${isInverse ? "text-[color:var(--color-content-inverse-primary,black)]" : "text-[color:var(--color-content-default-brand-primary,#fefcc9)]"}`}
>
{addButtonText}
</span>
)}
@@ -3,7 +3,10 @@
import { memo, useCallback, useId } from "react";
import { RadioButtonView } from "./RadioButton.view";
import type { RadioButtonProps } from "./RadioButton.types";
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
const RadioButtonContainer = ({
checked = false,
@@ -22,10 +25,10 @@ const RadioButtonContainer = ({
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp);
const state = normalizeState(stateProp);
// If state is "selected", it means checked in Figma terms
const normalizedState = state === "selected" || checked ? "selected" : state;
const isInverse = mode === "inverse";
const isStandard = mode === "standard";
@@ -42,7 +45,9 @@ const RadioButtonContainer = ({
duration-200
ease-in-out
p-[4px]
`.trim().replace(/\s+/g, " ");
`
.trim()
.replace(/\s+/g, " ");
// Get box styles based on mode and checked status per Figma designs
const getBoxStyles = (): string => {
@@ -55,12 +60,12 @@ const RadioButtonContainer = ({
const defaultBorder = checked
? "border-[var(--color-border-default-brand-primary,#fdfaa8)]"
: "border-[var(--color-border-default-tertiary,#464646)]";
// When focused and checked, border should be invert tertiary (#2d2d2d) per Figma
const focusBorder = checked
? "focus:border-[var(--color-content-invert-tertiary,#2d2d2d)]"
: "focus:border-[var(--color-border-default-tertiary,#464646)]";
return `${baseBox} bg-[var(--color-surface-default-primary)] border border-solid ${defaultBorder} hover:border-[var(--color-border-default-brand-primary,#fdfaa8)] ${focusBorder} focus:shadow-[0px_0px_0px_2px_var(--color-border-invert-primary,white),0px_0px_0px_4px_var(--color-border-default-primary,#141414)] focus:outline-none`;
}
@@ -73,15 +78,16 @@ const RadioButtonContainer = ({
const defaultBorder = checked
? "border-[var(--color-border-default-brand-primary,#fdfaa8)]"
: "border-[var(--color-border-invert-primary,white)]";
// Hover border: inverse brand primary for both selected and unselected per Figma
const hoverBorder = "hover:border-[var(--color-border-invert-brand-primary,#6c6701)]";
const hoverBorder =
"hover:border-[var(--color-border-invert-brand-primary,#6c6701)]";
// Focus border: when focused and checked, border should be white per Figma
const focusBorder = checked
? "focus:border-[var(--color-border-invert-primary,white)]"
: "focus:border-[var(--color-border-invert-primary,white)]";
return `${baseBox} bg-transparent border border-solid ${defaultBorder} ${hoverBorder} ${focusBorder} focus:shadow-[0px_0px_0px_2px_var(--color-border-default-primary,#141414),0px_0px_0px_4px_var(--color-border-invert-primary,white)] focus:outline-none`;
}
@@ -41,8 +41,8 @@ export function RadioButtonView({
checked && mode === "standard"
? "bg-[var(--color-content-default-brand-primary,#fefcc9)] group-hover:!bg-[#333000]"
: checked && mode === "inverse"
? "bg-[var(--color-content-default-primary,#000000)]"
: "bg-transparent"
? "bg-[var(--color-content-default-primary,#000000)]"
: "bg-transparent"
}`}
/>
</span>
@@ -3,7 +3,10 @@
import { memo, useCallback, useId } from "react";
import { RadioGroupView } from "./RadioGroup.view";
import type { RadioGroupProps } from "./RadioGroup.types";
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
const RadioGroupContainer = ({
name,
@@ -19,10 +22,11 @@ const RadioGroupContainer = ({
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp);
// Normalize state, but handle "With Subtext" separately (it's represented by options with subtext)
const state = typeof stateProp === "string" &&
const state =
typeof stateProp === "string" &&
(stateProp.toLowerCase() === "with subtext" || stateProp === "With Subtext")
? "default" // "With Subtext" is handled via RadioOption.subtext, use default state
: normalizeState(stateProp);
? "default" // "With Subtext" is handled via RadioOption.subtext, use default state
: normalizeState(stateProp);
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const groupId = name || `radio-group-${generatedId}`;
@@ -24,10 +24,7 @@ export function RadioGroupView({
// If there's subtext, render radio button without label and handle layout separately
if (option.subtext) {
return (
<div
key={option.value}
className="flex gap-[8px] items-start"
>
<div key={option.value} className="flex gap-[8px] items-start">
<RadioButton
checked={isSelected}
mode={mode}
@@ -16,7 +16,11 @@ import React, {
import { useClickOutside } from "../../../hooks";
import { SelectInputView } from "./SelectInput.view";
import type { SelectInputProps } from "./SelectInput.types";
import { normalizeState, normalizeSmallMediumLargeSize, normalizeLabelVariant } from "../../../../lib/propNormalization";
import {
normalizeState,
normalizeSmallMediumLargeSize,
normalizeLabelVariant,
} from "../../../../lib/propNormalization";
const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
(
@@ -46,23 +50,28 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
ref,
) => {
// Determine if label should be shown
const shouldShowLabel = showLabel !== undefined ? showLabel : (labelText !== undefined);
const shouldShowLabel =
showLabel !== undefined ? showLabel : labelText !== undefined;
// Normalize state - handle "state5" as disabled
let normalizedState = externalStateProp;
if (normalizedState === "state5" || normalizedState === "State5") {
normalizedState = "default"; // Map to default, disabled prop handles the disabled state
}
const externalState = normalizeState(normalizedState);
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
// Note: labelVariant and size are normalized for future use but not yet implemented in the view
const _labelVariant = labelVariantProp ? normalizeLabelVariant(labelVariantProp) : undefined;
const _size = sizeProp ? normalizeSmallMediumLargeSize(sizeProp) : undefined;
const _labelVariant = labelVariantProp
? normalizeLabelVariant(labelVariantProp)
: undefined;
const _size = sizeProp
? normalizeSmallMediumLargeSize(sizeProp)
: undefined;
// Mark as intentionally unused for future implementation
void _labelVariant;
void _size;
const generatedId = useId();
const selectId = id || `select-input-${generatedId}`;
const labelId = `${selectId}-label`;
@@ -73,11 +82,14 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
// 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 [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)
const shouldAutoManageFocus = externalState === "default" || externalState === undefined;
const shouldAutoManageFocus =
externalState === "default" || externalState === undefined;
// Sync internal state with external value prop
useEffect(() => {
@@ -7,8 +7,18 @@ export interface SelectOptionData {
import type { StateValue } from "../../../../lib/propNormalization";
export type SelectInputLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal";
export type SelectInputSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
export type SelectInputLabelVariantValue =
| "default"
| "horizontal"
| "Default"
| "Horizontal";
export type SelectInputSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export interface SelectInputProps {
id?: string;
@@ -1,4 +1,10 @@
export type SelectOptionSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
export type SelectOptionSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export interface SelectOptionProps {
children?: React.ReactNode;
@@ -17,7 +17,7 @@ const SwitchContainer = memo(
className = "",
...rest
} = props;
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const state = normalizeState(stateProp);
@@ -4,7 +4,12 @@ import { memo, forwardRef } from "react";
import { useComponentId, useFormField } from "../../../hooks";
import { TextAreaView } from "./TextArea.view";
import type { TextAreaProps } from "./TextArea.types";
import { normalizeInputState, normalizeSmallMediumLargeSize, normalizeLabelVariant, normalizeTextAreaAppearance } from "../../../../lib/propNormalization";
import {
normalizeInputState,
normalizeSmallMediumLargeSize,
normalizeLabelVariant,
normalizeTextAreaAppearance,
} from "../../../../lib/propNormalization";
const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
(
@@ -1,9 +1,23 @@
import type { InputStateValue } from "../../../../lib/propNormalization";
export type TextAreaSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
export type TextAreaLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal";
export type TextAreaSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export type TextAreaLabelVariantValue =
| "default"
| "horizontal"
| "Default"
| "Horizontal";
export type TextAreaAppearanceValue = "default" | "embedded" | "Default" | "Embedded";
export type TextAreaAppearanceValue =
| "default"
| "embedded"
| "Default"
| "Embedded";
export interface TextAreaProps extends Omit<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
@@ -4,7 +4,10 @@ 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, normalizeTextInputSize } from "../../../../lib/propNormalization";
import {
normalizeInputState,
normalizeTextInputSize,
} from "../../../../lib/propNormalization";
const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
(
@@ -33,18 +36,21 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const externalState = normalizeInputState(externalStateProp);
const inputSize = normalizeTextInputSize(inputSizeProp);
// 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 [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;
const shouldAutoManageFocus =
externalState === "default" || externalState === undefined;
// Determine actual state:
// - Active: when clicked (mouse focus)
@@ -62,19 +68,20 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
const isFilled = Boolean(value && value.trim().length > 0);
// Size styles based on inputSize prop
const sizeStyles = inputSize === "small"
? {
input: "h-[32px] px-[10px] py-[6px] text-[14px]",
label: "text-[12px] leading-[16px] font-medium",
container: "gap-[6px]",
radius: "var(--measures-radius-200,8px)",
}
: {
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)",
};
const sizeStyles =
inputSize === "small"
? {
input: "h-[32px] px-[10px] py-[6px] text-[14px]",
label: "text-[12px] leading-[16px] font-medium",
container: "gap-[6px]",
radius: "var(--measures-radius-200,8px)",
}
: {
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 = (): {
@@ -167,17 +174,20 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
: "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);
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 = () => {
@@ -189,19 +199,19 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
// 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);
};
@@ -3,7 +3,10 @@
import { memo, useCallback, useId, forwardRef } from "react";
import { ToggleGroupView } from "./ToggleGroup.view";
import type { ToggleGroupProps } from "./ToggleGroup.types";
import { normalizeToggleState, normalizeToggleGroupPosition } from "../../../../lib/propNormalization";
import {
normalizeToggleState,
normalizeToggleGroupPosition,
} from "../../../../lib/propNormalization";
const ToggleGroupContainer = memo(
forwardRef<HTMLButtonElement, ToggleGroupProps>((props, _ref) => {
@@ -19,7 +22,7 @@ const ToggleGroupContainer = memo(
onBlur,
...rest
} = props;
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const position = normalizeToggleGroupPosition(positionProp);
const state = normalizeToggleState(stateProp);
@@ -1,6 +1,12 @@
import type { StateValue } from "../../../../lib/propNormalization";
export type ToggleGroupPositionValue = "left" | "middle" | "right" | "Left" | "Middle" | "Right";
export type ToggleGroupPositionValue =
| "left"
| "middle"
| "right"
| "Left"
| "Middle"
| "Right";
export interface ToggleGroupProps extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
@@ -5,13 +5,7 @@ import UploadView from "./Upload.view";
import type { UploadProps } from "./Upload.types";
const UploadContainer = memo<UploadProps>(
({
active = true,
label,
showHelpIcon = true,
onClick,
className = "",
}) => {
({ active = true, label, showHelpIcon = true, onClick, className = "" }) => {
return (
<UploadView
active={active}
@@ -33,7 +33,9 @@ function UploadView({
: "text-[color:var(--color-content-invert-tertiary,#2d2d2d)]";
return (
<div className={`flex flex-col gap-[var(--measures-spacing-300,12px)] items-start relative w-full ${className}`}>
<div
className={`flex flex-col gap-[var(--measures-spacing-300,12px)] items-start relative w-full ${className}`}
>
{/* Label using InputLabel component */}
{label && (
<InputLabel
@@ -92,13 +94,17 @@ function UploadView({
</svg>
</div>
{/* Button text */}
<div className={`flex flex-col font-inter font-medium justify-center leading-[0] relative shrink-0 text-[length:var(--sizing-400,16px)] whitespace-nowrap ${buttonTextColor}`}>
<div
className={`flex flex-col font-inter font-medium justify-center leading-[0] relative shrink-0 text-[length:var(--sizing-400,16px)] whitespace-nowrap ${buttonTextColor}`}
>
<p className="leading-[20px]">Upload</p>
</div>
</button>
{/* Description text */}
<div className={`flex flex-[1_0_0] flex-col font-inter font-normal h-[32px] justify-center leading-[0] min-h-px min-w-px relative text-[length:var(--sizing-350,14px)] ${descriptionTextColor}`}>
<div
className={`flex flex-[1_0_0] flex-col font-inter font-normal h-[32px] justify-center leading-[0] min-h-px min-w-px relative text-[length:var(--sizing-350,14px)] ${descriptionTextColor}`}
>
<p className="leading-[20px] whitespace-pre-wrap">
Add images, PDFs, and other files to the policy
</p>