Update props in components

This commit is contained in:
adilallo
2026-02-06 17:36:12 -07:00
parent 85ff3b8f01
commit 1ca11a2229
11 changed files with 405 additions and 44 deletions
+133 -9
View File
@@ -1,19 +1,64 @@
import { memo } from "react";
import type { VariantValue, SizeValue } from "../../../lib/propNormalization";
import { normalizeVariant, normalizeSize } from "../../../lib/propNormalization";
import type {
VariantValue,
SizeValue,
ButtonTypeValue,
ButtonPaletteValue,
ButtonStateValue,
} from "../../../lib/propNormalization";
import {
normalizeVariant,
normalizeSize,
normalizeButtonType,
normalizeButtonPalette,
} from "../../../lib/propNormalization";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
/**
* @deprecated Use `type` and `palette` props instead. This prop is maintained for backward compatibility.
* Button variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
variant?: VariantValue;
/**
* Button type (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* @default "filled"
*/
buttonType?: ButtonTypeValue;
/**
* Button palette (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses "Invert", codebase uses "inverse" - both are supported.
* @default "default"
*/
palette?: ButtonPaletteValue;
/**
* Button size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* @default "xsmall"
*/
size?: SizeValue;
/**
* Button state (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
* @default "default"
*/
state?: ButtonStateValue;
/**
* Whether to show a leading icon (Figma prop).
* @default false
*/
hasIconLeading?: boolean;
/**
* Whether to show a following icon (Figma prop).
* @default false
*/
hasIconFollowing?: boolean;
/**
* Whether to show text (Figma prop).
* @default true
*/
hasText?: boolean;
className?: string;
disabled?: boolean;
type?: "button" | "submit" | "reset";
@@ -29,11 +74,17 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
const Button = memo<ButtonProps>(
({
children,
variant: variantProp = "filled",
variant: variantProp,
buttonType: typeProp,
palette: paletteProp,
size: sizeProp = "xsmall",
state: _stateProp,
hasIconLeading = false,
hasIconFollowing = false,
hasText = true,
className = "",
disabled = false,
type = "button",
type: htmlType = "button",
onClick,
href,
target,
@@ -41,9 +92,68 @@ const Button = memo<ButtonProps>(
ariaLabel,
...props
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const variant = normalizeVariant(variantProp);
// Determine type and palette from either new props or legacy variant prop
let buttonType: "filled" | "outline" | "ghost" | "danger";
let buttonPalette: "default" | "inverse";
if (variantProp) {
// Backward compatibility: map old variant to new type/palette
const variant = normalizeVariant(variantProp);
if (variant === "filled") {
buttonType = "filled";
buttonPalette = "default";
} else if (variant === "filled-inverse") {
buttonType = "filled";
buttonPalette = "inverse";
} else if (variant === "outline") {
buttonType = "outline";
buttonPalette = "default";
} else if (variant === "outline-inverse") {
buttonType = "outline";
buttonPalette = "inverse";
} else if (variant === "ghost") {
buttonType = "ghost";
buttonPalette = "default";
} else if (variant === "ghost-inverse") {
buttonType = "ghost";
buttonPalette = "inverse";
} else if (variant === "danger") {
buttonType = "danger";
buttonPalette = "default";
} else {
// danger-inverse
buttonType = "danger";
buttonPalette = "inverse";
}
} else {
// Use new type/palette props
buttonType = normalizeButtonType(typeProp, "filled");
buttonPalette = normalizeButtonPalette(paletteProp, "default");
}
// Normalize other props
const size = normalizeSize(sizeProp);
// State prop is for Figma alignment - actual state is handled by CSS pseudo-classes
// We accept it for API alignment but don't use it for styling (CSS handles states)
// Map type + palette to legacy variant for styling (maintains existing styles)
const getVariantFromTypeAndPalette = (
type: typeof buttonType,
palette: typeof buttonPalette,
): string => {
if (type === "filled" && palette === "default") return "filled";
if (type === "filled" && palette === "inverse") return "filled-inverse";
if (type === "outline" && palette === "default") return "outline";
if (type === "outline" && palette === "inverse") return "outline-inverse";
if (type === "ghost" && palette === "default") return "ghost";
if (type === "ghost" && palette === "inverse") return "ghost-inverse";
if (type === "danger" && palette === "default") return "danger";
// danger + inverse
return "danger-inverse";
};
const variant = getVariantFromTypeAndPalette(buttonType, buttonPalette);
const sizeStyles: Record<string, string> = {
xsmall:
"p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]",
@@ -102,6 +212,10 @@ const Button = memo<ButtonProps>(
? ""
: hoverOutlineStyles[size];
// Apply state-based styles if state prop is provided (overrides default hover/focus/active)
// Note: State prop is informational for Figma alignment - actual state is handled by CSS pseudo-classes
// For now, we maintain existing behavior and state prop is for documentation/alignment purposes
const baseStyles = `inline-flex items-center justify-start box-border whitespace-nowrap shrink-0 ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${fontStyles[size]} transition-all duration-500 ease-in-out cursor-pointer ${variantStyles[variant]} ${outlineStyles}`;
const combinedStyles = `${baseStyles} ${className}`;
@@ -111,6 +225,16 @@ const Button = memo<ButtonProps>(
tabIndex: disabled ? -1 : 0,
};
// Filter children based on hasIconLeading, hasIconFollowing, hasText props
// For now, we render all children but these props are available for future icon support
const renderContent = () => {
if (!hasText && !hasIconLeading && !hasIconFollowing) {
return children; // If all are false, render children as-is (backward compatibility)
}
// TODO: When icon support is added, filter children based on these props
return children;
};
if (href && !disabled) {
const anchorProps: React.AnchorHTMLAttributes<HTMLAnchorElement> = {
href,
@@ -121,11 +245,11 @@ const Button = memo<ButtonProps>(
...(rel && { rel }),
};
return <a {...anchorProps}>{children}</a>;
return <a {...anchorProps}>{renderContent()}</a>;
}
const buttonProps: React.ButtonHTMLAttributes<HTMLButtonElement> = {
type,
type: htmlType,
className: combinedStyles,
disabled,
onClick,
@@ -133,7 +257,7 @@ const Button = memo<ButtonProps>(
...props,
};
return <button {...buttonProps}>{children}</button>;
return <button {...buttonProps}>{renderContent()}</button>;
},
);
@@ -22,10 +22,18 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
(
{
id,
label,
label: labelProp,
labelText,
showLabel,
labelVariant: labelVariantProp,
size: sizeProp,
state: externalStateProp = "default",
asterisk = false,
iconHelp = true,
textOptional = false,
textData = true,
iconRight = true,
textHint = false,
disabled = false,
error = false,
placeholder = "Choose an option",
@@ -38,6 +46,17 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
},
ref,
) => {
// Handle backward compatibility: if label is string, use it as labelText
const actualLabelText = labelText || labelProp;
const shouldShowLabel = showLabel !== undefined ? showLabel : (actualLabelText !== 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;
@@ -45,7 +64,6 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
// Mark as intentionally unused for future implementation
void _labelVariant;
void _size;
const externalState = normalizeState(externalStateProp);
const generatedId = useId();
const selectId = id || `select-input-${generatedId}`;
@@ -193,7 +211,7 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
return (
<SelectInputView
label={label}
label={shouldShowLabel ? actualLabelText : undefined}
placeholder={placeholder}
state={actualState}
disabled={disabled}
@@ -214,8 +232,14 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
onOptionClick={handleOptionSelect}
selectRef={selectRef}
menuRef={menuRef}
ariaLabelledby={label ? labelId : undefined}
ariaLabelledby={shouldShowLabel ? labelId : undefined}
ariaInvalid={error}
asterisk={asterisk}
iconHelp={iconHelp}
textOptional={textOptional}
textData={textData}
iconRight={iconRight}
textHint={textHint}
{...props}
/>
);
@@ -12,7 +12,21 @@ export type SelectInputSizeValue = "small" | "medium" | "large" | "Small" | "Med
export interface SelectInputProps {
id?: string;
/**
* Label text (backward compatibility - if provided, label is shown).
* For Figma alignment, use `labelText` prop instead.
*/
label?: string;
/**
* Label text (Figma prop - use this for new code).
*/
labelText?: string;
/**
* Whether to show label above input (Figma prop).
* If `label` or `labelText` is provided, defaults to true.
* @default true
*/
showLabel?: boolean;
/**
* Label variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
@@ -24,10 +38,40 @@ export interface SelectInputProps {
*/
size?: SelectInputSizeValue;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Visual state. Accepts "default"/"Default", "active"/"Active", "focus"/"Focus", "error"/"Error", "state5"/"State5" (State5 = Disabled).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
state?: StateValue;
state?: StateValue | "state5" | "State5";
/**
* Whether to show asterisk (*) in label (Figma prop).
* @default false
*/
asterisk?: boolean;
/**
* Whether to show help icon in label (Figma prop).
* @default true
*/
iconHelp?: boolean;
/**
* Whether to show "Optional" text in label (Figma prop).
* @default false
*/
textOptional?: boolean;
/**
* Whether to show data text (placeholder/entered text) - internal, always true (Figma prop).
* @default true
*/
textData?: boolean;
/**
* Whether to show dropdown icon on the right (Figma prop).
* @default true
*/
iconRight?: boolean;
/**
* Whether to show hint text below input (Figma prop).
* @default false
*/
textHint?: boolean;
disabled?: boolean;
error?: boolean;
placeholder?: string;
@@ -33,6 +33,13 @@ export interface SelectInputViewProps {
// Additional props
ariaLabelledby?: string;
ariaInvalid?: boolean;
// Figma props
asterisk?: boolean;
iconHelp?: boolean;
textOptional?: boolean;
textData?: boolean;
iconRight?: boolean;
textHint?: boolean;
}
export function SelectInputView({
@@ -59,6 +66,12 @@ export function SelectInputView({
menuRef,
ariaLabelledby,
ariaInvalid,
asterisk = false,
iconHelp = true,
textOptional = false,
textData = true,
iconRight = true,
textHint = false,
}: SelectInputViewProps) {
// Styles based on Figma design
const containerClasses = "flex flex-col gap-[8px]";
@@ -135,14 +148,26 @@ export function SelectInputView({
>
{label}
</label>
<div className="relative shrink-0 size-[12px]">
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt="Help"
className="block max-w-none size-full"
/>
</div>
{asterisk && (
<span className="text-[var(--color-content-default-negative-primary,#ea4845)] text-[10px] leading-[12px] font-medium">
*
</span>
)}
{iconHelp && (
<div className="relative shrink-0 size-[12px]">
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt="Help"
className="block max-w-none size-full"
/>
</div>
)}
</div>
{textOptional && (
<span className="text-[var(--color-content-default-tertiary,#b4b4b4)] text-[10px] leading-[14px] font-normal">
Optional text
</span>
)}
</div>
)}
<div className="relative">
@@ -161,24 +186,26 @@ export function SelectInputView({
onFocus={onButtonFocus}
onBlur={onButtonBlur}
>
<span className={`flex-1 text-left pr-[32px] ${textColorClass}`}>
{displayText}
<span className={`flex-1 text-left ${iconRight ? "pr-[32px]" : ""} ${textColorClass}`}>
{textData ? displayText : placeholder}
</span>
<div className="flex items-center justify-center shrink-0">
<svg
className={chevronClasses}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
{iconRight && (
<div className="flex items-center justify-center shrink-0">
<svg
className={chevronClasses}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
)}
</button>
{state === "focus" && (
<div
@@ -235,6 +262,13 @@ export function SelectInputView({
</div>
)}
</div>
{textHint && (
<div className="flex items-start relative shrink-0 w-full">
<p className="flex-[1_0_0] font-inter font-normal leading-[16px] min-h-px min-w-px relative text-[color:var(--color-content-default-tertiary,#b4b4b4)] text-[length:var(--sizing-300,12px)]">
Hint text here
</p>
</div>
)}
</div>
);
}
@@ -23,6 +23,8 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
type = "text",
className = "",
showHelpIcon = true,
textHint = false,
formHeader = true,
...props
},
ref,
@@ -220,6 +222,8 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
isFilled={isFilled}
inputWrapperClasses={stateStyles.inputWrapper}
focusRingClasses={stateStyles.focusRing}
textHint={textHint}
formHeader={formHeader}
{...props}
/>
);
@@ -19,6 +19,16 @@ export interface TextInputProps extends Omit<
onBlur?: (_e: React.FocusEvent<HTMLInputElement>) => void;
className?: string;
showHelpIcon?: boolean;
/**
* Whether to show hint text below input (Figma prop).
* @default false
*/
textHint?: boolean;
/**
* Whether to show form header (label and help icon) above input (Figma prop).
* @default true
*/
formHeader?: boolean;
}
export interface TextInputViewProps {
@@ -45,4 +55,6 @@ export interface TextInputViewProps {
isFilled?: boolean;
inputWrapperClasses?: string;
focusRingClasses?: string;
textHint?: boolean;
formHeader?: boolean;
}
@@ -26,12 +26,14 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
showHelpIcon = true,
inputWrapperClasses = "relative",
focusRingClasses = "",
textHint = false,
formHeader = true,
},
ref,
) => {
return (
<div className={containerClasses}>
{label && (
{formHeader && 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
@@ -75,6 +77,13 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
<div className={focusRingClasses} aria-hidden="true" />
)}
</div>
{textHint && (
<div className="flex items-start relative shrink-0 w-full">
<p className="flex-[1_0_0] font-inter font-normal leading-[16px] min-h-px min-w-px relative text-[color:var(--color-content-default-tertiary,#b4b4b4)] text-[length:var(--sizing-300,12px)]">
Hint text here
</p>
</div>
)}
</div>
);
},
@@ -11,6 +11,8 @@ const AlertContainer = memo<AlertProps>(
description,
status: statusProp = "default",
type: typeProp = "toast",
hasLeadingIcon = true,
hasBodyText = true,
onClose,
className = "",
}) => {
@@ -104,6 +106,8 @@ const AlertContainer = memo<AlertProps>(
description={description}
status={status}
type={type}
hasLeadingIcon={hasLeadingIcon}
hasBodyText={hasBodyText}
className={className}
containerClasses={containerClasses}
containerStyle={containerStyle}
@@ -23,6 +23,16 @@ export interface AlertProps {
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
type?: AlertTypeValue;
/**
* Whether to show the leading icon (Figma prop).
* @default true
*/
hasLeadingIcon?: boolean;
/**
* Whether to show body text/description (Figma prop).
* @default true
*/
hasBodyText?: boolean;
onClose?: () => void;
className?: string;
}
@@ -32,6 +42,8 @@ export interface AlertViewProps {
description?: string;
status: "default" | "positive" | "warning" | "danger";
type: "toast" | "banner";
hasLeadingIcon: boolean;
hasBodyText: boolean;
className: string;
containerClasses: string;
containerStyle?: React.CSSProperties;
+10 -4
View File
@@ -6,6 +6,8 @@ export function AlertView({
description,
status: _status,
type: _type,
hasLeadingIcon,
hasBodyText,
className,
containerClasses,
containerStyle,
@@ -41,12 +43,16 @@ export function AlertView({
style={containerStyle}
role="alert"
>
<div className="shrink-0 w-[24px] h-[24px] flex items-center justify-center">
{getIcon()}
</div>
{hasLeadingIcon && (
<div className="shrink-0 w-[24px] h-[24px] flex items-center justify-center">
{getIcon()}
</div>
)}
<div className="flex flex-1 flex-col items-start justify-center min-h-0 min-w-0">
<p className={titleClasses}>{title}</p>
{description && <p className={descriptionClasses}>{description}</p>}
{hasBodyText && description && (
<p className={descriptionClasses}>{description}</p>
)}
</div>
<Button
variant="ghost"
+88
View File
@@ -692,3 +692,91 @@ export function normalizeMenuBarItemSize(
return defaultValue;
}
/**
* Normalize button type prop values (Filled/Outline/Ghost/Danger -> filled/outline/ghost/danger)
*/
export function normalizeButtonType(
value: string | undefined,
defaultValue: "filled" = "filled"
): "filled" | "outline" | "ghost" | "danger" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
const types = ["filled", "outline", "ghost", "danger"];
if (types.includes(normalized)) {
return normalized as typeof defaultValue;
}
return defaultValue;
}
/**
* Normalize button palette prop values (Default/Invert -> default/inverse)
*/
export function normalizeButtonPalette(
value: string | undefined,
defaultValue: "default" = "default"
): "default" | "inverse" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
// Handle "invert" -> "inverse" mapping
if (normalized === "invert" || normalized === "inverse") {
return "inverse";
}
if (normalized === "default") {
return "default";
}
return defaultValue;
}
/**
* Normalize button state prop values (Default/Focus/Active/Hover/Disabled -> default/focus/active/hover/disabled)
*/
export function normalizeButtonState(
value: string | undefined,
defaultValue: "default" = "default"
): "default" | "focus" | "active" | "hover" | "disabled" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
const states = ["default", "focus", "active", "hover", "disabled"];
if (states.includes(normalized)) {
return normalized as typeof defaultValue;
}
return defaultValue;
}
/**
* Type helper for case-insensitive button type prop
*/
export type ButtonTypeValue =
| "filled"
| "outline"
| "ghost"
| "danger"
| "Filled"
| "Outline"
| "Ghost"
| "Danger";
/**
* Type helper for case-insensitive button palette prop
*/
export type ButtonPaletteValue =
| "default"
| "inverse"
| "Default"
| "Invert"
| "Inverse";
/**
* Type helper for case-insensitive button state prop
*/
export type ButtonStateValue =
| "default"
| "focus"
| "active"
| "hover"
| "disabled"
| "Default"
| "Focus"
| "Active"
| "Hover"
| "Disabled";