adilallo/maintanence/ComponentOrganizationPolish #40
@@ -1,19 +1,64 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import type { VariantValue, SizeValue } from "../../../lib/propNormalization";
|
import type {
|
||||||
import { normalizeVariant, normalizeSize } from "../../../lib/propNormalization";
|
VariantValue,
|
||||||
|
SizeValue,
|
||||||
|
ButtonTypeValue,
|
||||||
|
ButtonPaletteValue,
|
||||||
|
ButtonStateValue,
|
||||||
|
} from "../../../lib/propNormalization";
|
||||||
|
import {
|
||||||
|
normalizeVariant,
|
||||||
|
normalizeSize,
|
||||||
|
normalizeButtonType,
|
||||||
|
normalizeButtonPalette,
|
||||||
|
} from "../../../lib/propNormalization";
|
||||||
|
|
||||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
children: React.ReactNode;
|
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).
|
* Button variant. Accepts both lowercase and PascalCase (case-insensitive).
|
||||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||||
*/
|
*/
|
||||||
variant?: VariantValue;
|
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).
|
* Button size. Accepts both lowercase and PascalCase (case-insensitive).
|
||||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||||
|
* @default "xsmall"
|
||||||
*/
|
*/
|
||||||
size?: SizeValue;
|
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;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
type?: "button" | "submit" | "reset";
|
type?: "button" | "submit" | "reset";
|
||||||
@@ -29,11 +74,17 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
const Button = memo<ButtonProps>(
|
const Button = memo<ButtonProps>(
|
||||||
({
|
({
|
||||||
children,
|
children,
|
||||||
variant: variantProp = "filled",
|
variant: variantProp,
|
||||||
|
buttonType: typeProp,
|
||||||
|
palette: paletteProp,
|
||||||
size: sizeProp = "xsmall",
|
size: sizeProp = "xsmall",
|
||||||
|
state: _stateProp,
|
||||||
|
hasIconLeading = false,
|
||||||
|
hasIconFollowing = false,
|
||||||
|
hasText = true,
|
||||||
className = "",
|
className = "",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
type = "button",
|
type: htmlType = "button",
|
||||||
onClick,
|
onClick,
|
||||||
href,
|
href,
|
||||||
target,
|
target,
|
||||||
@@ -41,9 +92,68 @@ const Button = memo<ButtonProps>(
|
|||||||
ariaLabel,
|
ariaLabel,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
// Determine type and palette from either new props or legacy variant prop
|
||||||
const variant = normalizeVariant(variantProp);
|
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);
|
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> = {
|
const sizeStyles: Record<string, string> = {
|
||||||
xsmall:
|
xsmall:
|
||||||
"p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]",
|
"p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]",
|
||||||
@@ -102,6 +212,10 @@ const Button = memo<ButtonProps>(
|
|||||||
? ""
|
? ""
|
||||||
: hoverOutlineStyles[size];
|
: 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 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}`;
|
const combinedStyles = `${baseStyles} ${className}`;
|
||||||
|
|
||||||
@@ -111,6 +225,16 @@ const Button = memo<ButtonProps>(
|
|||||||
tabIndex: disabled ? -1 : 0,
|
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) {
|
if (href && !disabled) {
|
||||||
const anchorProps: React.AnchorHTMLAttributes<HTMLAnchorElement> = {
|
const anchorProps: React.AnchorHTMLAttributes<HTMLAnchorElement> = {
|
||||||
href,
|
href,
|
||||||
@@ -121,11 +245,11 @@ const Button = memo<ButtonProps>(
|
|||||||
...(rel && { rel }),
|
...(rel && { rel }),
|
||||||
};
|
};
|
||||||
|
|
||||||
return <a {...anchorProps}>{children}</a>;
|
return <a {...anchorProps}>{renderContent()}</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttonProps: React.ButtonHTMLAttributes<HTMLButtonElement> = {
|
const buttonProps: React.ButtonHTMLAttributes<HTMLButtonElement> = {
|
||||||
type,
|
type: htmlType,
|
||||||
className: combinedStyles,
|
className: combinedStyles,
|
||||||
disabled,
|
disabled,
|
||||||
onClick,
|
onClick,
|
||||||
@@ -133,7 +257,7 @@ const Button = memo<ButtonProps>(
|
|||||||
...props,
|
...props,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <button {...buttonProps}>{children}</button>;
|
return <button {...buttonProps}>{renderContent()}</button>;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,18 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
|||||||
(
|
(
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
label,
|
label: labelProp,
|
||||||
|
labelText,
|
||||||
|
showLabel,
|
||||||
labelVariant: labelVariantProp,
|
labelVariant: labelVariantProp,
|
||||||
size: sizeProp,
|
size: sizeProp,
|
||||||
state: externalStateProp = "default",
|
state: externalStateProp = "default",
|
||||||
|
asterisk = false,
|
||||||
|
iconHelp = true,
|
||||||
|
textOptional = false,
|
||||||
|
textData = true,
|
||||||
|
iconRight = true,
|
||||||
|
textHint = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
error = false,
|
error = false,
|
||||||
placeholder = "Choose an option",
|
placeholder = "Choose an option",
|
||||||
@@ -38,6 +46,17 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
|||||||
},
|
},
|
||||||
ref,
|
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)
|
// 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
|
// Note: labelVariant and size are normalized for future use but not yet implemented in the view
|
||||||
const _labelVariant = labelVariantProp ? normalizeLabelVariant(labelVariantProp) : undefined;
|
const _labelVariant = labelVariantProp ? normalizeLabelVariant(labelVariantProp) : undefined;
|
||||||
@@ -45,7 +64,6 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
|||||||
// Mark as intentionally unused for future implementation
|
// Mark as intentionally unused for future implementation
|
||||||
void _labelVariant;
|
void _labelVariant;
|
||||||
void _size;
|
void _size;
|
||||||
const externalState = normalizeState(externalStateProp);
|
|
||||||
|
|
||||||
const generatedId = useId();
|
const generatedId = useId();
|
||||||
const selectId = id || `select-input-${generatedId}`;
|
const selectId = id || `select-input-${generatedId}`;
|
||||||
@@ -193,7 +211,7 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectInputView
|
<SelectInputView
|
||||||
label={label}
|
label={shouldShowLabel ? actualLabelText : undefined}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
state={actualState}
|
state={actualState}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -214,8 +232,14 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
|||||||
onOptionClick={handleOptionSelect}
|
onOptionClick={handleOptionSelect}
|
||||||
selectRef={selectRef}
|
selectRef={selectRef}
|
||||||
menuRef={menuRef}
|
menuRef={menuRef}
|
||||||
ariaLabelledby={label ? labelId : undefined}
|
ariaLabelledby={shouldShowLabel ? labelId : undefined}
|
||||||
ariaInvalid={error}
|
ariaInvalid={error}
|
||||||
|
asterisk={asterisk}
|
||||||
|
iconHelp={iconHelp}
|
||||||
|
textOptional={textOptional}
|
||||||
|
textData={textData}
|
||||||
|
iconRight={iconRight}
|
||||||
|
textHint={textHint}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,21 @@ export type SelectInputSizeValue = "small" | "medium" | "large" | "Small" | "Med
|
|||||||
|
|
||||||
export interface SelectInputProps {
|
export interface SelectInputProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
/**
|
||||||
|
* Label text (backward compatibility - if provided, label is shown).
|
||||||
|
* For Figma alignment, use `labelText` prop instead.
|
||||||
|
*/
|
||||||
label?: string;
|
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).
|
* Label variant. Accepts both lowercase and PascalCase (case-insensitive).
|
||||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||||
@@ -24,10 +38,40 @@ export interface SelectInputProps {
|
|||||||
*/
|
*/
|
||||||
size?: SelectInputSizeValue;
|
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.
|
* 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;
|
disabled?: boolean;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ export interface SelectInputViewProps {
|
|||||||
// Additional props
|
// Additional props
|
||||||
ariaLabelledby?: string;
|
ariaLabelledby?: string;
|
||||||
ariaInvalid?: boolean;
|
ariaInvalid?: boolean;
|
||||||
|
// Figma props
|
||||||
|
asterisk?: boolean;
|
||||||
|
iconHelp?: boolean;
|
||||||
|
textOptional?: boolean;
|
||||||
|
textData?: boolean;
|
||||||
|
iconRight?: boolean;
|
||||||
|
textHint?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectInputView({
|
export function SelectInputView({
|
||||||
@@ -59,6 +66,12 @@ export function SelectInputView({
|
|||||||
menuRef,
|
menuRef,
|
||||||
ariaLabelledby,
|
ariaLabelledby,
|
||||||
ariaInvalid,
|
ariaInvalid,
|
||||||
|
asterisk = false,
|
||||||
|
iconHelp = true,
|
||||||
|
textOptional = false,
|
||||||
|
textData = true,
|
||||||
|
iconRight = true,
|
||||||
|
textHint = false,
|
||||||
}: SelectInputViewProps) {
|
}: SelectInputViewProps) {
|
||||||
// Styles based on Figma design
|
// Styles based on Figma design
|
||||||
const containerClasses = "flex flex-col gap-[8px]";
|
const containerClasses = "flex flex-col gap-[8px]";
|
||||||
@@ -135,14 +148,26 @@ export function SelectInputView({
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative shrink-0 size-[12px]">
|
{asterisk && (
|
||||||
<img
|
<span className="text-[var(--color-content-default-negative-primary,#ea4845)] text-[10px] leading-[12px] font-medium">
|
||||||
src={getAssetPath(ASSETS.ICON_HELP)}
|
*
|
||||||
alt="Help"
|
</span>
|
||||||
className="block max-w-none size-full"
|
)}
|
||||||
/>
|
{iconHelp && (
|
||||||
</div>
|
<div className="relative shrink-0 size-[12px]">
|
||||||
|
<img
|
||||||
|
src={getAssetPath(ASSETS.ICON_HELP)}
|
||||||
|
alt="Help"
|
||||||
|
className="block max-w-none size-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{textOptional && (
|
||||||
|
<span className="text-[var(--color-content-default-tertiary,#b4b4b4)] text-[10px] leading-[14px] font-normal">
|
||||||
|
Optional text
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -161,24 +186,26 @@ export function SelectInputView({
|
|||||||
onFocus={onButtonFocus}
|
onFocus={onButtonFocus}
|
||||||
onBlur={onButtonBlur}
|
onBlur={onButtonBlur}
|
||||||
>
|
>
|
||||||
<span className={`flex-1 text-left pr-[32px] ${textColorClass}`}>
|
<span className={`flex-1 text-left ${iconRight ? "pr-[32px]" : ""} ${textColorClass}`}>
|
||||||
{displayText}
|
{textData ? displayText : placeholder}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center justify-center shrink-0">
|
{iconRight && (
|
||||||
<svg
|
<div className="flex items-center justify-center shrink-0">
|
||||||
className={chevronClasses}
|
<svg
|
||||||
fill="none"
|
className={chevronClasses}
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
stroke="currentColor"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
<path
|
>
|
||||||
strokeLinecap="round"
|
<path
|
||||||
strokeLinejoin="round"
|
strokeLinecap="round"
|
||||||
strokeWidth={2}
|
strokeLinejoin="round"
|
||||||
d="M19 9l-7 7-7-7"
|
strokeWidth={2}
|
||||||
/>
|
d="M19 9l-7 7-7-7"
|
||||||
</svg>
|
/>
|
||||||
</div>
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
{state === "focus" && (
|
{state === "focus" && (
|
||||||
<div
|
<div
|
||||||
@@ -235,6 +262,13 @@ export function SelectInputView({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
|||||||
type = "text",
|
type = "text",
|
||||||
className = "",
|
className = "",
|
||||||
showHelpIcon = true,
|
showHelpIcon = true,
|
||||||
|
textHint = false,
|
||||||
|
formHeader = true,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -220,6 +222,8 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
|||||||
isFilled={isFilled}
|
isFilled={isFilled}
|
||||||
inputWrapperClasses={stateStyles.inputWrapper}
|
inputWrapperClasses={stateStyles.inputWrapper}
|
||||||
focusRingClasses={stateStyles.focusRing}
|
focusRingClasses={stateStyles.focusRing}
|
||||||
|
textHint={textHint}
|
||||||
|
formHeader={formHeader}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ export interface TextInputProps extends Omit<
|
|||||||
onBlur?: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
onBlur?: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
showHelpIcon?: boolean;
|
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 {
|
export interface TextInputViewProps {
|
||||||
@@ -45,4 +55,6 @@ export interface TextInputViewProps {
|
|||||||
isFilled?: boolean;
|
isFilled?: boolean;
|
||||||
inputWrapperClasses?: string;
|
inputWrapperClasses?: string;
|
||||||
focusRingClasses?: string;
|
focusRingClasses?: string;
|
||||||
|
textHint?: boolean;
|
||||||
|
formHeader?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
|
|||||||
showHelpIcon = true,
|
showHelpIcon = true,
|
||||||
inputWrapperClasses = "relative",
|
inputWrapperClasses = "relative",
|
||||||
focusRingClasses = "",
|
focusRingClasses = "",
|
||||||
|
textHint = false,
|
||||||
|
formHeader = true,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<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 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">
|
<div className="flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0">
|
||||||
<label
|
<label
|
||||||
@@ -75,6 +77,13 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
|
|||||||
<div className={focusRingClasses} aria-hidden="true" />
|
<div className={focusRingClasses} aria-hidden="true" />
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const AlertContainer = memo<AlertProps>(
|
|||||||
description,
|
description,
|
||||||
status: statusProp = "default",
|
status: statusProp = "default",
|
||||||
type: typeProp = "toast",
|
type: typeProp = "toast",
|
||||||
|
hasLeadingIcon = true,
|
||||||
|
hasBodyText = true,
|
||||||
onClose,
|
onClose,
|
||||||
className = "",
|
className = "",
|
||||||
}) => {
|
}) => {
|
||||||
@@ -104,6 +106,8 @@ const AlertContainer = memo<AlertProps>(
|
|||||||
description={description}
|
description={description}
|
||||||
status={status}
|
status={status}
|
||||||
type={type}
|
type={type}
|
||||||
|
hasLeadingIcon={hasLeadingIcon}
|
||||||
|
hasBodyText={hasBodyText}
|
||||||
className={className}
|
className={className}
|
||||||
containerClasses={containerClasses}
|
containerClasses={containerClasses}
|
||||||
containerStyle={containerStyle}
|
containerStyle={containerStyle}
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ export interface AlertProps {
|
|||||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||||
*/
|
*/
|
||||||
type?: AlertTypeValue;
|
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;
|
onClose?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@@ -32,6 +42,8 @@ export interface AlertViewProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
status: "default" | "positive" | "warning" | "danger";
|
status: "default" | "positive" | "warning" | "danger";
|
||||||
type: "toast" | "banner";
|
type: "toast" | "banner";
|
||||||
|
hasLeadingIcon: boolean;
|
||||||
|
hasBodyText: boolean;
|
||||||
className: string;
|
className: string;
|
||||||
containerClasses: string;
|
containerClasses: string;
|
||||||
containerStyle?: React.CSSProperties;
|
containerStyle?: React.CSSProperties;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export function AlertView({
|
|||||||
description,
|
description,
|
||||||
status: _status,
|
status: _status,
|
||||||
type: _type,
|
type: _type,
|
||||||
|
hasLeadingIcon,
|
||||||
|
hasBodyText,
|
||||||
className,
|
className,
|
||||||
containerClasses,
|
containerClasses,
|
||||||
containerStyle,
|
containerStyle,
|
||||||
@@ -41,12 +43,16 @@ export function AlertView({
|
|||||||
style={containerStyle}
|
style={containerStyle}
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<div className="shrink-0 w-[24px] h-[24px] flex items-center justify-center">
|
{hasLeadingIcon && (
|
||||||
{getIcon()}
|
<div className="shrink-0 w-[24px] h-[24px] flex items-center justify-center">
|
||||||
</div>
|
{getIcon()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex flex-1 flex-col items-start justify-center min-h-0 min-w-0">
|
<div className="flex flex-1 flex-col items-start justify-center min-h-0 min-w-0">
|
||||||
<p className={titleClasses}>{title}</p>
|
<p className={titleClasses}>{title}</p>
|
||||||
{description && <p className={descriptionClasses}>{description}</p>}
|
{hasBodyText && description && (
|
||||||
|
<p className={descriptionClasses}>{description}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -692,3 +692,91 @@ export function normalizeMenuBarItemSize(
|
|||||||
return defaultValue;
|
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";
|
||||||
Reference in New Issue
Block a user