diff --git a/app/components/buttons/Button.tsx b/app/components/buttons/Button.tsx index d9894fd..109092c 100644 --- a/app/components/buttons/Button.tsx +++ b/app/components/buttons/Button.tsx @@ -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 { 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 { const Button = memo( ({ 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( 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 = { xsmall: "p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]", @@ -102,6 +212,10 @@ const Button = memo( ? "" : 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( 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 = { href, @@ -121,11 +245,11 @@ const Button = memo( ...(rel && { rel }), }; - return {children}; + return {renderContent()}; } const buttonProps: React.ButtonHTMLAttributes = { - type, + type: htmlType, className: combinedStyles, disabled, onClick, @@ -133,7 +257,7 @@ const Button = memo( ...props, }; - return ; + return ; }, ); diff --git a/app/components/controls/SelectInput/SelectInput.container.tsx b/app/components/controls/SelectInput/SelectInput.container.tsx index 63c1338..ce60a5a 100644 --- a/app/components/controls/SelectInput/SelectInput.container.tsx +++ b/app/components/controls/SelectInput/SelectInput.container.tsx @@ -22,10 +22,18 @@ const SelectInputContainer = forwardRef( ( { 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( }, 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( // 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( return ( ( 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} /> ); diff --git a/app/components/controls/SelectInput/SelectInput.types.ts b/app/components/controls/SelectInput/SelectInput.types.ts index 3dbb427..62b64f0 100644 --- a/app/components/controls/SelectInput/SelectInput.types.ts +++ b/app/components/controls/SelectInput/SelectInput.types.ts @@ -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; diff --git a/app/components/controls/SelectInput/SelectInput.view.tsx b/app/components/controls/SelectInput/SelectInput.view.tsx index caeea8c..c0ed884 100644 --- a/app/components/controls/SelectInput/SelectInput.view.tsx +++ b/app/components/controls/SelectInput/SelectInput.view.tsx @@ -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} -
- Help -
+ {asterisk && ( + + * + + )} + {iconHelp && ( +
+ Help +
+ )} + {textOptional && ( + + Optional text + + )} )}
@@ -161,24 +186,26 @@ export function SelectInputView({ onFocus={onButtonFocus} onBlur={onButtonBlur} > - - {displayText} + + {textData ? displayText : placeholder} -
- - - -
+ {iconRight && ( +
+ + + +
+ )} {state === "focus" && (
)}
+ {textHint && ( +
+

+ Hint text here +

+
+ )}
); } diff --git a/app/components/controls/TextInput/TextInput.container.tsx b/app/components/controls/TextInput/TextInput.container.tsx index 26429cb..45294d4 100644 --- a/app/components/controls/TextInput/TextInput.container.tsx +++ b/app/components/controls/TextInput/TextInput.container.tsx @@ -23,6 +23,8 @@ const TextInputContainer = forwardRef( type = "text", className = "", showHelpIcon = true, + textHint = false, + formHeader = true, ...props }, ref, @@ -220,6 +222,8 @@ const TextInputContainer = forwardRef( isFilled={isFilled} inputWrapperClasses={stateStyles.inputWrapper} focusRingClasses={stateStyles.focusRing} + textHint={textHint} + formHeader={formHeader} {...props} /> ); diff --git a/app/components/controls/TextInput/TextInput.types.ts b/app/components/controls/TextInput/TextInput.types.ts index 3814d0a..a09d4ef 100644 --- a/app/components/controls/TextInput/TextInput.types.ts +++ b/app/components/controls/TextInput/TextInput.types.ts @@ -19,6 +19,16 @@ export interface TextInputProps extends Omit< onBlur?: (_e: React.FocusEvent) => 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; } diff --git a/app/components/controls/TextInput/TextInput.view.tsx b/app/components/controls/TextInput/TextInput.view.tsx index 99766d2..661e0b9 100644 --- a/app/components/controls/TextInput/TextInput.view.tsx +++ b/app/components/controls/TextInput/TextInput.view.tsx @@ -26,12 +26,14 @@ export const TextInputView = forwardRef( showHelpIcon = true, inputWrapperClasses = "relative", focusRingClasses = "", + textHint = false, + formHeader = true, }, ref, ) => { return (
- {label && ( + {formHeader && label && (
); }, diff --git a/app/components/modals/Alert/Alert.container.tsx b/app/components/modals/Alert/Alert.container.tsx index 417a045..df27eff 100644 --- a/app/components/modals/Alert/Alert.container.tsx +++ b/app/components/modals/Alert/Alert.container.tsx @@ -11,6 +11,8 @@ const AlertContainer = memo( description, status: statusProp = "default", type: typeProp = "toast", + hasLeadingIcon = true, + hasBodyText = true, onClose, className = "", }) => { @@ -104,6 +106,8 @@ const AlertContainer = memo( description={description} status={status} type={type} + hasLeadingIcon={hasLeadingIcon} + hasBodyText={hasBodyText} className={className} containerClasses={containerClasses} containerStyle={containerStyle} diff --git a/app/components/modals/Alert/Alert.types.ts b/app/components/modals/Alert/Alert.types.ts index 236e883..e43ff15 100644 --- a/app/components/modals/Alert/Alert.types.ts +++ b/app/components/modals/Alert/Alert.types.ts @@ -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; diff --git a/app/components/modals/Alert/Alert.view.tsx b/app/components/modals/Alert/Alert.view.tsx index 5bae88e..db8726b 100644 --- a/app/components/modals/Alert/Alert.view.tsx +++ b/app/components/modals/Alert/Alert.view.tsx @@ -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" > -
- {getIcon()} -
+ {hasLeadingIcon && ( +
+ {getIcon()} +
+ )}

{title}

- {description &&

{description}

} + {hasBodyText && description && ( +

{description}

+ )}