Align props with figma

This commit is contained in:
adilallo
2026-02-04 16:52:03 -07:00
parent ee9784271f
commit af7e2d3e51
53 changed files with 1287 additions and 108 deletions
+6 -2
View File
@@ -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<AlertProps>(
({
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) {
+22 -2
View File
@@ -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;
}
@@ -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<AskOrganizerProps>(
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");
@@ -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;
+11 -2
View File
@@ -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<HTMLImageElement> {
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<AvatarProps>(
({ 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<string, string> = {
small: "w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)]",
medium: "w-[18px] h-[18px]",
+11 -2
View File
@@ -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<HTMLDivElement> {
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<AvatarContainerProps>(
({ 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<string, string> = {
small: "flex -space-x-[var(--spacing-scale-008)]",
medium: "flex -space-x-[9px]",
+17 -12
View File
@@ -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<HTMLButtonElement> {
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<HTMLButtonElement> {
const Button = memo<ButtonProps>(
({
children,
variant = "filled",
size = "xsmall",
variant: variantProp = "filled",
size: sizeProp = "xsmall",
className = "",
disabled = false,
type = "button",
@@ -39,6 +41,9 @@ const Button = memo<ButtonProps>(
ariaLabel,
...props
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const variant = normalizeVariant(variantProp);
const size = normalizeSize(sizeProp);
const sizeStyles: Record<string, string> = {
xsmall:
"p-[var(--spacing-scale-006)] gap-[var(--spacing-scale-002)]",
@@ -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<CheckboxProps>(
({
checked = false,
mode = "standard",
state = "default",
mode: modeProp = "standard",
state: stateProp = "default",
disabled = false,
label,
className = "",
@@ -20,6 +21,10 @@ const CheckboxContainer = memo<CheckboxProps>(
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";
+12 -2
View File
@@ -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;
@@ -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}`;
@@ -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;
@@ -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<ContentContainerProps>(
({ 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 = [
@@ -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 {
@@ -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<ContentLockupProps>(
({
@@ -11,12 +12,15 @@ const ContentLockupContainer = memo<ContentLockupProps>(
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<string, VariantStyle> = {
hero: {
@@ -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.
@@ -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<ContentThumbnailTemplateProps>(
({ 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"],
@@ -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[];
}
@@ -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":
@@ -1,3 +1,5 @@
export type ContextMenuItemSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
selected?: boolean;
@@ -7,7 +9,11 @@ export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElemen
onClick?: (
_e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
) => 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 {
+23 -2
View File
@@ -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<ImagePlaceholderProps>(
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<string, string> = {
blue: "bg-blue-500",
green: "bg-green-500",
+19 -2
View File
@@ -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<HTMLElement> {
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<MenuBarProps>(
({ 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<string, string> = {
xsmall:
@@ -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<MenuBarItemProps>(
({
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<string, string> = {
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",
+34 -12
View File
@@ -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<HTMLAnchorElement> {
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;
@@ -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<NavigationItemProps>(
({
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<string, string> = {
default:
@@ -1,11 +1,22 @@
export type NavigationItemVariantValue = "default" | "Default";
export type NavigationItemSizeValue = "default" | "xsmall" | "Default" | "XSmall";
export interface NavigationItemProps extends Omit<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
"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;
+20 -2
View File
@@ -3,15 +3,33 @@
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<NumberCardProps>(({ number, text, size }) => {
const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeNumberCardSize(sizeProp);
// Base classes common to all sizes
const baseClasses = "bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg";
@@ -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<QuoteBlockProps>(
({
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<QuoteBlockProps>(
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);
+13 -1
View File
@@ -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;
@@ -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 = true, // From Figma: whether to show the indicator dot
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}
@@ -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;
@@ -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}`;
+13 -2
View File
@@ -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;
+11 -2
View File
@@ -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<SectionHeaderProps>(
({ 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 (
<div
className={
@@ -16,13 +16,16 @@ import React, {
import { useClickOutside } from "../../hooks";
import { SelectInputView } from "./SelectInput.view";
import type { SelectInputProps } from "./SelectInput.types";
import { normalizeState, normalizeSmallMediumLargeSize, normalizeLabelVariant } from "../../../lib/propNormalization";
const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
(
{
id,
label,
state: externalState = "default",
labelVariant: labelVariantProp,
size: sizeProp,
state: externalStateProp = "default",
disabled = false,
error = false,
placeholder = "Choose an option",
@@ -35,6 +38,11 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
},
ref,
) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
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`;
@@ -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;
@@ -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<HTMLDivElement, SelectOptionProps>(
(
@@ -12,11 +13,13 @@ const SelectOptionContainer = forwardRef<HTMLDivElement, SelectOptionProps>(
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":
@@ -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<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
) => 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 {
+5 -1
View File
@@ -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<HTMLButtonElement, SwitchProps>((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();
+7 -1
View File
@@ -1,3 +1,5 @@
import type { StateValue } from "../../../lib/propNormalization";
export interface SwitchProps extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
"onChange"
@@ -10,7 +12,11 @@ export interface SwitchProps extends Omit<
) => void;
onFocus?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => 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;
}
@@ -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<HTMLTextAreaElement, TextAreaProps>(
(
{
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<HTMLTextAreaElement, TextAreaProps>(
},
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);
+20 -3
View File
@@ -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<HTMLTextAreaElement>,
"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;
@@ -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<HTMLInputElement, TextInputProps>(
(
{
state: externalState = "default",
state: externalStateProp = "default",
disabled = false,
error = false,
label,
@@ -26,6 +27,9 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
},
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);
+7 -1
View File
@@ -1,8 +1,14 @@
import type { InputStateValue } from "../../../lib/propNormalization";
export interface TextInputProps extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
"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;
+4 -1
View File
@@ -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<HTMLButtonElement, ToggleProps>(
(
@@ -13,7 +14,7 @@ const ToggleContainer = forwardRef<HTMLButtonElement, ToggleProps>(
onFocus,
onBlur,
disabled = false,
state = "default",
state: stateProp = "default",
showIcon = false,
showText = false,
icon = "I",
@@ -23,6 +24,8 @@ const ToggleContainer = forwardRef<HTMLButtonElement, ToggleProps>(
},
ref,
) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const state = normalizeState(stateProp);
const toggleId = useId();
const labelId = useId();
+7 -1
View File
@@ -1,3 +1,5 @@
import type { StateValue } from "../../../lib/propNormalization";
export interface ToggleProps extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
"onChange"
@@ -12,7 +14,11 @@ export interface ToggleProps extends Omit<
onFocus?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => 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;
@@ -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<HTMLButtonElement, ToggleGroupProps>((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();
@@ -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<HTMLButtonElement>,
"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?: (
+4 -1
View File
@@ -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<TooltipProps>(
({ 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) {
+7 -1
View File
@@ -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;
}
+152
View File
@@ -0,0 +1,152 @@
# Figma Property Alignment Documentation
This document tracks components that have been aligned with Figma design specifications and those that were skipped.
## Alignment Strategy
All components now support case-insensitive property values:
- **Figma uses PascalCase** (e.g., "Standard", "Inverse", "Default")
- **Codebase uses lowercase** (e.g., "standard", "inverse", "default")
- **Both formats are accepted** and normalized internally
## Components Aligned with Figma
### Form Components ✅
- **RadioButton, RadioGroup** - mode, state props (added `indicator` prop from Figma)
- **Checkbox, CheckboxGroup** - mode, state props
- **TextInput** - state props
- **TextArea** - state, size, labelVariant props
- **SelectInput** - state, size, labelVariant props
- **SelectOption** - size prop
- **Toggle** - state props
- **ToggleGroup** - state, position props
- **Switch** - state props
### Core UI Components ✅
- **Button** - variant, size props
- **Alert** - status, type props
- **Tooltip** - position prop
- **Progress** - progress state (complex type, no normalization needed)
- **Stepper** - numeric props (no normalization needed)
- **ModalHeader, ModalFooter** - no enum props
### Navigation & Layout ✅
- **MenuBar** - size prop
- **MenuBarItem** - variant, size props
- **NavigationItem** - variant, size props
- **Header, HomeHeader** - complex internal size types
- **Footer** - no props
- **HeaderTab** - no enum props
- **ConditionalHeader** - no props
### Content Components ✅
- **ContentLockup** - variant, alignment props
- **ContentContainer** - size prop
- **ContentThumbnailTemplate** - variant prop
- **SectionHeader** - variant prop
- **ContentBanner** - no enum props
- **SectionNumber** - no enum props
- **HeroBanner, HeroDecor** - no enum props
### Card & Display Components ✅
- **NumberCard** - size prop (already PascalCase, now supports both)
- **NumberedCards** - no enum props
- **IconCard** - no enum props
- **MiniCard** - no enum props
- **RuleCard, RuleStack** - no enum props
- **QuoteBlock** - variant prop
- **QuoteDecor** - no props
- **Logo** - complex size enum (internal use)
- **LogoWall** - no enum props
### Feature Components ✅
- **FeatureGrid** - no enum props
- **AskOrganizer** - variant prop
- **RelatedArticles** - no enum props
- **Create** - no enum props
### Form Extensions ✅
- **InputWithCounter** - no enum props
- **SelectDropdown** - no enum props
- **SelectOption** - size prop
### Context & Menu Components ✅
- **ContextMenu** - no enum props
- **ContextMenuItem** - size prop
- **ContextMenuDivider, ContextMenuSection** - no enum props
- **LanguageSwitcher** - no enum props
### Utility Components ✅
- **Avatar** - size prop
- **AvatarContainer** - size prop
- **ImagePlaceholder** - color prop
## Components Skipped (No Figma Design)
The following components were skipped as they don't have corresponding Figma designs:
- **ErrorBoundary** - Error handling utility component
- **WebVitalsDashboard** - Development tool for performance monitoring
- **Separator** - Simple visual divider component
## Implementation Details
### Normalization Functions
All normalization functions are located in `lib/propNormalization.ts`:
- `normalizeMode()` - Standard/Inverse modes
- `normalizeState()` - Default/Hover/Focus/Selected states
- `normalizeInputState()` - Default/Active/Hover/Focus for inputs
- `normalizeVariant()` - Button variants
- `normalizeSize()` - Button sizes
- `normalizeAlertStatus()` - Alert statuses
- `normalizeAlertType()` - Alert types
- `normalizeTooltipPosition()` - Tooltip positions
- `normalizeMenuBarSize()` - Menu bar sizes
- `normalizeMenuBarItemVariant()` - Menu bar item variants
- `normalizeNavigationItemVariant()` - Navigation item variants
- `normalizeNavigationItemSize()` - Navigation item sizes
- `normalizeContentLockupVariant()` - Content lockup variants
- `normalizeAlignment()` - Text alignment
- `normalizeContentContainerSize()` - Content container sizes
- `normalizeContentThumbnailVariant()` - Content thumbnail variants
- `normalizeSectionHeaderVariant()` - Section header variants
- `normalizeQuoteBlockVariant()` - Quote block variants
- `normalizeNumberCardSize()` - Number card sizes
- `normalizeAskOrganizerVariant()` - Ask organizer variants
- `normalizeContextMenuItemSize()` - Context menu item sizes
- `normalizeImagePlaceholderColor()` - Image placeholder colors
- `normalizeToggleGroupPosition()` - Toggle group positions
- `normalizeLabelVariant()` - Label variants (default/horizontal)
- `normalizeSmallMediumLargeSize()` - Small/medium/large sizes (for SelectInput, TextArea, etc.)
### Usage Pattern
All container components follow this pattern:
```typescript
const ComponentContainer = ({
variant: variantProp = "default",
// ... other props
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const variant = normalizeVariant(variantProp);
// Use normalized value in component logic
// ...
};
```
## Backward Compatibility
All changes maintain full backward compatibility:
- Existing lowercase prop usage continues to work
- New PascalCase props from Figma are accepted
- TypeScript types accept both formats
- No breaking changes to existing code
## Testing
Storybook stories have been updated to demonstrate both naming conventions:
- Existing stories use lowercase (backward compatibility)
- New stories added for PascalCase (Figma alignment)
+514
View File
@@ -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;
}
+54 -4
View File
@@ -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 = () => {
</div>
);
};
// Test PascalCase props from Figma
export const FigmaPascalCase = () => {
const [standardChecked, setStandardChecked] = React.useState(false);
const [inverseChecked, setInverseChecked] = React.useState(false);
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4 text-white">Figma PascalCase Props (Standard/Inverse)</h3>
<p className="text-sm text-gray-400 mb-4">
These components accept both PascalCase (from Figma) and lowercase (from codebase) prop values.
</p>
<div className="space-y-4">
<Checkbox
label="Standard Mode (PascalCase)"
checked={standardChecked}
mode="Standard"
state="Default"
onChange={({ checked }) => setStandardChecked(checked)}
/>
<Checkbox
label="Inverse Mode (PascalCase)"
checked={inverseChecked}
mode="Inverse"
state="Default"
onChange={({ checked }) => setInverseChecked(checked)}
/>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4 text-white">Mixed Case (backward compatibility)</h3>
<div className="space-y-4">
<Checkbox
label="Standard mode (lowercase) - still works"
checked={false}
mode="standard"
state="default"
/>
<Checkbox
label="Inverse Mode (mixed) - still works"
checked={false}
mode="inverse"
state="Default"
/>
</div>
</div>
</div>
);
};
+54 -4
View File
@@ -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 = () => {
</div>
);
};
// Test PascalCase props from Figma
export const FigmaPascalCase = () => {
const [standardChecked, setStandardChecked] = React.useState(false);
const [inverseChecked, setInverseChecked] = React.useState(false);
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4 text-white">Figma PascalCase Props (Standard/Inverse)</h3>
<p className="text-sm text-gray-400 mb-4">
These components accept both PascalCase (from Figma) and lowercase (from codebase) prop values.
</p>
<div className="space-y-4">
<RadioButton
label="Standard Mode (PascalCase)"
checked={standardChecked}
mode="Standard"
state="Default"
onChange={({ checked }) => setStandardChecked(checked)}
/>
<RadioButton
label="Inverse Mode (PascalCase)"
checked={inverseChecked}
mode="Inverse"
state="Default"
onChange={({ checked }) => setInverseChecked(checked)}
/>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4 text-white">Mixed Case (backward compatibility)</h3>
<div className="space-y-4">
<RadioButton
label="Standard mode (lowercase) - still works"
checked={false}
mode="standard"
state="default"
/>
<RadioButton
label="Inverse Mode (mixed) - still works"
checked={false}
mode="inverse"
state="Default"
/>
</div>
</div>
</div>
);
};