App reorganization

This commit is contained in:
adilallo
2026-04-18 14:12:49 -06:00
parent f866d11ff8
commit e9dab04b34
288 changed files with 2698 additions and 5029 deletions
+7 -18
View File
@@ -5,34 +5,26 @@ import type {
ButtonPaletteValue,
ButtonStateValue,
} from "../../../lib/propNormalization";
import {
normalizeSize,
normalizeButtonType,
normalizeButtonPalette,
} from "../../../lib/propNormalization";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
/**
* Button type (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Button type (Figma prop).
* @default "filled"
*/
buttonType?: ButtonTypeValue;
/**
* Button palette (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses "Invert", codebase uses "inverse" - both are supported.
* Button palette (Figma prop).
* @default "default"
*/
palette?: ButtonPaletteValue;
/**
* Button size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Button size.
* @default "xsmall"
*/
size?: SizeValue;
/**
* Button state (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
* Button state (Figma prop).
* @default "default"
*/
state?: ButtonStateValue;
@@ -83,12 +75,9 @@ const Button = memo<ButtonProps>(
ariaLabel,
...props
}) => {
// Normalize props
const buttonType = normalizeButtonType(typeProp, "filled");
const buttonPalette = normalizeButtonPalette(paletteProp, "default");
const size = normalizeSize(sizeProp);
// State prop is for Figma alignment - actual state is handled by CSS pseudo-classes
// We accept it for API alignment but don't use it for styling (CSS handles states)
const buttonType = typeProp ?? "filled";
const buttonPalette = paletteProp ?? "default";
const size = sizeProp;
// Map type + palette to variant string for styling (internal use only)
const getVariantFromTypeAndPalette = (
+19 -42
View File
@@ -3,80 +3,57 @@
import { memo } from "react";
import SectionNumber from "../sections/SectionNumber";
import { normalizeNumberCardSize } from "../../../lib/propNormalization";
export type NumberCardSizeValue =
| "Small"
| "Medium"
| "Large"
| "XLarge"
| "small"
| "medium"
| "large"
| "xlarge";
export type NumberCardSizeValue = "small" | "medium" | "large" | "xlarge";
interface NumberCardProps {
number: number;
text: string;
/**
* 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: 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 (sizeProp) {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeNumberCardSize(sizeProp);
// Size-specific classes
const size = sizeProp;
const sizeClasses = {
Small: "flex flex-col items-end justify-center gap-4 p-5 relative",
Medium: "flex flex-row items-center gap-8 p-8 relative",
Large:
small: "flex flex-col items-end justify-center gap-4 p-5 relative",
medium: "flex flex-row items-center gap-8 p-8 relative",
large:
"flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
XLarge:
xlarge:
"flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
};
// Text size classes
const textClasses = {
Small:
small:
"font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]",
Medium:
medium:
"font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
Large:
large:
"font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
XLarge:
xlarge:
"font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[#141414]",
};
// Section number wrapper classes - Small doesn't need a wrapper
const sectionNumberWrapperClasses = {
Small: "relative shrink-0",
Medium: "flex justify-start flex-shrink-0",
Large: "absolute top-8 right-8",
XLarge: "absolute top-8 right-8",
small: "relative shrink-0",
medium: "flex justify-start flex-shrink-0",
large: "absolute top-8 right-8",
xlarge: "absolute top-8 right-8",
};
// Content container classes
const contentClasses = {
Small: "min-w-full relative shrink-0",
Medium: "flex-1",
Large: "absolute bottom-8 left-8 right-16",
XLarge: "absolute bottom-8 left-8 right-16",
small: "min-w-full relative shrink-0",
medium: "flex-1",
large: "absolute bottom-8 left-8 right-16",
xlarge: "absolute bottom-8 left-8 right-16",
};
// Small variant has section number as direct child, others need wrapper
if (size === "Small") {
if (size === "small") {
return (
<div className={`${baseClasses} ${sizeClasses[size]}`}>
{/* Section Number - Direct child for Small */}
@@ -3,7 +3,6 @@
import { memo } from "react";
import { RuleCardView } from "./RuleCard.view";
import type { RuleCardProps } from "./RuleCard.types";
import { normalizeRuleCardSize } from "../../../../lib/propNormalization";
declare global {
interface Window {
@@ -33,8 +32,7 @@ const RuleCardContainer = memo<RuleCardProps>(
logoAlt,
communityInitials,
}) => {
// Normalize size prop
const size = normalizeRuleCardSize(sizeProp, "L");
const size = sizeProp ?? "L";
const handleClick = () => {
// Basic analytics event tracking
@@ -21,7 +21,7 @@ export interface RuleCardProps {
className?: string;
onClick?: () => void;
expanded?: boolean;
size?: "XS" | "S" | "M" | "L" | "xs" | "s" | "m" | "l";
size?: "XS" | "S" | "M" | "L";
categories?: Category[];
logoUrl?: string;
logoAlt?: string;
@@ -261,8 +261,8 @@ export function RuleCardView({
key={categoryIndex}
label={category.name}
showHelpIcon={false}
size="S"
palette="Inverse"
size="s"
palette="inverse"
options={category.chipOptions}
onChipClick={(chipId) => {
category.onChipClick?.(category.name, chipId);
@@ -4,12 +4,10 @@ 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: sizeProp = "responsive" }) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeContentContainerSize(sizeProp);
const size = sizeProp;
// Get the corresponding icon based on the same logic as background images
const getIconImage = (slug: string): string => {
const icons = [
@@ -1,17 +1,12 @@
import type { BlogPost } from "../../../../lib/content";
export type ContentContainerSizeValue =
| "xs"
| "responsive"
| "Xs"
| "Responsive";
export type ContentContainerSizeValue = "xs" | "responsive";
export interface ContentContainerProps {
post: BlogPost;
width?: string;
/**
* Content container size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Content container size.
*/
size?: ContentContainerSizeValue;
}
@@ -4,12 +4,10 @@ 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: variantProp = "vertical" }) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const variant = normalizeContentThumbnailVariant(variantProp);
const variant = variantProp;
// Get article-specific background image from frontmatter
const getBackgroundImage = (
post: ContentThumbnailTemplateProps["post"],
@@ -1,17 +1,12 @@
import type { BlogPost } from "../../../../lib/content";
export type ContentThumbnailTemplateVariantValue =
| "vertical"
| "horizontal"
| "Vertical"
| "Horizontal";
export type ContentThumbnailTemplateVariantValue = "vertical" | "horizontal";
export interface ContentThumbnailTemplateProps {
post: BlogPost;
className?: string;
/**
* Content thumbnail variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Content thumbnail variant.
*/
variant?: ContentThumbnailTemplateVariantValue;
slugOrder?: string[];
@@ -4,11 +4,11 @@ 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";
/**
* Figma: "Control / Checkbox" (TODO(figma)). Single boolean checkbox with
* optional label, supporting standard and inverse modes.
*/
const CheckboxContainer = memo<CheckboxProps>(
({
checked = false,
@@ -24,9 +24,8 @@ 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 mode = modeProp;
const state = stateProp;
const isInverse = mode === "inverse";
const isStandard = mode === "standard";
@@ -2,15 +2,9 @@ import type { ModeValue, StateValue } from "../../../../lib/propNormalization";
export interface CheckboxProps {
checked?: boolean;
/**
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
/** Mode variant (Figma: Mode). */
mode?: ModeValue;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
/** Visual state (Figma: State). */
state?: StateValue;
disabled?: boolean;
label?: string;
@@ -3,8 +3,11 @@
import { memo, useCallback, useId, useState } from "react";
import { CheckboxGroupView } from "./CheckboxGroup.view";
import type { CheckboxGroupProps } from "./CheckboxGroup.types";
import { normalizeMode } from "../../../../lib/propNormalization";
/**
* Figma: "Control / CheckboxGroup" (TODO(figma)). Group of checkboxes sharing
* a name that emits the array of currently selected values.
*/
const CheckboxGroupContainer = ({
name,
value,
@@ -15,8 +18,7 @@ const CheckboxGroupContainer = ({
className = "",
...props
}: CheckboxGroupProps) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp);
const mode = modeProp;
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const groupId = name || `checkbox-group-${generatedId}`;
@@ -12,8 +12,7 @@ export interface CheckboxGroupProps {
value?: string[];
onChange?: (_data: { value: string[] }) => void;
/**
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Mode variant.
*/
mode?: ModeValue;
disabled?: boolean;
+10 -11
View File
@@ -3,18 +3,17 @@
import { memo, useState, useEffect, useRef } from "react";
import ChipView from "./Chip.view";
import type { ChipProps } from "./Chip.types";
import {
normalizeChipPalette,
normalizeChipSize,
normalizeChipState,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / Chip" (TODO(figma)). Compact pill-shaped tag with
* selectable, removable, and inline-editable (custom) states.
*/
const ChipContainer = memo<ChipProps>(
({
label,
state: stateProp = "Unselected",
palette: paletteProp = "Default",
size: sizeProp = "S",
state: stateProp = "unselected",
palette: paletteProp = "default",
size: sizeProp = "s",
className = "",
disabled,
onClick,
@@ -23,9 +22,9 @@ const ChipContainer = memo<ChipProps>(
onClose,
ariaLabel,
}) => {
const state = normalizeChipState(stateProp);
const palette = normalizeChipPalette(paletteProp);
const size = normalizeChipSize(sizeProp);
const state = stateProp;
const palette = paletteProp;
const size = sizeProp;
const isDisabled = disabled ?? state === "disabled";
const isCustom = state === "custom";
+13 -19
View File
@@ -7,38 +7,32 @@ import type {
export interface ChipProps {
label: string;
/**
* Visual state of the chip, aligned with Figma:
* - "Unselected"
* - "Selected"
* - "Disabled"
* - "Custom" (editable chips with check/close buttons)
*
* Accepts both PascalCase (Figma) and lowercase values.
* Visual state of the chip:
* - "unselected"
* - "selected"
* - "disabled"
* - "custom" (editable chips with check/close buttons)
*/
state?: ChipStateValue;
/**
* Palette of the chip, aligned with Figma:
* - "Default"
* - "Inverse"
*
* Accepts both PascalCase (Figma) and lowercase values.
* Palette of the chip:
* - "default"
* - "inverse"
*/
palette?: ChipPaletteValue;
/**
* Size of the chip, aligned with Figma:
* - "S"
* - "M"
*
* Accepts both uppercase (Figma) and lowercase values.
* Size of the chip:
* - "s"
* - "m"
*/
size?: ChipSizeValue;
className?: string;
/**
* Whether the chip should be non-interactive. Defaults to `true` when
* `state === "disabled"` to preserve historical behavior. Pass
* `disabled={false}` alongside `state="Disabled"` to render the dimmed
* `disabled={false}` alongside `state="disabled"` to render the dimmed
* "disabled" visual while keeping the chip clickable — useful for toggle
* groups where the unselected state is the disabled Figma visual.
* groups where the unselected state is the disabled visual.
*/
disabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
@@ -0,0 +1,18 @@
"use client";
import { memo } from "react";
import { InputWithCounterView } from "./InputWithCounter.view";
import type { InputWithCounterProps } from "./InputWithCounter.types";
/**
* Figma: "Control / InputWithCounter" (TODO(figma)).
* Single-line text input with a label, optional help glyph, and a live
* `value.length / maxLength` counter underneath.
*/
const InputWithCounterContainer = memo<InputWithCounterProps>((props) => {
return <InputWithCounterView {...props} />;
});
InputWithCounterContainer.displayName = "InputWithCounter";
export default InputWithCounterContainer;
@@ -1,2 +1,2 @@
export { InputWithCounterView as default } from "./InputWithCounter.view";
export { default } from "./InputWithCounter.container";
export type { InputWithCounterProps } from "./InputWithCounter.types";
@@ -3,17 +3,17 @@
import { memo } from "react";
import MultiSelectView from "./MultiSelect.view";
import type { MultiSelectProps } from "./MultiSelect.types";
import {
normalizeMultiSelectSize,
normalizeChipPalette,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / MultiSelect" (TODO(figma)). Labelled set of chips for
* picking multiple values, with an optional add button for custom entries.
*/
const MultiSelectContainer = memo<MultiSelectProps>(
({
label,
showHelpIcon = true,
size: sizeProp = "M",
palette: paletteProp = "Default",
size: sizeProp = "m",
palette: paletteProp = "default",
options,
onChipClick,
onAddClick,
@@ -24,8 +24,8 @@ const MultiSelectContainer = memo<MultiSelectProps>(
onCustomChipClose,
className = "",
}) => {
const size = normalizeMultiSelectSize(sizeProp);
const palette = normalizeChipPalette(paletteProp);
const size = sizeProp;
const palette = paletteProp;
return (
<MultiSelectView
@@ -9,7 +9,7 @@ export interface ChipOption {
state?: ChipStateValue;
}
export type MultiSelectSizeValue = "S" | "M" | "s" | "m";
export type MultiSelectSizeValue = "s" | "m";
export interface MultiSelectProps {
/**
@@ -21,13 +21,11 @@ export interface MultiSelectProps {
*/
showHelpIcon?: boolean;
/**
* Size variant: "S" (small) or "M" (medium)
* Accepts both uppercase (Figma) and lowercase values.
* Size variant: "s" (small) or "m" (medium)
*/
size?: MultiSelectSizeValue;
/**
* Palette for chips: "Default" or "Inverse"
* Accepts both PascalCase (Figma) and lowercase values.
* Palette for chips: "default" or "inverse"
*/
palette?: ChipPaletteValue;
/**
@@ -28,7 +28,7 @@ function MultiSelectView({
? "gap-[var(--measures-spacing-200,8px)]"
: "gap-[var(--measures-spacing-300,12px)]";
const chipSize = isSmall ? "S" : "M";
const chipSize = size;
return (
<div
@@ -41,8 +41,8 @@ function MultiSelectView({
helpIcon={showHelpIcon}
asterisk={false}
helperText={false}
size={size === "s" ? "S" : "M"}
palette={palette === "inverse" ? "Inverse" : "Default"}
size={size}
palette={palette}
/>
)}
@@ -53,13 +53,12 @@ function MultiSelectView({
{options.map((option) => (
<Chip
key={option.id}
label={option.state === "Custom" ? "" : option.label}
state={option.state || "Unselected"}
palette={palette === "inverse" ? "Inverse" : "Default"}
label={option.state === "custom" ? "" : option.label}
state={option.state || "unselected"}
palette={palette}
size={chipSize}
onClick={() => {
// Only toggle if not in Custom state
if (option.state !== "Custom" && onChipClick) {
if (option.state !== "custom" && onChipClick) {
onChipClick(option.id);
}
}}
@@ -3,10 +3,6 @@
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,
@@ -22,9 +18,8 @@ const RadioButtonContainer = ({
ariaLabel,
className = "",
}: RadioButtonProps) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp);
const state = normalizeState(stateProp);
const mode = modeProp;
const state = stateProp;
// If state is "selected", it means checked in Figma terms
const normalizedState = state === "selected" || checked ? "selected" : state;
@@ -3,14 +3,12 @@ import type { ModeValue, StateValue } from "../../../../lib/propNormalization";
export interface RadioButtonProps {
checked?: boolean;
/**
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Mode variant.
*/
mode?: ModeValue;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus", "selected"/"Selected" (case-insensitive).
* Visual state.
* Note: "selected" state is represented by the `checked` prop in practice.
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
state?: StateValue;
/**
@@ -3,11 +3,11 @@
import { memo, useCallback, useId } from "react";
import { RadioGroupView } from "./RadioGroup.view";
import type { RadioGroupProps } from "./RadioGroup.types";
import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / RadioGroup" (TODO(figma)). Group of radio buttons sharing
* a name that emits the single currently selected value.
*/
const RadioGroupContainer = ({
name,
value,
@@ -19,14 +19,11 @@ const RadioGroupContainer = ({
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);
const mode = modeProp;
const state: "default" | "hover" | "focus" | "selected" =
stateProp === "With Subtext" || stateProp === "with subtext"
? "default"
: stateProp;
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const groupId = name || `radio-group-${generatedId}`;
@@ -12,14 +12,12 @@ export interface RadioGroupProps {
value?: string;
onChange?: (_data: { value: string }) => void;
/**
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Mode variant.
*/
mode?: ModeValue;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Visual state.
* 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;
@@ -16,12 +16,11 @@ import React, {
import { useClickOutside } from "../../../hooks";
import { SelectInputView } from "./SelectInput.view";
import type { SelectInputProps } from "./SelectInput.types";
import {
normalizeState,
normalizeSmallMediumLargeSize,
normalizeLabelVariant,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / SelectInput" (TODO(figma)). Custom-styled select dropdown
* with a labelled trigger button and floating option menu.
*/
const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
(
{
@@ -53,22 +52,14 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
const shouldShowLabel =
showLabel !== undefined ? showLabel : labelText !== undefined;
// Normalize state - handle "state5" as disabled
let normalizedState = externalStateProp;
if (normalizedState === "state5" || normalizedState === "State5") {
normalizedState = "default"; // Map to default, disabled prop handles the disabled state
normalizedState = "default";
}
const externalState = normalizeState(normalizedState);
const externalState = normalizedState;
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
// Note: labelVariant and size are normalized for future use but not yet implemented in the view
const _labelVariant = labelVariantProp
? normalizeLabelVariant(labelVariantProp)
: undefined;
const _size = sizeProp
? normalizeSmallMediumLargeSize(sizeProp)
: undefined;
// Mark as intentionally unused for future implementation
const _labelVariant = labelVariantProp;
const _size = sizeProp;
void _labelVariant;
void _size;
@@ -7,18 +7,8 @@ export interface SelectOptionData {
import type { StateValue } from "../../../../lib/propNormalization";
export type SelectInputLabelVariantValue =
| "default"
| "horizontal"
| "Default"
| "Horizontal";
export type SelectInputSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export type SelectInputLabelVariantValue = "default" | "horizontal";
export type SelectInputSizeValue = "small" | "medium" | "large";
export interface SelectInputProps {
id?: string;
@@ -33,18 +23,15 @@ export interface SelectInputProps {
*/
showLabel?: boolean;
/**
* Label variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Label variant.
*/
labelVariant?: SelectInputLabelVariantValue;
/**
* Select input size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Select input size.
*/
size?: SelectInputSizeValue;
/**
* Visual state. Accepts "default"/"Default", "active"/"Active", "focus"/"Focus", "error"/"Error", "state5"/"State5" (State5 = Disabled).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Visual state. "state5" maps to disabled.
*/
state?: StateValue | "state5" | "State5";
/**
@@ -1,7 +1,7 @@
import React, { Children, type ReactNode } from "react";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import SelectDropdown from "./SelectDropdown";
import SelectOption from "./SelectOption";
import SelectOption from "../SelectOption";
import type { SelectOptionData } from "./SelectInput.types";
export interface SelectInputViewProps {
@@ -3,8 +3,11 @@
import { forwardRef, memo, useCallback } from "react";
import { SelectOptionView } from "./SelectOption.view";
import type { SelectOptionProps } from "./SelectOption.types";
import { normalizeContextMenuItemSize } from "../../../../../lib/propNormalization";
/**
* Figma: "Control / SelectOption" (TODO(figma)). Single option row rendered
* inside `SelectInput`'s dropdown menu.
*/
const SelectOptionContainer = forwardRef<HTMLDivElement, SelectOptionProps>(
(
{
@@ -18,8 +21,7 @@ const SelectOptionContainer = forwardRef<HTMLDivElement, SelectOptionProps>(
},
ref,
) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeContextMenuItemSize(sizeProp);
const size = sizeProp;
const getTextSize = (): string => {
switch (size) {
case "small":
@@ -1,10 +1,4 @@
export type SelectOptionSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export type SelectOptionSizeValue = "small" | "medium" | "large";
export interface SelectOptionProps {
children?: React.ReactNode;
@@ -15,8 +9,7 @@ export interface SelectOptionProps {
_e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
) => void;
/**
* Select option size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Select option size.
*/
size?: SelectOptionSizeValue;
}
@@ -3,8 +3,11 @@
import { memo, useCallback, useId, forwardRef } from "react";
import { SwitchView } from "./Switch.view";
import type { SwitchProps } from "./Switch.types";
import { normalizeState } from "../../../../lib/propNormalization";
/**
* Figma: "Control / Switch" (TODO(figma)). Animated on/off toggle switch,
* optionally paired with a trailing text label.
*/
const SwitchContainer = memo(
forwardRef<HTMLButtonElement, SwitchProps>((props, ref) => {
const {
@@ -18,8 +21,7 @@ const SwitchContainer = memo(
...rest
} = props;
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const state = normalizeState(stateProp);
const state = stateProp;
const switchId = useId();
@@ -17,8 +17,7 @@ export interface SwitchProps extends Omit<
onFocus?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Visual state.
*/
state?: StateValue;
/**
@@ -4,13 +4,11 @@ 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,
normalizeTextAreaAppearance,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / TextArea" (TODO(figma)). Multi-line text input with size
* variants, an embedded appearance, and an optional label and help glyph.
*/
const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
(
{
@@ -37,11 +35,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);
const appearance = normalizeTextAreaAppearance(appearanceProp);
const size = sizeProp;
const labelVariant = labelVariantProp;
const state = stateProp;
const appearance = appearanceProp;
// Generate unique ID for accessibility if not provided
const { id: textareaId, labelId } = useComponentId("textarea", id);
@@ -1,41 +1,24 @@
import type { InputStateValue } from "../../../../lib/propNormalization";
export type TextAreaSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export type TextAreaLabelVariantValue =
| "default"
| "horizontal"
| "Default"
| "Horizontal";
export type TextAreaSizeValue = "small" | "medium" | "large";
export type TextAreaLabelVariantValue = "default" | "horizontal";
export type TextAreaAppearanceValue =
| "default"
| "embedded"
| "Default"
| "Embedded";
export type TextAreaAppearanceValue = "default" | "embedded";
export interface TextAreaProps extends Omit<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
"size" | "onChange" | "onFocus" | "onBlur"
> {
/**
* Text area size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Text area size.
*/
size?: TextAreaSizeValue;
/**
* Label variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Label variant.
*/
labelVariant?: TextAreaLabelVariantValue;
/**
* Visual state. Accepts "default"/"Default", "active"/"Active", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Visual state.
*/
state?: InputStateValue;
disabled?: boolean;
@@ -4,11 +4,11 @@ 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,
normalizeTextInputSize,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / TextInput" (TODO(figma)). Single-line text input with size
* variants and managed default/active/focus/error states.
*/
const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
(
{
@@ -33,9 +33,8 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
},
ref,
) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const externalState = normalizeInputState(externalStateProp);
const inputSize = normalizeTextInputSize(inputSizeProp);
const externalState = externalStateProp;
const inputSize = inputSizeProp;
// Generate unique ID for accessibility if not provided
const { id: inputId, labelId } = useComponentId("text-input", id);
@@ -1,19 +1,17 @@
import type { InputStateValue } from "../../../../lib/propNormalization";
export type TextInputSizeValue = "small" | "medium" | "Small" | "Medium";
export type TextInputSizeValue = "small" | "medium";
export interface TextInputProps extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
"size" | "onChange" | "onFocus" | "onBlur"
> {
/**
* Visual state. Accepts "default"/"Default", "active"/"Active", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Visual state.
*/
state?: InputStateValue;
/**
* Size variant. Accepts both PascalCase (Figma) and lowercase (codebase).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Size variant.
* @default "medium"
*/
inputSize?: TextInputSizeValue;
@@ -3,8 +3,11 @@
import { memo, useCallback, useId, forwardRef } from "react";
import { ToggleView } from "./Toggle.view";
import type { ToggleProps } from "./Toggle.types";
import { normalizeState } from "../../../../lib/propNormalization";
/**
* Figma: "Control / Toggle" (TODO(figma)). Pill-shaped toggle button with
* checked/unchecked states and optional leading icon and text.
*/
const ToggleContainer = forwardRef<HTMLButtonElement, ToggleProps>(
(
{
@@ -24,8 +27,7 @@ const ToggleContainer = forwardRef<HTMLButtonElement, ToggleProps>(
},
ref,
) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const state = normalizeState(stateProp);
const state = stateProp;
const toggleId = useId();
const labelId = useId();
@@ -15,8 +15,7 @@ export interface ToggleProps extends Omit<
onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
disabled?: boolean;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Visual state.
*/
state?: StateValue;
showIcon?: boolean;
@@ -3,11 +3,11 @@
import { memo, useCallback, useId, forwardRef } from "react";
import { ToggleGroupView } from "./ToggleGroup.view";
import type { ToggleGroupProps } from "./ToggleGroup.types";
import {
normalizeToggleState,
normalizeToggleGroupPosition,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / ToggleGroup" (TODO(figma)). Segmented row of `Toggle`
* buttons whose corner radii are shared based on position (left/middle/right).
*/
const ToggleGroupContainer = memo(
forwardRef<HTMLButtonElement, ToggleGroupProps>((props, _ref) => {
const {
@@ -23,9 +23,8 @@ const ToggleGroupContainer = memo(
...rest
} = props;
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const position = normalizeToggleGroupPosition(positionProp);
const state = normalizeToggleState(stateProp);
const position = positionProp;
const state = stateProp;
const groupId = useId();
@@ -1,12 +1,6 @@
import type { StateValue } from "../../../../lib/propNormalization";
export type ToggleGroupPositionValue =
| "left"
| "middle"
| "right"
| "Left"
| "Middle"
| "Right";
export type ToggleGroupPositionValue = "left" | "middle" | "right";
export interface ToggleGroupProps extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
@@ -15,15 +9,13 @@ export interface ToggleGroupProps extends Omit<
children?: React.ReactNode;
className?: string;
/**
* Toggle group position. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Toggle group position.
*/
position?: ToggleGroupPositionValue;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus", "selected"/"Selected" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Visual state.
*/
state?: StateValue | "selected" | "Selected";
state?: StateValue | "selected";
showText?: boolean;
ariaLabel?: string;
onChange?: (
@@ -4,6 +4,10 @@ import { memo } from "react";
import UploadView from "./Upload.view";
import type { UploadProps } from "./Upload.types";
/**
* Figma: "Control / Upload" (TODO(figma)). Click-to-upload tile with a label
* and hint text used to add an image from the user's device.
*/
const UploadContainer = memo<UploadProps>(
({
active = true,
@@ -44,8 +44,8 @@ function UploadView({
helpIcon={showHelpIcon}
asterisk={false}
helperText={false}
size="S"
palette="Default"
size="s"
palette="default"
/>
)}
+2 -16
View File
@@ -1,31 +1,17 @@
import { memo } from "react";
import { normalizeSize } from "../../../lib/propNormalization";
export type AvatarSizeValue =
| "small"
| "medium"
| "large"
| "xlarge"
| "Small"
| "Medium"
| "Large"
| "XLarge";
export type AvatarSizeValue = "small" | "medium" | "large" | "xlarge";
interface AvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> {
src: string;
alt: string;
/**
* 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: sizeProp = "small", className = "", ...props }) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeSize(sizeProp, "small");
const size = sizeProp;
const sizeStyles: Record<string, string> = {
small:
"w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)] border-[1.5px] border-[#FFFFFF4D] border-solid",
@@ -3,10 +3,6 @@
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>(
({
@@ -19,9 +15,8 @@ const AlertContainer = memo<AlertProps>(
onClose,
className = "",
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const status = normalizeAlertStatus(statusProp);
const type = normalizeAlertType(typeProp);
const status = statusProp;
const type = typeProp;
// Determine background and border colors based on status and type
const getStatusStyles = () => {
switch (status) {
+4 -14
View File
@@ -1,26 +1,16 @@
export type AlertStatusValue =
| "default"
| "positive"
| "warning"
| "danger"
| "Default"
| "Positive"
| "Warning"
| "Danger";
export type AlertStatusValue = "default" | "positive" | "warning" | "danger";
export type AlertTypeValue = "toast" | "banner" | "Toast" | "Banner";
export type AlertTypeValue = "toast" | "banner";
export interface AlertProps {
title: string;
description?: string;
/**
* Alert status. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Alert status.
*/
status?: AlertStatusValue;
/**
* Alert type. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Alert type.
*/
type?: AlertTypeValue;
/**
@@ -3,7 +3,6 @@
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,
@@ -22,8 +21,7 @@ const ContextMenuItemContainer = forwardRef<
},
ref,
) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeContextMenuItemSize(sizeProp);
const size = sizeProp;
const getTextSize = (): string => {
switch (size) {
case "small":
@@ -1,10 +1,4 @@
export type ContextMenuItemSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export type ContextMenuItemSizeValue = "small" | "medium" | "large";
export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
@@ -16,8 +10,7 @@ export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElemen
_e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
) => void;
/**
* Context menu item size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Context menu item size.
*/
size?: ContextMenuItemSizeValue;
}
+1 -1
View File
@@ -9,7 +9,7 @@ import TextInput from "../../controls/TextInput";
import ContentLockup from "../../type/ContentLockup";
import { requestMagicLink } from "../../../../lib/create/api";
import { safeInternalPath } from "../../../../lib/safeInternalPath";
import { setTransferPendingFlag } from "../../../create/utils/anonymousDraftStorage";
import { setTransferPendingFlag } from "../../../(app)/create/utils/anonymousDraftStorage";
/** Mail icon for login modal (inline SVG; same pattern as InfoMessageBox ExclamationIconInline). */
function MailIconInline() {
@@ -3,7 +3,6 @@
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>(
({
@@ -13,8 +12,7 @@ const TooltipContainer = memo<TooltipProps>(
className = "",
disabled = false,
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const position = normalizeTooltipPosition(positionProp);
const position = positionProp;
const [isVisible, setIsVisible] = useState(false);
if (disabled) {
@@ -1,11 +1,10 @@
export type TooltipPositionValue = "top" | "bottom" | "Top" | "Bottom";
export type TooltipPositionValue = "top" | "bottom";
export interface TooltipProps {
children: React.ReactNode;
text: string;
/**
* Tooltip position. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Tooltip position.
*/
position?: TooltipPositionValue;
className?: string;
@@ -1,33 +0,0 @@
"use client";
import { memo } from "react";
import { usePathname } from "next/navigation";
import dynamic from "next/dynamic";
// Code split Footer - below the fold, can be lazy loaded
const Footer = dynamic(() => import("./Footer"), {
loading: () => (
<footer className="bg-[var(--color-surface-default-primary)] w-full min-h-[200px]" />
),
ssr: true, // Keep SSR for SEO
});
/**
* Conditionally renders Footer based on pathname.
* Hides footer for /create/* and /login (full-screen flows; no site chrome).
*/
const ConditionalFooter = memo(() => {
const pathname = usePathname();
const isCreateFlow = pathname?.startsWith("/create");
const isLogin = pathname === "/login";
if (isCreateFlow || isLogin) {
return null;
}
return <Footer />;
});
ConditionalFooter.displayName = "ConditionalFooter";
export default ConditionalFooter;
+1 -2
View File
@@ -2,7 +2,6 @@
import { memo } from "react";
import { useTranslation } from "../../contexts/MessagesContext";
import { normalizeMenuBarSize } from "../../../lib/propNormalization";
export type MenuBarSizeValue =
| "X Small"
@@ -23,7 +22,7 @@ interface MenuBarProps extends React.HTMLAttributes<HTMLElement> {
const MenuBar = memo<MenuBarProps>(
({ children, className = "", size: sizeProp = "X Small", ...props }) => {
const size = normalizeMenuBarSize(sizeProp);
const size = sizeProp ?? "X Small";
const t = useTranslation("menuBar");
// Size styles based on Figma specifications
@@ -3,11 +3,6 @@
import { memo } from "react";
import MenuBarItemView from "./MenuBarItem.view";
import type { MenuBarItemProps } from "./MenuBarItem.types";
import {
normalizeMenuBarItemState,
normalizeMenuBarItemMode,
normalizeMenuBarItemSize,
} from "../../../../lib/propNormalization";
const MenuBarItemContainer = memo<MenuBarItemProps>(
({
@@ -24,9 +19,9 @@ const MenuBarItemContainer = memo<MenuBarItemProps>(
ariaLabel,
...props
}) => {
const state = normalizeMenuBarItemState(stateProp, "default");
const mode = normalizeMenuBarItemMode(modeProp, "default");
const size = normalizeMenuBarItemSize(sizeProp, "X Small");
const state = stateProp ?? "default";
const mode = modeProp ?? "default";
const size = sizeProp ?? "X Small";
// Size styles based on Figma specifications
const sizeStyles: Record<
@@ -3,10 +3,6 @@
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>(
({
@@ -19,9 +15,8 @@ const NavigationItemContainer = memo<NavigationItemProps>(
isActive = false,
...props
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const variant = normalizeNavigationItemVariant(variantProp);
const size = normalizeNavigationItemSize(sizeProp);
const variant = variantProp;
const size = sizeProp;
// Variant styles
const variantStyles: Record<string, string> = {
default:
@@ -1,9 +1,5 @@
export type NavigationItemVariantValue = "default" | "Default";
export type NavigationItemSizeValue =
| "default"
| "xsmall"
| "Default"
| "XSmall";
export type NavigationItemVariantValue = "default";
export type NavigationItemSizeValue = "default" | "xsmall";
export interface NavigationItemProps extends Omit<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
@@ -12,13 +8,11 @@ export interface NavigationItemProps extends Omit<
href?: string;
children?: React.ReactNode;
/**
* Navigation item variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Navigation item variant.
*/
variant?: NavigationItemVariantValue;
/**
* Navigation item size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Navigation item size.
*/
size?: NavigationItemSizeValue;
className?: string;
@@ -1,13 +1,12 @@
"use client";
import { memo } from "react";
import { normalizeProportionBarVariant } from "../../../../lib/propNormalization";
import { ProportionBarView } from "./ProportionBar.view";
import type { ProportionBarProps } from "./ProportionBar.types";
const ProportionBarContainer = memo<ProportionBarProps>(
({ progress = "3-2", className = "", variant: variantProp }) => {
const variant = normalizeProportionBarVariant(variantProp);
const variant = variantProp ?? "default";
const barClasses = `h-[8px] relative w-full`;
return (
@@ -22,8 +22,8 @@ export interface ProportionBarProps {
className?: string;
/**
* Kept for backwards compatibility. Both `default` and `segmented` render the
* same fill geometry (square leading edges, matching Figma). Future variants
* can differentiate here without API changes.
* same fill geometry. Future variants can differentiate here without API
* changes.
*/
variant?: ProportionBarVariant;
}
@@ -8,7 +8,6 @@ import type {
AskOrganizerProps,
AskOrganizerVariant,
} from "./AskOrganizer.types";
import { normalizeAskOrganizerVariant } from "../../../../lib/propNormalization";
const VARIANT_STYLES: Record<
"centered" | "left-aligned" | "compact" | "inverse",
@@ -43,10 +42,7 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
variant: variantProp = "centered",
onContactClick,
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const variant = normalizeAskOrganizerVariant(
variantProp,
) as AskOrganizerVariant;
const variant = variantProp;
const t = useTranslation();
const defaultButtonText = buttonText ?? t("askOrganizer.buttonText");
const defaultButtonHref = buttonHref ?? t("askOrganizer.buttonHref");
@@ -4,11 +4,7 @@ export type AskOrganizerVariant =
| "centered"
| "left-aligned"
| "compact"
| "inverse"
| "Centered"
| "Left-Aligned"
| "Compact"
| "Inverse";
| "inverse";
export interface AskOrganizerProps {
title?: string;
@@ -18,8 +14,7 @@ export interface AskOrganizerProps {
buttonHref?: string;
className?: string;
/**
* Ask organizer variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Ask organizer variant.
*/
variant?: AskOrganizerVariant;
onContactClick?: (_data: {
@@ -4,7 +4,6 @@ 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>(
({
@@ -18,8 +17,7 @@ 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 variant = variantProp;
const [imageError, setImageError] = useState(false);
const [imageLoading, setImageLoading] = useState(true);
@@ -1,15 +1,8 @@
export type QuoteBlockVariantValue =
| "compact"
| "standard"
| "extended"
| "Compact"
| "Standard"
| "Extended";
export type QuoteBlockVariantValue = "compact" | "standard" | "extended";
export interface QuoteBlockProps {
/**
* Quote block variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Quote block variant.
*/
variant?: QuoteBlockVariantValue;
className?: string;
+2 -12
View File
@@ -1,29 +1,19 @@
"use client";
import { memo } from "react";
import { normalizeSectionHeaderVariant } from "../../../lib/propNormalization";
export type SectionHeaderVariantValue =
| "default"
| "multi-line"
| "Default"
| "Multi-Line";
export type SectionHeaderVariantValue = "default" | "multi-line";
interface SectionHeaderProps {
title: string;
subtitle: string;
titleLg?: string;
/**
* 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: variantProp = "default" }) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const variant = normalizeSectionHeaderVariant(variantProp);
const variant = variantProp;
return (
<div
className={
@@ -1,7 +1,7 @@
"use client";
import { memo, useEffect, useState } from "react";
import { logger } from "../../../lib/logger";
import { logger } from "../../../../lib/logger";
import WebVitalsDashboardView from "./WebVitalsDashboard.view";
import type { Metrics, Vitals, VitalData } from "./WebVitalsDashboard.types";
@@ -3,10 +3,6 @@
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>(
({
@@ -21,9 +17,8 @@ const ContentLockupContainer = memo<ContentLockupProps>(
alignment: alignmentProp = "center",
titleId,
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const variant = normalizeContentLockupVariant(variantProp);
const alignment = normalizeAlignment(alignmentProp);
const variant = variantProp;
const alignment = alignmentProp;
// Variant-specific styling
const variantStyles: Record<string, VariantStyle> = {
hero: {
@@ -5,16 +5,9 @@ export type ContentLockupVariantValue =
| "ask"
| "ask-inverse"
| "modal"
| "login"
| "Hero"
| "Feature"
| "Learn"
| "Ask"
| "Ask-Inverse"
| "Modal"
| "Login";
| "login";
export type ContentLockupAlignmentValue = "center" | "left" | "Center" | "Left";
export type ContentLockupAlignmentValue = "center" | "left";
export interface ContentLockupProps {
title?: string;
@@ -24,15 +17,13 @@ export interface ContentLockupProps {
ctaHref?: string;
buttonClassName?: string;
/**
* Content lockup variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Content lockup variant.
*/
variant?: ContentLockupVariantValue;
linkText?: string;
linkHref?: string;
/**
* Text alignment. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Text alignment.
*/
alignment?: ContentLockupAlignmentValue;
/**
@@ -3,11 +3,6 @@
import { memo } from "react";
import HeaderLockupView from "./HeaderLockup.view";
import type { HeaderLockupProps } from "./HeaderLockup.types";
import {
normalizeHeaderLockupJustification,
normalizeHeaderLockupSize,
normalizeHeaderLockupPalette,
} from "../../../../lib/propNormalization";
const HeaderLockupContainer = memo<HeaderLockupProps>(
({
@@ -17,10 +12,9 @@ const HeaderLockupContainer = memo<HeaderLockupProps>(
size: sizeProp = "L",
palette: paletteProp = "default",
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const justification = normalizeHeaderLockupJustification(justificationProp);
const size = normalizeHeaderLockupSize(sizeProp);
const palette = normalizeHeaderLockupPalette(paletteProp);
const justification = justificationProp;
const size = sizeProp;
const palette = paletteProp;
return (
<HeaderLockupView
@@ -1,16 +1,8 @@
import type { ReactNode } from "react";
export type HeaderLockupJustificationValue =
| "left"
| "center"
| "Left"
| "Center";
export type HeaderLockupSizeValue = "L" | "M" | "l" | "m";
export type HeaderLockupPaletteValue =
| "default"
| "inverse"
| "Default"
| "Inverse";
export type HeaderLockupJustificationValue = "left" | "center";
export type HeaderLockupSizeValue = "L" | "M";
export type HeaderLockupPaletteValue = "default" | "inverse";
export interface HeaderLockupProps {
/**
@@ -22,18 +14,15 @@ export interface HeaderLockupProps {
*/
description?: ReactNode;
/**
* Text justification. Accepts both PascalCase (Figma) and lowercase (codebase).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Text justification.
*/
justification?: HeaderLockupJustificationValue;
/**
* Size variant. Accepts both PascalCase (Figma) and lowercase (codebase).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Size variant.
*/
size?: HeaderLockupSizeValue;
/**
* Palette. Default = light text (dark bg); Inverse = dark text (light bg).
* Accepts both PascalCase (Figma) and lowercase (codebase).
* Palette. default = light text (dark bg); inverse = dark text (light bg).
*/
palette?: HeaderLockupPaletteValue;
}
@@ -3,12 +3,10 @@
import { memo } from "react";
import NumberedListView from "./NumberedList.view";
import type { NumberedListProps } from "./NumberedList.types";
import { normalizeNumberedListSize } from "../../../../lib/propNormalization";
const NumberedListContainer = memo<NumberedListProps>(
({ items, size: sizeProp = "M" }) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeNumberedListSize(sizeProp);
const size = sizeProp;
return <NumberedListView items={items} size={size} />;
},
@@ -1,4 +1,4 @@
export type NumberedListSizeValue = "M" | "S" | "m" | "s";
export type NumberedListSizeValue = "M" | "S";
export interface NumberedListItem {
title: string;
@@ -11,8 +11,7 @@ export interface NumberedListProps {
*/
items: NumberedListItem[];
/**
* Size variant. Accepts both PascalCase (Figma) and lowercase (codebase).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Size variant.
*/
size?: NumberedListSizeValue;
}
@@ -1,30 +1,16 @@
import { memo } from "react";
import { normalizeSize } from "../../../lib/propNormalization";
export type AvatarContainerSizeValue =
| "small"
| "medium"
| "large"
| "xlarge"
| "Small"
| "Medium"
| "Large"
| "XLarge";
export type AvatarContainerSizeValue = "small" | "medium" | "large" | "xlarge";
interface AvatarContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
/**
* 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: sizeProp = "small", className = "", ...props }) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeSize(sizeProp, "small");
const size = sizeProp;
const sizeStyles: Record<string, string> = {
small: "flex -space-x-[var(--spacing-scale-008)]",
medium: "flex -space-x-[9px]",
@@ -0,0 +1,2 @@
export { default } from "./AvatarContainer";
export type { AvatarContainerSizeValue } from "./AvatarContainer";
@@ -7,6 +7,10 @@ import type { CardStackProps } from "./CardStack.types";
const DEFAULT_TOGGLE_LABEL = "See all communication approaches";
const DEFAULT_SHOW_LESS_LABEL = "Show less";
/**
* Figma: "Utility / CardStack" (TODO(figma)). Selectable stack of cards with
* an optional "see all"/"show less" expand toggle.
*/
const CardStackContainer = memo<CardStackProps>(
({
cards,
@@ -4,6 +4,10 @@ import { memo } from "react";
import { CreateFlowFooterView } from "./CreateFlowFooter.view";
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
/**
* Figma: "Utility / CreateFlowFooter" (TODO(figma)). Sticky footer for the
* create flow with a back action, optional secondary button, and progress bar.
*/
const CreateFlowFooterContainer = memo<CreateFlowFooterProps>(
({
secondButton,
@@ -1,4 +1,3 @@
import { normalizeProportionBarVariant } from "../../../../lib/propNormalization";
import ProportionBar from "../../progress/ProportionBar";
import Button from "../../buttons/Button";
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
@@ -11,9 +10,7 @@ export function CreateFlowFooterView({
onBackClick,
className = "",
}: CreateFlowFooterProps) {
const proportionBarVariant = normalizeProportionBarVariant(
proportionBarVariantProp,
);
const proportionBarVariant = proportionBarVariantProp ?? "default";
return (
<footer
className={`bg-black w-full ${className}`}
@@ -5,6 +5,10 @@ import { useRouter } from "next/navigation";
import { CreateFlowTopNavView } from "./CreateFlowTopNav.view";
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
/**
* Figma: "Utility / CreateFlowTopNav" (TODO(figma)). Top navigation bar for
* the create flow with exit, share, export, and edit actions.
*/
const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
({
hasShare = false,
@@ -3,11 +3,11 @@
import { memo } from "react";
import DecisionMakingSidebarView from "./DecisionMakingSidebar.view";
import type { DecisionMakingSidebarProps } from "./DecisionMakingSidebar.types";
import {
normalizeHeaderLockupJustification,
normalizeHeaderLockupSize,
} from "../../../../lib/propNormalization";
/**
* Figma: "Utility / DecisionMakingSidebar" (TODO(figma)). Sidebar pairing a
* header lockup with an `InfoMessageBox` checklist for decision-making screens.
*/
const DecisionMakingSidebarContainer = memo<DecisionMakingSidebarProps>(
({
title,
@@ -20,8 +20,8 @@ const DecisionMakingSidebarContainer = memo<DecisionMakingSidebarProps>(
justification: justificationProp = "left",
className = "",
}) => {
const size = normalizeHeaderLockupSize(sizeProp);
const justification = normalizeHeaderLockupJustification(justificationProp);
const size = sizeProp;
const justification = justificationProp;
return (
<DecisionMakingSidebarView
-58
View File
@@ -1,58 +0,0 @@
"use client";
import React, { Component, type ReactNode } from "react";
import { logger } from "../../../lib/logger";
interface ErrorBoundaryProps {
children: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
// Update state so the next render will show the fallback UI
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log the error to an error reporting service
logger.error("ErrorBoundary caught an error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// Fallback UI using design tokens
return (
<div className="min-h-[200px] flex items-center justify-center p-[var(--spacing-scale-016)]">
<div className="text-center">
<h2 className="text-xl font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-008)]">
Something went wrong
</h2>
<p className="text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-016)]">
We&apos;re sorry, but something unexpected happened.
</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
className="px-[var(--spacing-scale-016)] py-[var(--spacing-scale-008)] bg-[var(--color-surface-default-brand-royal)] text-[var(--color-content-inverse-primary)] rounded-[var(--radius-measures-radius-small)] hover:bg-[var(--color-surface-hover-brand-royal)] transition-colors"
>
Try again
</button>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
@@ -1,70 +0,0 @@
"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;
/**
* Image placeholder color. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
color?: ImagePlaceholderColorValue;
className?: string;
}
/**
* Simple image placeholder component for testing
* Generates colored backgrounds with text overlays
*/
const ImagePlaceholder = memo<ImagePlaceholderProps>(
({
width = 260,
height = 390,
text = "Blog Image",
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",
purple: "bg-purple-500",
red: "bg-red-500",
orange: "bg-orange-500",
teal: "bg-teal-500",
};
const bgColor = colors[color] || colors.blue;
return (
<div
className={`${bgColor} flex items-center justify-center text-white font-bold text-lg ${className}`}
style={{ width: `${width}px`, height: `${height}px` }}
>
{text}
</div>
);
},
);
ImagePlaceholder.displayName = "ImagePlaceholder";
export default ImagePlaceholder;
@@ -4,6 +4,10 @@ import { memo, useCallback, useState } from "react";
import InfoMessageBoxView from "./InfoMessageBox.view";
import type { InfoMessageBoxProps } from "./InfoMessageBox.types";
/**
* Figma: "Utility / InfoMessageBox" (TODO(figma)). Bordered message box that
* lists checkbox items under a title with an optional leading icon.
*/
const InfoMessageBoxContainer = memo<InfoMessageBoxProps>(
({
title,
@@ -3,23 +3,23 @@
import { memo } from "react";
import InputLabelView from "./InputLabel.view";
import type { InputLabelProps } from "./InputLabel.types";
import {
normalizeInputLabelSize,
normalizeInputLabelPalette,
} from "../../../../lib/propNormalization";
/**
* Figma: "Utility / InputLabel" (TODO(figma)). Reusable form-input label with
* optional asterisk, help icon, and helper text.
*/
const InputLabelContainer = memo<InputLabelProps>(
({
label,
helpIcon = false,
asterisk = false,
helperText = false,
size: sizeProp = "S",
palette: paletteProp = "Default",
size: sizeProp = "s",
palette: paletteProp = "default",
className = "",
}) => {
const size = normalizeInputLabelSize(sizeProp);
const palette = normalizeInputLabelPalette(paletteProp);
const size = sizeProp;
const palette = paletteProp;
return (
<InputLabelView
@@ -1,9 +1,5 @@
export type InputLabelSizeValue = "S" | "M" | "s" | "m";
export type InputLabelPaletteValue =
| "Default"
| "Inverse"
| "default"
| "inverse";
export type InputLabelSizeValue = "s" | "m";
export type InputLabelPaletteValue = "default" | "inverse";
export interface InputLabelProps {
/**
@@ -25,13 +21,11 @@ export interface InputLabelProps {
*/
helperText?: boolean | string;
/**
* Size variant: "S" (small) or "M" (medium)
* Accepts both uppercase (Figma) and lowercase values.
* Size variant: "s" (small) or "m" (medium)
*/
size?: InputLabelSizeValue;
/**
* Palette variant: "Default" or "Inverse"
* Accepts both PascalCase (Figma) and lowercase values.
* Palette variant: "default" or "inverse"
*/
palette?: InputLabelPaletteValue;
className?: string;
@@ -0,0 +1,18 @@
"use client";
import { memo } from "react";
import { ModalFooterView } from "./ModalFooter.view";
import type { ModalFooterProps } from "./ModalFooter.types";
/**
* Figma: "Utility / ModalFooter" (TODO(figma)).
* Sticky modal footer slot used by the create-flow + login modals to host
* primary/secondary actions.
*/
const ModalFooterContainer = memo<ModalFooterProps>((props) => {
return <ModalFooterView {...props} />;
});
ModalFooterContainer.displayName = "ModalFooter";
export default ModalFooterContainer;
+1 -1
View File
@@ -1,2 +1,2 @@
export { ModalFooterView as default } from "./ModalFooter.view";
export { default } from "./ModalFooter.container";
export type { ModalFooterProps } from "./ModalFooter.types";
@@ -0,0 +1,18 @@
"use client";
import { memo } from "react";
import { ModalHeaderView } from "./ModalHeader.view";
import type { ModalHeaderProps } from "./ModalHeader.types";
/**
* Figma: "Utility / ModalHeader" (TODO(figma)).
* Sticky 48px modal header with optional close (left) and more-options
* (right) icon buttons.
*/
const ModalHeaderContainer = memo<ModalHeaderProps>((props) => {
return <ModalHeaderView {...props} />;
});
ModalHeaderContainer.displayName = "ModalHeader";
export default ModalHeaderContainer;
+1 -1
View File
@@ -1,2 +1,2 @@
export { ModalHeaderView as default } from "./ModalHeader.view";
export { default } from "./ModalHeader.container";
export type { ModalHeaderProps } from "./ModalHeader.types";
@@ -0,0 +1,18 @@
"use client";
import { memo } from "react";
import { ScrollbarView } from "./Scrollbar.view";
import type { ScrollbarProps } from "./Scrollbar.types";
/**
* Figma: "Utility / Scrollbar" (TODO(figma)).
* Custom-styled scrollable wrapper. Most surfaces should attach
* `SCROLLBAR_DESIGN_CLASS` directly instead of nesting through this view.
*/
const ScrollbarContainer = memo<ScrollbarProps>((props) => {
return <ScrollbarView {...props} />;
});
ScrollbarContainer.displayName = "Scrollbar";
export default ScrollbarContainer;

Some files were not shown because too many files have changed in this diff Show More