adilallo/maintanence/ComponentOrganizationPolish #40
@@ -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
|
||||
@@ -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";
|
||||
+1
-1
@@ -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,
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default } from "./Header.container";
|
||||
export type { HeaderProps } from "./Header.types";
|
||||
export { avatarImages, logoConfig } from "./Header.container";
|
||||
@@ -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);
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from "./HomeHeader.container";
|
||||
export type { HomeHeaderProps } from "./HomeHeader.types";
|
||||
@@ -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 +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[];
|
||||
}
|
||||
@@ -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"
|
||||
+1
-1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import type { ChipOption } from "../MultiSelect/MultiSelect.types";
|
||||
import type { ChipOption } from "../../controls/MultiSelect/MultiSelect.types";
|
||||
|
||||
export interface Category {
|
||||
name: string;
|
||||
+3
-3
@@ -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"
|
||||
/>
|
||||
+2
-2
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import type { BlogPost } from "../../../lib/content";
|
||||
import type { BlogPost } from "../../../../lib/content";
|
||||
|
||||
export type ContentContainerSizeValue = "xs" | "responsive" | "Xs" | "Responsive";
|
||||
|
||||
+2
-2
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import type { BlogPost } from "../../../lib/content";
|
||||
import type { BlogPost } from "../../../../lib/content";
|
||||
|
||||
export type ContentThumbnailTemplateVariantValue = "vertical" | "horizontal" | "Vertical" | "Horizontal";
|
||||
|
||||
+2
-2
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import type { ModeValue, StateValue } from "../../../lib/propNormalization";
|
||||
import type { ModeValue, StateValue } from "../../../../lib/propNormalization";
|
||||
|
||||
export interface CheckboxProps {
|
||||
checked?: boolean;
|
||||
+1
-1
@@ -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,
|
||||
+1
-1
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
+5
-3
@@ -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}
|
||||
+11
-4
@@ -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;
|
||||
+5
-4
@@ -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"}
|
||||
+1
-1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import type { ModeValue, StateValue } from "../../../lib/propNormalization";
|
||||
import type { ModeValue, StateValue } from "../../../../lib/propNormalization";
|
||||
|
||||
export interface RadioButtonProps {
|
||||
checked?: boolean;
|
||||
+1
-1
@@ -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,
|
||||
+1
-1
@@ -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;
|
||||
+28
-6
@@ -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[];
|
||||
}
|
||||
+61
-27
@@ -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>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -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>(
|
||||
(
|
||||
+10
-10
@@ -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}
|
||||
+12
-5
@@ -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
-5
@@ -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>
|
||||
);
|
||||
},
|
||||
+8
-2
@@ -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}
|
||||
/>
|
||||
);
|
||||
+19
-1
@@ -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";
|
||||
+6
-2
@@ -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
Reference in New Issue
Block a user