diff --git a/.gitignore b/.gitignore index b78b105..99e3022 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,6 @@ act_runner # OS files Thumbs.db .DS_Store + +# Cursor rules (local development) +.cursorrules diff --git a/app/components/Alert/Alert.container.tsx b/app/components/Alert/Alert.container.tsx index 0435ab6..26b704a 100644 --- a/app/components/Alert/Alert.container.tsx +++ b/app/components/Alert/Alert.container.tsx @@ -3,16 +3,20 @@ import { memo } from "react"; import { AlertView } from "./Alert.view"; import type { AlertProps } from "./Alert.types"; +import { normalizeAlertStatus, normalizeAlertType } from "../../../lib/propNormalization"; const AlertContainer = memo( ({ title, description, - status = "default", - type = "toast", + status: statusProp = "default", + type: typeProp = "toast", onClose, className = "", }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const status = normalizeAlertStatus(statusProp); + const type = normalizeAlertType(typeProp); // Determine background and border colors based on status and type const getStatusStyles = () => { switch (status) { diff --git a/app/components/Alert/Alert.types.ts b/app/components/Alert/Alert.types.ts index 466a09a..236e883 100644 --- a/app/components/Alert/Alert.types.ts +++ b/app/components/Alert/Alert.types.ts @@ -1,8 +1,28 @@ +export type AlertStatusValue = + | "default" + | "positive" + | "warning" + | "danger" + | "Default" + | "Positive" + | "Warning" + | "Danger"; + +export type AlertTypeValue = "toast" | "banner" | "Toast" | "Banner"; + export interface AlertProps { title: string; description?: string; - status?: "default" | "positive" | "warning" | "danger"; - type?: "toast" | "banner"; + /** + * Alert status. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + status?: AlertStatusValue; + /** + * Alert type. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + type?: AlertTypeValue; onClose?: () => void; className?: string; } diff --git a/app/components/AskOrganizer/AskOrganizer.container.tsx b/app/components/AskOrganizer/AskOrganizer.container.tsx index d9c6ac5..831f4ff 100644 --- a/app/components/AskOrganizer/AskOrganizer.container.tsx +++ b/app/components/AskOrganizer/AskOrganizer.container.tsx @@ -8,6 +8,7 @@ import type { AskOrganizerProps, AskOrganizerVariant, } from "./AskOrganizer.types"; +import { normalizeAskOrganizerVariant } from "../../../lib/propNormalization"; const VARIANT_STYLES: Record< AskOrganizerVariant, @@ -39,9 +40,11 @@ const AskOrganizerContainer = memo( buttonText, buttonHref, className = "", - variant = "centered", + variant: variantProp = "centered", onContactClick, }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const variant = normalizeAskOrganizerVariant(variantProp) as AskOrganizerVariant; const t = useTranslation(); const defaultButtonText = buttonText ?? t("askOrganizer.buttonText"); const defaultButtonHref = buttonHref ?? t("askOrganizer.buttonHref"); diff --git a/app/components/AskOrganizer/AskOrganizer.types.ts b/app/components/AskOrganizer/AskOrganizer.types.ts index e7bd031..ed425f2 100644 --- a/app/components/AskOrganizer/AskOrganizer.types.ts +++ b/app/components/AskOrganizer/AskOrganizer.types.ts @@ -4,7 +4,11 @@ export type AskOrganizerVariant = | "centered" | "left-aligned" | "compact" - | "inverse"; + | "inverse" + | "Centered" + | "Left-Aligned" + | "Compact" + | "Inverse"; export interface AskOrganizerProps { title?: string; @@ -13,6 +17,10 @@ export interface AskOrganizerProps { buttonText?: string; buttonHref?: string; className?: string; + /** + * Ask organizer variant. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ variant?: AskOrganizerVariant; onContactClick?: (_data: { event: string; diff --git a/app/components/Avatar.tsx b/app/components/Avatar.tsx index e17cb49..712b847 100644 --- a/app/components/Avatar.tsx +++ b/app/components/Avatar.tsx @@ -1,14 +1,23 @@ import { memo } from "react"; +import { normalizeSize } from "../../lib/propNormalization"; + +export type AvatarSizeValue = "small" | "medium" | "large" | "xlarge" | "Small" | "Medium" | "Large" | "XLarge"; interface AvatarProps extends React.ImgHTMLAttributes { src: string; alt: string; - size?: "small" | "medium" | "large" | "xlarge"; + /** + * Avatar size. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + size?: AvatarSizeValue; className?: string; } const Avatar = memo( - ({ src, alt, size = "small", className = "", ...props }) => { + ({ src, alt, size: sizeProp = "small", className = "", ...props }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const size = normalizeSize(sizeProp, "small"); const sizeStyles: Record = { small: "w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)]", medium: "w-[18px] h-[18px]", diff --git a/app/components/AvatarContainer.tsx b/app/components/AvatarContainer.tsx index e036968..e5d7c08 100644 --- a/app/components/AvatarContainer.tsx +++ b/app/components/AvatarContainer.tsx @@ -1,13 +1,22 @@ import { memo } from "react"; +import { normalizeSize } from "../../lib/propNormalization"; + +export type AvatarContainerSizeValue = "small" | "medium" | "large" | "xlarge" | "Small" | "Medium" | "Large" | "XLarge"; interface AvatarContainerProps extends React.HTMLAttributes { children?: React.ReactNode; - size?: "small" | "medium" | "large" | "xlarge"; + /** + * Avatar container size. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + size?: AvatarContainerSizeValue; className?: string; } const AvatarContainer = memo( - ({ children, size = "small", className = "", ...props }) => { + ({ children, size: sizeProp = "small", className = "", ...props }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const size = normalizeSize(sizeProp, "small"); const sizeStyles: Record = { small: "flex -space-x-[var(--spacing-scale-008)]", medium: "flex -space-x-[9px]", diff --git a/app/components/Button.tsx b/app/components/Button.tsx index 7640d96..dcd65bb 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -1,17 +1,19 @@ import { memo } from "react"; +import type { VariantValue, SizeValue } from "../../lib/propNormalization"; +import { normalizeVariant, normalizeSize } from "../../lib/propNormalization"; interface ButtonProps extends React.ButtonHTMLAttributes { children: React.ReactNode; - variant?: - | "filled" - | "filled-inverse" - | "outline" - | "outline-inverse" - | "ghost" - | "ghost-inverse" - | "danger" - | "danger-inverse"; - size?: "xsmall" | "small" | "medium" | "large" | "xlarge"; + /** + * Button variant. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + variant?: VariantValue; + /** + * Button size. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + size?: SizeValue; className?: string; disabled?: boolean; type?: "button" | "submit" | "reset"; @@ -27,8 +29,8 @@ interface ButtonProps extends React.ButtonHTMLAttributes { const Button = memo( ({ children, - variant = "filled", - size = "xsmall", + variant: variantProp = "filled", + size: sizeProp = "xsmall", className = "", disabled = false, type = "button", @@ -39,6 +41,9 @@ const Button = memo( ariaLabel, ...props }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const variant = normalizeVariant(variantProp); + const size = normalizeSize(sizeProp); const sizeStyles: Record = { xsmall: "p-[var(--spacing-scale-006)] gap-[var(--spacing-scale-002)]", diff --git a/app/components/Checkbox/Checkbox.container.tsx b/app/components/Checkbox/Checkbox.container.tsx index a8a00b1..42c531b 100644 --- a/app/components/Checkbox/Checkbox.container.tsx +++ b/app/components/Checkbox/Checkbox.container.tsx @@ -4,12 +4,13 @@ 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"; const CheckboxContainer = memo( ({ checked = false, - mode = "standard", - state = "default", + mode: modeProp = "standard", + state: stateProp = "default", disabled = false, label, className = "", @@ -20,6 +21,10 @@ const CheckboxContainer = memo( ariaLabel, ...props }) => { + // 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"; diff --git a/app/components/Checkbox/Checkbox.types.ts b/app/components/Checkbox/Checkbox.types.ts index 70ba736..8faefb9 100644 --- a/app/components/Checkbox/Checkbox.types.ts +++ b/app/components/Checkbox/Checkbox.types.ts @@ -1,7 +1,17 @@ +import type { ModeValue, StateValue } from "../../../lib/propNormalization"; + export interface CheckboxProps { checked?: boolean; - mode?: "standard" | "inverse"; - state?: "default" | "hover" | "focus"; + /** + * Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + mode?: ModeValue; + /** + * Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + state?: StateValue; disabled?: boolean; label?: string; className?: string; diff --git a/app/components/CheckboxGroup/CheckboxGroup.container.tsx b/app/components/CheckboxGroup/CheckboxGroup.container.tsx index a45bd5c..cde1c3c 100644 --- a/app/components/CheckboxGroup/CheckboxGroup.container.tsx +++ b/app/components/CheckboxGroup/CheckboxGroup.container.tsx @@ -3,17 +3,20 @@ import { memo, useCallback, useId, useState } from "react"; import { CheckboxGroupView } from "./CheckboxGroup.view"; import type { CheckboxGroupProps } from "./CheckboxGroup.types"; +import { normalizeMode } from "../../../lib/propNormalization"; const CheckboxGroupContainer = ({ name, value, onChange, - mode = "standard", + mode: modeProp = "standard", disabled = false, options = [], className = "", ...props }: CheckboxGroupProps) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const mode = normalizeMode(modeProp); // Generate unique ID for accessibility if not provided const generatedId = useId(); const groupId = name || `checkbox-group-${generatedId}`; diff --git a/app/components/CheckboxGroup/CheckboxGroup.types.ts b/app/components/CheckboxGroup/CheckboxGroup.types.ts index 0d98ee5..2c1e6a5 100644 --- a/app/components/CheckboxGroup/CheckboxGroup.types.ts +++ b/app/components/CheckboxGroup/CheckboxGroup.types.ts @@ -5,11 +5,17 @@ export interface CheckboxOption { ariaLabel?: string; } +import type { ModeValue } from "../../../lib/propNormalization"; + export interface CheckboxGroupProps { name?: string; value?: string[]; onChange?: (_data: { value: string[] }) => void; - mode?: "standard" | "inverse"; + /** + * Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + mode?: ModeValue; disabled?: boolean; options?: CheckboxOption[]; className?: string; diff --git a/app/components/ContentContainer/ContentContainer.container.tsx b/app/components/ContentContainer/ContentContainer.container.tsx index 5c201b7..4cacb27 100644 --- a/app/components/ContentContainer/ContentContainer.container.tsx +++ b/app/components/ContentContainer/ContentContainer.container.tsx @@ -4,9 +4,12 @@ import { memo } from "react"; import { getAssetPath, ASSETS } from "../../../lib/assetUtils"; import ContentContainerView from "./ContentContainer.view"; import type { ContentContainerProps } from "./ContentContainer.types"; +import { normalizeContentContainerSize } from "../../../lib/propNormalization"; const ContentContainerContainer = memo( - ({ post, width = "200px", size = "responsive" }) => { + ({ post, width = "200px", size: sizeProp = "responsive" }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const size = normalizeContentContainerSize(sizeProp); // Get the corresponding icon based on the same logic as background images const getIconImage = (slug: string): string => { const icons = [ diff --git a/app/components/ContentContainer/ContentContainer.types.ts b/app/components/ContentContainer/ContentContainer.types.ts index f29387e..8bfbff7 100644 --- a/app/components/ContentContainer/ContentContainer.types.ts +++ b/app/components/ContentContainer/ContentContainer.types.ts @@ -1,9 +1,15 @@ import type { BlogPost } from "../../../lib/content"; +export type ContentContainerSizeValue = "xs" | "responsive" | "Xs" | "Responsive"; + export interface ContentContainerProps { post: BlogPost; width?: string; - size?: "xs" | "responsive"; + /** + * Content container size. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + size?: ContentContainerSizeValue; } export interface ContentContainerViewProps { diff --git a/app/components/ContentLockup/ContentLockup.container.tsx b/app/components/ContentLockup/ContentLockup.container.tsx index d240cb2..70cd5ac 100644 --- a/app/components/ContentLockup/ContentLockup.container.tsx +++ b/app/components/ContentLockup/ContentLockup.container.tsx @@ -3,6 +3,7 @@ import { memo } from "react"; import ContentLockupView from "./ContentLockup.view"; import type { ContentLockupProps, VariantStyle } from "./ContentLockup.types"; +import { normalizeContentLockupVariant, normalizeAlignment } from "../../../lib/propNormalization"; const ContentLockupContainer = memo( ({ @@ -11,12 +12,15 @@ const ContentLockupContainer = memo( description, ctaText, buttonClassName = "", - variant = "hero", + variant: variantProp = "hero", linkText, linkHref, - alignment = "center", + alignment: alignmentProp = "center", titleId, }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const variant = normalizeContentLockupVariant(variantProp); + const alignment = normalizeAlignment(alignmentProp); // Variant-specific styling const variantStyles: Record = { hero: { diff --git a/app/components/ContentLockup/ContentLockup.types.ts b/app/components/ContentLockup/ContentLockup.types.ts index 61a59f8..92efaca 100644 --- a/app/components/ContentLockup/ContentLockup.types.ts +++ b/app/components/ContentLockup/ContentLockup.types.ts @@ -1,3 +1,19 @@ +export type ContentLockupVariantValue = + | "hero" + | "feature" + | "learn" + | "ask" + | "ask-inverse" + | "modal" + | "Hero" + | "Feature" + | "Learn" + | "Ask" + | "Ask-Inverse" + | "Modal"; + +export type ContentLockupAlignmentValue = "center" | "left" | "Center" | "Left"; + export interface ContentLockupProps { title?: string; subtitle?: string; @@ -5,10 +21,18 @@ export interface ContentLockupProps { ctaText?: string; ctaHref?: string; buttonClassName?: string; - variant?: "hero" | "feature" | "learn" | "ask" | "ask-inverse" | "modal"; + /** + * Content lockup variant. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + variant?: ContentLockupVariantValue; linkText?: string; linkHref?: string; - alignment?: "center" | "left"; + /** + * Text alignment. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + alignment?: ContentLockupAlignmentValue; /** * Optional id to attach to the primary title heading. * Useful when a parent section uses aria-labelledby. diff --git a/app/components/ContentThumbnailTemplate/ContentThumbnailTemplate.container.tsx b/app/components/ContentThumbnailTemplate/ContentThumbnailTemplate.container.tsx index 6fdddbc..31a1353 100644 --- a/app/components/ContentThumbnailTemplate/ContentThumbnailTemplate.container.tsx +++ b/app/components/ContentThumbnailTemplate/ContentThumbnailTemplate.container.tsx @@ -4,9 +4,12 @@ import { memo } from "react"; import { getAssetPath, ASSETS } from "../../../lib/assetUtils"; import ContentThumbnailTemplateView from "./ContentThumbnailTemplate.view"; import type { ContentThumbnailTemplateProps } from "./ContentThumbnailTemplate.types"; +import { normalizeContentThumbnailVariant } from "../../../lib/propNormalization"; const ContentThumbnailTemplateContainer = memo( - ({ post, className = "", variant = "vertical" }) => { + ({ post, className = "", variant: variantProp = "vertical" }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const variant = normalizeContentThumbnailVariant(variantProp); // Get article-specific background image from frontmatter const getBackgroundImage = ( post: ContentThumbnailTemplateProps["post"], diff --git a/app/components/ContentThumbnailTemplate/ContentThumbnailTemplate.types.ts b/app/components/ContentThumbnailTemplate/ContentThumbnailTemplate.types.ts index c680288..245abfb 100644 --- a/app/components/ContentThumbnailTemplate/ContentThumbnailTemplate.types.ts +++ b/app/components/ContentThumbnailTemplate/ContentThumbnailTemplate.types.ts @@ -1,9 +1,15 @@ import type { BlogPost } from "../../../lib/content"; +export type ContentThumbnailTemplateVariantValue = "vertical" | "horizontal" | "Vertical" | "Horizontal"; + export interface ContentThumbnailTemplateProps { post: BlogPost; className?: string; - variant?: "vertical" | "horizontal"; + /** + * Content thumbnail variant. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + variant?: ContentThumbnailTemplateVariantValue; slugOrder?: string[]; } diff --git a/app/components/ContextMenuItem/ContextMenuItem.container.tsx b/app/components/ContextMenuItem/ContextMenuItem.container.tsx index ddaa795..6b524dc 100644 --- a/app/components/ContextMenuItem/ContextMenuItem.container.tsx +++ b/app/components/ContextMenuItem/ContextMenuItem.container.tsx @@ -3,6 +3,7 @@ import { forwardRef, memo, useCallback } from "react"; import { ContextMenuItemView } from "./ContextMenuItem.view"; import type { ContextMenuItemProps } from "./ContextMenuItem.types"; +import { normalizeContextMenuItemSize } from "../../../lib/propNormalization"; const ContextMenuItemContainer = forwardRef< HTMLDivElement, @@ -16,11 +17,13 @@ const ContextMenuItemContainer = forwardRef< disabled = false, className = "", onClick, - size = "medium", + size: sizeProp = "medium", ...props }, ref, ) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const size = normalizeContextMenuItemSize(sizeProp); const getTextSize = (): string => { switch (size) { case "small": diff --git a/app/components/ContextMenuItem/ContextMenuItem.types.ts b/app/components/ContextMenuItem/ContextMenuItem.types.ts index 1580f10..ab57073 100644 --- a/app/components/ContextMenuItem/ContextMenuItem.types.ts +++ b/app/components/ContextMenuItem/ContextMenuItem.types.ts @@ -1,3 +1,5 @@ +export type ContextMenuItemSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large"; + export interface ContextMenuItemProps extends React.HTMLAttributes { children?: React.ReactNode; selected?: boolean; @@ -7,7 +9,11 @@ export interface ContextMenuItemProps extends React.HTMLAttributes | React.KeyboardEvent, ) => void; - size?: "small" | "medium" | "large"; + /** + * Context menu item size. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + size?: ContextMenuItemSizeValue; } export interface ContextMenuItemViewProps { diff --git a/app/components/ImagePlaceholder.tsx b/app/components/ImagePlaceholder.tsx index e25b572..c1a3101 100644 --- a/app/components/ImagePlaceholder.tsx +++ b/app/components/ImagePlaceholder.tsx @@ -1,12 +1,31 @@ "use client"; import { memo } from "react"; +import { normalizeImagePlaceholderColor } from "../../lib/propNormalization"; + +export type ImagePlaceholderColorValue = + | "blue" + | "green" + | "purple" + | "red" + | "orange" + | "teal" + | "Blue" + | "Green" + | "Purple" + | "Red" + | "Orange" + | "Teal"; interface ImagePlaceholderProps { width?: number; height?: number; text?: string; - color?: "blue" | "green" | "purple" | "red" | "orange" | "teal"; + /** + * Image placeholder color. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + color?: ImagePlaceholderColorValue; className?: string; } @@ -19,9 +38,11 @@ const ImagePlaceholder = memo( width = 260, height = 390, text = "Blog Image", - color = "blue", + color: colorProp = "blue", className = "", }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const color = normalizeImagePlaceholderColor(colorProp); const colors: Record = { blue: "bg-blue-500", green: "bg-green-500", diff --git a/app/components/MenuBar.tsx b/app/components/MenuBar.tsx index 18173b6..4d4cd13 100644 --- a/app/components/MenuBar.tsx +++ b/app/components/MenuBar.tsx @@ -2,15 +2,32 @@ import { memo } from "react"; import { useTranslation } from "../contexts/MessagesContext"; +import { normalizeMenuBarSize } from "../../lib/propNormalization"; + +export type MenuBarSizeValue = + | "xsmall" + | "default" + | "medium" + | "large" + | "XSmall" + | "Default" + | "Medium" + | "Large"; interface MenuBarProps extends React.HTMLAttributes { children?: React.ReactNode; className?: string; - size?: "xsmall" | "default" | "medium" | "large"; + /** + * Menu bar size. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + size?: MenuBarSizeValue; } const MenuBar = memo( - ({ children, className = "", size = "default", ...props }) => { + ({ children, className = "", size: sizeProp = "default", ...props }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const size = normalizeMenuBarSize(sizeProp); const t = useTranslation("menuBar"); const sizeStyles: Record = { xsmall: diff --git a/app/components/MenuBarItem/MenuBarItem.container.tsx b/app/components/MenuBarItem/MenuBarItem.container.tsx index 1ca4ba7..d55a605 100644 --- a/app/components/MenuBarItem/MenuBarItem.container.tsx +++ b/app/components/MenuBarItem/MenuBarItem.container.tsx @@ -3,19 +3,24 @@ import { memo } from "react"; import MenuBarItemView from "./MenuBarItem.view"; import type { MenuBarItemProps } from "./MenuBarItem.types"; +import { normalizeMenuBarItemVariant } from "../../../lib/propNormalization"; const MenuBarItemContainer = memo( ({ href = "#", children, - variant = "default", - size = "default", + variant: variantProp = "default", + size: sizeProp = "default", className = "", disabled = false, isActive = false, ariaLabel, ...props }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const variant = normalizeMenuBarItemVariant(variantProp); + // Size has many values, normalize by lowercasing + const size = (sizeProp?.toLowerCase() || "default") as typeof sizeProp; const variantStyles: Record = { default: "bg-transparent text-[var(--color-content-default-brand-primary)] hover:bg-[var(--color-surface-default-tertiary)] hover:text-[var(--color-content-default-brand-primary)] hover:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-brand-primary)] active:scale-[0.98] disabled:bg-[var(--color-surface-default-tertiary)] disabled:text-[var(--color-content-default-tertiary)] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 disabled:active:scale-100", diff --git a/app/components/MenuBarItem/MenuBarItem.types.ts b/app/components/MenuBarItem/MenuBarItem.types.ts index d8da10b..3878a49 100644 --- a/app/components/MenuBarItem/MenuBarItem.types.ts +++ b/app/components/MenuBarItem/MenuBarItem.types.ts @@ -1,18 +1,40 @@ +export type MenuBarItemSizeValue = + | "default" + | "xsmall" + | "xsmallUseCases" + | "home" + | "homeMd" + | "homeUseCases" + | "large" + | "largeUseCases" + | "homeXlarge" + | "xlarge" + | "Default" + | "XSmall" + | "XSmallUseCases" + | "Home" + | "HomeMd" + | "HomeUseCases" + | "Large" + | "LargeUseCases" + | "HomeXlarge" + | "XLarge"; + +export type MenuBarItemVariantValue = "default" | "home" | "Default" | "Home"; + export interface MenuBarItemProps extends React.AnchorHTMLAttributes { href?: string; children?: React.ReactNode; - variant?: "default" | "home"; - size?: - | "default" - | "xsmall" - | "xsmallUseCases" - | "home" - | "homeMd" - | "homeUseCases" - | "large" - | "largeUseCases" - | "homeXlarge" - | "xlarge"; + /** + * Menu bar item variant. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + variant?: MenuBarItemVariantValue; + /** + * Menu bar item size. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + size?: MenuBarItemSizeValue; className?: string; disabled?: boolean; isActive?: boolean; diff --git a/app/components/NavigationItem/NavigationItem.container.tsx b/app/components/NavigationItem/NavigationItem.container.tsx index 7e84509..05ac949 100644 --- a/app/components/NavigationItem/NavigationItem.container.tsx +++ b/app/components/NavigationItem/NavigationItem.container.tsx @@ -3,18 +3,22 @@ import { memo } from "react"; import NavigationItemView from "./NavigationItem.view"; import type { NavigationItemProps } from "./NavigationItem.types"; +import { normalizeNavigationItemVariant, normalizeNavigationItemSize } from "../../../lib/propNormalization"; const NavigationItemContainer = memo( ({ href = "#", children, - variant = "default", - size = "default", + variant: variantProp = "default", + size: sizeProp = "default", className = "", disabled = false, isActive = false, ...props }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const variant = normalizeNavigationItemVariant(variantProp); + const size = normalizeNavigationItemSize(sizeProp); // Variant styles const variantStyles: Record = { default: diff --git a/app/components/NavigationItem/NavigationItem.types.ts b/app/components/NavigationItem/NavigationItem.types.ts index c5f9de1..2338b59 100644 --- a/app/components/NavigationItem/NavigationItem.types.ts +++ b/app/components/NavigationItem/NavigationItem.types.ts @@ -1,11 +1,22 @@ +export type NavigationItemVariantValue = "default" | "Default"; +export type NavigationItemSizeValue = "default" | "xsmall" | "Default" | "XSmall"; + export interface NavigationItemProps extends Omit< React.AnchorHTMLAttributes, "isActive" > { href?: string; children?: React.ReactNode; - variant?: "default"; - size?: "default" | "xsmall"; + /** + * Navigation item variant. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + variant?: NavigationItemVariantValue; + /** + * Navigation item size. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + size?: NavigationItemSizeValue; className?: string; disabled?: boolean; isActive?: boolean; diff --git a/app/components/NumberCard.tsx b/app/components/NumberCard.tsx index 5b4c1b4..c1722a3 100644 --- a/app/components/NumberCard.tsx +++ b/app/components/NumberCard.tsx @@ -3,21 +3,39 @@ import { memo } from "react"; import SectionNumber from "./SectionNumber"; +import { normalizeNumberCardSize } from "../../lib/propNormalization"; + +export type NumberCardSizeValue = + | "Small" + | "Medium" + | "Large" + | "XLarge" + | "small" + | "medium" + | "large" + | "xlarge"; + interface NumberCardProps { number: number; text: string; - size?: "Small" | "Medium" | "Large" | "XLarge"; + /** + * Number card size. Accepts both PascalCase (Figma default) and lowercase (case-insensitive). + * Figma uses PascalCase, codebase uses PascalCase - both are supported. + */ + size?: NumberCardSizeValue; iconShape?: string; iconColor?: string; } -const NumberCard = memo(({ number, text, size }) => { +const NumberCard = memo(({ number, text, size: sizeProp }) => { // Base classes common to all sizes const baseClasses = "bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg"; // If size prop is provided, use explicit size classes // Otherwise, use responsive breakpoints for backward compatibility - if (size) { + if (sizeProp) { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const size = normalizeNumberCardSize(sizeProp); // Size-specific classes const sizeClasses = { Small: "flex flex-col items-end justify-center gap-4 p-5 relative", diff --git a/app/components/QuoteBlock/QuoteBlock.container.tsx b/app/components/QuoteBlock/QuoteBlock.container.tsx index eb3cf5d..0d8bd4f 100644 --- a/app/components/QuoteBlock/QuoteBlock.container.tsx +++ b/app/components/QuoteBlock/QuoteBlock.container.tsx @@ -4,10 +4,11 @@ import { memo, useState } from "react"; import { logger } from "../../../lib/logger"; import QuoteBlockView from "./QuoteBlock.view"; import type { QuoteBlockProps, VariantConfig } from "./QuoteBlock.types"; +import { normalizeQuoteBlockVariant } from "../../../lib/propNormalization"; const QuoteBlockContainer = memo( ({ - variant = "standard", + variant: variantProp = "standard", className = "", quote = "The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized.", author = "Jo Freeman", @@ -17,6 +18,8 @@ const QuoteBlockContainer = memo( fallbackAvatarSrc = "/assets/Quote_Avatar.svg", onError, }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const variant = normalizeQuoteBlockVariant(variantProp); const [imageError, setImageError] = useState(false); const [imageLoading, setImageLoading] = useState(true); diff --git a/app/components/QuoteBlock/QuoteBlock.types.ts b/app/components/QuoteBlock/QuoteBlock.types.ts index 478aa29..dc2555f 100644 --- a/app/components/QuoteBlock/QuoteBlock.types.ts +++ b/app/components/QuoteBlock/QuoteBlock.types.ts @@ -1,5 +1,17 @@ +export type QuoteBlockVariantValue = + | "compact" + | "standard" + | "extended" + | "Compact" + | "Standard" + | "Extended"; + export interface QuoteBlockProps { - variant?: "compact" | "standard" | "extended"; + /** + * Quote block variant. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + variant?: QuoteBlockVariantValue; className?: string; quote?: string; author?: string; diff --git a/app/components/RadioButton/RadioButton.container.tsx b/app/components/RadioButton/RadioButton.container.tsx index 91812a5..24628b6 100644 --- a/app/components/RadioButton/RadioButton.container.tsx +++ b/app/components/RadioButton/RadioButton.container.tsx @@ -3,11 +3,13 @@ import { memo, useCallback, useId } from "react"; import { RadioButtonView } from "./RadioButton.view"; import type { RadioButtonProps } from "./RadioButton.types"; +import { normalizeMode, normalizeState } from "../../../lib/propNormalization"; const RadioButtonContainer = ({ checked = false, - mode = "standard", - state = "default", // This state prop is now only for static display in Storybook/Preview + mode: modeProp = "standard", + state: stateProp = "default", // This state prop is now only for static display in Storybook/Preview + indicator: _indicator = true, // From Figma: whether to show the indicator dot (currently not used in view) disabled = false, label, onChange, @@ -17,6 +19,13 @@ const RadioButtonContainer = ({ ariaLabel, className = "", }: RadioButtonProps) => { + // 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"; @@ -113,7 +122,7 @@ const RadioButtonContainer = ({ radioId={radioId} checked={checked} mode={mode} - state={state} // Passed for static display in Storybook/Preview + state={normalizedState} // Normalized state (handles "selected" from Figma) disabled={disabled} label={label} name={name} diff --git a/app/components/RadioButton/RadioButton.types.ts b/app/components/RadioButton/RadioButton.types.ts index ed1c247..63900c7 100644 --- a/app/components/RadioButton/RadioButton.types.ts +++ b/app/components/RadioButton/RadioButton.types.ts @@ -1,7 +1,22 @@ +import type { ModeValue, StateValue } from "../../../lib/propNormalization"; + export interface RadioButtonProps { checked?: boolean; - mode?: "standard" | "inverse"; - state?: "default" | "hover" | "focus"; + /** + * Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + mode?: ModeValue; + /** + * Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus", "selected"/"Selected" (case-insensitive). + * Note: "selected" state is represented by the `checked` prop in practice. + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + state?: StateValue; + /** + * Whether to show the indicator dot. From Figma specification. + */ + indicator?: boolean; disabled?: boolean; label?: string; onChange?: (_data: { checked: boolean; value?: string }) => void; @@ -16,7 +31,7 @@ export interface RadioButtonViewProps { radioId: string; checked: boolean; mode: "standard" | "inverse"; - state: "default" | "hover" | "focus"; + state: "default" | "hover" | "focus" | "selected"; disabled: boolean; label?: string; name?: string; diff --git a/app/components/RadioGroup/RadioGroup.container.tsx b/app/components/RadioGroup/RadioGroup.container.tsx index c31689f..25695c7 100644 --- a/app/components/RadioGroup/RadioGroup.container.tsx +++ b/app/components/RadioGroup/RadioGroup.container.tsx @@ -3,18 +3,26 @@ import { memo, useCallback, useId } from "react"; import { RadioGroupView } from "./RadioGroup.view"; import type { RadioGroupProps } from "./RadioGroup.types"; +import { normalizeMode, normalizeState } from "../../../lib/propNormalization"; const RadioGroupContainer = ({ name, value, onChange, - mode = "standard", - state = "default", + mode: modeProp = "standard", + state: stateProp = "default", disabled = false, options = [], className = "", ...props }: RadioGroupProps) => { + // 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" && + (stateProp.toLowerCase() === "with subtext" || stateProp === "With Subtext") + ? "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}`; diff --git a/app/components/RadioGroup/RadioGroup.types.ts b/app/components/RadioGroup/RadioGroup.types.ts index b044e1b..db44f87 100644 --- a/app/components/RadioGroup/RadioGroup.types.ts +++ b/app/components/RadioGroup/RadioGroup.types.ts @@ -5,12 +5,23 @@ export interface RadioOption { ariaLabel?: string; } +import type { ModeValue, StateValue } from "../../../lib/propNormalization"; + export interface RadioGroupProps { name?: string; value?: string; onChange?: (_data: { value: string }) => void; - mode?: "standard" | "inverse"; - state?: "default" | "hover" | "focus"; + /** + * Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + mode?: ModeValue; + /** + * Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive). + * Figma also supports "With Subtext" state, which is handled via RadioOption.subtext. + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + state?: StateValue | "With Subtext" | "with subtext"; disabled?: boolean; options?: RadioOption[]; className?: string; diff --git a/app/components/SectionHeader.tsx b/app/components/SectionHeader.tsx index 69d2c8f..3c06485 100644 --- a/app/components/SectionHeader.tsx +++ b/app/components/SectionHeader.tsx @@ -1,16 +1,25 @@ "use client"; import { memo } from "react"; +import { normalizeSectionHeaderVariant } from "../../lib/propNormalization"; + +export type SectionHeaderVariantValue = "default" | "multi-line" | "Default" | "Multi-Line"; interface SectionHeaderProps { title: string; subtitle: string; titleLg?: string; - variant?: "default" | "multi-line"; + /** + * Section header variant. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + variant?: SectionHeaderVariantValue; } const SectionHeader = memo( - ({ title, subtitle, titleLg, variant = "default" }) => { + ({ title, subtitle, titleLg, variant: variantProp = "default" }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const variant = normalizeSectionHeaderVariant(variantProp); return (
( ( { id, label, - state: externalState = "default", + labelVariant: labelVariantProp, + size: sizeProp, + state: externalStateProp = "default", disabled = false, error = false, placeholder = "Choose an option", @@ -35,6 +38,12 @@ const SelectInputContainer = forwardRef( }, ref, ) => { + // 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 externalState = normalizeState(externalStateProp); + const generatedId = useId(); const selectId = id || `select-input-${generatedId}`; const labelId = `${selectId}-label`; diff --git a/app/components/SelectInput/SelectInput.types.ts b/app/components/SelectInput/SelectInput.types.ts index b3dd7b4..364b800 100644 --- a/app/components/SelectInput/SelectInput.types.ts +++ b/app/components/SelectInput/SelectInput.types.ts @@ -5,12 +5,29 @@ export interface SelectOptionData { label: string; } +import type { StateValue } from "../../../lib/propNormalization"; + +export type SelectInputLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal"; +export type SelectInputSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large"; + export interface SelectInputProps { id?: string; label?: string; - labelVariant?: "default" | "horizontal"; - size?: "small" | "medium" | "large"; - state?: "default" | "hover" | "focus"; + /** + * Label variant. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + labelVariant?: SelectInputLabelVariantValue; + /** + * Select input size. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + size?: SelectInputSizeValue; + /** + * Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + state?: StateValue; disabled?: boolean; error?: boolean; placeholder?: string; diff --git a/app/components/SelectOption/SelectOption.container.tsx b/app/components/SelectOption/SelectOption.container.tsx index c3143b0..f387a1a 100644 --- a/app/components/SelectOption/SelectOption.container.tsx +++ b/app/components/SelectOption/SelectOption.container.tsx @@ -3,6 +3,7 @@ import { forwardRef, memo, useCallback } from "react"; import { SelectOptionView } from "./SelectOption.view"; import type { SelectOptionProps } from "./SelectOption.types"; +import { normalizeContextMenuItemSize } from "../../../lib/propNormalization"; const SelectOptionContainer = forwardRef( ( @@ -12,11 +13,13 @@ const SelectOptionContainer = forwardRef( disabled = false, className = "", onClick, - size = "medium", + size: sizeProp = "medium", ...props }, ref, ) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const size = normalizeContextMenuItemSize(sizeProp); const getTextSize = (): string => { switch (size) { case "small": diff --git a/app/components/SelectOption/SelectOption.types.ts b/app/components/SelectOption/SelectOption.types.ts index 4fca4b9..4fb9c5e 100644 --- a/app/components/SelectOption/SelectOption.types.ts +++ b/app/components/SelectOption/SelectOption.types.ts @@ -1,3 +1,5 @@ +export type SelectOptionSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large"; + export interface SelectOptionProps { children?: React.ReactNode; selected?: boolean; @@ -6,7 +8,11 @@ export interface SelectOptionProps { onClick?: ( _e: React.MouseEvent | React.KeyboardEvent, ) => void; - size?: "small" | "medium" | "large"; + /** + * Select option size. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + size?: SelectOptionSizeValue; } export interface SelectOptionViewProps { diff --git a/app/components/Switch/Switch.container.tsx b/app/components/Switch/Switch.container.tsx index b4d374d..64602b5 100644 --- a/app/components/Switch/Switch.container.tsx +++ b/app/components/Switch/Switch.container.tsx @@ -3,6 +3,7 @@ import { memo, useCallback, useId, forwardRef } from "react"; import { SwitchView } from "./Switch.view"; import type { SwitchProps } from "./Switch.types"; +import { normalizeState } from "../../../lib/propNormalization"; const SwitchContainer = memo( forwardRef((props, ref) => { @@ -11,11 +12,14 @@ const SwitchContainer = memo( onChange, onFocus, onBlur, - state = "default", + state: stateProp = "default", label, className = "", ...rest } = props; + + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const state = normalizeState(stateProp); const switchId = useId(); diff --git a/app/components/Switch/Switch.types.ts b/app/components/Switch/Switch.types.ts index abaa740..93569ad 100644 --- a/app/components/Switch/Switch.types.ts +++ b/app/components/Switch/Switch.types.ts @@ -1,3 +1,5 @@ +import type { StateValue } from "../../../lib/propNormalization"; + export interface SwitchProps extends Omit< React.ButtonHTMLAttributes, "onChange" @@ -10,7 +12,11 @@ export interface SwitchProps extends Omit< ) => void; onFocus?: (_e: React.FocusEvent) => void; onBlur?: (_e: React.FocusEvent) => void; - state?: "default" | "hover" | "focus"; + /** + * Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + state?: StateValue; label?: string; className?: string; } diff --git a/app/components/TextArea/TextArea.container.tsx b/app/components/TextArea/TextArea.container.tsx index ae875ee..7d99dee 100644 --- a/app/components/TextArea/TextArea.container.tsx +++ b/app/components/TextArea/TextArea.container.tsx @@ -4,13 +4,14 @@ 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 } from "../../../lib/propNormalization"; const TextAreaContainer = forwardRef( ( { - size = "medium", - labelVariant = "default", - state = "default", + size: sizeProp = "medium", + labelVariant: labelVariantProp = "default", + state: stateProp = "default", disabled = false, error = false, label, @@ -27,6 +28,10 @@ const TextAreaContainer = forwardRef( }, ref, ) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const size = normalizeSmallMediumLargeSize(sizeProp); + const labelVariant = normalizeLabelVariant(labelVariantProp); + const state = normalizeInputState(stateProp); // Generate unique ID for accessibility if not provided const { id: textareaId, labelId } = useComponentId("textarea", id); diff --git a/app/components/TextArea/TextArea.types.ts b/app/components/TextArea/TextArea.types.ts index 0482fea..11f158c 100644 --- a/app/components/TextArea/TextArea.types.ts +++ b/app/components/TextArea/TextArea.types.ts @@ -1,10 +1,27 @@ +import type { InputStateValue } from "../../../lib/propNormalization"; + +export type TextAreaSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large"; +export type TextAreaLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal"; + export interface TextAreaProps extends Omit< React.TextareaHTMLAttributes, "size" | "onChange" | "onFocus" | "onBlur" > { - size?: "small" | "medium" | "large"; - labelVariant?: "default" | "horizontal"; - state?: "default" | "active" | "hover" | "focus"; + /** + * Text area size. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + size?: TextAreaSizeValue; + /** + * Label variant. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + labelVariant?: TextAreaLabelVariantValue; + /** + * Visual state. Accepts "default"/"Default", "active"/"Active", "hover"/"Hover", "focus"/"Focus" (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + state?: InputStateValue; disabled?: boolean; error?: boolean; label?: string; diff --git a/app/components/TextInput/TextInput.container.tsx b/app/components/TextInput/TextInput.container.tsx index bb7ff0d..4236354 100644 --- a/app/components/TextInput/TextInput.container.tsx +++ b/app/components/TextInput/TextInput.container.tsx @@ -4,11 +4,12 @@ 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 } from "../../../lib/propNormalization"; const TextInputContainer = forwardRef( ( { - state: externalState = "default", + state: externalStateProp = "default", disabled = false, error = false, label, @@ -26,6 +27,9 @@ const TextInputContainer = forwardRef( }, ref, ) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const externalState = normalizeInputState(externalStateProp); + // Generate unique ID for accessibility if not provided const { id: inputId, labelId } = useComponentId("text-input", id); diff --git a/app/components/TextInput/TextInput.types.ts b/app/components/TextInput/TextInput.types.ts index ef8e0fd..beff4cc 100644 --- a/app/components/TextInput/TextInput.types.ts +++ b/app/components/TextInput/TextInput.types.ts @@ -1,8 +1,14 @@ +import type { InputStateValue } from "../../../lib/propNormalization"; + export interface TextInputProps extends Omit< React.InputHTMLAttributes, "size" | "onChange" | "onFocus" | "onBlur" > { - state?: "default" | "active" | "hover" | "focus"; + /** + * Visual state. Accepts "default"/"Default", "active"/"Active", "hover"/"Hover", "focus"/"Focus" (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + state?: InputStateValue; disabled?: boolean; error?: boolean; label?: string; diff --git a/app/components/Toggle/Toggle.container.tsx b/app/components/Toggle/Toggle.container.tsx index c2dfe15..197eebe 100644 --- a/app/components/Toggle/Toggle.container.tsx +++ b/app/components/Toggle/Toggle.container.tsx @@ -3,6 +3,7 @@ import { memo, useCallback, useId, forwardRef } from "react"; import { ToggleView } from "./Toggle.view"; import type { ToggleProps } from "./Toggle.types"; +import { normalizeState } from "../../../lib/propNormalization"; const ToggleContainer = forwardRef( ( @@ -13,7 +14,7 @@ const ToggleContainer = forwardRef( onFocus, onBlur, disabled = false, - state = "default", + state: stateProp = "default", showIcon = false, showText = false, icon = "I", @@ -23,6 +24,8 @@ const ToggleContainer = forwardRef( }, ref, ) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const state = normalizeState(stateProp); const toggleId = useId(); const labelId = useId(); diff --git a/app/components/Toggle/Toggle.types.ts b/app/components/Toggle/Toggle.types.ts index efd36ff..a0fe919 100644 --- a/app/components/Toggle/Toggle.types.ts +++ b/app/components/Toggle/Toggle.types.ts @@ -1,3 +1,5 @@ +import type { StateValue } from "../../../lib/propNormalization"; + export interface ToggleProps extends Omit< React.ButtonHTMLAttributes, "onChange" @@ -12,7 +14,11 @@ export interface ToggleProps extends Omit< onFocus?: (_e: React.FocusEvent) => void; onBlur?: (_e: React.FocusEvent) => void; disabled?: boolean; - state?: "default" | "hover" | "focus"; + /** + * Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + state?: StateValue; showIcon?: boolean; showText?: boolean; icon?: string; diff --git a/app/components/ToggleGroup/ToggleGroup.container.tsx b/app/components/ToggleGroup/ToggleGroup.container.tsx index 37b9182..4e2ad1d 100644 --- a/app/components/ToggleGroup/ToggleGroup.container.tsx +++ b/app/components/ToggleGroup/ToggleGroup.container.tsx @@ -3,14 +3,15 @@ import { memo, useCallback, useId, forwardRef } from "react"; import { ToggleGroupView } from "./ToggleGroup.view"; import type { ToggleGroupProps } from "./ToggleGroup.types"; +import { normalizeToggleState, normalizeToggleGroupPosition } from "../../../lib/propNormalization"; const ToggleGroupContainer = memo( forwardRef((props, _ref) => { const { children, className = "", - position = "left", - state = "default", + position: positionProp = "left", + state: stateProp = "default", showText = true, ariaLabel, onChange, @@ -18,6 +19,10 @@ const ToggleGroupContainer = memo( onBlur, ...rest } = props; + + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const position = normalizeToggleGroupPosition(positionProp); + const state = normalizeToggleState(stateProp); const groupId = useId(); diff --git a/app/components/ToggleGroup/ToggleGroup.types.ts b/app/components/ToggleGroup/ToggleGroup.types.ts index 30fc2be..993a8d8 100644 --- a/app/components/ToggleGroup/ToggleGroup.types.ts +++ b/app/components/ToggleGroup/ToggleGroup.types.ts @@ -1,11 +1,23 @@ +import type { StateValue } from "../../../lib/propNormalization"; + +export type ToggleGroupPositionValue = "left" | "middle" | "right" | "Left" | "Middle" | "Right"; + export interface ToggleGroupProps extends Omit< React.ButtonHTMLAttributes, "onChange" > { children?: React.ReactNode; className?: string; - position?: "left" | "middle" | "right"; - state?: "default" | "hover" | "focus" | "selected"; + /** + * Toggle group position. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + position?: ToggleGroupPositionValue; + /** + * Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus", "selected"/"Selected" (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + state?: StateValue | "selected" | "Selected"; showText?: boolean; ariaLabel?: string; onChange?: ( diff --git a/app/components/Tooltip/Tooltip.container.tsx b/app/components/Tooltip/Tooltip.container.tsx index 0a16aba..32e2b2f 100644 --- a/app/components/Tooltip/Tooltip.container.tsx +++ b/app/components/Tooltip/Tooltip.container.tsx @@ -3,9 +3,12 @@ import { memo, useState } from "react"; import { TooltipView } from "./Tooltip.view"; import type { TooltipProps } from "./Tooltip.types"; +import { normalizeTooltipPosition } from "../../../lib/propNormalization"; const TooltipContainer = memo( - ({ children, text, position = "top", className = "", disabled = false }) => { + ({ children, text, position: positionProp = "top", className = "", disabled = false }) => { + // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) + const position = normalizeTooltipPosition(positionProp); const [isVisible, setIsVisible] = useState(false); if (disabled) { diff --git a/app/components/Tooltip/Tooltip.types.ts b/app/components/Tooltip/Tooltip.types.ts index b746924..fc778ba 100644 --- a/app/components/Tooltip/Tooltip.types.ts +++ b/app/components/Tooltip/Tooltip.types.ts @@ -1,7 +1,13 @@ +export type TooltipPositionValue = "top" | "bottom" | "Top" | "Bottom"; + export interface TooltipProps { children: React.ReactNode; text: string; - position?: "top" | "bottom"; + /** + * Tooltip position. Accepts both lowercase and PascalCase (case-insensitive). + * Figma uses PascalCase, codebase uses lowercase - both are supported. + */ + position?: TooltipPositionValue; className?: string; disabled?: boolean; } diff --git a/lib/propNormalization.ts b/lib/propNormalization.ts new file mode 100644 index 0000000..da0d570 --- /dev/null +++ b/lib/propNormalization.ts @@ -0,0 +1,514 @@ +/** + * Utility functions for normalizing component props to match Figma specifications + * while maintaining backward compatibility with existing lowercase usage. + * + * Figma uses PascalCase (e.g., "Standard", "Inverse") but codebase uses lowercase. + * These helpers accept both formats and normalize to lowercase for internal use. + */ + +/** + * Normalize mode prop values (Standard/Inverse -> standard/inverse) + */ +export function normalizeMode( + value: string | undefined, + defaultValue: "standard" | "inverse" = "standard" +): "standard" | "inverse" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + if (normalized === "standard" || normalized === "inverse") { + return normalized; + } + return defaultValue; +} + +/** + * Normalize state prop values (Default/Hover/Focus/Selected -> default/hover/focus/selected) + */ +export function normalizeState( + value: string | undefined, + defaultValue: "default" | "hover" | "focus" | "selected" = "default" +): "default" | "hover" | "focus" | "selected" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + if ( + normalized === "default" || + normalized === "hover" || + normalized === "focus" || + normalized === "selected" + ) { + return normalized; + } + return defaultValue; +} + +/** + * Normalize state prop values for form inputs (Default/Active/Hover/Focus) + */ +export function normalizeInputState( + value: string | undefined, + defaultValue: "default" | "active" | "hover" | "focus" = "default" +): "default" | "active" | "hover" | "focus" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + if ( + normalized === "default" || + normalized === "active" || + normalized === "hover" || + normalized === "focus" + ) { + return normalized; + } + return defaultValue; +} + +/** + * Normalize toggle state prop values (Default/Hover/Focus/Selected) + */ +export function normalizeToggleState( + value: string | undefined, + defaultValue: "default" | "hover" | "focus" | "selected" = "default" +): "default" | "hover" | "focus" | "selected" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + if ( + normalized === "default" || + normalized === "hover" || + normalized === "focus" || + normalized === "selected" + ) { + return normalized; + } + return defaultValue; +} + +/** + * Type helper for case-insensitive mode prop + */ +export type ModeValue = "standard" | "inverse" | "Standard" | "Inverse"; + +/** + * Type helper for case-insensitive state prop + */ +export type StateValue = + | "default" + | "hover" + | "focus" + | "selected" + | "Default" + | "Hover" + | "Focus" + | "Selected"; + +/** + * Type helper for case-insensitive input state prop + */ +export type InputStateValue = + | "default" + | "active" + | "hover" + | "focus" + | "Default" + | "Active" + | "Hover" + | "Focus"; + +/** + * Normalize button variant prop values + */ +export function normalizeVariant( + value: string | undefined, + defaultValue: "filled" = "filled" +): "filled" | "filled-inverse" | "outline" | "outline-inverse" | "ghost" | "ghost-inverse" | "danger" | "danger-inverse" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const variants = [ + "filled", + "filled-inverse", + "outline", + "outline-inverse", + "ghost", + "ghost-inverse", + "danger", + "danger-inverse", + ]; + if (variants.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize button size prop values + */ +export function normalizeSize( + value: string | undefined, + defaultValue: "xsmall" = "xsmall" +): "xsmall" | "small" | "medium" | "large" | "xlarge" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const sizes = ["xsmall", "small", "medium", "large", "xlarge"]; + if (sizes.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize alert status prop values + */ +export function normalizeAlertStatus( + value: string | undefined, + defaultValue: "default" = "default" +): "default" | "positive" | "warning" | "danger" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const statuses = ["default", "positive", "warning", "danger"]; + if (statuses.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize alert type prop values + */ +export function normalizeAlertType( + value: string | undefined, + defaultValue: "toast" = "toast" +): "toast" | "banner" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const types = ["toast", "banner"]; + if (types.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize tooltip position prop values + */ +export function normalizeTooltipPosition( + value: string | undefined, + defaultValue: "top" = "top" +): "top" | "bottom" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const positions = ["top", "bottom"]; + if (positions.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Type helper for case-insensitive variant prop + */ +export type VariantValue = + | "filled" + | "filled-inverse" + | "outline" + | "outline-inverse" + | "ghost" + | "ghost-inverse" + | "danger" + | "danger-inverse" + | "Filled" + | "Filled-Inverse" + | "Outline" + | "Outline-Inverse" + | "Ghost" + | "Ghost-Inverse" + | "Danger" + | "Danger-Inverse"; + +/** + * Type helper for case-insensitive size prop + */ +export type SizeValue = + | "xsmall" + | "small" + | "medium" + | "large" + | "xlarge" + | "XSmall" + | "Small" + | "Medium" + | "Large" + | "XLarge"; + +/** + * Normalize menu bar size prop values + */ +export function normalizeMenuBarSize( + value: string | undefined, + defaultValue: "default" = "default" +): "xsmall" | "default" | "medium" | "large" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const sizes = ["xsmall", "default", "medium", "large"]; + if (sizes.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize menu bar item variant prop values + */ +export function normalizeMenuBarItemVariant( + value: string | undefined, + defaultValue: "default" = "default" +): "default" | "home" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const variants = ["default", "home"]; + if (variants.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize navigation item variant prop values + */ +export function normalizeNavigationItemVariant( + value: string | undefined, + defaultValue: "default" = "default" +): "default" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + if (normalized === "default") { + return "default"; + } + return defaultValue; +} + +/** + * Normalize navigation item size prop values + */ +export function normalizeNavigationItemSize( + value: string | undefined, + defaultValue: "default" = "default" +): "default" | "xsmall" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const sizes = ["default", "xsmall"]; + if (sizes.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize content lockup variant prop values + */ +export function normalizeContentLockupVariant( + value: string | undefined, + defaultValue: "hero" = "hero" +): "hero" | "feature" | "learn" | "ask" | "ask-inverse" | "modal" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const variants = ["hero", "feature", "learn", "ask", "ask-inverse", "modal"]; + if (variants.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize alignment prop values + */ +export function normalizeAlignment( + value: string | undefined, + defaultValue: "center" = "center" +): "center" | "left" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const alignments = ["center", "left"]; + if (alignments.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize content container size prop values + */ +export function normalizeContentContainerSize( + value: string | undefined, + defaultValue: "responsive" = "responsive" +): "xs" | "responsive" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const sizes = ["xs", "responsive"]; + if (sizes.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize content thumbnail variant prop values + */ +export function normalizeContentThumbnailVariant( + value: string | undefined, + defaultValue: "vertical" = "vertical" +): "vertical" | "horizontal" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const variants = ["vertical", "horizontal"]; + if (variants.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize section header variant prop values + */ +export function normalizeSectionHeaderVariant( + value: string | undefined, + defaultValue: "default" = "default" +): "default" | "multi-line" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const variants = ["default", "multi-line"]; + if (variants.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize quote block variant prop values + */ +export function normalizeQuoteBlockVariant( + value: string | undefined, + defaultValue: "standard" = "standard" +): "compact" | "standard" | "extended" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const variants = ["compact", "standard", "extended"]; + if (variants.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize number card size prop values (already PascalCase in codebase, supports both) + */ +export function normalizeNumberCardSize( + value: string | undefined, + defaultValue: "Medium" = "Medium" +): "Small" | "Medium" | "Large" | "XLarge" { + if (!value) return defaultValue; + // Check if already PascalCase + if (value === "Small" || value === "Medium" || value === "Large" || value === "XLarge") { + return value; + } + // Normalize lowercase to PascalCase + const normalized = value.toLowerCase(); + if (normalized === "small") return "Small"; + if (normalized === "medium") return "Medium"; + if (normalized === "large") return "Large"; + if (normalized === "xlarge") return "XLarge"; + return defaultValue; +} + +/** + * Normalize ask organizer variant prop values + */ +export function normalizeAskOrganizerVariant( + value: string | undefined, + defaultValue: "centered" = "centered" +): "centered" | "left-aligned" | "compact" | "inverse" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const variants = ["centered", "left-aligned", "compact", "inverse"]; + if (variants.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize context menu item size prop values + */ +export function normalizeContextMenuItemSize( + value: string | undefined, + defaultValue: "medium" = "medium" +): "small" | "medium" | "large" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const sizes = ["small", "medium", "large"]; + if (sizes.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize image placeholder color prop values + */ +export function normalizeImagePlaceholderColor( + value: string | undefined, + defaultValue: "blue" = "blue" +): "blue" | "green" | "purple" | "red" | "orange" | "teal" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const colors = ["blue", "green", "purple", "red", "orange", "teal"]; + if (colors.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize toggle group position prop values + */ +export function normalizeToggleGroupPosition( + value: string | undefined, + defaultValue: "left" = "left" +): "left" | "middle" | "right" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const positions = ["left", "middle", "right"]; + if (positions.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize label variant prop values + */ +export function normalizeLabelVariant( + value: string | undefined, + defaultValue: "default" = "default" +): "default" | "horizontal" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const variants = ["default", "horizontal"]; + if (variants.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} + +/** + * Normalize small/medium/large size prop values (for SelectInput, TextArea, etc.) + */ +export function normalizeSmallMediumLargeSize( + value: string | undefined, + defaultValue: "medium" = "medium" +): "small" | "medium" | "large" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + const sizes = ["small", "medium", "large"]; + if (sizes.includes(normalized)) { + return normalized as typeof defaultValue; + } + return defaultValue; +} diff --git a/stories/Checkbox.stories.js b/stories/Checkbox.stories.js index 0ef1c4b..fc81d8e 100644 --- a/stories/Checkbox.stories.js +++ b/stories/Checkbox.stories.js @@ -46,13 +46,13 @@ export default { }, mode: { control: "select", - options: ["standard", "inverse"], - description: "Visual mode of the checkbox", + options: ["standard", "inverse", "Standard", "Inverse"], + description: "Visual mode of the checkbox (case-insensitive: accepts both lowercase and PascalCase)", }, state: { control: "select", - options: ["default", "hover", "focus"], - description: "Interaction state for static display", + options: ["default", "hover", "focus", "Default", "Hover", "Focus"], + description: "Interaction state for static display (case-insensitive: accepts both lowercase and PascalCase)", }, disabled: { control: "boolean", @@ -206,3 +206,53 @@ export const AllModes = () => {
); }; + +// Test PascalCase props from Figma +export const FigmaPascalCase = () => { + const [standardChecked, setStandardChecked] = React.useState(false); + const [inverseChecked, setInverseChecked] = React.useState(false); + + return ( +
+
+

Figma PascalCase Props (Standard/Inverse)

+

+ These components accept both PascalCase (from Figma) and lowercase (from codebase) prop values. +

+
+ setStandardChecked(checked)} + /> + setInverseChecked(checked)} + /> +
+
+
+

Mixed Case (backward compatibility)

+
+ + +
+
+
+ ); +}; diff --git a/stories/RadioButton.stories.js b/stories/RadioButton.stories.js index 40d0921..1d3f286 100644 --- a/stories/RadioButton.stories.js +++ b/stories/RadioButton.stories.js @@ -21,13 +21,13 @@ export default { }, mode: { control: "select", - options: ["standard", "inverse"], - description: "Visual mode of the radio button", + options: ["standard", "inverse", "Standard", "Inverse"], + description: "Visual mode of the radio button (case-insensitive: accepts both lowercase and PascalCase)", }, state: { control: "select", - options: ["default", "hover", "focus"], - description: "Interaction state for static display", + options: ["default", "hover", "focus", "selected", "Default", "Hover", "Focus", "Selected"], + description: "Interaction state for static display (case-insensitive: accepts both lowercase and PascalCase)", }, disabled: { control: "boolean", @@ -247,3 +247,53 @@ export const InverseAllStates = () => { ); }; + +// Test PascalCase props from Figma +export const FigmaPascalCase = () => { + const [standardChecked, setStandardChecked] = React.useState(false); + const [inverseChecked, setInverseChecked] = React.useState(false); + + return ( +
+
+

Figma PascalCase Props (Standard/Inverse)

+

+ These components accept both PascalCase (from Figma) and lowercase (from codebase) prop values. +

+
+ setStandardChecked(checked)} + /> + setInverseChecked(checked)} + /> +
+
+
+

Mixed Case (backward compatibility)

+
+ + +
+
+
+ ); +}; diff --git a/tests/unit/ContentContainer.test.jsx b/tests/unit/ContentContainer.test.jsx index 4f4c689..af10a80 100644 --- a/tests/unit/ContentContainer.test.jsx +++ b/tests/unit/ContentContainer.test.jsx @@ -85,14 +85,14 @@ describe("ContentContainer", () => { }); it("applies correct width when specified", () => { - render(); + render(); const container = document.querySelector("div[class*='relative z-20']"); expect(container).toHaveStyle("width: 300px"); }); it("applies default width when not specified", () => { - render(); + render(); const container = document.querySelector("div[class*='relative z-20']"); expect(container).toHaveStyle("width: 200px"); @@ -183,8 +183,8 @@ describe("ContentContainer", () => { expect(screen.getByText("Incomplete Post")).toBeInTheDocument(); }); - it("applies correct responsive sizing for sm breakpoint", () => { - render(); + it("applies correct responsive sizing for xs breakpoint", () => { + render(); const icon = screen.getByAltText("Icon for Test Article Title"); expect(icon).toHaveClass("w-[60px]", "h-[30px]"); @@ -196,8 +196,8 @@ describe("ContentContainer", () => { expect(description).toHaveClass("text-[12px]", "leading-[16px]"); }); - it("applies correct responsive sizing for md breakpoint", () => { - render(); + it("applies correct responsive sizing for responsive breakpoint", () => { + render(); const icon = screen.getByAltText("Icon for Test Article Title"); expect(icon).toHaveClass("w-[60px]", "h-[30px]"); diff --git a/tests/unit/NumberCard.test.jsx b/tests/unit/NumberCard.test.jsx index 69cd9c9..aefb1cb 100644 --- a/tests/unit/NumberCard.test.jsx +++ b/tests/unit/NumberCard.test.jsx @@ -39,7 +39,7 @@ describe("NumberCard Component", () => { const card = screen .getByText("Test Card Text") .closest("div").parentElement; - expect(card).toHaveClass("flex", "flex-col", "sm:flex-row", "lg:flex-col"); + expect(card).toHaveClass("flex", "flex-col", "sm:flex-row", "sm:items-center", "lg:flex-col", "lg:items-start", "lg:justify-end", "lg:relative"); }); it("applies proper responsive spacing when size is not specified", () => { @@ -66,7 +66,7 @@ describe("NumberCard Component", () => { const card = screen .getByText("Test Card Text") .closest("div").parentElement; - expect(card).toHaveClass("lg:h-[238px]"); + expect(card).toHaveClass("lg:h-[238px]", "lg:relative"); }); it("applies proper background and shadow", () => { @@ -128,8 +128,11 @@ describe("NumberCard Component", () => { expect(textElement).toHaveClass( "text-[24px]", "sm:text-[24px]", + "sm:leading-[24px]", "lg:text-[24px]", + "lg:leading-[24px]", "xl:text-[32px]", + "xl:leading-[32px]", ); }); @@ -156,7 +159,7 @@ describe("NumberCard Component", () => { .closest("div").parentElement; // Mobile first approach - expect(card).toHaveClass("flex-col", "gap-4", "p-5"); + expect(card).toHaveClass("flex", "flex-col", "gap-4", "p-5"); // Small breakpoint expect(card).toHaveClass( @@ -172,7 +175,9 @@ describe("NumberCard Component", () => { "lg:gap-[22px]", "lg:p-8", "lg:items-start", + "lg:justify-end", "lg:relative", + "lg:h-[238px]", ); });