Completed template
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
Reference in New Issue
Block a user