Merge pull request 'adilallo/maintanence/ComponentOrganizationPolish' (#40) from adilallo/maintanence/ComponentOrganizationPolish into main

Reviewed-on: #40
This commit was merged in pull request #40.
This commit is contained in:
2026-02-07 05:31:44 +00:00
307 changed files with 2446 additions and 2500 deletions
@@ -1,11 +1,11 @@
import WebVitalsDashboard from "../components/WebVitalsDashboard";
import Header from "../components/Header";
import Footer from "../components/Footer";
import WebVitalsDashboard from "../../components/WebVitalsDashboard";
import TopNav from "../../components/navigation/TopNav";
import Footer from "../../components/navigation/Footer";
export default function MonitorPage() {
return (
<div className="min-h-screen bg-[var(--color-surface-default-primary)]">
<Header />
<TopNav folderTop={false} />
<main className="container mx-auto px-[var(--spacing-scale-024)] py-[var(--spacing-scale-032)]">
<div className="max-w-6xl mx-auto">
@@ -1,11 +1,11 @@
"use client";
import { useState } from "react";
import RuleCard from "../components/RuleCard";
import Chip from "../components/Chip";
import MultiSelect from "../components/MultiSelect";
import RuleCard from "../components/cards/RuleCard";
import Chip from "../components/controls/Chip";
import MultiSelect from "../components/controls/MultiSelect";
import Image from "next/image";
import { getAssetPath } from "../../lib/assetUtils";
import { getAssetPath } from "../../../lib/assetUtils";
interface ChipData {
id: string;
@@ -5,16 +5,16 @@ import {
getBlogPostBySlug,
getAllBlogPosts as getAllPosts,
type BlogPost,
} from "../../../lib/content";
import { logger } from "../../../lib/logger";
import ContentBanner from "../../components/ContentBanner";
import AskOrganizer from "../../components/AskOrganizer";
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
} from "../../../../lib/content";
import { logger } from "../../../../lib/logger";
import ContentBanner from "../../../components/sections/ContentBanner";
import AskOrganizer from "../../../components/sections/AskOrganizer";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import "../blog.css";
// Code split RelatedArticles - blog-specific, below the fold
const RelatedArticles = dynamic(
() => import("../../components/RelatedArticles"),
() => import("../../../components/sections/RelatedArticles"),
{
loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[400px]" />
@@ -1,5 +1,5 @@
import { getAllBlogPosts } from "../../lib/content";
import ContentThumbnailTemplate from "../components/ContentThumbnailTemplate";
import { getAllBlogPosts } from "../../../lib/content";
import ContentThumbnailTemplate from "../../../components/content/ContentThumbnailTemplate";
import type { Metadata } from "next";
export const metadata: Metadata = {
@@ -1,9 +1,9 @@
import messages from "../../messages/en/index";
import { getTranslation } from "../../lib/i18n/getTranslation";
import ContentThumbnailTemplate from "../components/ContentThumbnailTemplate";
import ContentLockup from "../components/ContentLockup";
import AskOrganizer from "../components/AskOrganizer";
import { getAllBlogPosts } from "../../lib/content";
import messages from "../../../messages/en/index";
import { getTranslation } from "../../../lib/i18n/getTranslation";
import ContentThumbnailTemplate from "../../components/content/ContentThumbnailTemplate";
import ContentLockup from "../../components/type/ContentLockup";
import AskOrganizer from "../../components/sections/AskOrganizer";
import { getAllBlogPosts } from "../../../lib/content";
export default function LearnPage() {
// Get real blog posts from the content system
+9 -9
View File
@@ -1,39 +1,39 @@
import dynamic from "next/dynamic";
import messages from "../messages/en/index";
import { getTranslation } from "../lib/i18n/getTranslation";
import HeroBanner from "./components/HeroBanner";
import AskOrganizer from "./components/AskOrganizer";
import messages from "../../messages/en/index";
import { getTranslation } from "../../lib/i18n/getTranslation";
import HeroBanner from "../components/sections/HeroBanner";
import AskOrganizer from "../components/sections/AskOrganizer";
// Code split below-the-fold components to reduce initial bundle size
const LogoWall = dynamic(() => import("./components/LogoWall"), {
const LogoWall = dynamic(() => import("../components/sections/LogoWall"), {
loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[200px]" />
),
ssr: true,
});
const NumberedCards = dynamic(() => import("./components/NumberedCards"), {
const NumberedCards = dynamic(() => import("../components/sections/NumberedCards"), {
loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[300px]" />
),
ssr: true,
});
const RuleStack = dynamic(() => import("./components/RuleStack"), {
const RuleStack = dynamic(() => import("../components/sections/RuleStack"), {
loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[400px]" />
),
ssr: true,
});
const FeatureGrid = dynamic(() => import("./components/FeatureGrid"), {
const FeatureGrid = dynamic(() => import("../components/sections/FeatureGrid"), {
loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[500px]" />
),
ssr: true,
});
const QuoteBlock = dynamic(() => import("./components/QuoteBlock"), {
const QuoteBlock = dynamic(() => import("../components/sections/QuoteBlock"), {
loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[300px]" />
),
@@ -1,17 +0,0 @@
"use client";
import { memo } from "react";
import { usePathname } from "next/navigation";
import { ConditionalHeaderView } from "./ConditionalHeader.view";
import type { ConditionalHeaderProps } from "./ConditionalHeader.types";
const ConditionalHeaderContainer = memo<ConditionalHeaderProps>(() => {
const pathname = usePathname();
const isHomePage = pathname === "/";
return <ConditionalHeaderView isHomePage={isHomePage} />;
});
ConditionalHeaderContainer.displayName = "ConditionalHeader";
export default ConditionalHeaderContainer;
@@ -1,7 +0,0 @@
export interface ConditionalHeaderProps {
// Currently no props, but keeping interface for future extensibility
}
export interface ConditionalHeaderViewProps {
isHomePage: boolean;
}
@@ -1,12 +0,0 @@
import HomeHeader from "../HomeHeader";
import Header from "../Header";
import type { ConditionalHeaderViewProps } from "./ConditionalHeader.types";
export function ConditionalHeaderView({
isHomePage,
}: ConditionalHeaderViewProps) {
if (isHomePage) {
return <HomeHeader />;
}
return <Header />;
}
@@ -1,2 +0,0 @@
export { default } from "./ConditionalHeader.container";
export type { ConditionalHeaderProps } from "./ConditionalHeader.types";
@@ -3,7 +3,7 @@
import { forwardRef, memo, useCallback } from "react";
import { ContextMenuItemView } from "./ContextMenuItem.view";
import type { ContextMenuItemProps } from "./ContextMenuItem.types";
import { normalizeContextMenuItemSize } from "../../../lib/propNormalization";
import { normalizeContextMenuItemSize } from "../../../../lib/propNormalization";
const ContextMenuItemContainer = forwardRef<
HTMLDivElement,
-155
View File
@@ -1,155 +0,0 @@
"use client";
import { memo } from "react";
import { usePathname } from "next/navigation";
import { useTranslation } from "../../contexts/MessagesContext";
import MenuBarItem from "../MenuBarItem";
import Button from "../Button";
import AvatarContainer from "../AvatarContainer";
import Avatar from "../Avatar";
import Logo from "../Logo";
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
import { HeaderView } from "./Header.view";
import type { HeaderProps, NavSize } from "./Header.types";
export const avatarImages = [
{ src: getAssetPath(ASSETS.AVATAR_1), alt: "Avatar 1" },
{ src: getAssetPath(ASSETS.AVATAR_2), alt: "Avatar 2" },
{ src: getAssetPath(ASSETS.AVATAR_3), alt: "Avatar 3" },
];
export const logoConfig = [
{ breakpoint: "block sm:hidden", size: "header" as const, showText: false },
{
breakpoint: "hidden sm:block md:hidden",
size: "header" as const,
showText: true,
},
{
breakpoint: "hidden md:block lg:hidden",
size: "headerMd" as const,
showText: true,
},
{
breakpoint: "hidden lg:block xl:hidden",
size: "headerLg" as const,
showText: true,
},
{ breakpoint: "hidden xl:block", size: "headerXl" as const, showText: true },
];
const HeaderContainer = memo<HeaderProps>(() => {
const pathname = usePathname();
const t = useTranslation("header");
// Schema markup for site navigation
const schemaData = {
"@context": "https://schema.org",
"@type": "WebSite",
name: "CommunityRule",
url: "https://communityrule.com",
potentialAction: {
"@type": "SearchAction",
target: "https://communityrule.com/search?q={search_term_string}",
"query-input": "required name=search_term_string",
},
};
// Navigation items with translations
const navigationItems = [
{ href: "#", text: t("navigation.useCases"), extraPadding: true },
{ href: "/learn", text: t("navigation.learn") },
{ href: "#", text: t("navigation.about") },
];
const renderNavigationItems = (size: NavSize) => {
return navigationItems.map((item, index) => (
<MenuBarItem
key={index}
href={item.href}
size={item.extraPadding && size === "xsmall" ? "xsmallUseCases" : size}
isActive={pathname === item.href}
ariaLabel={t("ariaLabels.navigateToPage").replace("{text}", item.text)}
>
{item.text}
</MenuBarItem>
));
};
const renderAvatarGroup = (
containerSize: "small" | "medium" | "large" | "xlarge",
avatarSize: "small" | "medium" | "large" | "xlarge",
) => {
return (
<AvatarContainer size={containerSize}>
{avatarImages.map((avatar, index) => (
<Avatar
key={index}
src={avatar.src}
alt={avatar.alt}
size={avatarSize}
/>
))}
</AvatarContainer>
);
};
const renderLoginButton = (size: NavSize) => {
return (
<MenuBarItem
href="#"
size={size}
ariaLabel={t("ariaLabels.logInToAccount")}
>
{t("buttons.logIn")}
</MenuBarItem>
);
};
const renderCreateRuleButton = (
buttonSize: "xsmall" | "small" | "medium" | "large" | "xlarge",
containerSize: "small" | "medium" | "large" | "xlarge",
avatarSize: "small" | "medium" | "large" | "xlarge",
) => {
return (
<Button size={buttonSize} ariaLabel={t("ariaLabels.createNewRule")}>
{renderAvatarGroup(containerSize, avatarSize)}
<span>{t("buttons.createRule")}</span>
</Button>
);
};
const renderLogo = (
size:
| "default"
| "homeHeaderXsmall"
| "homeHeaderSm"
| "homeHeaderMd"
| "homeHeaderLg"
| "homeHeaderXl"
| "header"
| "headerMd"
| "headerLg"
| "headerXl"
| "footer"
| "footerLg",
showText: boolean,
) => {
return <Logo size={size} showText={showText} />;
};
return (
<HeaderView
schemaData={schemaData}
logoConfig={logoConfig}
renderNavigationItems={renderNavigationItems}
renderLoginButton={renderLoginButton}
renderCreateRuleButton={renderCreateRuleButton}
renderLogo={renderLogo}
/>
);
});
HeaderContainer.displayName = "Header";
export default HeaderContainer;
-124
View File
@@ -1,124 +0,0 @@
"use client";
import { useTranslation } from "../../contexts/MessagesContext";
import MenuBar from "../MenuBar";
import type { HeaderViewProps } from "./Header.types";
export function HeaderView({
schemaData,
logoConfig,
renderNavigationItems,
renderLoginButton,
renderCreateRuleButton,
renderLogo,
}: HeaderViewProps) {
const t = useTranslation("header");
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
/>
<header
className="sticky top-0 z-50 bg-[var(--color-surface-default-primary)] w-full border-b border-[var(--border-color-default-tertiary)]"
role="banner"
aria-label={t("ariaLabels.mainNavigationHeader")}
>
<nav
className="flex items-center justify-between mx-auto h-[40px] lg:h-[84px] xl:h-[88px] px-[var(--spacing-measures-spacing-016)] py-[var(--spacing-measures-spacing-008)] lg:px-[var(--spacing-measures-spacing-64,64px)] lg:py-[var(--spacing-measures-spacing-016,16px)]"
role="navigation"
aria-label={t("ariaLabels.mainNavigation")}
>
{/* Logo - Consistent left positioning across all breakpoints */}
<div className="flex items-center">
{logoConfig.map((config, index) => (
<div
key={index}
className={config.breakpoint}
data-testid="logo-wrapper"
>
{renderLogo(config.size, config.showText)}
</div>
))}
</div>
{/* Navigation Links - Consistent center positioning */}
<div className="flex items-center">
{/* XSmall breakpoint - Navigation items moved to right section */}
<div className="block sm:hidden" data-testid="nav-xs">
{/* Empty for XSmall - navigation moved to right */}
</div>
{/* Small breakpoint - All items grouped together, centered */}
<div className="hidden sm:block md:hidden" data-testid="nav-sm">
<MenuBar size="default">
{renderNavigationItems("xsmall")}
{renderLoginButton("xsmall")}
</MenuBar>
</div>
<div className="hidden md:block lg:hidden" data-testid="nav-md">
<MenuBar size="default">
{renderNavigationItems("xsmall")}
</MenuBar>
</div>
<div className="hidden lg:block xl:hidden" data-testid="nav-lg">
<MenuBar size="large">{renderNavigationItems("large")}</MenuBar>
</div>
<div className="hidden xl:block" data-testid="nav-xl">
<MenuBar size="large">{renderNavigationItems("xlarge")}</MenuBar>
</div>
</div>
{/* Authentication Elements - Consistent right alignment across all breakpoints */}
<div className="flex items-center">
{/* XSmall breakpoint - All navigation items + Create Rule button */}
<div className="block sm:hidden" data-testid="auth-xs">
<div className="flex items-center gap-[var(--spacing-scale-001)]">
<MenuBar size="default">
{renderNavigationItems("xsmall")}
{renderLoginButton("xsmall")}
</MenuBar>
{renderCreateRuleButton("xsmall", "small", "small")}
</div>
</div>
{/* Small breakpoint - Only Create Rule button */}
<div className="hidden sm:block md:hidden" data-testid="auth-sm">
<div className="flex items-center gap-[var(--spacing-scale-004)]">
{renderCreateRuleButton("xsmall", "small", "small")}
</div>
</div>
{/* Medium breakpoint */}
<div className="hidden md:block lg:hidden" data-testid="auth-md">
<div className="flex items-center gap-[var(--spacing-measures-spacing-010)]">
<MenuBar size="default">{renderLoginButton("xsmall")}</MenuBar>
{renderCreateRuleButton("xsmall", "medium", "medium")}
</div>
</div>
{/* Large breakpoint */}
<div className="hidden lg:block xl:hidden" data-testid="auth-lg">
<div className="flex items-center gap-[var(--spacing-measures-spacing-004)]">
<MenuBar size="large">{renderLoginButton("large")}</MenuBar>
{renderCreateRuleButton("large", "xlarge", "xlarge")}
</div>
</div>
{/* XLarge breakpoint */}
<div className="hidden xl:block" data-testid="auth-xl">
<div className="flex items-center gap-[var(--spacing-measures-spacing-004)]">
<MenuBar size="large">{renderLoginButton("xlarge")}</MenuBar>
{renderCreateRuleButton("xlarge", "xlarge", "xlarge")}
</div>
</div>
</div>
</nav>
</header>
</>
);
}
-3
View File
@@ -1,3 +0,0 @@
export { default } from "./Header.container";
export type { HeaderProps } from "./Header.types";
export { avatarImages, logoConfig } from "./Header.container";
-47
View File
@@ -1,47 +0,0 @@
import { memo } from "react";
import { getAssetPath } from "../../lib/assetUtils";
interface HeaderTabProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
className?: string;
stretch?: boolean;
}
const HeaderTab = memo<HeaderTabProps>(
({ children, className = "", stretch = false, ...props }) => {
const stretchClasses = stretch
? "flex-1 sm:mr-[var(--spacing-scale-008)] md:mr-[185px] lg:mr-[var(--spacing-scale-024)] xl:mr-[var(--spacing-scale-032)]"
: "";
return (
<div
className={`HeaderTab header-breakpoint-transition relative bg-[var(--color-surface-inverse-brand-primary)] rounded-t-[32px] sm:rounded-t-[32px] md:rounded-t-[32px] lg:rounded-t-[32px] xl:rounded-t-[32px] pl-[var(--spacing-scale-012)] h-[40px] sm:h-[52px] md:h-[52px] lg:h-[52px] xl:h-[64px] sm:pr-[var(--spacing-scale-006)] md:pl-[var(--spacing-scale-024)] lg:pl-[var(--spacing-scale-024)] xl:pl-[var(--spacing-scale-032)] md:pr-[var(--spacing-scale-012)] lg:pr-[var(--spacing-scale-048)] xl:pr-[var(--spacing-scale-120)] md:gap-[var(--spacing-scale-032)] ${stretchClasses} ${className}`}
{...props}
>
{children}
<img
src={getAssetPath("assets/Union_xsm.svg")}
alt=""
role="presentation"
className="absolute -bottom-[3px] -right-[52px] w-[61px] h-[24px] sm:w-[61px] sm:h-[31.5px] sm:hidden -z-10"
/>
<img
src={getAssetPath("assets/Union_sm_md_lg.svg")}
alt=""
role="presentation"
className="absolute -bottom-[3.7px] -right-[53px] w-[61px] h-[24px] sm:w-[61px] sm:h-[31.5px] hidden sm:block xl:hidden -z-10"
/>
<img
src={getAssetPath("assets/Union_xlg.svg")}
alt=""
role="presentation"
className="absolute -bottom-[6px] -right-[94px] w-[105px] h-[53px] hidden xl:block -z-10"
/>
</div>
);
},
);
HeaderTab.displayName = "HeaderTab";
export default HeaderTab;
@@ -1,195 +0,0 @@
"use client";
import { memo } from "react";
import { usePathname } from "next/navigation";
import { useTranslation } from "../../contexts/MessagesContext";
import MenuBarItem from "../MenuBarItem";
import Button from "../Button";
import AvatarContainer from "../AvatarContainer";
import Avatar from "../Avatar";
import Logo from "../Logo";
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
import HomeHeaderView from "./HomeHeader.view";
import type { HomeHeaderProps, NavSize } from "./HomeHeader.types";
export const avatarImages = [
{ src: getAssetPath(ASSETS.AVATAR_1), alt: "Avatar 1" },
{ src: getAssetPath(ASSETS.AVATAR_2), alt: "Avatar 2" },
{ src: getAssetPath(ASSETS.AVATAR_3), alt: "Avatar 3" },
];
export const logoConfig = [
{
breakpoint: "block sm:hidden",
size: "homeHeaderXsmall" as const,
showText: false,
},
{
breakpoint: "hidden sm:block md:hidden",
size: "homeHeaderSm" as const,
showText: true,
},
{
breakpoint: "hidden md:block lg:hidden",
size: "homeHeaderMd" as const,
showText: true,
},
{
breakpoint: "hidden lg:block xl:hidden",
size: "homeHeaderLg" as const,
showText: true,
},
{
breakpoint: "hidden xl:block",
size: "homeHeaderXl" as const,
showText: true,
},
];
const HomeHeaderContainer = memo<HomeHeaderProps>(() => {
const pathname = usePathname();
const t = useTranslation("header");
// Schema markup for site navigation (home page specific)
const schemaData = {
"@context": "https://schema.org",
"@type": "WebSite",
name: "CommunityRule",
url: "https://communityrule.com",
description: "Build operating manuals for successful communities",
potentialAction: {
"@type": "SearchAction",
target: "https://communityrule.com/search?q={search_term_string}",
"query-input": "required name=search_term_string",
},
};
// Navigation items with translations
const navigationItems = [
{ href: "#", text: t("navigation.useCases"), extraPadding: true },
{ href: "/learn", text: t("navigation.learn") },
{ href: "#", text: t("navigation.about") },
];
const renderNavigationItems = (size: NavSize) => {
return navigationItems.map((item, index) => (
<MenuBarItem
key={index}
href={item.href}
size={
item.extraPadding &&
(size === "xsmall" ||
size === "default" ||
size === "home" ||
size === "homeMd" ||
size === "large" ||
size === "homeXlarge")
? size === "home" || size === "homeMd"
? "homeMd"
: size === "large"
? "large"
: size === "homeXlarge"
? "homeXlarge"
: "xsmallUseCases"
: size
}
variant={
size === "xsmall" ||
size === "default" ||
size === "home" ||
size === "homeMd" ||
size === "large" ||
size === "homeXlarge"
? "home"
: "default"
}
isActive={pathname === item.href}
ariaLabel={t("ariaLabels.navigateToPage").replace("{text}", item.text)}
>
{item.text}
</MenuBarItem>
));
};
const renderAvatarGroup = (
containerSize: "small" | "medium" | "large" | "xlarge",
avatarSize: "small" | "medium" | "large" | "xlarge",
) => {
return (
<AvatarContainer size={containerSize}>
{avatarImages.map((avatar, index) => (
<Avatar
key={index}
src={avatar.src}
alt={avatar.alt}
size={avatarSize}
/>
))}
</AvatarContainer>
);
};
const renderLoginButton = (size: NavSize) => {
return (
<MenuBarItem
href="#"
size={size}
variant={size === "xsmall" || size === "default" ? "home" : "default"}
ariaLabel={t("ariaLabels.logInToAccount")}
>
{t("buttons.logIn")}
</MenuBarItem>
);
};
const renderCreateRuleButton = (
buttonSize: "xsmall" | "small" | "medium" | "large" | "xlarge",
containerSize: "small" | "medium" | "large" | "xlarge",
avatarSize: "small" | "medium" | "large" | "xlarge",
) => {
return (
<Button
size={buttonSize}
variant="ghost"
ariaLabel={t("ariaLabels.createNewRule")}
>
{renderAvatarGroup(containerSize, avatarSize)}
<span>{t("buttons.createRule")}</span>
</Button>
);
};
const renderLogo = (
size:
| "default"
| "homeHeaderXsmall"
| "homeHeaderSm"
| "homeHeaderMd"
| "homeHeaderLg"
| "homeHeaderXl"
| "header"
| "headerMd"
| "headerLg"
| "headerXl"
| "footer"
| "footerLg",
showText: boolean,
) => {
return <Logo size={size} showText={showText} />;
};
return (
<HomeHeaderView
schemaData={schemaData}
logoConfig={logoConfig}
renderNavigationItems={renderNavigationItems}
renderLoginButton={renderLoginButton}
renderCreateRuleButton={renderCreateRuleButton}
renderLogo={renderLogo}
/>
);
});
HomeHeaderContainer.displayName = "HomeHeader";
export default HomeHeaderContainer;
@@ -1,61 +0,0 @@
import type React from "react";
export interface HomeHeaderProps {
// Currently no props, but keeping interface for future extensibility
}
export type NavSize =
| "default"
| "xsmall"
| "xsmallUseCases"
| "home"
| "homeMd"
| "homeUseCases"
| "large"
| "largeUseCases"
| "homeXlarge"
| "xlarge";
export interface HomeHeaderViewProps {
schemaData: object;
logoConfig: Array<{
breakpoint: string;
size:
| "default"
| "homeHeaderXsmall"
| "homeHeaderSm"
| "homeHeaderMd"
| "homeHeaderLg"
| "homeHeaderXl"
| "header"
| "headerMd"
| "headerLg"
| "headerXl"
| "footer"
| "footerLg";
showText: boolean;
}>;
renderNavigationItems: (_size: NavSize) => React.ReactNode;
renderLoginButton: (_size: NavSize) => React.ReactNode;
renderCreateRuleButton: (
_buttonSize: "xsmall" | "small" | "medium" | "large" | "xlarge",
_containerSize: "small" | "medium" | "large" | "xlarge",
_avatarSize: "small" | "medium" | "large" | "xlarge",
) => React.ReactNode;
renderLogo: (
_size:
| "default"
| "homeHeaderXsmall"
| "homeHeaderSm"
| "homeHeaderMd"
| "homeHeaderLg"
| "homeHeaderXl"
| "header"
| "headerMd"
| "headerLg"
| "headerXl"
| "footer"
| "footerLg",
_showText: boolean,
) => React.ReactNode;
}
@@ -1,119 +0,0 @@
"use client";
import { memo } from "react";
import Script from "next/script";
import { useTranslation } from "../../contexts/MessagesContext";
import HeaderTab from "../HeaderTab";
import MenuBar from "../MenuBar";
import type { HomeHeaderViewProps } from "./HomeHeader.types";
function HomeHeaderView({
schemaData,
logoConfig,
renderNavigationItems,
renderLoginButton,
renderCreateRuleButton,
renderLogo,
}: HomeHeaderViewProps) {
const t = useTranslation("homeHeader");
return (
<>
<Script
id="home-header-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
/>
<header
className="w-full bg-transparent overflow-hidden"
role="banner"
aria-label={t("ariaLabels.homePageNavigationHeader")}
>
<nav
className="relative flex items-center justify-between mx-auto h-[50px] sm:h-[62px] md:h-[68px] lg:h-[68px] xl:h-[88px] px-[var(--spacing-scale-008)] pr-[var(--spacing-scale-016)] pt-[var(--spacing-scale-010)] sm:px-[var(--spacing-scale-010)] sm:pr-[var(--spacing-scale-020)] sm:pt-[var(--spacing-scale-010)] md:px-[var(--spacing-scale-016)] md:pr-[var(--spacing-scale-032)] md:pt-[var(--spacing-scale-016)] lg:pl-[var(--spacing-scale-024)] lg:pt-[var(--spacing-scale-016)] lg:pr-[var(--spacing-scale-056)] xl:pl-[var(--spacing-scale-048)] xl:pt-[var(--spacing-scale-024)] xl:pr-[var(--spacing-scale-056)]"
role="navigation"
aria-label={t("ariaLabels.mainNavigation")}
>
<HeaderTab className="flex items-center self-end" stretch={true}>
{/* Logo - Consistent left positioning within HeaderTab */}
<div>
{logoConfig.map((config, index) => (
<div key={index} className={config.breakpoint}>
{renderLogo(config.size, config.showText)}
</div>
))}
</div>
{/* XSmall menu bar - positioned next to logo */}
<div className="block sm:hidden -me-[2px]">
<MenuBar size="default">
{renderNavigationItems("xsmall")}
{renderLoginButton("xsmall")}
</MenuBar>
</div>
</HeaderTab>
{/* Navigation Links - Centered in header for SM and up */}
<div className="absolute left-1/2 transform -translate-x-1/2 hidden sm:block">
<div className="hidden sm:block md:hidden">
<MenuBar size="default">
{renderNavigationItems("xsmall")}
{renderLoginButton("xsmall")}
</MenuBar>
</div>
<div className="hidden md:block lg:hidden">
<MenuBar size="medium">{renderNavigationItems("homeMd")}</MenuBar>
</div>
<div className="hidden lg:block xl:hidden">
<MenuBar size="large">{renderNavigationItems("large")}</MenuBar>
</div>
<div className="hidden xl:block">
<MenuBar size="large">
{renderNavigationItems("homeXlarge")}
</MenuBar>
</div>
</div>
{/* Authentication Elements - Consistent right alignment outside HeaderTab */}
<div className="flex items-center">
{/* XSmall and Small breakpoints - create rule button outside HeaderTab */}
<div className="block md:hidden">
{renderCreateRuleButton("xsmall", "small", "small")}
</div>
{/* Medium breakpoint - login outside HeaderTab, create rule outside */}
<div className="hidden md:block lg:hidden absolute right-[var(--spacing-measures-spacing-016)]">
<div className="flex items-center gap-[var(--spacing-scale-010)]">
{renderLoginButton("homeMd")}
{renderCreateRuleButton("small", "medium", "medium")}
</div>
</div>
{/* Large breakpoint */}
<div className="hidden lg:flex xl:hidden items-center">
<div className="flex items-center gap-[var(--spacing-scale-004)]">
{renderLoginButton("large")}
{renderCreateRuleButton("large", "large", "large")}
</div>
</div>
{/* XLarge breakpoint */}
<div className="hidden xl:flex items-center">
<div className="flex items-center gap-[var(--spacing-scale-004)]">
{renderLoginButton("homeXlarge")}
{renderCreateRuleButton("xlarge", "xlarge", "xlarge")}
</div>
</div>
</div>
</nav>
</header>
</>
);
}
HomeHeaderView.displayName = "HomeHeaderView";
export default memo(HomeHeaderView);
-2
View File
@@ -1,2 +0,0 @@
export { default } from "./HomeHeader.container";
export type { HomeHeaderProps } from "./HomeHeader.types";
-60
View File
@@ -1,60 +0,0 @@
"use client";
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;
/**
* 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: 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:
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)] rounded-[4px]",
default:
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)]",
medium:
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-004)]",
large:
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-012)]",
};
const baseStyles = `flex items-center ${sizeStyles[size]} ${className}`;
return (
<nav
className={baseStyles}
role="menubar"
aria-label={t("ariaLabel")}
{...props}
>
{children}
</nav>
);
},
);
MenuBar.displayName = "MenuBar";
export default MenuBar;
@@ -1,173 +0,0 @@
"use client";
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: 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",
home: "bg-transparent text-[var(--color-content-inverse-primary)] hover:bg-[var(--color-content-default-brand-accent)] hover:text-[var(--color-content-inverse-primary)] hover:scale-[1.02] active:bg-transparent active:text-[var(--color-content-inverse-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",
};
const activeOutlineStyles: Record<string, string> = {
xsmall:
"active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]",
xsmallUseCases:
"active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]",
default:
"active:outline-[1.5px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-brand-primary)]",
homeMd:
"active:outline-[1.5px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-brand-primary)]",
homeUseCases:
"active:outline-[1.5px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-brand-primary)]",
large:
"active:outline-[1.75px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-brand-primary)]",
largeUseCases:
"active:outline-[1.75px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-brand-primary)]",
homeXlarge:
"active:outline-[2px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[2px] focus:outline-[var(--color-content-default-brand-primary)]",
xlarge:
"active:outline-2 active:outline-[var(--color-content-default-brand-primary)] focus:outline-2 focus:outline-[var(--color-content-default-brand-primary)]",
};
const homeOutlineStyles: Record<string, string> = {
xsmall:
"active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]",
xsmallUseCases:
"active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]",
default:
"active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]",
homeMd:
"active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]",
homeUseCases:
"active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]",
largeUseCases:
"active:outline-[1.75px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-primary)]",
large:
"active:outline-[1.75px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-primary)]",
homeXlarge:
"active:outline-[2px] active:outline-[var(--color-content-default-primary)] focus:outline-[2px] focus:outline-[var(--color-content-default-primary)]",
xlarge:
"active:outline-2 active:outline-[var(--color-content-default-primary)] focus:outline-2 focus:outline-[var(--color-content-default-primary)]",
};
const activeStateStyles: Record<string, string> = {
xsmall:
"!outline-1 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-1 focus:!outline-[var(--color-content-default-brand-primary)]",
xsmallUseCases:
"!outline-1 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-1 focus:!outline-[var(--color-content-default-brand-primary)]",
default:
"!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]",
homeMd:
"!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]",
homeUseCases:
"!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]",
large:
"!outline-[1.75px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.75px] focus:!outline-[var(--color-content-default-brand-primary)]",
largeUseCases:
"!outline-[1.75px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.75px] focus:!outline-[var(--color-content-default-brand-primary)]",
homeXlarge:
"!outline-[2px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[2px] focus:!outline-[var(--color-content-default-brand-primary)]",
xlarge:
"!outline-2 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-2 focus:!outline-[var(--color-content-default-brand-primary)]",
};
const sizeStyles: Record<string, string> = {
default:
"px-[var(--spacing-measures-spacing-016)] py-[var(--spacing-measures-spacing-016)] gap-[var(--spacing-scale-004)]",
xsmall:
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)] gap-[var(--spacing-scale-004)]",
xsmallUseCases:
"px-[var(--spacing-scale-002)] py-[var(--spacing-scale-002)] gap-[var(--spacing-scale-004)]",
homeMd:
"px-[var(--spacing-scale-008)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)]",
homeUseCases:
"px-[var(--spacing-scale-002)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)]",
large:
"px-[var(--spacing-scale-012)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-004)] h-[44px]",
largeUseCases:
"px-[var(--spacing-scale-012)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-004)] h-[44px]",
homeXlarge:
"px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] gap-[var(--spacing-scale-004)] h-[44px]",
xlarge:
"px-[var(--spacing-scale-016)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)] h-[44px]",
};
const smallTextStyle =
"font-inter text-[10px] leading-[12px] font-medium tracking-[0%]";
const mediumTextStyle =
"font-inter text-[12px] leading-[14px] font-medium tracking-[0%]";
const largeTextStyle =
"font-inter text-[16px] leading-[20px] font-medium tracking-[0%]";
const xlargeTextStyle =
"font-inter text-[24px] leading-[28px] font-normal tracking-[0%]";
const textStyles: Record<string, string> = {
default: smallTextStyle,
xsmall: smallTextStyle,
xsmallUseCases: smallTextStyle,
home: smallTextStyle,
homeMd: mediumTextStyle,
homeUseCases: mediumTextStyle,
large: largeTextStyle,
largeUseCases: largeTextStyle,
homeXlarge: xlargeTextStyle,
xlarge: xlargeTextStyle,
};
const baseStyles = `inline-flex items-center ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${textStyles[size]} transition-all duration-200 ease-in-out cursor-pointer focus:scale-[1.02]`;
let finalVariant = variant;
if (disabled) {
finalVariant = "default";
}
const combinedStyles = `${baseStyles} ${variantStyles[finalVariant]} ${
finalVariant === "home"
? homeOutlineStyles[size]
: activeOutlineStyles[size]
} ${isActive ? activeStateStyles[size] : ""} ${className}`;
const accessibilityProps = {
...(ariaLabel && { "aria-label": ariaLabel }),
...(disabled && { "aria-disabled": true }),
role: "menuitem" as const,
tabIndex: disabled ? -1 : 0,
...props,
};
return (
<MenuBarItemView
href={href}
disabled={disabled}
className={className}
combinedStyles={combinedStyles}
accessibilityProps={accessibilityProps}
>
{children}
</MenuBarItemView>
);
},
);
MenuBarItemContainer.displayName = "MenuBarItem";
export default MenuBarItemContainer;
@@ -1,51 +0,0 @@
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;
/**
* 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;
ariaLabel?: string;
}
export interface MenuBarItemViewProps {
href: string;
children?: React.ReactNode;
disabled: boolean;
className: string;
combinedStyles: string;
accessibilityProps: React.HTMLAttributes<HTMLAnchorElement | HTMLSpanElement>;
}
@@ -1,23 +0,0 @@
"use client";
import { memo } from "react";
import { ProgressView } from "./Progress.view";
import type { ProgressProps } from "./Progress.types";
const ProgressContainer = memo<ProgressProps>(
({ progress = "3-2", className = "" }) => {
const barClasses = `h-[8px] relative w-full`;
return (
<ProgressView
progress={progress}
className={className}
barClasses={barClasses}
/>
);
},
);
ProgressContainer.displayName = "Progress";
export default ProgressContainer;
-1
View File
@@ -1 +0,0 @@
export { default } from "./Progress.container";
@@ -1,39 +0,0 @@
import type { ReactNode } from "react";
export interface SelectOptionData {
value: string;
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;
/**
* 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;
className?: string;
children?: ReactNode;
value?: string;
onChange?: (_data: { target: { value: string; text: string } }) => void;
options?: SelectOptionData[];
}
-62
View File
@@ -1,62 +0,0 @@
import { forwardRef } from "react";
import type { TextAreaViewProps } from "./TextArea.types";
export const TextAreaView = forwardRef<HTMLTextAreaElement, TextAreaViewProps>(
(
{
textareaId,
labelId,
label,
placeholder,
value,
name,
disabled,
className: _className,
rows,
containerClasses,
labelClasses,
textareaClasses,
borderRadius,
handleChange,
handleFocus,
handleBlur,
...props
},
ref,
) => {
return (
<div className={containerClasses}>
{label && (
<label
id={labelId}
htmlFor={textareaId}
className={`${labelClasses} font-inter font-medium text-[var(--color-content-default-secondary)]`}
>
{label}
</label>
)}
<div className={disabled ? "opacity-40" : ""}>
<textarea
ref={ref}
id={textareaId}
name={name}
value={value}
placeholder={placeholder}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
rows={rows}
className={textareaClasses}
style={{ borderRadius }}
aria-disabled={disabled}
aria-invalid={props["aria-invalid"]}
{...props}
/>
</div>
</div>
);
},
);
TextAreaView.displayName = "TextAreaView";
@@ -1,19 +1,56 @@
import { memo } from "react";
import type { VariantValue, SizeValue } from "../../lib/propNormalization";
import { normalizeVariant, normalizeSize } from "../../lib/propNormalization";
import type {
SizeValue,
ButtonTypeValue,
ButtonPaletteValue,
ButtonStateValue,
} from "../../../lib/propNormalization";
import {
normalizeSize,
normalizeButtonType,
normalizeButtonPalette,
} from "../../../lib/propNormalization";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
/**
* Button variant. Accepts both lowercase and PascalCase (case-insensitive).
* Button type (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* @default "filled"
*/
variant?: VariantValue;
buttonType?: ButtonTypeValue;
/**
* Button palette (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses "Invert", codebase uses "inverse" - both are supported.
* @default "default"
*/
palette?: ButtonPaletteValue;
/**
* Button size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* @default "xsmall"
*/
size?: SizeValue;
/**
* Button state (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
* @default "default"
*/
state?: ButtonStateValue;
/**
* Whether to show a leading icon (Figma prop).
* @default false
*/
hasIconLeading?: boolean;
/**
* Whether to show a following icon (Figma prop).
* @default false
*/
hasIconFollowing?: boolean;
/**
* Whether to show text (Figma prop).
* @default true
*/
hasText?: boolean;
className?: string;
disabled?: boolean;
type?: "button" | "submit" | "reset";
@@ -29,11 +66,16 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
const Button = memo<ButtonProps>(
({
children,
variant: variantProp = "filled",
buttonType: typeProp,
palette: paletteProp,
size: sizeProp = "xsmall",
state: _stateProp,
hasIconLeading = false,
hasIconFollowing = false,
hasText = true,
className = "",
disabled = false,
type = "button",
type: htmlType = "button",
onClick,
href,
target,
@@ -41,12 +83,34 @@ const Button = memo<ButtonProps>(
ariaLabel,
...props
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const variant = normalizeVariant(variantProp);
// 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)
// Map type + palette to variant string for styling (internal use only)
const getVariantFromTypeAndPalette = (
type: typeof buttonType,
palette: typeof buttonPalette,
): string => {
if (type === "filled" && palette === "default") return "filled";
if (type === "filled" && palette === "inverse") return "filled-inverse";
if (type === "outline" && palette === "default") return "outline";
if (type === "outline" && palette === "inverse") return "outline-inverse";
if (type === "ghost" && palette === "default") return "ghost";
if (type === "ghost" && palette === "inverse") return "ghost-inverse";
if (type === "danger" && palette === "default") return "danger";
// danger + inverse
return "danger-inverse";
};
const variant = getVariantFromTypeAndPalette(buttonType, buttonPalette);
const sizeStyles: Record<string, string> = {
xsmall:
"p-[var(--spacing-scale-006)] gap-[var(--spacing-scale-002)]",
"p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]",
small:
"p-[var(--spacing-scale-008)] gap-[var(--spacing-scale-002)]",
medium: "p-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]",
@@ -102,7 +166,11 @@ const Button = memo<ButtonProps>(
? ""
: hoverOutlineStyles[size];
const baseStyles = `inline-flex items-center justify-start box-border ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${fontStyles[size]} transition-all duration-500 ease-in-out cursor-pointer ${variantStyles[variant]} ${outlineStyles}`;
// Apply state-based styles if state prop is provided (overrides default hover/focus/active)
// Note: State prop is informational for Figma alignment - actual state is handled by CSS pseudo-classes
// For now, we maintain existing behavior and state prop is for documentation/alignment purposes
const baseStyles = `inline-flex items-center justify-start box-border whitespace-nowrap shrink-0 ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${fontStyles[size]} transition-all duration-500 ease-in-out cursor-pointer ${variantStyles[variant]} ${outlineStyles}`;
const combinedStyles = `${baseStyles} ${className}`;
const sharedA11y = {
@@ -111,6 +179,16 @@ const Button = memo<ButtonProps>(
tabIndex: disabled ? -1 : 0,
};
// Filter children based on hasIconLeading, hasIconFollowing, hasText props
// For now, we render all children but these props are available for future icon support
const renderContent = () => {
if (!hasText && !hasIconLeading && !hasIconFollowing) {
return children; // If all are false, render children as-is (backward compatibility)
}
// TODO: When icon support is added, filter children based on these props
return children;
};
if (href && !disabled) {
const anchorProps: React.AnchorHTMLAttributes<HTMLAnchorElement> = {
href,
@@ -121,11 +199,11 @@ const Button = memo<ButtonProps>(
...(rel && { rel }),
};
return <a {...anchorProps}>{children}</a>;
return <a {...anchorProps}>{renderContent()}</a>;
}
const buttonProps: React.ButtonHTMLAttributes<HTMLButtonElement> = {
type,
type: htmlType,
className: combinedStyles,
disabled,
onClick,
@@ -133,7 +211,7 @@ const Button = memo<ButtonProps>(
...props,
};
return <button {...buttonProps}>{children}</button>;
return <button {...buttonProps}>{renderContent()}</button>;
},
);
@@ -1,9 +1,9 @@
"use client";
import { memo } from "react";
import SectionNumber from "./SectionNumber";
import SectionNumber from "../sections/SectionNumber";
import { normalizeNumberCardSize } from "../../lib/propNormalization";
import { normalizeNumberCardSize } from "../../../lib/propNormalization";
export type NumberCardSizeValue =
| "Small"
@@ -3,7 +3,7 @@
import { memo } from "react";
import { RuleCardView } from "./RuleCard.view";
import type { RuleCardProps } from "./RuleCard.types";
import { normalizeRuleCardSize } from "../../../lib/propNormalization";
import { normalizeRuleCardSize } from "../../../../lib/propNormalization";
declare global {
interface Window {
@@ -1,4 +1,4 @@
import type { ChipOption } from "../MultiSelect/MultiSelect.types";
import type { ChipOption } from "../../controls/MultiSelect/MultiSelect.types";
export interface Category {
name: string;
@@ -1,8 +1,8 @@
"use client";
import Image from "next/image";
import { useTranslation } from "../../contexts/MessagesContext";
import MultiSelect from "../MultiSelect";
import { useTranslation } from "../../../contexts/MessagesContext";
import MultiSelect from "../../controls/MultiSelect";
import type { RuleCardViewProps } from "./RuleCard.types";
export function RuleCardView({
@@ -242,7 +242,7 @@ export function RuleCardView({
onCustomChipClose={(chipId) => {
category.onCustomChipClose?.(category.name, chipId);
}}
showAddButton={true}
addButton={true}
addButtonText="" // Empty text for icon-only circular button
className="w-full"
/>
@@ -1,10 +1,10 @@
"use client";
import { memo } from "react";
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import ContentContainerView from "./ContentContainer.view";
import type { ContentContainerProps } from "./ContentContainer.types";
import { normalizeContentContainerSize } from "../../../lib/propNormalization";
import { normalizeContentContainerSize } from "../../../../lib/propNormalization";
const ContentContainerContainer = memo<ContentContainerProps>(
({ post, width = "200px", size: sizeProp = "responsive" }) => {
@@ -1,4 +1,4 @@
import type { BlogPost } from "../../../lib/content";
import type { BlogPost } from "../../../../lib/content";
export type ContentContainerSizeValue = "xs" | "responsive" | "Xs" | "Responsive";
@@ -1,10 +1,10 @@
"use client";
import { memo } from "react";
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import ContentThumbnailTemplateView from "./ContentThumbnailTemplate.view";
import type { ContentThumbnailTemplateProps } from "./ContentThumbnailTemplate.types";
import { normalizeContentThumbnailVariant } from "../../../lib/propNormalization";
import { normalizeContentThumbnailVariant } from "../../../../lib/propNormalization";
const ContentThumbnailTemplateContainer = memo<ContentThumbnailTemplateProps>(
({ post, className = "", variant: variantProp = "vertical" }) => {
@@ -1,4 +1,4 @@
import type { BlogPost } from "../../../lib/content";
import type { BlogPost } from "../../../../lib/content";
export type ContentThumbnailTemplateVariantValue = "vertical" | "horizontal" | "Vertical" | "Horizontal";
@@ -1,10 +1,10 @@
"use client";
import { memo } from "react";
import { useComponentId } from "../../hooks";
import { useComponentId } from "../../../hooks";
import { CheckboxView } from "./Checkbox.view";
import type { CheckboxProps } from "./Checkbox.types";
import { normalizeMode, normalizeState } from "../../../lib/propNormalization";
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
const CheckboxContainer = memo<CheckboxProps>(
({
@@ -1,4 +1,4 @@
import type { ModeValue, StateValue } from "../../../lib/propNormalization";
import type { ModeValue, StateValue } from "../../../../lib/propNormalization";
export interface CheckboxProps {
checked?: boolean;
@@ -3,7 +3,7 @@
import { memo, useCallback, useId, useState } from "react";
import { CheckboxGroupView } from "./CheckboxGroup.view";
import type { CheckboxGroupProps } from "./CheckboxGroup.types";
import { normalizeMode } from "../../../lib/propNormalization";
import { normalizeMode } from "../../../../lib/propNormalization";
const CheckboxGroupContainer = ({
name,
@@ -5,7 +5,7 @@ export interface CheckboxOption {
ariaLabel?: string;
}
import type { ModeValue } from "../../../lib/propNormalization";
import type { ModeValue } from "../../../../lib/propNormalization";
export interface CheckboxGroupProps {
name?: string;
@@ -7,7 +7,7 @@ import {
normalizeChipPalette,
normalizeChipSize,
normalizeChipState,
} from "../../../lib/propNormalization";
} from "../../../../lib/propNormalization";
const ChipContainer = memo<ChipProps>(
({
@@ -2,7 +2,7 @@ import type {
ChipPaletteValue,
ChipSizeValue,
ChipStateValue,
} from "../../../lib/propNormalization";
} from "../../../../lib/propNormalization";
export interface ChipProps {
label: string;
@@ -3,7 +3,7 @@
import { memo } from "react";
import MultiSelectView from "./MultiSelect.view";
import type { MultiSelectProps } from "./MultiSelect.types";
import { normalizeMultiSelectSize, normalizeChipPalette } from "../../../lib/propNormalization";
import { normalizeMultiSelectSize, normalizeChipPalette } from "../../../../lib/propNormalization";
const MultiSelectContainer = memo<MultiSelectProps>(
({
@@ -14,8 +14,9 @@ const MultiSelectContainer = memo<MultiSelectProps>(
options,
onChipClick,
onAddClick,
showAddButton = true,
addButton: addButtonProp = true,
addButtonText = "Add organization type",
formHeader = true,
onCustomChipConfirm,
onCustomChipClose,
className = "",
@@ -32,8 +33,9 @@ const MultiSelectContainer = memo<MultiSelectProps>(
options={options}
onChipClick={onChipClick}
onAddClick={onAddClick}
showAddButton={showAddButton}
addButton={addButtonProp}
addButtonText={addButtonText}
formHeader={formHeader}
onCustomChipConfirm={onCustomChipConfirm}
onCustomChipClose={onCustomChipClose}
className={className}
@@ -1,4 +1,4 @@
import type { ChipStateValue, ChipPaletteValue } from "../../../lib/propNormalization";
import type { ChipStateValue, ChipPaletteValue } from "../../../../lib/propNormalization";
export interface ChipOption {
id: string;
@@ -40,13 +40,19 @@ export interface MultiSelectProps {
*/
onAddClick?: () => void;
/**
* Show the add button
* Whether to show add button (Figma prop).
* @default true
*/
showAddButton?: boolean;
addButton?: boolean;
/**
* Text for the add button
*/
addButtonText?: string;
/**
* Whether to show form header (label and help icon) above multi-select (Figma prop).
* @default true
*/
formHeader?: boolean;
/**
* Callback when a custom chip is confirmed (check button clicked)
*/
@@ -66,8 +72,9 @@ export interface MultiSelectViewProps {
options: ChipOption[];
onChipClick?: (chipId: string) => void;
onAddClick?: () => void;
showAddButton: boolean;
addButton: boolean;
addButtonText: string;
formHeader: boolean;
onCustomChipConfirm?: (chipId: string, value: string) => void;
onCustomChipClose?: (chipId: string) => void;
className: string;
@@ -2,7 +2,7 @@
import { memo } from "react";
import Chip from "../Chip";
import InputLabel from "../InputLabel";
import InputLabel from "../../utility/InputLabel";
import type { MultiSelectViewProps } from "./MultiSelect.types";
function MultiSelectView({
@@ -13,8 +13,9 @@ function MultiSelectView({
options,
onChipClick,
onAddClick,
showAddButton,
addButton,
addButtonText,
formHeader = true,
onCustomChipConfirm,
onCustomChipClose,
className,
@@ -32,7 +33,7 @@ function MultiSelectView({
return (
<div className={`flex flex-col ${isSmall ? "gap-[var(--measures-spacing-200,8px)]" : "gap-[var(--measures-spacing-300,12px)]"} items-start relative w-full ${className}`}>
{/* Label using InputLabel component */}
{label && (
{formHeader && label && (
<InputLabel
label={label}
helpIcon={showHelpIcon}
@@ -74,7 +75,7 @@ function MultiSelectView({
))}
{/* Add button - Circular button with border (not ghost) when no text, ghost style when text provided */}
{showAddButton && (
{addButton && (
<button
type="button"
aria-label={addButtonText || "Add option"}
@@ -3,7 +3,7 @@
import { memo, useCallback, useId } from "react";
import { RadioButtonView } from "./RadioButton.view";
import type { RadioButtonProps } from "./RadioButton.types";
import { normalizeMode, normalizeState } from "../../../lib/propNormalization";
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
const RadioButtonContainer = ({
checked = false,
@@ -1,4 +1,4 @@
import type { ModeValue, StateValue } from "../../../lib/propNormalization";
import type { ModeValue, StateValue } from "../../../../lib/propNormalization";
export interface RadioButtonProps {
checked?: boolean;
@@ -3,7 +3,7 @@
import { memo, useCallback, useId } from "react";
import { RadioGroupView } from "./RadioGroup.view";
import type { RadioGroupProps } from "./RadioGroup.types";
import { normalizeMode, normalizeState } from "../../../lib/propNormalization";
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
const RadioGroupContainer = ({
name,
@@ -5,7 +5,7 @@ export interface RadioOption {
ariaLabel?: string;
}
import type { ModeValue, StateValue } from "../../../lib/propNormalization";
import type { ModeValue, StateValue } from "../../../../lib/propNormalization";
export interface RadioGroupProps {
name?: string;
@@ -13,19 +13,26 @@ import React, {
useImperativeHandle,
useEffect,
} from "react";
import { useClickOutside } from "../../hooks";
import { useClickOutside } from "../../../hooks";
import { SelectInputView } from "./SelectInput.view";
import type { SelectInputProps } from "./SelectInput.types";
import { normalizeState, normalizeSmallMediumLargeSize, normalizeLabelVariant } from "../../../lib/propNormalization";
import { normalizeState, normalizeSmallMediumLargeSize, normalizeLabelVariant } from "../../../../lib/propNormalization";
const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
(
{
id,
label,
labelText,
showLabel,
labelVariant: labelVariantProp,
size: sizeProp,
state: externalStateProp = "default",
asterisk = false,
iconHelp = true,
textOptional = false,
textData = true,
iconRight = true,
textHint = false,
disabled = false,
error = false,
placeholder = "Choose an option",
@@ -38,6 +45,16 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
},
ref,
) => {
// Determine if label should be shown
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
}
const externalState = normalizeState(normalizedState);
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
// Note: labelVariant and size are normalized for future use but not yet implemented in the view
const _labelVariant = labelVariantProp ? normalizeLabelVariant(labelVariantProp) : undefined;
@@ -45,7 +62,6 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
// Mark as intentionally unused for future implementation
void _labelVariant;
void _size;
const externalState = normalizeState(externalStateProp);
const generatedId = useId();
const selectId = id || `select-input-${generatedId}`;
@@ -193,7 +209,7 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
return (
<SelectInputView
label={label}
label={shouldShowLabel ? labelText : undefined}
placeholder={placeholder}
state={actualState}
disabled={disabled}
@@ -214,8 +230,14 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
onOptionClick={handleOptionSelect}
selectRef={selectRef}
menuRef={menuRef}
ariaLabelledby={label ? labelId : undefined}
ariaLabelledby={shouldShowLabel ? labelId : undefined}
ariaInvalid={error}
asterisk={asterisk}
iconHelp={iconHelp}
textOptional={textOptional}
textData={textData}
iconRight={iconRight}
textHint={textHint}
{...props}
/>
);
@@ -0,0 +1,78 @@
import type { ReactNode } from "react";
export interface SelectOptionData {
value: string;
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 text (Figma prop).
*/
labelText?: string;
/**
* Whether to show label above input (Figma prop).
* If `labelText` is provided, defaults to true.
* @default true
*/
showLabel?: boolean;
/**
* Label variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
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", "active"/"Active", "focus"/"Focus", "error"/"Error", "state5"/"State5" (State5 = Disabled).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
state?: StateValue | "state5" | "State5";
/**
* Whether to show asterisk (*) in label (Figma prop).
* @default false
*/
asterisk?: boolean;
/**
* Whether to show help icon in label (Figma prop).
* @default true
*/
iconHelp?: boolean;
/**
* Whether to show "Optional" text in label (Figma prop).
* @default false
*/
textOptional?: boolean;
/**
* Whether to show data text (placeholder/entered text) - internal, always true (Figma prop).
* @default true
*/
textData?: boolean;
/**
* Whether to show dropdown icon on the right (Figma prop).
* @default true
*/
iconRight?: boolean;
/**
* Whether to show hint text below input (Figma prop).
* @default false
*/
textHint?: boolean;
disabled?: boolean;
error?: boolean;
placeholder?: string;
className?: string;
children?: ReactNode;
value?: string;
onChange?: (_data: { target: { value: string; text: string } }) => void;
options?: SelectOptionData[];
}
@@ -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 { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import SelectDropdown from "./SelectDropdown";
import SelectOption from "./SelectOption";
import type { SelectOptionData } from "./SelectInput.types";
export interface SelectInputViewProps {
@@ -33,6 +33,13 @@ export interface SelectInputViewProps {
// Additional props
ariaLabelledby?: string;
ariaInvalid?: boolean;
// Figma props
asterisk?: boolean;
iconHelp?: boolean;
textOptional?: boolean;
textData?: boolean;
iconRight?: boolean;
textHint?: boolean;
}
export function SelectInputView({
@@ -59,6 +66,12 @@ export function SelectInputView({
menuRef,
ariaLabelledby,
ariaInvalid,
asterisk = false,
iconHelp = true,
textOptional = false,
textData = true,
iconRight = true,
textHint = false,
}: SelectInputViewProps) {
// Styles based on Figma design
const containerClasses = "flex flex-col gap-[8px]";
@@ -135,14 +148,26 @@ export function SelectInputView({
>
{label}
</label>
<div className="relative shrink-0 size-[12px]">
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt="Help"
className="block max-w-none size-full"
/>
</div>
{asterisk && (
<span className="text-[var(--color-content-default-negative-primary,#ea4845)] text-[10px] leading-[12px] font-medium">
*
</span>
)}
{iconHelp && (
<div className="relative shrink-0 size-[12px]">
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt="Help"
className="block max-w-none size-full"
/>
</div>
)}
</div>
{textOptional && (
<span className="text-[var(--color-content-default-tertiary,#b4b4b4)] text-[10px] leading-[14px] font-normal">
Optional text
</span>
)}
</div>
)}
<div className="relative">
@@ -161,24 +186,26 @@ export function SelectInputView({
onFocus={onButtonFocus}
onBlur={onButtonBlur}
>
<span className={`flex-1 text-left pr-[32px] ${textColorClass}`}>
{displayText}
<span className={`flex-1 text-left ${iconRight ? "pr-[32px]" : ""} ${textColorClass}`}>
{textData ? displayText : placeholder}
</span>
<div className="flex items-center justify-center shrink-0">
<svg
className={chevronClasses}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
{iconRight && (
<div className="flex items-center justify-center shrink-0">
<svg
className={chevronClasses}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
)}
</button>
{state === "focus" && (
<div
@@ -235,6 +262,13 @@ export function SelectInputView({
</div>
)}
</div>
{textHint && (
<div className="flex items-start relative shrink-0 w-full">
<p className="flex-[1_0_0] font-inter font-normal leading-[16px] min-h-px min-w-px relative text-[color:var(--color-content-default-tertiary,#b4b4b4)] text-[length:var(--sizing-300,12px)]">
Hint text here
</p>
</div>
)}
</div>
);
}
@@ -3,7 +3,7 @@
import { forwardRef, memo, useCallback } from "react";
import { SelectOptionView } from "./SelectOption.view";
import type { SelectOptionProps } from "./SelectOption.types";
import { normalizeContextMenuItemSize } from "../../../lib/propNormalization";
import { normalizeContextMenuItemSize } from "../../../../../lib/propNormalization";
const SelectOptionContainer = forwardRef<HTMLDivElement, SelectOptionProps>(
(
@@ -3,17 +3,17 @@
import { memo, useCallback, useId, forwardRef } from "react";
import { SwitchView } from "./Switch.view";
import type { SwitchProps } from "./Switch.types";
import { normalizeState } from "../../../lib/propNormalization";
import { normalizeState } from "../../../../lib/propNormalization";
const SwitchContainer = memo(
forwardRef<HTMLButtonElement, SwitchProps>((props, ref) => {
const {
checked = false,
propSwitch = false,
onChange,
onFocus,
onBlur,
state: stateProp = "default",
label,
text,
className = "",
...rest
} = props;
@@ -62,14 +62,14 @@ const SwitchContainer = memo(
[onBlur],
);
// Switch track styles based on checked state
// Switch track styles based on propSwitch state
const getTrackStyles = useCallback(() => {
return checked
return propSwitch
? "bg-[var(--color-surface-inverse-tertiary)]"
: "bg-[var(--color-surface-default-tertiary)]";
}, [checked]);
}, [propSwitch]);
// Switch thumb styles based on checked state
// Switch thumb styles based on propSwitch state
const getThumbStyles = useCallback(() => {
return "bg-[var(--color-gray-000)]";
}, []);
@@ -111,7 +111,7 @@ const SwitchContainer = memo(
duration-200
flex
items-center
${checked ? "justify-end" : "justify-start"}
${propSwitch ? "justify-end" : "justify-start"}
p-[2px]
`
.trim()
@@ -144,9 +144,9 @@ const SwitchContainer = memo(
<SwitchView
ref={ref}
switchId={switchId}
checked={checked}
propSwitch={propSwitch}
state={state}
label={label}
text={text}
className={className}
switchClasses={switchClasses}
trackClasses={trackClasses}
@@ -1,10 +1,14 @@
import type { StateValue } from "../../../lib/propNormalization";
import type { StateValue } from "../../../../lib/propNormalization";
export interface SwitchProps extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
"onChange"
> {
checked?: boolean;
/**
* Whether the switch is checked (Figma prop).
* @default false
*/
propSwitch?: boolean;
onChange?: (
_e:
| React.MouseEvent<HTMLButtonElement>
@@ -17,15 +21,18 @@ export interface SwitchProps extends Omit<
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
state?: StateValue;
label?: string;
/**
* Label text (Figma prop).
*/
text?: string;
className?: string;
}
export interface SwitchViewProps {
switchId: string;
checked: boolean;
propSwitch: boolean;
state: "default" | "hover" | "focus" | "selected";
label?: string;
text?: string;
className: string;
switchClasses: string;
trackClasses: string;
@@ -5,8 +5,8 @@ export const SwitchView = forwardRef<HTMLButtonElement, SwitchViewProps>(
(
{
switchId,
checked,
label,
propSwitch,
text,
switchClasses,
trackClasses,
thumbClasses,
@@ -26,8 +26,8 @@ export const SwitchView = forwardRef<HTMLButtonElement, SwitchViewProps>(
id={switchId}
type="button"
role="switch"
aria-checked={checked}
aria-label={label || "Toggle switch"}
aria-checked={propSwitch}
aria-label={text || "Toggle switch"}
onClick={onClick}
onKeyDown={onKeyDown}
onFocus={onFocus}
@@ -39,7 +39,7 @@ export const SwitchView = forwardRef<HTMLButtonElement, SwitchViewProps>(
<div className={thumbClasses} />
</div>
</button>
{label && <span className={labelClasses}>{label}</span>}
{text && <span className={labelClasses}>{text}</span>}
</div>
);
},
@@ -1,10 +1,10 @@
"use client";
import { memo, forwardRef } from "react";
import { useComponentId, useFormField } from "../../hooks";
import { useComponentId, useFormField } from "../../../hooks";
import { TextAreaView } from "./TextArea.view";
import type { TextAreaProps } from "./TextArea.types";
import { normalizeInputState, normalizeSmallMediumLargeSize, normalizeLabelVariant } from "../../../lib/propNormalization";
import { normalizeInputState, normalizeSmallMediumLargeSize, normalizeLabelVariant } from "../../../../lib/propNormalization";
const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
(
@@ -24,6 +24,9 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
name,
className = "",
rows,
textHint = false,
formHeader = true,
showHelpIcon = false,
...props
},
ref,
@@ -174,6 +177,9 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
handleFocus={handleFocus}
handleBlur={handleBlur}
aria-invalid={error}
textHint={textHint}
formHeader={formHeader}
showHelpIcon={showHelpIcon}
{...props}
/>
);
@@ -1,4 +1,4 @@
import type { InputStateValue } from "../../../lib/propNormalization";
import type { InputStateValue } from "../../../../lib/propNormalization";
export type TextAreaSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
export type TextAreaLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal";
@@ -32,6 +32,21 @@ export interface TextAreaProps extends Omit<
onBlur?: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
className?: string;
rows?: number;
/**
* Whether to show hint text below textarea (Figma prop).
* @default false
*/
textHint?: boolean;
/**
* Whether to show form header (label and help icon) above textarea (Figma prop).
* @default true
*/
formHeader?: boolean;
/**
* Whether to show help icon in label.
* @default false
*/
showHelpIcon?: boolean;
}
export interface TextAreaViewProps {
@@ -55,4 +70,7 @@ export interface TextAreaViewProps {
handleChange: (_e: React.ChangeEvent<HTMLTextAreaElement>) => void;
handleFocus: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
handleBlur: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
textHint?: boolean;
formHeader?: boolean;
showHelpIcon?: boolean;
}
@@ -0,0 +1,86 @@
import { forwardRef } from "react";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import type { TextAreaViewProps } from "./TextArea.types";
export const TextAreaView = forwardRef<HTMLTextAreaElement, TextAreaViewProps>(
(
{
textareaId,
labelId,
label,
placeholder,
value,
name,
disabled,
className: _className,
rows,
containerClasses,
labelClasses,
textareaClasses,
borderRadius,
handleChange,
handleFocus,
handleBlur,
textHint = false,
formHeader = true,
showHelpIcon = false,
...props
},
ref,
) => {
return (
<div className={containerClasses}>
{formHeader && label && (
<div className="flex flex-wrap gap-[var(--measures-spacing-200,4px_8px)] items-baseline pr-[var(--measures-spacing-100,4px)] relative shrink-0 w-full">
<div className="flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0">
<label
id={labelId}
htmlFor={textareaId}
className={`${labelClasses} font-inter font-medium text-[var(--color-content-default-secondary)]`}
>
{label}
</label>
{showHelpIcon && (
<div className="relative shrink-0 size-[12px]">
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt="Help"
className="block max-w-none size-full"
/>
</div>
)}
</div>
</div>
)}
<div className={disabled ? "opacity-40" : ""}>
<textarea
ref={ref}
id={textareaId}
name={name}
value={value}
placeholder={placeholder}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
rows={rows}
className={textareaClasses}
style={{ borderRadius }}
aria-disabled={disabled}
aria-invalid={props["aria-invalid"]}
{...props}
/>
</div>
{textHint && (
<div className="flex items-start relative shrink-0 w-full">
<p className="flex-[1_0_0] font-inter font-normal leading-[16px] min-h-px min-w-px relative text-[color:var(--color-content-default-tertiary,#b4b4b4)] text-[length:var(--sizing-300,12px)]">
Hint text here
</p>
</div>
)}
</div>
);
},
);
TextAreaView.displayName = "TextAreaView";
@@ -1,10 +1,10 @@
"use client";
import { memo, forwardRef, useState, useRef } from "react";
import { useComponentId, useFormField } from "../../hooks";
import { useComponentId, useFormField } from "../../../hooks";
import { TextInputView } from "./TextInput.view";
import type { TextInputProps } from "./TextInput.types";
import { normalizeInputState } from "../../../lib/propNormalization";
import { normalizeInputState } from "../../../../lib/propNormalization";
const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
(
@@ -23,6 +23,8 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
type = "text",
className = "",
showHelpIcon = true,
textHint = false,
formHeader = true,
...props
},
ref,
@@ -220,6 +222,8 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
isFilled={isFilled}
inputWrapperClasses={stateStyles.inputWrapper}
focusRingClasses={stateStyles.focusRing}
textHint={textHint}
formHeader={formHeader}
{...props}
/>
);

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