Merge pull request 'Create Rule Flow Core Infrastructure' (#41) from adilallo/feature/CreateRuleFlowCoreInfrastructure into main
Reviewed-on: #41
This commit was merged in pull request #41.
This commit is contained in:
+51
-134
@@ -5,17 +5,10 @@ import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
|||||||
interface LogoProps {
|
interface LogoProps {
|
||||||
size?:
|
size?:
|
||||||
| "default"
|
| "default"
|
||||||
| "homeHeaderXsmall"
|
|
||||||
| "homeHeaderSm"
|
|
||||||
| "homeHeaderMd"
|
|
||||||
| "homeHeaderLg"
|
|
||||||
| "homeHeaderXl"
|
|
||||||
| "header"
|
|
||||||
| "headerMd"
|
|
||||||
| "headerLg"
|
|
||||||
| "headerXl"
|
|
||||||
| "footer"
|
| "footer"
|
||||||
| "footerLg";
|
| "createFlow"
|
||||||
|
| "topNavFolderTop"
|
||||||
|
| "topNavHeader";
|
||||||
showText?: boolean;
|
showText?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,109 +30,37 @@ const Logo = memo<LogoProps>(({ size = "default", showText = true }) => {
|
|||||||
lineHeight: "leading-[27.05px]",
|
lineHeight: "leading-[27.05px]",
|
||||||
iconSize: "w-[27.05px] h-[27.05px]",
|
iconSize: "w-[27.05px] h-[27.05px]",
|
||||||
},
|
},
|
||||||
homeHeaderXsmall: {
|
|
||||||
containerHeight: "h-[14.11px]",
|
|
||||||
gap: "gap-[4.21px]",
|
|
||||||
textSize: "text-[11.57px]",
|
|
||||||
lineHeight: "leading-[14.24px]",
|
|
||||||
iconSize: "w-[14.11px] h-[14.11px]",
|
|
||||||
},
|
|
||||||
homeHeaderSm: {
|
|
||||||
containerHeight: "h-[21.06px]",
|
|
||||||
gap: "gap-[3.19px]",
|
|
||||||
textSize: "text-[11.69px]",
|
|
||||||
lineHeight: "leading-[14.39px]",
|
|
||||||
iconSize: "w-[14.39px] h-[14.39px]",
|
|
||||||
},
|
|
||||||
homeHeaderMd: {
|
|
||||||
containerHeight: "h-[32.24px]",
|
|
||||||
gap: "gap-[4.89px]",
|
|
||||||
textSize: "text-[17.89px]",
|
|
||||||
lineHeight: "leading-[22.02px]",
|
|
||||||
iconSize: "w-[22.02px] h-[22.02px]",
|
|
||||||
},
|
|
||||||
homeHeaderLg: {
|
|
||||||
containerHeight: "h-[28px]",
|
|
||||||
gap: "gap-[6.55px]",
|
|
||||||
textSize: "text-[21.97px]",
|
|
||||||
lineHeight: "leading-[27.05px]",
|
|
||||||
iconSize: "w-[27.05px] h-[27.05px]",
|
|
||||||
},
|
|
||||||
homeHeaderXl: {
|
|
||||||
containerHeight: "h-[36px]",
|
|
||||||
gap: "gap-[8.64px]",
|
|
||||||
textSize: "text-[29.01px]",
|
|
||||||
lineHeight: "leading-[35.7px]",
|
|
||||||
iconSize: "w-[35.7px] h-[35.7px]",
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
containerHeight: "h-[20.85px]",
|
|
||||||
gap: "gap-[4.21px]",
|
|
||||||
textSize: "text-[11.57px]",
|
|
||||||
lineHeight: "leading-[14.24px]",
|
|
||||||
iconSize: "w-[14.24px] h-[14.24px]",
|
|
||||||
},
|
|
||||||
headerMd: {
|
|
||||||
containerHeight: "h-[17.91px]",
|
|
||||||
gap: "gap-[6.51px]",
|
|
||||||
textSize: "text-[17.89px]",
|
|
||||||
lineHeight: "leading-[22.02px]",
|
|
||||||
iconSize: "w-[22.02px] h-[22.02px]",
|
|
||||||
},
|
|
||||||
headerLg: {
|
|
||||||
containerHeight: "h-[28px]",
|
|
||||||
gap: "gap-[6.55px]",
|
|
||||||
textSize: "text-[21.97px]",
|
|
||||||
lineHeight: "leading-[27.05px]",
|
|
||||||
iconSize: "w-[27.05px] h-[27.05px]",
|
|
||||||
},
|
|
||||||
headerXl: {
|
|
||||||
containerHeight: "h-[34px]",
|
|
||||||
gap: "gap-[8.19px]",
|
|
||||||
textSize: "text-[27.47px]",
|
|
||||||
lineHeight: "leading-[33.81px]",
|
|
||||||
iconSize: "w-[33.81px] h-[33.81px]",
|
|
||||||
},
|
|
||||||
footer: {
|
footer: {
|
||||||
containerHeight: "h-[calc(40px*1.37)]",
|
containerHeight: "h-[41px] sm:h-[calc(40px*1.37)] lg:h-[calc(40px*2.05)]",
|
||||||
gap: "gap-[calc(8px*1.37)]",
|
gap: "gap-[8.28px] sm:gap-[calc(8px*1.37)] lg:gap-[calc(8px*2.05)]",
|
||||||
textSize: "text-[calc(21.97px*1.37)]",
|
textSize: "text-[21.97px] sm:text-[calc(21.97px*1.37)] lg:text-[calc(21.97px*2.05)]",
|
||||||
lineHeight: "leading-[calc(27.05px*1.37)]",
|
lineHeight: "leading-[27.05px] sm:leading-[calc(27.05px*1.37)] lg:leading-[calc(27.05px*2.05)]",
|
||||||
iconSize: "w-[calc(27.05px*1.37)] h-[calc(27.05px*1.37)]",
|
iconSize: "w-[27.05px] h-[27.05px] sm:w-[calc(27.05px*1.37)] sm:h-[calc(27.05px*1.37)] lg:w-[calc(27.05px*2.05)] lg:h-[calc(27.05px*2.05)]",
|
||||||
},
|
},
|
||||||
footerLg: {
|
createFlow: {
|
||||||
containerHeight: "h-[calc(40px*2.05)]",
|
containerHeight: "h-[30px] md:h-[41px]",
|
||||||
gap: "gap-[calc(8px*2.05)]",
|
gap: "gap-[6px] md:gap-[8.28px]",
|
||||||
textSize: "text-[calc(21.97px*2.05)]",
|
textSize: "text-[16.48px] md:text-[21.97px]",
|
||||||
lineHeight: "leading-[calc(27.05px*2.05)]",
|
lineHeight: "leading-[20.28px] md:leading-[27.05px]",
|
||||||
iconSize: "w-[calc(27.05px*2.05)] h-[calc(27.05px*2.05)]",
|
iconSize: "w-[20.28px] h-[20.28px] md:w-[27.05px] md:h-[27.05px]",
|
||||||
|
},
|
||||||
|
topNavFolderTop: {
|
||||||
|
containerHeight: "h-[14.11px] sm:h-[21.06px] md:h-[32.24px] lg:h-[28px] xl:h-[36px]",
|
||||||
|
gap: "gap-0 sm:gap-[3.19px] md:gap-[4.89px] lg:gap-[6.55px] xl:gap-[8.64px]",
|
||||||
|
textSize: "text-[11.57px] sm:text-[11.69px] md:text-[17.89px] lg:text-[21.97px] xl:text-[29.01px]",
|
||||||
|
lineHeight: "leading-[14.24px] sm:leading-[14.39px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[35.7px]",
|
||||||
|
iconSize: "w-[14.11px] h-[14.11px] sm:w-[14.39px] sm:h-[14.39px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[35.7px] xl:h-[35.7px]",
|
||||||
|
},
|
||||||
|
topNavHeader: {
|
||||||
|
containerHeight: "h-[20.85px] sm:h-[20.85px] md:h-[17.91px] lg:h-[28px] xl:h-[34px]",
|
||||||
|
gap: "gap-0 sm:gap-[4.21px] md:gap-[6.51px] lg:gap-[6.55px] xl:gap-[8.19px]",
|
||||||
|
textSize: "text-[11.57px] sm:text-[11.57px] md:text-[17.89px] lg:text-[21.97px] xl:text-[27.47px]",
|
||||||
|
lineHeight: "leading-[14.24px] sm:leading-[14.24px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[33.81px]",
|
||||||
|
iconSize: "w-[14.24px] h-[14.24px] sm:w-[14.24px] sm:h-[14.24px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[33.81px] xl:h-[33.81px]",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const config =
|
const config = sizes[size || "default"] || sizes.default;
|
||||||
size === "homeHeaderXsmall"
|
|
||||||
? sizes.homeHeaderXsmall
|
|
||||||
: size === "homeHeaderSm"
|
|
||||||
? sizes.homeHeaderSm
|
|
||||||
: size === "homeHeaderMd"
|
|
||||||
? sizes.homeHeaderMd
|
|
||||||
: size === "homeHeaderLg"
|
|
||||||
? sizes.homeHeaderLg
|
|
||||||
: size === "homeHeaderXl"
|
|
||||||
? sizes.homeHeaderXl
|
|
||||||
: size === "header"
|
|
||||||
? sizes.header
|
|
||||||
: size === "headerMd"
|
|
||||||
? sizes.headerMd
|
|
||||||
: size === "headerLg"
|
|
||||||
? sizes.headerLg
|
|
||||||
: size === "headerXl"
|
|
||||||
? sizes.headerXl
|
|
||||||
: size === "footer"
|
|
||||||
? sizes.footer
|
|
||||||
: size === "footerLg"
|
|
||||||
? sizes.footerLg
|
|
||||||
: sizes.default;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href="/" className="block" aria-label="CommunityRule Logo">
|
<Link href="/" className="block" aria-label="CommunityRule Logo">
|
||||||
@@ -148,25 +69,27 @@ const Logo = memo<LogoProps>(({ size = "default", showText = true }) => {
|
|||||||
showText ? config.gap : ""
|
showText ? config.gap : ""
|
||||||
} transition-all duration-200 ease-in-out hover:scale-[1.02] cursor-pointer`}
|
} transition-all duration-200 ease-in-out hover:scale-[1.02] cursor-pointer`}
|
||||||
>
|
>
|
||||||
{/* Logo Text - only show if showText is true */}
|
{/* Logo Text - responsive visibility for topNav sizes */}
|
||||||
{showText && (
|
<div
|
||||||
<div
|
className={`font-bricolage-grotesque ${
|
||||||
className={`font-bricolage-grotesque ${
|
size === "topNavFolderTop"
|
||||||
size === "homeHeaderXsmall" ||
|
? "text-[var(--color-content-inverse-primary)]"
|
||||||
size === "homeHeaderSm" ||
|
: "text-[var(--color-content-default-primary)]"
|
||||||
size === "homeHeaderMd" ||
|
} ${config.textSize} ${
|
||||||
size === "homeHeaderLg" ||
|
config.lineHeight
|
||||||
size === "homeHeaderXl"
|
} font-normal tracking-[0px] transition-colors duration-200 ${
|
||||||
? "text-[var(--color-content-inverse-primary)]"
|
size === "topNavFolderTop" || size === "topNavHeader"
|
||||||
: "text-[var(--color-content-default-primary)]"
|
? showText
|
||||||
} ${config.textSize} ${
|
? "hidden sm:block"
|
||||||
config.lineHeight
|
: "hidden"
|
||||||
} font-normal tracking-[0px] transition-colors duration-200`}
|
: showText
|
||||||
aria-label="CommunityRule"
|
? ""
|
||||||
>
|
: "hidden"
|
||||||
CommunityRule
|
}`}
|
||||||
</div>
|
aria-label="CommunityRule"
|
||||||
)}
|
>
|
||||||
|
CommunityRule
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Vector Icon */}
|
{/* Vector Icon */}
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
@@ -178,13 +101,7 @@ const Logo = memo<LogoProps>(({ size = "default", showText = true }) => {
|
|||||||
className={`flex-shrink-0 ${
|
className={`flex-shrink-0 ${
|
||||||
config.iconSize
|
config.iconSize
|
||||||
} transition-all duration-200 ${
|
} transition-all duration-200 ${
|
||||||
size === "homeHeaderXsmall" ||
|
size === "topNavFolderTop" ? "filter brightness-0" : ""
|
||||||
size === "homeHeaderSm" ||
|
|
||||||
size === "homeHeaderMd" ||
|
|
||||||
size === "homeHeaderLg" ||
|
|
||||||
size === "homeHeaderXl"
|
|
||||||
? "filter brightness-0"
|
|
||||||
: ""
|
|
||||||
}`}
|
}`}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
// Code split Footer - below the fold, can be lazy loaded
|
||||||
|
const Footer = dynamic(() => import("./Footer"), {
|
||||||
|
loading: () => (
|
||||||
|
<footer className="bg-[var(--color-surface-default-primary)] w-full min-h-[200px]" />
|
||||||
|
),
|
||||||
|
ssr: true, // Keep SSR for SEO
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditionally renders Footer based on pathname.
|
||||||
|
* Hides footer for /create/* routes (full-screen create flow).
|
||||||
|
*/
|
||||||
|
const ConditionalFooter = memo(() => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isCreateFlow = pathname?.startsWith("/create");
|
||||||
|
|
||||||
|
if (isCreateFlow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Footer />;
|
||||||
|
});
|
||||||
|
|
||||||
|
ConditionalFooter.displayName = "ConditionalFooter";
|
||||||
|
|
||||||
|
export default ConditionalFooter;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import TopNavWithPathname from "./TopNav/TopNavWithPathname";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditionally renders TopNav based on pathname.
|
||||||
|
* Hides navigation for /create/* routes (full-screen create flow).
|
||||||
|
*/
|
||||||
|
const ConditionalNavigation = memo(() => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isCreateFlow = pathname?.startsWith("/create");
|
||||||
|
|
||||||
|
if (isCreateFlow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TopNavWithPathname />;
|
||||||
|
});
|
||||||
|
|
||||||
|
ConditionalNavigation.displayName = "ConditionalNavigation";
|
||||||
|
|
||||||
|
export default ConditionalNavigation;
|
||||||
@@ -39,16 +39,8 @@ const Footer = memo(() => {
|
|||||||
lg:py-[var(--spacing-measures-spacing-096,96px)]
|
lg:py-[var(--spacing-measures-spacing-096,96px)]
|
||||||
lg:gap-[var(--spacing-measures-spacing-060,60px)]"
|
lg:gap-[var(--spacing-measures-spacing-060,60px)]"
|
||||||
>
|
>
|
||||||
{/* Logo*/}
|
{/* Logo */}
|
||||||
<div className="block sm:hidden">
|
<Logo size="footer" />
|
||||||
<Logo />
|
|
||||||
</div>
|
|
||||||
<div className="hidden sm:block lg:hidden">
|
|
||||||
<Logo size="footer" />
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:block">
|
|
||||||
<Logo size="footerLg" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content section */}
|
{/* Content section */}
|
||||||
<div className="flex flex-col items-start w-full gap-[var(--spacing-measures-spacing-048,48px)] sm:flex-row sm:justify-between sm:gap-0">
|
<div className="flex flex-col items-start w-full gap-[var(--spacing-measures-spacing-048,48px)] sm:flex-row sm:justify-between sm:gap-0">
|
||||||
|
|||||||
@@ -42,54 +42,8 @@ const TopNavContainer = memo<TopNavProps>(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Logo configuration based on folderTop prop
|
// Logo size based on folderTop prop
|
||||||
const logoConfig = folderTop
|
const logoSize = folderTop ? "topNavFolderTop" : "topNavHeader";
|
||||||
? [
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{ 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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Navigation items with translations
|
// Navigation items with translations
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
@@ -205,6 +159,7 @@ const TopNavContainer = memo<TopNavProps>(
|
|||||||
size={buttonSize}
|
size={buttonSize}
|
||||||
buttonType={buttonType}
|
buttonType={buttonType}
|
||||||
palette={palette}
|
palette={palette}
|
||||||
|
href="/create/informational"
|
||||||
ariaLabel={t("ariaLabels.createNewRule")}
|
ariaLabel={t("ariaLabels.createNewRule")}
|
||||||
>
|
>
|
||||||
{renderAvatarGroup(containerSize, avatarSize)}
|
{renderAvatarGroup(containerSize, avatarSize)}
|
||||||
@@ -213,25 +168,6 @@ const TopNavContainer = memo<TopNavProps>(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLogo = (
|
|
||||||
size:
|
|
||||||
| "default"
|
|
||||||
| "homeHeaderXsmall"
|
|
||||||
| "homeHeaderSm"
|
|
||||||
| "homeHeaderMd"
|
|
||||||
| "homeHeaderLg"
|
|
||||||
| "homeHeaderXl"
|
|
||||||
| "header"
|
|
||||||
| "headerMd"
|
|
||||||
| "headerLg"
|
|
||||||
| "headerXl"
|
|
||||||
| "footer"
|
|
||||||
| "footerLg",
|
|
||||||
showText: boolean,
|
|
||||||
) => {
|
|
||||||
return <Logo size={size} showText={showText} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopNavView
|
<TopNavView
|
||||||
folderTop={folderTop}
|
folderTop={folderTop}
|
||||||
@@ -239,11 +175,10 @@ const TopNavContainer = memo<TopNavProps>(
|
|||||||
profile={profile}
|
profile={profile}
|
||||||
logIn={logIn}
|
logIn={logIn}
|
||||||
schemaData={schemaData}
|
schemaData={schemaData}
|
||||||
logoConfig={logoConfig}
|
logoSize={logoSize}
|
||||||
renderNavigationItems={renderNavigationItems}
|
renderNavigationItems={renderNavigationItems}
|
||||||
renderLoginButton={renderLoginButton}
|
renderLoginButton={renderLoginButton}
|
||||||
renderCreateRuleButton={renderCreateRuleButton}
|
renderCreateRuleButton={renderCreateRuleButton}
|
||||||
renderLogo={renderLogo}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,23 +38,7 @@ export interface TopNavViewProps {
|
|||||||
"query-input": string;
|
"query-input": string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
logoConfig: Array<{
|
logoSize: "topNavFolderTop" | "topNavHeader";
|
||||||
breakpoint: string;
|
|
||||||
size:
|
|
||||||
| "default"
|
|
||||||
| "homeHeaderXsmall"
|
|
||||||
| "homeHeaderSm"
|
|
||||||
| "homeHeaderMd"
|
|
||||||
| "homeHeaderLg"
|
|
||||||
| "homeHeaderXl"
|
|
||||||
| "header"
|
|
||||||
| "headerMd"
|
|
||||||
| "headerLg"
|
|
||||||
| "headerXl"
|
|
||||||
| "footer"
|
|
||||||
| "footerLg";
|
|
||||||
showText: boolean;
|
|
||||||
}>;
|
|
||||||
renderNavigationItems: (_size: NavSize) => React.ReactNode;
|
renderNavigationItems: (_size: NavSize) => React.ReactNode;
|
||||||
renderLoginButton: (_size: NavSize) => React.ReactNode;
|
renderLoginButton: (_size: NavSize) => React.ReactNode;
|
||||||
renderCreateRuleButton: (
|
renderCreateRuleButton: (
|
||||||
@@ -62,20 +46,4 @@ export interface TopNavViewProps {
|
|||||||
_containerSize: "small" | "medium" | "large" | "xlarge",
|
_containerSize: "small" | "medium" | "large" | "xlarge",
|
||||||
_avatarSize: "small" | "medium" | "large" | "xlarge",
|
_avatarSize: "small" | "medium" | "large" | "xlarge",
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
renderLogo: (
|
|
||||||
_size:
|
|
||||||
| "default"
|
|
||||||
| "homeHeaderXsmall"
|
|
||||||
| "homeHeaderSm"
|
|
||||||
| "homeHeaderMd"
|
|
||||||
| "homeHeaderLg"
|
|
||||||
| "homeHeaderXl"
|
|
||||||
| "header"
|
|
||||||
| "headerMd"
|
|
||||||
| "headerLg"
|
|
||||||
| "headerXl"
|
|
||||||
| "footer"
|
|
||||||
| "footerLg",
|
|
||||||
_showText: boolean,
|
|
||||||
) => React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,18 @@ import { getAssetPath } from "../../../../lib/assetUtils";
|
|||||||
import MenuBar from "../MenuBar";
|
import MenuBar from "../MenuBar";
|
||||||
import type { TopNavViewProps } from "./TopNav.types";
|
import type { TopNavViewProps } from "./TopNav.types";
|
||||||
|
|
||||||
|
import Logo from "../../icons/Logo";
|
||||||
|
|
||||||
function TopNavView({
|
function TopNavView({
|
||||||
folderTop,
|
folderTop,
|
||||||
loggedIn: _loggedIn,
|
loggedIn: _loggedIn,
|
||||||
profile: _profile,
|
profile: _profile,
|
||||||
logIn,
|
logIn,
|
||||||
schemaData,
|
schemaData,
|
||||||
logoConfig,
|
logoSize,
|
||||||
renderNavigationItems,
|
renderNavigationItems,
|
||||||
renderLoginButton,
|
renderLoginButton,
|
||||||
renderCreateRuleButton,
|
renderCreateRuleButton,
|
||||||
renderLogo,
|
|
||||||
}: TopNavViewProps) {
|
}: TopNavViewProps) {
|
||||||
const t = useTranslation(folderTop ? "homeHeader" : "header");
|
const t = useTranslation(folderTop ? "homeHeader" : "header");
|
||||||
|
|
||||||
@@ -43,13 +44,7 @@ function TopNavView({
|
|||||||
{/* Header Tab - Yellow tab container with decorative Union images */}
|
{/* Header Tab - Yellow tab container with decorative Union images */}
|
||||||
<div className="HeaderTab header-breakpoint-transition relative bg-[var(--color-surface-inverse-brand-primary)] rounded-tl-[var(--radius-measures-radius-medium)] rounded-tr-[var(--radius-measures-radius-medium)] sm:rounded-t-[var(--radius-measures-radius-xlarge)] md:rounded-t-[var(--radius-measures-radius-xlarge)] lg:rounded-t-[var(--radius-measures-radius-xlarge)] xl:rounded-t-[var(--radius-measures-radius-xlarge)] pl-[var(--spacing-scale-012)] pr-[var(--spacing-scale-048)] h-[var(--spacing-scale-040)] sm:pl-[var(--spacing-scale-012)] sm:h-[52px] sm:pr-[var(--spacing-scale-006)] md:h-[52px] md:pl-[var(--spacing-scale-024)] md:pr-[var(--spacing-scale-012)] lg:h-[52px] lg:pl-[var(--spacing-scale-024)] lg:pr-[var(--spacing-scale-048)] xl:h-[64px] xl:pl-[var(--spacing-scale-032)] xl:pr-[var(--spacing-scale-120)] md:gap-[var(--spacing-scale-032)] flex-1 min-w-0 min-w-[197px] sm:min-w-0 sm:mr-[var(--spacing-scale-008)] md:mr-[185px] lg:mr-[var(--spacing-scale-024)] xl:mr-[var(--spacing-scale-032)] flex items-center self-end">
|
<div className="HeaderTab header-breakpoint-transition relative bg-[var(--color-surface-inverse-brand-primary)] rounded-tl-[var(--radius-measures-radius-medium)] rounded-tr-[var(--radius-measures-radius-medium)] sm:rounded-t-[var(--radius-measures-radius-xlarge)] md:rounded-t-[var(--radius-measures-radius-xlarge)] lg:rounded-t-[var(--radius-measures-radius-xlarge)] xl:rounded-t-[var(--radius-measures-radius-xlarge)] pl-[var(--spacing-scale-012)] pr-[var(--spacing-scale-048)] h-[var(--spacing-scale-040)] sm:pl-[var(--spacing-scale-012)] sm:h-[52px] sm:pr-[var(--spacing-scale-006)] md:h-[52px] md:pl-[var(--spacing-scale-024)] md:pr-[var(--spacing-scale-012)] lg:h-[52px] lg:pl-[var(--spacing-scale-024)] lg:pr-[var(--spacing-scale-048)] xl:h-[64px] xl:pl-[var(--spacing-scale-032)] xl:pr-[var(--spacing-scale-120)] md:gap-[var(--spacing-scale-032)] flex-1 min-w-0 min-w-[197px] sm:min-w-0 sm:mr-[var(--spacing-scale-008)] md:mr-[185px] lg:mr-[var(--spacing-scale-024)] xl:mr-[var(--spacing-scale-032)] flex items-center self-end">
|
||||||
{/* Logo - Consistent left positioning within HeaderTab */}
|
{/* Logo - Consistent left positioning within HeaderTab */}
|
||||||
<div>
|
<Logo size={logoSize} showText={true} />
|
||||||
{logoConfig.map((config, index) => (
|
|
||||||
<div key={index} className={config.breakpoint}>
|
|
||||||
{renderLogo(config.size, config.showText)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* XSmall menu bar - positioned next to logo */}
|
{/* XSmall menu bar - positioned next to logo */}
|
||||||
<div className="block sm:hidden -me-[2px]">
|
<div className="block sm:hidden -me-[2px]">
|
||||||
@@ -163,17 +158,7 @@ function TopNavView({
|
|||||||
aria-label={t("ariaLabels.mainNavigation")}
|
aria-label={t("ariaLabels.mainNavigation")}
|
||||||
>
|
>
|
||||||
{/* Logo - Consistent left positioning across all breakpoints */}
|
{/* Logo - Consistent left positioning across all breakpoints */}
|
||||||
<div className="flex items-center">
|
<Logo size={logoSize} showText={true} />
|
||||||
{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 */}
|
{/* Navigation Links - Consistent center positioning */}
|
||||||
<div className="flex items-center flex-1 justify-end sm:flex-none sm:justify-center">
|
<div className="flex items-center flex-1 justify-end sm:flex-none sm:justify-center">
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import { CreateFlowFooterView } from "./CreateFlowFooter.view";
|
||||||
|
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
||||||
|
|
||||||
|
const CreateFlowFooterContainer = memo<CreateFlowFooterProps>(
|
||||||
|
({ secondButton, progressBar = true, className = "" }) => {
|
||||||
|
return (
|
||||||
|
<CreateFlowFooterView
|
||||||
|
secondButton={secondButton}
|
||||||
|
progressBar={progressBar}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
CreateFlowFooterContainer.displayName = "CreateFlowFooter";
|
||||||
|
|
||||||
|
export default CreateFlowFooterContainer;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Type definitions for CreateFlowFooter component
|
||||||
|
*
|
||||||
|
* Footer component for the create rule flow with progress bar and buttons.
|
||||||
|
*/
|
||||||
|
export interface CreateFlowFooterProps {
|
||||||
|
/**
|
||||||
|
* The second button (typically "Next" button) to display on the right side
|
||||||
|
*/
|
||||||
|
secondButton?: React.ReactNode;
|
||||||
|
/**
|
||||||
|
* Whether to show the progress bar
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
progressBar?: boolean;
|
||||||
|
/**
|
||||||
|
* Additional CSS classes
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import ProportionBar from "../../progress/ProportionBar";
|
||||||
|
import Button from "../../buttons/Button";
|
||||||
|
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
||||||
|
|
||||||
|
export function CreateFlowFooterView({
|
||||||
|
secondButton,
|
||||||
|
progressBar = true,
|
||||||
|
className = "",
|
||||||
|
}: CreateFlowFooterProps) {
|
||||||
|
return (
|
||||||
|
<footer
|
||||||
|
className={`sticky bottom-0 z-50 bg-black w-full ${className}`}
|
||||||
|
role="contentinfo"
|
||||||
|
aria-label="Create Flow Footer"
|
||||||
|
>
|
||||||
|
{/* Progress Bar - Top */}
|
||||||
|
{progressBar && (
|
||||||
|
<div className="px-[var(--spacing-measures-spacing-500,20px)] md:px-[var(--spacing-measures-spacing-1200,48px)] pt-[var(--spacing-measures-spacing-300,12px)]">
|
||||||
|
<ProportionBar progress="1-0" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Buttons Container */}
|
||||||
|
<div className="flex items-center justify-between mx-auto max-w-[639px] md:max-w-[1920px] px-[var(--spacing-measures-spacing-500,20px)] md:px-[var(--spacing-measures-spacing-1200,48px)] py-[var(--spacing-measures-spacing-300,12px)] gap-[var(--spacing-measures-spacing-300,12px)]">
|
||||||
|
{/* Back Button - Left */}
|
||||||
|
<Button
|
||||||
|
buttonType="ghost"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
className="md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Second Button - Right */}
|
||||||
|
{secondButton && (
|
||||||
|
<div className="flex-shrink-0">{secondButton}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./CreateFlowFooter.container";
|
||||||
|
export type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { CreateFlowTopNavView } from "./CreateFlowTopNav.view";
|
||||||
|
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
|
||||||
|
|
||||||
|
const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
|
||||||
|
({
|
||||||
|
hasShare = false,
|
||||||
|
hasExport = false,
|
||||||
|
hasEdit = false,
|
||||||
|
loggedIn = false,
|
||||||
|
onShare,
|
||||||
|
onExport,
|
||||||
|
onEdit,
|
||||||
|
onExit,
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleExit = () => {
|
||||||
|
if (onExit) {
|
||||||
|
onExit();
|
||||||
|
} else {
|
||||||
|
// Default behavior: navigate to home
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CreateFlowTopNavView
|
||||||
|
hasShare={hasShare}
|
||||||
|
hasExport={hasExport}
|
||||||
|
hasEdit={hasEdit}
|
||||||
|
loggedIn={loggedIn}
|
||||||
|
onShare={onShare}
|
||||||
|
onExport={onExport}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onExit={handleExit}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
CreateFlowTopNavContainer.displayName = "CreateFlowTopNav";
|
||||||
|
|
||||||
|
export default CreateFlowTopNavContainer;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Type definitions for CreateFlowTopNav component
|
||||||
|
*
|
||||||
|
* Top navigation bar for the create rule flow.
|
||||||
|
* Includes logo and action buttons (Share, Export, Edit, Exit).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CreateFlowTopNavProps {
|
||||||
|
/**
|
||||||
|
* Whether to show the Share button
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hasShare?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to show the Export button
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hasExport?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to show the Edit button
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hasEdit?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether the user is logged in
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
loggedIn?: boolean;
|
||||||
|
/**
|
||||||
|
* Callback when Share button is clicked
|
||||||
|
*/
|
||||||
|
onShare?: () => void;
|
||||||
|
/**
|
||||||
|
* Callback when Export button is clicked
|
||||||
|
*/
|
||||||
|
onExport?: () => void;
|
||||||
|
/**
|
||||||
|
* Callback when Edit button is clicked
|
||||||
|
*/
|
||||||
|
onEdit?: () => void;
|
||||||
|
/**
|
||||||
|
* Callback when Exit/Save & Exit button is clicked
|
||||||
|
*/
|
||||||
|
onExit?: () => void;
|
||||||
|
/**
|
||||||
|
* Additional CSS classes
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import Logo from "../../icons/Logo";
|
||||||
|
import Button from "../../buttons/Button";
|
||||||
|
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
|
||||||
|
|
||||||
|
export function CreateFlowTopNavView({
|
||||||
|
hasShare = false,
|
||||||
|
hasExport = false,
|
||||||
|
hasEdit = false,
|
||||||
|
loggedIn = false,
|
||||||
|
onShare,
|
||||||
|
onExport,
|
||||||
|
onEdit,
|
||||||
|
onExit,
|
||||||
|
className = "",
|
||||||
|
}: CreateFlowTopNavProps) {
|
||||||
|
const exitButtonText = loggedIn ? "Save & Exit" : "Exit";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={`bg-black w-full border-b border-[var(--color-border-default-tertiary)] ${className}`}
|
||||||
|
role="banner"
|
||||||
|
aria-label="Create Rule Flow Navigation"
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
className="flex items-center justify-between mx-auto max-w-[639px] md:max-w-[1920px] px-[var(--spacing-measures-spacing-500,20px)] md:px-[48px] py-[var(--spacing-measures-spacing-300,12px)] md:py-[var(--spacing-measures-spacing-016,16px)]"
|
||||||
|
role="navigation"
|
||||||
|
aria-label="Create Flow Navigation"
|
||||||
|
>
|
||||||
|
{/* Logo - Left */}
|
||||||
|
<Logo size="createFlow" showText={true} />
|
||||||
|
|
||||||
|
{/* Button Group - Right */}
|
||||||
|
<div className="flex items-center gap-[var(--spacing-scale-012,12px)]">
|
||||||
|
{hasShare && (
|
||||||
|
<Button
|
||||||
|
buttonType="outline"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
onClick={onShare}
|
||||||
|
ariaLabel="Share"
|
||||||
|
className="md:!text-[12px] md:!leading-[14px] !text-[10px] !leading-[12px] !px-[var(--spacing-scale-006,6px)] md:!px-[var(--spacing-scale-008,8px)] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]"
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasExport && (
|
||||||
|
<Button
|
||||||
|
buttonType="outline"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
onClick={onExport}
|
||||||
|
ariaLabel="Export"
|
||||||
|
className="justify-center gap-[var(--spacing-scale-002,2px)] !pl-[var(--spacing-scale-012,12px)] !pr-[var(--spacing-scale-006,6px)] md:!pr-[var(--spacing-scale-006,6px)] !text-[10px] md:!text-[12px] !leading-[12px] md:!leading-[14px] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]"
|
||||||
|
>
|
||||||
|
<span>Export</span>
|
||||||
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="shrink-0 md:w-[14px] md:h-[14px]"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasEdit && (
|
||||||
|
<Button
|
||||||
|
buttonType="outline"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
onClick={onEdit}
|
||||||
|
ariaLabel="Edit"
|
||||||
|
className="md:!text-[12px] md:!leading-[14px] !text-[10px] !leading-[12px] !px-[var(--spacing-scale-006,6px)] md:!px-[var(--spacing-scale-008,8px)] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
buttonType="outline"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
onClick={onExit}
|
||||||
|
ariaLabel={exitButtonText}
|
||||||
|
className="md:!text-[12px] md:!leading-[14px] !text-[10px] !leading-[12px] !px-[var(--spacing-scale-006,6px)] md:!px-[var(--spacing-scale-008,8px)] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]"
|
||||||
|
>
|
||||||
|
{exitButtonText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./CreateFlowTopNav.container";
|
||||||
|
export type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { use } from "react";
|
||||||
|
import type { CreateFlowStep } from "../types";
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
params: Promise<{ step: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid step IDs for the create rule flow
|
||||||
|
*/
|
||||||
|
const VALID_STEPS: CreateFlowStep[] = [
|
||||||
|
"informational",
|
||||||
|
"text",
|
||||||
|
"select",
|
||||||
|
"upload",
|
||||||
|
"review",
|
||||||
|
"compact-cards",
|
||||||
|
"expanded-cards",
|
||||||
|
"right-rail",
|
||||||
|
"final-review",
|
||||||
|
"completed",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic route handler for create flow steps
|
||||||
|
*
|
||||||
|
* Handles all flow steps via dynamic routing: /create/[step]
|
||||||
|
* Validates step exists and renders appropriate template (placeholder for now)
|
||||||
|
*/
|
||||||
|
export default function CreateFlowStepPage({ params }: PageProps) {
|
||||||
|
const { step } = use(params);
|
||||||
|
|
||||||
|
// Validate step exists
|
||||||
|
if (!VALID_STEPS.includes(step as CreateFlowStep)) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder content - templates will be implemented in CR-51-55
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-white text-2xl font-bold mb-4">
|
||||||
|
Create Flow Step: {step}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-400">
|
||||||
|
Template implementation coming in CR-51 through CR-55
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext, useState, type ReactNode } from "react";
|
||||||
|
import type {
|
||||||
|
CreateFlowState,
|
||||||
|
CreateFlowContextValue,
|
||||||
|
CreateFlowStep,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
const CreateFlowContext = createContext<CreateFlowContextValue | null>(null);
|
||||||
|
|
||||||
|
interface CreateFlowProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
initialStep?: CreateFlowStep | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider component for Create Flow state management
|
||||||
|
*
|
||||||
|
* This is a basic implementation that will be expanded in CR-56
|
||||||
|
* with full navigation logic, state persistence, and validation.
|
||||||
|
*/
|
||||||
|
export function CreateFlowProvider({
|
||||||
|
children,
|
||||||
|
initialStep = null,
|
||||||
|
}: CreateFlowProviderProps) {
|
||||||
|
const [state, setState] = useState<CreateFlowState>({});
|
||||||
|
const [currentStep] = useState<CreateFlowStep | null>(
|
||||||
|
initialStep,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateState = (updates: Partial<CreateFlowState>) => {
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
...updates,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextValue: CreateFlowContextValue = {
|
||||||
|
state,
|
||||||
|
currentStep,
|
||||||
|
updateState,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CreateFlowContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</CreateFlowContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to access Create Flow context
|
||||||
|
*
|
||||||
|
* @throws Error if used outside CreateFlowProvider
|
||||||
|
* @returns CreateFlowContextValue
|
||||||
|
*/
|
||||||
|
export function useCreateFlow(): CreateFlowContextValue {
|
||||||
|
const context = useContext(CreateFlowContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useCreateFlow must be used within CreateFlowProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { CreateFlowProvider } from "./context/CreateFlowContext";
|
||||||
|
import CreateFlowTopNav from "../components/utility/CreateFlowTopNav";
|
||||||
|
import CreateFlowFooter from "../components/utility/CreateFlowFooter";
|
||||||
|
import Button from "../components/buttons/Button";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout for the Create Rule Flow
|
||||||
|
*
|
||||||
|
* Provides a full-screen layout without the root layout's TopNav/Footer.
|
||||||
|
* This layout wraps all create flow pages and provides the CreateFlowContext.
|
||||||
|
* Includes the create flow-specific TopNav and Footer components.
|
||||||
|
*/
|
||||||
|
export default function CreateFlowLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<CreateFlowProvider>
|
||||||
|
<div className="min-h-screen bg-black flex flex-col">
|
||||||
|
<CreateFlowTopNav />
|
||||||
|
<main className="flex-1 overflow-auto">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<CreateFlowFooter
|
||||||
|
secondButton={
|
||||||
|
<Button
|
||||||
|
buttonType="filled"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
className="md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CreateFlowProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Type definitions for the Create Rule Flow
|
||||||
|
*
|
||||||
|
* These types define the structure for the full-screen create rule flow,
|
||||||
|
* including step types, state management, and context interfaces.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid step IDs for the create rule flow
|
||||||
|
*/
|
||||||
|
export type CreateFlowStep =
|
||||||
|
| "informational"
|
||||||
|
| "text"
|
||||||
|
| "select"
|
||||||
|
| "upload"
|
||||||
|
| "review"
|
||||||
|
| "compact-cards"
|
||||||
|
| "expanded-cards"
|
||||||
|
| "right-rail"
|
||||||
|
| "final-review"
|
||||||
|
| "completed";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flow state interface for storing user inputs across all steps
|
||||||
|
* Will be expanded in CR-56 with specific field definitions
|
||||||
|
*/
|
||||||
|
export interface CreateFlowState {
|
||||||
|
// Placeholder structure - will be expanded in CR-56
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context value interface for CreateFlowContext
|
||||||
|
* Provides state management and navigation capabilities
|
||||||
|
*/
|
||||||
|
export interface CreateFlowContextValue {
|
||||||
|
state: CreateFlowState;
|
||||||
|
currentStep: CreateFlowStep | null;
|
||||||
|
updateState: (updates: Partial<CreateFlowState>) => void;
|
||||||
|
// Navigation handlers will be added in CR-56
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base props interface for page templates
|
||||||
|
* Will be expanded in template implementation tickets (CR-51-55)
|
||||||
|
*/
|
||||||
|
export interface PageTemplateProps {
|
||||||
|
// Base props for all page templates
|
||||||
|
// Will be expanded in template tickets
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation handlers interface
|
||||||
|
* Will be implemented in CR-56
|
||||||
|
*/
|
||||||
|
export interface NavigationHandlers {
|
||||||
|
goToNextStep: () => void;
|
||||||
|
goToPreviousStep: () => void;
|
||||||
|
goToStep: (step: CreateFlowStep) => void;
|
||||||
|
canGoNext: () => boolean;
|
||||||
|
canGoBack: () => boolean;
|
||||||
|
}
|
||||||
+4
-12
@@ -1,19 +1,11 @@
|
|||||||
import { Inter, Bricolage_Grotesque, Space_Grotesk } from "next/font/google";
|
import { Inter, Bricolage_Grotesque, Space_Grotesk } from "next/font/google";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { MessagesProvider } from "./contexts/MessagesContext";
|
import { MessagesProvider } from "./contexts/MessagesContext";
|
||||||
import messages from "../messages/en/index";
|
import messages from "../messages/en/index";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import TopNavWithPathname from "./components/navigation/TopNav/TopNavWithPathname";
|
import ConditionalNavigation from "./components/navigation/ConditionalNavigation";
|
||||||
|
import ConditionalFooter from "./components/navigation/ConditionalFooter";
|
||||||
// Code split Footer - below the fold, can be lazy loaded
|
|
||||||
const Footer = dynamic(() => import("./components/navigation/Footer"), {
|
|
||||||
loading: () => (
|
|
||||||
<footer className="bg-[var(--color-surface-default-primary)] w-full min-h-[200px]" />
|
|
||||||
),
|
|
||||||
ssr: true, // Keep SSR for SEO
|
|
||||||
});
|
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -107,9 +99,9 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|||||||
>
|
>
|
||||||
<MessagesProvider messages={messages}>
|
<MessagesProvider messages={messages}>
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
<TopNavWithPathname />
|
<ConditionalNavigation />
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1">{children}</main>
|
||||||
<Footer />
|
<ConditionalFooter />
|
||||||
</div>
|
</div>
|
||||||
</MessagesProvider>
|
</MessagesProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+27
-125
@@ -17,17 +17,10 @@ export default {
|
|||||||
control: { type: "select" },
|
control: { type: "select" },
|
||||||
options: [
|
options: [
|
||||||
"default",
|
"default",
|
||||||
"homeHeaderXsmall",
|
|
||||||
"homeHeaderSm",
|
|
||||||
"homeHeaderMd",
|
|
||||||
"homeHeaderLg",
|
|
||||||
"homeHeaderXl",
|
|
||||||
"header",
|
|
||||||
"headerMd",
|
|
||||||
"headerLg",
|
|
||||||
"headerXl",
|
|
||||||
"footer",
|
"footer",
|
||||||
"footerLg",
|
"createFlow",
|
||||||
|
"topNavFolderTop",
|
||||||
|
"topNavHeader",
|
||||||
],
|
],
|
||||||
description: "The size variant of the logo",
|
description: "The size variant of the logo",
|
||||||
},
|
},
|
||||||
@@ -53,40 +46,19 @@ export const Sizes = {
|
|||||||
render: (args) => (
|
render: (args) => (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-white font-semibold mb-3">Default Sizes</h3>
|
<h3 className="text-white font-semibold mb-3">Standard Sizes</h3>
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<Logo {...args} size="default" />
|
<Logo {...args} size="default" />
|
||||||
<Logo {...args} size="header" />
|
|
||||||
<Logo {...args} size="footer" />
|
<Logo {...args} size="footer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-white font-semibold mb-3">Header Sizes</h3>
|
<h3 className="text-white font-semibold mb-3">Responsive Sizes</h3>
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<Logo {...args} size="header" />
|
<Logo {...args} size="createFlow" />
|
||||||
<Logo {...args} size="headerMd" />
|
<Logo {...args} size="topNavFolderTop" />
|
||||||
<Logo {...args} size="headerLg" />
|
<Logo {...args} size="topNavHeader" />
|
||||||
<Logo {...args} size="headerXl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-white font-semibold mb-3">Home Header Sizes</h3>
|
|
||||||
<div className="space-x-4">
|
|
||||||
<Logo {...args} size="homeHeaderXsmall" />
|
|
||||||
<Logo {...args} size="homeHeaderSm" />
|
|
||||||
<Logo {...args} size="homeHeaderMd" />
|
|
||||||
<Logo {...args} size="homeHeaderLg" />
|
|
||||||
<Logo {...args} size="homeHeaderXl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-white font-semibold mb-3">Footer Sizes</h3>
|
|
||||||
<div className="space-x-4">
|
|
||||||
<Logo {...args} size="footer" />
|
|
||||||
<Logo {...args} size="footerLg" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,7 +67,7 @@ export const Sizes = {
|
|||||||
docs: {
|
docs: {
|
||||||
description: {
|
description: {
|
||||||
story:
|
story:
|
||||||
"Different size variants available for the logo component across different contexts.",
|
"Different size variants available for the logo component. Responsive sizes adapt to breakpoints.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -110,37 +82,22 @@ export const IconOnly = {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-white font-semibold mb-3">
|
<h3 className="text-white font-semibold mb-3">
|
||||||
Icon Only - Default Sizes
|
Icon Only - Standard Sizes
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<Logo {...args} size="default" />
|
<Logo {...args} size="default" />
|
||||||
<Logo {...args} size="header" />
|
|
||||||
<Logo {...args} size="footer" />
|
<Logo {...args} size="footer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-white font-semibold mb-3">
|
<h3 className="text-white font-semibold mb-3">
|
||||||
Icon Only - Header Sizes
|
Icon Only - Responsive Sizes
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<Logo {...args} size="header" />
|
<Logo {...args} size="createFlow" />
|
||||||
<Logo {...args} size="headerMd" />
|
<Logo {...args} size="topNavFolderTop" />
|
||||||
<Logo {...args} size="headerLg" />
|
<Logo {...args} size="topNavHeader" />
|
||||||
<Logo {...args} size="headerXl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-white font-semibold mb-3">
|
|
||||||
Icon Only - Home Header Sizes
|
|
||||||
</h3>
|
|
||||||
<div className="space-x-4">
|
|
||||||
<Logo {...args} size="homeHeaderXsmall" />
|
|
||||||
<Logo {...args} size="homeHeaderSm" />
|
|
||||||
<Logo {...args} size="homeHeaderMd" />
|
|
||||||
<Logo {...args} size="homeHeaderLg" />
|
|
||||||
<Logo {...args} size="homeHeaderXl" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,34 +112,22 @@ export const IconOnly = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HomeHeaderContext = {
|
export const TopNavContext = {
|
||||||
args: {},
|
args: {},
|
||||||
render: () => (
|
render: () => (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[var(--color-surface-default-primary)] to-[var(--color-surface-default-secondary)] p-8">
|
<div className="min-h-screen bg-gradient-to-b from-[var(--color-surface-default-primary)] to-[var(--color-surface-default-secondary)] p-8">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<h2 className="text-white font-semibold mb-6">
|
<h2 className="text-white font-semibold mb-6">
|
||||||
Home Header Context (White Text)
|
TopNav Context (Responsive)
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<span className="text-white text-sm w-24">XSmall:</span>
|
<span className="text-white text-sm w-32">FolderTop:</span>
|
||||||
<Logo size="homeHeaderXsmall" />
|
<Logo size="topNavFolderTop" showText={true} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<span className="text-white text-sm w-24">Small:</span>
|
<span className="text-white text-sm w-32">Header:</span>
|
||||||
<Logo size="homeHeaderSm" />
|
<Logo size="topNavHeader" showText={true} />
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-white text-sm w-24">Medium:</span>
|
|
||||||
<Logo size="homeHeaderMd" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-white text-sm w-24">Large:</span>
|
|
||||||
<Logo size="homeHeaderLg" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-white text-sm w-24">XLarge:</span>
|
|
||||||
<Logo size="homeHeaderXl" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,36 +137,24 @@ export const HomeHeaderContext = {
|
|||||||
docs: {
|
docs: {
|
||||||
description: {
|
description: {
|
||||||
story:
|
story:
|
||||||
"Home header context showing white text variants. These are used on dark/transparent backgrounds.",
|
"TopNav context showing responsive logo sizes. Text hides on smallest breakpoint, shows on larger breakpoints.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HeaderContext = {
|
export const CreateFlowContext = {
|
||||||
args: {},
|
args: {},
|
||||||
render: () => (
|
render: () => (
|
||||||
<div className="min-h-screen bg-[var(--color-surface-default-primary)] p-8">
|
<div className="min-h-screen bg-black p-8">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<h2 className="text-white font-semibold mb-6">
|
<h2 className="text-white font-semibold mb-6">
|
||||||
Header Context (Dark Text)
|
Create Flow Context
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<span className="text-white text-sm w-24">Default:</span>
|
<span className="text-white text-sm w-32">CreateFlow:</span>
|
||||||
<Logo size="header" />
|
<Logo size="createFlow" showText={true} />
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-white text-sm w-24">Medium:</span>
|
|
||||||
<Logo size="headerMd" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-white text-sm w-24">Large:</span>
|
|
||||||
<Logo size="headerLg" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-white text-sm w-24">XLarge:</span>
|
|
||||||
<Logo size="headerXl" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,38 +164,7 @@ export const HeaderContext = {
|
|||||||
docs: {
|
docs: {
|
||||||
description: {
|
description: {
|
||||||
story:
|
story:
|
||||||
"Header context showing dark text variants. These are used on light backgrounds.",
|
"Create flow context showing responsive logo. Used in CreateFlowTopNav component.",
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FooterContext = {
|
|
||||||
args: {},
|
|
||||||
render: () => (
|
|
||||||
<div className="min-h-screen bg-[var(--color-surface-default-primary)] p-8">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<h2 className="text-white font-semibold mb-6">
|
|
||||||
Footer Context (Larger Sizes)
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-white text-sm w-24">Default:</span>
|
|
||||||
<Logo size="footer" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-white text-sm w-24">Large:</span>
|
|
||||||
<Logo size="footerLg" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
parameters: {
|
|
||||||
docs: {
|
|
||||||
description: {
|
|
||||||
story:
|
|
||||||
"Footer context showing larger size variants for footer placement.",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import CreateFlowFooter from "../../app/components/utility/CreateFlowFooter";
|
||||||
|
import Button from "../../app/components/buttons/Button";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Utility/CreateFlowFooter",
|
||||||
|
component: CreateFlowFooter,
|
||||||
|
parameters: {
|
||||||
|
layout: "fullscreen",
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
"Footer component for the create rule flow with progress bar and buttons.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
progressBar: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether to show the progress bar",
|
||||||
|
},
|
||||||
|
secondButton: {
|
||||||
|
control: false,
|
||||||
|
description: "The second button (typically Next) to display on the right side",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: ["autodocs"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = {
|
||||||
|
args: {
|
||||||
|
progressBar: true,
|
||||||
|
secondButton: (
|
||||||
|
<Button buttonType="filled" palette="default" size="xsmall">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithoutProgressBar = {
|
||||||
|
args: {
|
||||||
|
progressBar: false,
|
||||||
|
secondButton: (
|
||||||
|
<Button buttonType="filled" palette="default" size="xsmall">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithoutSecondButton = {
|
||||||
|
args: {
|
||||||
|
progressBar: true,
|
||||||
|
secondButton: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import CreateFlowTopNav from "../../app/components/utility/CreateFlowTopNav";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Utility/CreateFlowTopNav",
|
||||||
|
component: CreateFlowTopNav,
|
||||||
|
parameters: {
|
||||||
|
layout: "fullscreen",
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
"Top navigation bar for the create rule flow. Includes logo and action buttons (Share, Export, Edit, Exit/Save & Exit).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
hasShare: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether to show the Share button",
|
||||||
|
},
|
||||||
|
hasExport: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether to show the Export button",
|
||||||
|
},
|
||||||
|
hasEdit: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether to show the Edit button",
|
||||||
|
},
|
||||||
|
loggedIn: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether the user is logged in (affects Exit button text)",
|
||||||
|
},
|
||||||
|
onShare: { action: "share clicked" },
|
||||||
|
onExport: { action: "export clicked" },
|
||||||
|
onEdit: { action: "edit clicked" },
|
||||||
|
onExit: { action: "exit clicked" },
|
||||||
|
},
|
||||||
|
tags: ["autodocs"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = {
|
||||||
|
args: {
|
||||||
|
hasShare: false,
|
||||||
|
hasExport: false,
|
||||||
|
hasEdit: false,
|
||||||
|
loggedIn: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllButtons = {
|
||||||
|
args: {
|
||||||
|
hasShare: true,
|
||||||
|
hasExport: true,
|
||||||
|
hasEdit: true,
|
||||||
|
loggedIn: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LoggedIn = {
|
||||||
|
args: {
|
||||||
|
hasShare: true,
|
||||||
|
hasExport: true,
|
||||||
|
hasEdit: true,
|
||||||
|
loggedIn: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
|
import CreateFlowFooter from "../../app/components/utility/CreateFlowFooter";
|
||||||
|
import Button from "../../app/components/buttons/Button";
|
||||||
|
import {
|
||||||
|
componentTestSuite,
|
||||||
|
ComponentTestSuiteConfig,
|
||||||
|
} from "../utils/componentTestSuite";
|
||||||
|
|
||||||
|
type CreateFlowFooterProps = React.ComponentProps<typeof CreateFlowFooter>;
|
||||||
|
|
||||||
|
const baseProps: CreateFlowFooterProps = {};
|
||||||
|
|
||||||
|
const config: ComponentTestSuiteConfig<CreateFlowFooterProps> = {
|
||||||
|
component: CreateFlowFooter,
|
||||||
|
name: "CreateFlowFooter",
|
||||||
|
props: baseProps,
|
||||||
|
requiredProps: [],
|
||||||
|
optionalProps: {
|
||||||
|
secondButton: <Button>Next</Button>,
|
||||||
|
progressBar: true,
|
||||||
|
className: "test-class",
|
||||||
|
},
|
||||||
|
primaryRole: "contentinfo",
|
||||||
|
testCases: {
|
||||||
|
renders: true,
|
||||||
|
accessibility: true,
|
||||||
|
keyboardNavigation: false,
|
||||||
|
disabledState: false,
|
||||||
|
errorState: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
componentTestSuite<CreateFlowFooterProps>(config);
|
||||||
|
|
||||||
|
describe("CreateFlowFooter (behavioral tests)", () => {
|
||||||
|
it("renders Back button", () => {
|
||||||
|
render(<CreateFlowFooter />);
|
||||||
|
const backButton = screen.getByRole("button", { name: "Back" });
|
||||||
|
expect(backButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders progress bar when progressBar is true", () => {
|
||||||
|
render(<CreateFlowFooter progressBar={true} />);
|
||||||
|
const footer = screen.getByRole("contentinfo", { name: "Create Flow Footer" });
|
||||||
|
expect(footer).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render progress bar when progressBar is false", () => {
|
||||||
|
const { container } = render(<CreateFlowFooter progressBar={false} />);
|
||||||
|
const progressBar = container.querySelector('[role="progressbar"]');
|
||||||
|
expect(progressBar).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders secondButton when provided", () => {
|
||||||
|
const secondButton = <Button>Next</Button>;
|
||||||
|
render(<CreateFlowFooter secondButton={secondButton} />);
|
||||||
|
const nextButton = screen.getByRole("button", { name: "Next" });
|
||||||
|
expect(nextButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render secondButton when not provided", () => {
|
||||||
|
render(<CreateFlowFooter />);
|
||||||
|
const buttons = screen.getAllByRole("button");
|
||||||
|
expect(buttons).toHaveLength(1);
|
||||||
|
expect(buttons[0]).toHaveTextContent("Back");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
|
import CreateFlowTopNav from "../../app/components/utility/CreateFlowTopNav";
|
||||||
|
import {
|
||||||
|
componentTestSuite,
|
||||||
|
ComponentTestSuiteConfig,
|
||||||
|
} from "../utils/componentTestSuite";
|
||||||
|
|
||||||
|
// Mock next/navigation
|
||||||
|
vi.mock("next/navigation", () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: vi.fn(),
|
||||||
|
replace: vi.fn(),
|
||||||
|
prefetch: vi.fn(),
|
||||||
|
back: vi.fn(),
|
||||||
|
forward: vi.fn(),
|
||||||
|
refresh: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
type CreateFlowTopNavProps = React.ComponentProps<typeof CreateFlowTopNav>;
|
||||||
|
|
||||||
|
const baseProps: CreateFlowTopNavProps = {};
|
||||||
|
|
||||||
|
const config: ComponentTestSuiteConfig<CreateFlowTopNavProps> = {
|
||||||
|
component: CreateFlowTopNav,
|
||||||
|
name: "CreateFlowTopNav",
|
||||||
|
props: baseProps,
|
||||||
|
requiredProps: [],
|
||||||
|
optionalProps: {
|
||||||
|
hasShare: true,
|
||||||
|
hasExport: true,
|
||||||
|
hasEdit: true,
|
||||||
|
loggedIn: true,
|
||||||
|
onShare: vi.fn(),
|
||||||
|
onExport: vi.fn(),
|
||||||
|
onEdit: vi.fn(),
|
||||||
|
onExit: vi.fn(),
|
||||||
|
className: "test-class",
|
||||||
|
},
|
||||||
|
primaryRole: "banner",
|
||||||
|
testCases: {
|
||||||
|
renders: true,
|
||||||
|
accessibility: true,
|
||||||
|
keyboardNavigation: false,
|
||||||
|
disabledState: false,
|
||||||
|
errorState: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
componentTestSuite<CreateFlowTopNavProps>(config);
|
||||||
|
|
||||||
|
describe("CreateFlowTopNav (behavioral tests)", () => {
|
||||||
|
it("renders Exit button by default", () => {
|
||||||
|
render(<CreateFlowTopNav />);
|
||||||
|
const exitButton = screen.getByRole("button", { name: "Exit" });
|
||||||
|
expect(exitButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows Save & Exit when loggedIn is true", () => {
|
||||||
|
render(<CreateFlowTopNav loggedIn={true} />);
|
||||||
|
const exitButton = screen.getByRole("button", { name: "Save & Exit" });
|
||||||
|
expect(exitButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows Exit when loggedIn is false", () => {
|
||||||
|
render(<CreateFlowTopNav loggedIn={false} />);
|
||||||
|
const exitButton = screen.getByRole("button", { name: "Exit" });
|
||||||
|
expect(exitButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Share button when hasShare is true", () => {
|
||||||
|
render(<CreateFlowTopNav hasShare={true} />);
|
||||||
|
const shareButton = screen.getByRole("button", { name: "Share" });
|
||||||
|
expect(shareButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render Share button when hasShare is false", () => {
|
||||||
|
render(<CreateFlowTopNav hasShare={false} />);
|
||||||
|
expect(screen.queryByRole("button", { name: "Share" })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Export button when hasExport is true", () => {
|
||||||
|
render(<CreateFlowTopNav hasExport={true} />);
|
||||||
|
const exportButton = screen.getByRole("button", { name: "Export" });
|
||||||
|
expect(exportButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Edit button when hasEdit is true", () => {
|
||||||
|
render(<CreateFlowTopNav hasEdit={true} />);
|
||||||
|
const editButton = screen.getByRole("button", { name: "Edit" });
|
||||||
|
expect(editButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onExit when Exit button is clicked", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const handleExit = vi.fn();
|
||||||
|
render(<CreateFlowTopNav onExit={handleExit} />);
|
||||||
|
|
||||||
|
const exitButton = screen.getByRole("button", { name: "Exit" });
|
||||||
|
await user.click(exitButton);
|
||||||
|
|
||||||
|
expect(handleExit).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onShare when Share button is clicked", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const handleShare = vi.fn();
|
||||||
|
render(<CreateFlowTopNav hasShare={true} onShare={handleShare} />);
|
||||||
|
|
||||||
|
const shareButton = screen.getByRole("button", { name: "Share" });
|
||||||
|
await user.click(shareButton);
|
||||||
|
|
||||||
|
expect(handleShare).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -46,19 +46,26 @@ describe("Logo (behavioral tests)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("hides text when showText is false", () => {
|
it("hides text when showText is false", () => {
|
||||||
render(<Logo showText={false} />);
|
const { container } = render(<Logo showText={false} />);
|
||||||
expect(screen.queryByText("CommunityRule")).not.toBeInTheDocument();
|
const textElement = container.querySelector('.hidden');
|
||||||
|
expect(textElement).toBeInTheDocument();
|
||||||
expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument();
|
expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders with different size variants", () => {
|
it("renders with different size variants", () => {
|
||||||
const { rerender } = render(<Logo size="header" />);
|
const { rerender } = render(<Logo size="default" />);
|
||||||
expect(screen.getByRole("link")).toBeInTheDocument();
|
expect(screen.getByRole("link")).toBeInTheDocument();
|
||||||
|
|
||||||
rerender(<Logo size="footer" />);
|
rerender(<Logo size="footer" />);
|
||||||
expect(screen.getByRole("link")).toBeInTheDocument();
|
expect(screen.getByRole("link")).toBeInTheDocument();
|
||||||
|
|
||||||
rerender(<Logo size="homeHeaderMd" />);
|
rerender(<Logo size="createFlow" />);
|
||||||
|
expect(screen.getByRole("link")).toBeInTheDocument();
|
||||||
|
|
||||||
|
rerender(<Logo size="topNavFolderTop" />);
|
||||||
|
expect(screen.getByRole("link")).toBeInTheDocument();
|
||||||
|
|
||||||
|
rerender(<Logo size="topNavHeader" />);
|
||||||
expect(screen.getByRole("link")).toBeInTheDocument();
|
expect(screen.getByRole("link")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user