Component cleanup
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { memo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import TopNavWithPathname from "./TopNav/TopNavWithPathname";
|
||||
import TopWithPathname from "./Top/TopWithPathname";
|
||||
|
||||
export type ConditionalNavigationClientProps = {
|
||||
initialSignedIn: boolean;
|
||||
@@ -22,7 +22,7 @@ const ConditionalNavigationClient = memo(
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TopNavWithPathname initialSignedIn={initialSignedIn} />;
|
||||
return <TopWithPathname initialSignedIn={initialSignedIn} />;
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { CreateFlowFooterView } from "./CreateFlowFooter.view";
|
||||
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
||||
|
||||
/**
|
||||
* Figma: "Utility / CreateFlowFooter". Sticky footer for the
|
||||
* create flow with a back action, optional secondary button, and progress bar.
|
||||
*/
|
||||
const CreateFlowFooterContainer = memo<CreateFlowFooterProps>(
|
||||
({
|
||||
secondButton,
|
||||
progressBar = true,
|
||||
proportionBarProgress,
|
||||
proportionBarVariant,
|
||||
onBackClick,
|
||||
className = "",
|
||||
}) => {
|
||||
return (
|
||||
<CreateFlowFooterView
|
||||
secondButton={secondButton}
|
||||
progressBar={progressBar}
|
||||
proportionBarProgress={proportionBarProgress}
|
||||
proportionBarVariant={proportionBarVariant}
|
||||
onBackClick={onBackClick}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CreateFlowFooterContainer.displayName = "CreateFlowFooter";
|
||||
|
||||
export default CreateFlowFooterContainer;
|
||||
@@ -0,0 +1,39 @@
|
||||
import type {
|
||||
ProportionBarState,
|
||||
ProportionBarVariant,
|
||||
} from "../../progress/ProportionBar/ProportionBar.types";
|
||||
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* `ProportionBar` state when the bar is shown (driven by create-flow step).
|
||||
* @default "1-0"
|
||||
*/
|
||||
proportionBarProgress?: ProportionBarState;
|
||||
/**
|
||||
* `ProportionBar` layout variant (Figma create-flow footer uses `segmented`).
|
||||
* @default "default"
|
||||
*/
|
||||
proportionBarVariant?: ProportionBarVariant;
|
||||
/**
|
||||
* Callback function for Back button click
|
||||
*/
|
||||
onBackClick?: () => void;
|
||||
/**
|
||||
* Additional CSS classes
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import ProportionBar from "../../progress/ProportionBar";
|
||||
import Button from "../../buttons/Button";
|
||||
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
||||
|
||||
export function CreateFlowFooterView({
|
||||
secondButton,
|
||||
progressBar = true,
|
||||
proportionBarProgress = "1-0",
|
||||
proportionBarVariant: proportionBarVariantProp,
|
||||
onBackClick,
|
||||
className = "",
|
||||
}: CreateFlowFooterProps) {
|
||||
const proportionBarVariant = proportionBarVariantProp ?? "default";
|
||||
return (
|
||||
<footer
|
||||
className={`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={proportionBarProgress}
|
||||
variant={proportionBarVariant}
|
||||
/>
|
||||
</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)]"
|
||||
onClick={onBackClick}
|
||||
disabled={!onBackClick}
|
||||
>
|
||||
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,55 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { CreateFlowTopNavView } from "./CreateFlowTopNav.view";
|
||||
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
|
||||
|
||||
/**
|
||||
* Figma: Utility / CreateFlowTopNav — wizard header (create-flow chrome).
|
||||
* Exit, optional share / export / edit; strings in `messages/en/create/topNav.json`.
|
||||
*/
|
||||
const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
|
||||
({
|
||||
hasShare = false,
|
||||
hasExport = false,
|
||||
hasEdit = false,
|
||||
saveDraftOnExit = false,
|
||||
onShare,
|
||||
onExport,
|
||||
onEdit,
|
||||
onExit,
|
||||
buttonPalette,
|
||||
className = "",
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleExit = (options?: { saveDraft?: boolean }) => {
|
||||
if (onExit) {
|
||||
onExit(options);
|
||||
} else {
|
||||
// Default behavior: navigate to home
|
||||
router.push("/");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CreateFlowTopNavView
|
||||
hasShare={hasShare}
|
||||
hasExport={hasExport}
|
||||
hasEdit={hasEdit}
|
||||
saveDraftOnExit={saveDraftOnExit}
|
||||
onShare={onShare}
|
||||
onExport={onExport}
|
||||
onEdit={onEdit}
|
||||
onExit={handleExit}
|
||||
buttonPalette={buttonPalette}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CreateFlowTopNavContainer.displayName = "CreateFlowTopNav";
|
||||
|
||||
export default CreateFlowTopNavContainer;
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* When true, exit control is "Save & Exit" and `onExit` receives `{ saveDraft: true }`.
|
||||
* When false, shows "Exit" and `{ saveDraft: false }` (caller may confirm data loss).
|
||||
* @default false
|
||||
*/
|
||||
saveDraftOnExit?: 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.
|
||||
* When `saveDraftOnExit` is true, called with `{ saveDraft: true }`.
|
||||
*/
|
||||
onExit?: (options?: { saveDraft?: boolean }) => void;
|
||||
/**
|
||||
* Palette for nav buttons (e.g. "inverse" on completed page to match teal background)
|
||||
* @default "default"
|
||||
*/
|
||||
buttonPalette?: "default" | "inverse";
|
||||
/**
|
||||
* Additional CSS classes
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import Logo from "../../asset/Logo";
|
||||
import Button from "../../buttons/Button";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
|
||||
|
||||
const exitButtonFigmaClass =
|
||||
"!rounded-[var(--radius-measures-radius-full,9999px)] !border-[1.25px] !px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!text-[12px] md:!leading-[14px]";
|
||||
|
||||
export function CreateFlowTopNavView({
|
||||
hasShare = false,
|
||||
hasExport = false,
|
||||
hasEdit = false,
|
||||
saveDraftOnExit = false,
|
||||
onShare,
|
||||
onExport,
|
||||
onEdit,
|
||||
onExit,
|
||||
buttonPalette = "default",
|
||||
className = "",
|
||||
}: CreateFlowTopNavProps) {
|
||||
const t = useTranslation("create.topNav");
|
||||
const exitButtonText = saveDraftOnExit ? t("saveAndExit") : t("exit");
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`bg-black w-full ${className}`}
|
||||
role="banner"
|
||||
aria-label={t("bannerAriaLabel")}
|
||||
>
|
||||
<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={t("navAriaLabel")}
|
||||
>
|
||||
<Logo size="createFlow" wordmark palette={buttonPalette} />
|
||||
|
||||
<div className="flex flex-wrap items-center justify-end gap-[var(--spacing-scale-012,12px)]">
|
||||
{hasShare && (
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette={buttonPalette}
|
||||
size="xsmall"
|
||||
onClick={onShare}
|
||||
ariaLabel={t("shareAriaLabel")}
|
||||
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]"
|
||||
>
|
||||
{t("share")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{hasExport && (
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette={buttonPalette}
|
||||
size="xsmall"
|
||||
onClick={onExport}
|
||||
ariaLabel={t("exportAriaLabel")}
|
||||
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>{t("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={buttonPalette}
|
||||
size="xsmall"
|
||||
onClick={onEdit}
|
||||
ariaLabel={t("editAriaLabel")}
|
||||
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]"
|
||||
>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette={buttonPalette}
|
||||
size="xsmall"
|
||||
type="button"
|
||||
onClick={() => void onExit?.({ saveDraft: saveDraftOnExit })}
|
||||
ariaLabel={exitButtonText}
|
||||
className={`md:!text-[12px] md:!leading-[14px] !text-[10px] !leading-[12px] !py-[6px] md:!py-[8px] shrink-0 ${exitButtonFigmaClass}`}
|
||||
>
|
||||
{exitButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./CreateFlowTopNav.container";
|
||||
export type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
|
||||
@@ -3,8 +3,8 @@
|
||||
import { memo } from "react";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import Link from "next/link";
|
||||
import Logo from "../asset/logo";
|
||||
import Separator from "../utility/Separator";
|
||||
import Logo from "../asset/Logo";
|
||||
import Divider from "../utility/Divider";
|
||||
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
||||
|
||||
/**
|
||||
@@ -158,7 +158,7 @@ const Footer = memo(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Divider />
|
||||
|
||||
<div
|
||||
className="flex w-full flex-col
|
||||
|
||||
@@ -3,27 +3,27 @@
|
||||
import { memo } from "react";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
|
||||
export type MenuBarSizeValue =
|
||||
export type MenuSizeValue =
|
||||
| "X Small"
|
||||
| "Small"
|
||||
| "Medium"
|
||||
| "Large"
|
||||
| "X Large";
|
||||
|
||||
interface MenuBarProps extends React.HTMLAttributes<HTMLElement> {
|
||||
interface MenuProps extends React.HTMLAttributes<HTMLElement> {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
/**
|
||||
* Menu bar size. Uses Figma format: "X Small", "Small", "Medium", "Large", "X Large".
|
||||
* Menu size. Uses Figma format: "X Small", "Small", "Medium", "Large", "X Large".
|
||||
* @default "X Small"
|
||||
*/
|
||||
size?: MenuBarSizeValue;
|
||||
size?: MenuSizeValue;
|
||||
}
|
||||
|
||||
const MenuBar = memo<MenuBarProps>(
|
||||
const Menu = memo<MenuProps>(
|
||||
({ children, className = "", size: sizeProp = "X Small", ...props }) => {
|
||||
const size = sizeProp ?? "X Small";
|
||||
const t = useTranslation("menuBar");
|
||||
const t = useTranslation("menu");
|
||||
|
||||
// Size styles based on Figma specifications
|
||||
const sizeStyles: Record<
|
||||
@@ -57,6 +57,6 @@ const MenuBar = memo<MenuBarProps>(
|
||||
},
|
||||
);
|
||||
|
||||
MenuBar.displayName = "MenuBar";
|
||||
Menu.displayName = "Menu";
|
||||
|
||||
export default MenuBar;
|
||||
export default Menu;
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from "./MenuBarItem.container";
|
||||
export type { MenuBarItemProps } from "./MenuBarItem.types";
|
||||
+7
-7
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import MenuBarItemView from "./MenuBarItem.view";
|
||||
import type { MenuBarItemProps } from "./MenuBarItem.types";
|
||||
import MenuItemView from "./MenuItem.view";
|
||||
import type { MenuItemProps } from "./MenuItem.types";
|
||||
|
||||
const MenuBarItemContainer = memo<MenuBarItemProps>(
|
||||
const MenuItemContainer = memo<MenuItemProps>(
|
||||
({
|
||||
href = "#",
|
||||
buttonOnClick,
|
||||
@@ -106,7 +106,7 @@ const MenuBarItemContainer = memo<MenuBarItemProps>(
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuBarItemView
|
||||
<MenuItemView
|
||||
href={href}
|
||||
buttonOnClick={buttonOnClick}
|
||||
disabled={disabled}
|
||||
@@ -115,11 +115,11 @@ const MenuBarItemContainer = memo<MenuBarItemProps>(
|
||||
accessibilityProps={accessibilityProps}
|
||||
>
|
||||
{children}
|
||||
</MenuBarItemView>
|
||||
</MenuItemView>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MenuBarItemContainer.displayName = "MenuBarItem";
|
||||
MenuItemContainer.displayName = "MenuItem";
|
||||
|
||||
export default MenuBarItemContainer;
|
||||
export default MenuItemContainer;
|
||||
+11
-11
@@ -1,40 +1,40 @@
|
||||
export type MenuBarItemSizeValue =
|
||||
export type MenuItemSizeValue =
|
||||
| "X Small"
|
||||
| "Small"
|
||||
| "Medium"
|
||||
| "Large"
|
||||
| "X Large";
|
||||
|
||||
export type MenuBarItemStateValue = "default" | "hover" | "selected";
|
||||
export type MenuItemStateValue = "default" | "hover" | "selected";
|
||||
|
||||
export type MenuBarItemModeValue = "default" | "inverse";
|
||||
export type MenuItemModeValue = "default" | "inverse";
|
||||
|
||||
export interface MenuBarItemProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
export interface MenuItemProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
href?: string;
|
||||
/** When set, renders a `<button type="button">` instead of a link (e.g. open login modal). */
|
||||
buttonOnClick?: () => void;
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* Menu bar item state: "default", "hover", or "selected".
|
||||
* Menu item state: "default", "hover", or "selected".
|
||||
* @default "default"
|
||||
*/
|
||||
state?: MenuBarItemStateValue;
|
||||
state?: MenuItemStateValue;
|
||||
/**
|
||||
* Menu bar item mode. Default mode has yellow text on dark background.
|
||||
* Menu item mode. Default mode has yellow text on dark background.
|
||||
* Inverse mode has black text on yellow background (for folderTop variant).
|
||||
* @default "default"
|
||||
*/
|
||||
mode?: MenuBarItemModeValue;
|
||||
mode?: MenuItemModeValue;
|
||||
/**
|
||||
* Whether to show an icon (for future icon support).
|
||||
* @default false
|
||||
*/
|
||||
icon?: boolean;
|
||||
/**
|
||||
* Menu bar item size. Uses Figma format: "X Small", "Small", "Medium", "Large", "X Large".
|
||||
* Menu item size. Uses Figma format: "X Small", "Small", "Medium", "Large", "X Large".
|
||||
* @default "X Small"
|
||||
*/
|
||||
size?: MenuBarItemSizeValue;
|
||||
size?: MenuItemSizeValue;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
/**
|
||||
@@ -45,7 +45,7 @@ export interface MenuBarItemProps extends React.AnchorHTMLAttributes<HTMLAnchorE
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export interface MenuBarItemViewProps {
|
||||
export interface MenuItemViewProps {
|
||||
href: string;
|
||||
buttonOnClick?: () => void;
|
||||
children?: React.ReactNode;
|
||||
+5
-5
@@ -1,14 +1,14 @@
|
||||
import { memo } from "react";
|
||||
import type { MenuBarItemViewProps } from "./MenuBarItem.types";
|
||||
import type { MenuItemViewProps } from "./MenuItem.types";
|
||||
|
||||
function MenuBarItemView({
|
||||
function MenuItemView({
|
||||
href,
|
||||
buttonOnClick,
|
||||
children,
|
||||
disabled,
|
||||
combinedStyles,
|
||||
accessibilityProps,
|
||||
}: MenuBarItemViewProps) {
|
||||
}: MenuItemViewProps) {
|
||||
if (disabled) {
|
||||
return (
|
||||
<span className={combinedStyles} {...accessibilityProps}>
|
||||
@@ -37,6 +37,6 @@ function MenuBarItemView({
|
||||
);
|
||||
}
|
||||
|
||||
MenuBarItemView.displayName = "MenuBarItemView";
|
||||
MenuItemView.displayName = "MenuItemView";
|
||||
|
||||
export default memo(MenuBarItemView);
|
||||
export default memo(MenuItemView);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./MenuItem.container";
|
||||
export type { MenuItemProps } from "./MenuItem.types";
|
||||
+36
-55
@@ -4,15 +4,31 @@ import { memo, useCallback } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useAuthModal } from "../../../contexts/AuthModalContext";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import MenuBarItem from "../MenuBarItem";
|
||||
import MenuItem from "../MenuItem";
|
||||
import Button from "../../buttons/Button";
|
||||
import AvatarContainer from "../../utility/AvatarContainer";
|
||||
import Avatar from "../../icons/Avatar";
|
||||
import AvatarContainer from "../../asset/AvatarContainer";
|
||||
import Avatar from "../../asset/Avatar";
|
||||
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
|
||||
import { clearAnonymousCreateFlowStorage } from "../../../(app)/create/utils/anonymousDraftStorage";
|
||||
import { clearCoreValueDetailsLocalStorage } from "../../../(app)/create/utils/coreValueDetailsLocalStorage";
|
||||
import { TopNavView } from "./TopNav.view";
|
||||
import type { TopNavProps, NavSize } from "./TopNav.types";
|
||||
import { TopView } from "./Top.view";
|
||||
import type { TopProps, NavSize } from "./Top.types";
|
||||
|
||||
type MenuClusterSize = "X Small" | "Small" | "Medium" | "Large" | "X Large";
|
||||
|
||||
/** Map responsive `NavSize` breakpoints to Figma menu item sizes (shared by nav links + login). */
|
||||
const NAV_SIZE_TO_MENU_ITEM_SIZE: Record<NavSize, MenuClusterSize> = {
|
||||
default: "Small",
|
||||
xsmall: "X Small",
|
||||
xsmallUseCases: "X Small",
|
||||
home: "X Small",
|
||||
homeMd: "Medium",
|
||||
homeUseCases: "Small",
|
||||
large: "Large",
|
||||
largeUseCases: "Large",
|
||||
homeXlarge: "X Large",
|
||||
xlarge: "X Large",
|
||||
};
|
||||
|
||||
export const avatarImages = [
|
||||
{ src: getAssetPath(ASSETS.AVATAR_1), alt: "Avatar 1" },
|
||||
@@ -20,7 +36,7 @@ export const avatarImages = [
|
||||
{ src: getAssetPath(ASSETS.AVATAR_3), alt: "Avatar 3" },
|
||||
];
|
||||
|
||||
const TopNavContainer = memo<TopNavProps>(
|
||||
const TopContainer = memo<TopProps>(
|
||||
({ folderTop = false, loggedIn = false, profile = false, logIn = true }) => {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
@@ -28,7 +44,7 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
const t = useTranslation("header");
|
||||
|
||||
/**
|
||||
* TopNav is hidden on `/create` routes by ConditionalNavigationClient, so
|
||||
* `Top` is hidden on `/create` routes by ConditionalNavigationClient, so
|
||||
* this button is always clicked from outside the wizard — there is no
|
||||
* mounted CreateFlowProvider to reset. Wiping the anonymous draft keys
|
||||
* here guarantees a fresh start; the provider that mounts on `/create`
|
||||
@@ -68,35 +84,15 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
];
|
||||
|
||||
const renderNavigationItems = (size: NavSize) => {
|
||||
// Map NavSize to Figma MenuBarItem sizes
|
||||
const sizeMap: Record<
|
||||
NavSize,
|
||||
"X Small" | "Small" | "Medium" | "Large" | "X Large"
|
||||
> = {
|
||||
default: "Small",
|
||||
xsmall: "X Small",
|
||||
xsmallUseCases: "X Small",
|
||||
home: "X Small",
|
||||
homeMd: "Medium",
|
||||
homeUseCases: "Small",
|
||||
large: "Large",
|
||||
largeUseCases: "Large",
|
||||
homeXlarge: "X Large",
|
||||
xlarge: "X Large",
|
||||
};
|
||||
|
||||
// Determine mode based on folderTop
|
||||
const mode = folderTop ? "inverse" : "default";
|
||||
|
||||
return navigationItems.map((item, index) => {
|
||||
// Map size to Figma size
|
||||
let itemSize = sizeMap[size] || "Small";
|
||||
const itemSize = NAV_SIZE_TO_MENU_ITEM_SIZE[size];
|
||||
|
||||
// Pass reducedPadding for "use cases" button (item with extraPadding: true)
|
||||
const isUseCases = item.extraPadding === true;
|
||||
|
||||
return (
|
||||
<MenuBarItem
|
||||
<MenuItem
|
||||
key={index}
|
||||
href={item.href}
|
||||
size={itemSize}
|
||||
@@ -109,7 +105,7 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
)}
|
||||
>
|
||||
{item.text}
|
||||
</MenuBarItem>
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -133,22 +129,7 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
};
|
||||
|
||||
const renderLoginButton = (size: NavSize) => {
|
||||
// Map NavSize to Figma MenuBarItem sizes
|
||||
const sizeMap: Record<
|
||||
NavSize,
|
||||
"X Small" | "Small" | "Medium" | "Large" | "X Large"
|
||||
> = {
|
||||
default: "Small",
|
||||
xsmall: "X Small",
|
||||
xsmallUseCases: "X Small",
|
||||
home: "X Small",
|
||||
homeMd: "Medium",
|
||||
homeUseCases: "Small",
|
||||
large: "Large",
|
||||
largeUseCases: "Large",
|
||||
homeXlarge: "X Large",
|
||||
xlarge: "X Large",
|
||||
};
|
||||
const itemSize = NAV_SIZE_TO_MENU_ITEM_SIZE[size];
|
||||
|
||||
// Determine mode based on folderTop and breakpoint size
|
||||
// folderTop: inverse mode (black text) for smallest breakpoints (xsmall/home)
|
||||
@@ -167,20 +148,20 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
|
||||
if (loggedIn) {
|
||||
return (
|
||||
<MenuBarItem
|
||||
<MenuItem
|
||||
href="/profile"
|
||||
size={sizeMap[size] || "Small"}
|
||||
size={itemSize}
|
||||
mode={mode}
|
||||
state={navSelected ? "selected" : "default"}
|
||||
ariaLabel={ariaLabel}
|
||||
>
|
||||
{label}
|
||||
</MenuBarItem>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuBarItem
|
||||
<MenuItem
|
||||
buttonOnClick={() =>
|
||||
openLogin({
|
||||
variant: "default",
|
||||
@@ -189,13 +170,13 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
})
|
||||
}
|
||||
href="/login"
|
||||
size={sizeMap[size] || "Small"}
|
||||
size={itemSize}
|
||||
mode={mode}
|
||||
state={navSelected ? "selected" : "default"}
|
||||
ariaLabel={ariaLabel}
|
||||
>
|
||||
{label}
|
||||
</MenuBarItem>
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -223,7 +204,7 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
};
|
||||
|
||||
return (
|
||||
<TopNavView
|
||||
<TopView
|
||||
folderTop={folderTop}
|
||||
loggedIn={loggedIn}
|
||||
profile={profile}
|
||||
@@ -238,6 +219,6 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
},
|
||||
);
|
||||
|
||||
TopNavContainer.displayName = "TopNav";
|
||||
TopContainer.displayName = "Top";
|
||||
|
||||
export default TopNavContainer;
|
||||
export default TopContainer;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import type React from "react";
|
||||
|
||||
export interface TopNavProps {
|
||||
export interface TopProps {
|
||||
className?: string;
|
||||
size?: "320-429" | "430-639" | "640-1023" | "1024-1440" | "1440+";
|
||||
loggedIn?: boolean;
|
||||
@@ -21,7 +21,7 @@ export type NavSize =
|
||||
| "homeXlarge"
|
||||
| "xlarge";
|
||||
|
||||
export interface TopNavViewProps {
|
||||
export interface TopViewProps {
|
||||
folderTop: boolean;
|
||||
loggedIn: boolean;
|
||||
profile: boolean;
|
||||
+42
-42
@@ -4,12 +4,12 @@ import { memo } from "react";
|
||||
import Script from "next/script";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { getAssetPath } from "../../../../lib/assetUtils";
|
||||
import MenuBar from "../MenuBar";
|
||||
import type { TopNavViewProps } from "./TopNav.types";
|
||||
import Menu from "../Menu";
|
||||
import type { TopViewProps } from "./Top.types";
|
||||
|
||||
import Logo from "../../asset/logo";
|
||||
import Logo from "../../asset/Logo";
|
||||
|
||||
function TopNavView({
|
||||
function TopView({
|
||||
folderTop,
|
||||
loggedIn: _loggedIn,
|
||||
profile: _profile,
|
||||
@@ -19,7 +19,7 @@ function TopNavView({
|
||||
renderNavigationItems,
|
||||
renderLoginButton,
|
||||
renderCreateRuleButton,
|
||||
}: TopNavViewProps) {
|
||||
}: TopViewProps) {
|
||||
const t = useTranslation(folderTop ? "homeHeader" : "header");
|
||||
|
||||
// Render folderTop variant (HomeHeader style)
|
||||
@@ -27,7 +27,7 @@ function TopNavView({
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
id="top-nav-schema"
|
||||
id="top-navigation-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
|
||||
/>
|
||||
@@ -50,12 +50,12 @@ function TopNavView({
|
||||
palette={folderTop ? "inverse" : "default"}
|
||||
/>
|
||||
|
||||
{/* XSmall menu bar - positioned next to logo */}
|
||||
{/* XSmall menu — positioned next to logo */}
|
||||
<div className="block sm:hidden -me-[2px]">
|
||||
<MenuBar size="X Small">
|
||||
<Menu size="X Small">
|
||||
{renderNavigationItems("xsmall")}
|
||||
{logIn && renderLoginButton("xsmall")}
|
||||
</MenuBar>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
{/* Decorative Union images for tab appearance */}
|
||||
@@ -84,31 +84,31 @@ function TopNavView({
|
||||
|
||||
{/* Navigation Links - Centered in header for SM and up */}
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 hidden sm:block">
|
||||
{/* 430-639px (sm: breakpoint): MenuBar X Small */}
|
||||
{/* 430-639px (sm: breakpoint): Menu X Small */}
|
||||
<div className="hidden sm:block md:hidden">
|
||||
<MenuBar size="X Small">
|
||||
<Menu size="X Small">
|
||||
{renderNavigationItems("xsmall")}
|
||||
{logIn && renderLoginButton("xsmall")}
|
||||
</MenuBar>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
{/* 640-1023px (md: breakpoint): MenuBar Small */}
|
||||
{/* 640-1023px (md: breakpoint): Menu Small */}
|
||||
<div className="hidden md:block lg:hidden">
|
||||
<MenuBar size="Small">
|
||||
<Menu size="Small">
|
||||
{renderNavigationItems("homeMd")}
|
||||
</MenuBar>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
{/* 1024-1440px (lg: breakpoint): MenuBar Large */}
|
||||
{/* 1024-1440px (lg: breakpoint): Menu Large */}
|
||||
<div className="hidden lg:block xl:hidden">
|
||||
<MenuBar size="Large">{renderNavigationItems("large")}</MenuBar>
|
||||
<Menu size="Large">{renderNavigationItems("large")}</Menu>
|
||||
</div>
|
||||
|
||||
{/* 1440px+ (xl: breakpoint): MenuBar X Large */}
|
||||
{/* 1440px+ (xl: breakpoint): Menu X Large */}
|
||||
<div className="hidden xl:block">
|
||||
<MenuBar size="X Large">
|
||||
<Menu size="X Large">
|
||||
{renderNavigationItems("homeXlarge")}
|
||||
</MenuBar>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -153,7 +153,7 @@ function TopNavView({
|
||||
* Standard marketing / app top nav.
|
||||
* Figma: "Navigation / Top" (Community-Rule-System, node 22078-808559) — horizontal
|
||||
* padding, logo ~200px left, menu cluster centered in the bar (`left-1/2` + translate),
|
||||
* log in + create rule on the right. Breakpoints and MenuBar sizes unchanged from prior map.
|
||||
* log in + create rule on the right. Breakpoints and Menu sizes unchanged from prior map.
|
||||
*/
|
||||
// Render standard variant (Header style)
|
||||
return (
|
||||
@@ -182,7 +182,7 @@ function TopNavView({
|
||||
>
|
||||
<div
|
||||
className="relative z-20 min-w-0 shrink-0 sm:w-[200px] sm:max-w-[200px] sm:shrink-0"
|
||||
data-topnav="logo"
|
||||
data-top="logo"
|
||||
>
|
||||
<Logo
|
||||
size={logoSize}
|
||||
@@ -194,51 +194,51 @@ function TopNavView({
|
||||
{/* XSmall: nav + login in flow (flex-1) — same as before */}
|
||||
<div
|
||||
className="flex min-w-0 flex-1 items-center justify-end sm:hidden"
|
||||
data-topnav="nav-xs-flow"
|
||||
data-top="nav-xs-flow"
|
||||
>
|
||||
<div className="block" data-testid="nav-xs">
|
||||
<MenuBar size="X Small">
|
||||
<Menu size="X Small">
|
||||
{renderNavigationItems("xsmall")}
|
||||
{logIn && renderLoginButton("xsmall")}
|
||||
</MenuBar>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* sm+ — Figma: nav cluster centered in bar (not between logo and actions) */}
|
||||
<div
|
||||
className="pointer-events-none hidden sm:absolute sm:left-1/2 sm:top-1/2 sm:z-10 sm:flex sm:-translate-x-1/2 sm:-translate-y-1/2 sm:items-center sm:justify-center"
|
||||
data-topnav="nav-center"
|
||||
data-top="nav-center"
|
||||
>
|
||||
<div
|
||||
className="pointer-events-auto hidden sm:flex md:hidden"
|
||||
data-testid="nav-sm"
|
||||
>
|
||||
<MenuBar size="X Small">
|
||||
<Menu size="X Small">
|
||||
{renderNavigationItems("xsmall")}
|
||||
{logIn && renderLoginButton("xsmall")}
|
||||
</MenuBar>
|
||||
</Menu>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-auto hidden md:flex lg:hidden"
|
||||
data-testid="nav-md"
|
||||
>
|
||||
<MenuBar size="X Small">
|
||||
<Menu size="X Small">
|
||||
{renderNavigationItems("xsmall")}
|
||||
</MenuBar>
|
||||
</Menu>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-auto hidden lg:flex xl:hidden"
|
||||
data-testid="nav-lg"
|
||||
>
|
||||
<MenuBar size="Large">{renderNavigationItems("large")}</MenuBar>
|
||||
<Menu size="Large">{renderNavigationItems("large")}</Menu>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-auto hidden xl:flex"
|
||||
data-testid="nav-xl"
|
||||
>
|
||||
<MenuBar size="X Large">
|
||||
<Menu size="X Large">
|
||||
{renderNavigationItems("xlarge")}
|
||||
</MenuBar>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -259,9 +259,9 @@ function TopNavView({
|
||||
{/* 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="Small">
|
||||
<Menu size="Small">
|
||||
{logIn && renderLoginButton("xsmall")}
|
||||
</MenuBar>
|
||||
</Menu>
|
||||
{renderCreateRuleButton("xsmall", "medium", "medium")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,9 +269,9 @@ function TopNavView({
|
||||
{/* 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">
|
||||
<Menu size="Large">
|
||||
{logIn && renderLoginButton("large")}
|
||||
</MenuBar>
|
||||
</Menu>
|
||||
{renderCreateRuleButton("large", "xlarge", "xlarge")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -279,9 +279,9 @@ function TopNavView({
|
||||
{/* XLarge breakpoint */}
|
||||
<div className="hidden xl:block" data-testid="auth-xl">
|
||||
<div className="flex items-center gap-[var(--spacing-measures-spacing-004)]">
|
||||
<MenuBar size="X Large">
|
||||
<Menu size="X Large">
|
||||
{logIn && renderLoginButton("xlarge")}
|
||||
</MenuBar>
|
||||
</Menu>
|
||||
{renderCreateRuleButton("xlarge", "xlarge", "xlarge")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -292,7 +292,7 @@ function TopNavView({
|
||||
);
|
||||
}
|
||||
|
||||
TopNavView.displayName = "TopNavView";
|
||||
TopView.displayName = "TopView";
|
||||
|
||||
export default memo(TopNavView);
|
||||
export { TopNavView };
|
||||
export default memo(TopView);
|
||||
export { TopView };
|
||||
+9
-9
@@ -2,17 +2,17 @@
|
||||
|
||||
import { memo, useCallback, useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import TopNav from "./TopNav.container";
|
||||
import type { TopNavProps } from "./TopNav.types";
|
||||
import Top from "./Top.container";
|
||||
import type { TopProps } from "./Top.types";
|
||||
import { fetchAuthSession } from "../../../../lib/create/api";
|
||||
|
||||
export type TopNavWithPathnameProps = Omit<TopNavProps, "folderTop"> & {
|
||||
export type TopWithPathnameProps = Omit<TopProps, "folderTop"> & {
|
||||
/** From Server Component (`getNavAuthSignedIn`); matches first HTML paint. */
|
||||
initialSignedIn?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* TopNav wrapper: `folderTop` from pathname; Log in vs Profile from session.
|
||||
* `Top` wrapper: `folderTop` from pathname; Log in vs Profile from session.
|
||||
*
|
||||
* **SSR:** Parent passes `initialSignedIn` from `getSessionUser()` so the hydrated
|
||||
* header matches the cookie (Next.js pattern for HttpOnly session UI).
|
||||
@@ -20,8 +20,8 @@ export type TopNavWithPathnameProps = Omit<TopNavProps, "folderTop"> & {
|
||||
* **Client:** Refetch on pathname change (magic-link redirect, stale layout after
|
||||
* `router.refresh()`), **popstate** / **pageshow** `persisted` (bfcache / back).
|
||||
*/
|
||||
const TopNavWithPathname = memo<TopNavWithPathnameProps>((props) => {
|
||||
const { initialSignedIn = false, ...topNavRest } = props;
|
||||
const TopWithPathname = memo<TopWithPathnameProps>((props) => {
|
||||
const { initialSignedIn = false, ...topRest } = props;
|
||||
const pathname = usePathname();
|
||||
const isHomePage = pathname === "/";
|
||||
const [loggedIn, setLoggedIn] = useState(initialSignedIn);
|
||||
@@ -69,9 +69,9 @@ const TopNavWithPathname = memo<TopNavWithPathnameProps>((props) => {
|
||||
return () => window.removeEventListener("popstate", onPopState);
|
||||
}, [syncSession]);
|
||||
|
||||
return <TopNav {...topNavRest} folderTop={isHomePage} loggedIn={loggedIn} />;
|
||||
return <Top {...topRest} folderTop={isHomePage} loggedIn={loggedIn} />;
|
||||
});
|
||||
|
||||
TopNavWithPathname.displayName = "TopNavWithPathname";
|
||||
TopWithPathname.displayName = "TopWithPathname";
|
||||
|
||||
export default TopNavWithPathname;
|
||||
export default TopWithPathname;
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default } from "./Top.container";
|
||||
export type { TopProps, NavSize } from "./Top.types";
|
||||
export { avatarImages } from "./Top.container";
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default } from "./TopNav.container";
|
||||
export type { TopNavProps, NavSize } from "./TopNav.types";
|
||||
export { avatarImages } from "./TopNav.container";
|
||||
Reference in New Issue
Block a user