diff --git a/app/(marketing)/blog/page.tsx b/app/(marketing)/blog/page.tsx index 62c0ad8..c6f633a 100644 --- a/app/(marketing)/blog/page.tsx +++ b/app/(marketing)/blog/page.tsx @@ -1,5 +1,5 @@ import { getAllBlogPosts } from "../../../lib/content"; -import ContentThumbnailTemplate from "../../../components/content/ContentThumbnailTemplate"; +import ContentThumbnailTemplate from "../../components/content/ContentThumbnailTemplate"; import type { Metadata } from "next"; export const metadata: Metadata = { diff --git a/app/components/navigation/TopNav/TopNav.container.tsx b/app/components/navigation/TopNav/TopNav.container.tsx index 2c1e432..8f72ff9 100644 --- a/app/components/navigation/TopNav/TopNav.container.tsx +++ b/app/components/navigation/TopNav/TopNav.container.tsx @@ -1,7 +1,7 @@ "use client"; import { memo } from "react"; -import { usePathname } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { useTranslation } from "../../../contexts/MessagesContext"; import MenuBarItem from "../MenuBarItem"; import Button from "../../buttons/Button"; @@ -20,6 +20,7 @@ export const avatarImages = [ const TopNavContainer = memo( ({ folderTop = false, loggedIn = false, profile = false, logIn = true }) => { const pathname = usePathname(); + const router = useRouter(); const t = useTranslation("header"); // Schema markup for site navigation @@ -164,7 +165,7 @@ const TopNavContainer = memo( size={buttonSize} buttonType={buttonType} palette={palette} - href="/create/informational" + onClick={() => router.push("/create/informational")} ariaLabel={t("ariaLabels.createNewRule")} > {renderAvatarGroup(containerSize, avatarSize)} diff --git a/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.container.tsx b/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.container.tsx index 707aa78..d732394 100644 --- a/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.container.tsx +++ b/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.container.tsx @@ -20,9 +20,9 @@ const CreateFlowTopNavContainer = memo( }) => { const router = useRouter(); - const handleExit = () => { + const handleExit = (options?: { saveDraft?: boolean }) => { if (onExit) { - onExit(); + onExit(options); } else { // Default behavior: navigate to home router.push("/"); diff --git a/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.types.ts b/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.types.ts index c5a9af4..69f9afc 100644 --- a/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.types.ts +++ b/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.types.ts @@ -39,9 +39,10 @@ export interface CreateFlowTopNavProps { */ onEdit?: () => void; /** - * Callback when Exit/Save & Exit button is clicked + * Callback when Exit/Save & Exit button is clicked. + * When user is logged in, called with { saveDraft: true } to stub "Save & Exit". */ - onExit?: () => void; + onExit?: (options?: { saveDraft?: boolean }) => void; /** * Palette for nav buttons (e.g. "inverse" on completed page to match teal background) * @default "default" diff --git a/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.view.tsx b/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.view.tsx index 24f486e..fd8cbb2 100644 --- a/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.view.tsx +++ b/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.view.tsx @@ -89,7 +89,7 @@ export function CreateFlowTopNavView({ buttonType="outline" palette={buttonPalette} size="xsmall" - onClick={onExit} + onClick={() => onExit?.({ saveDraft: loggedIn })} 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]" > diff --git a/app/create/[step]/page.tsx b/app/create/[step]/page.tsx index 90f096a..94517c2 100644 --- a/app/create/[step]/page.tsx +++ b/app/create/[step]/page.tsx @@ -2,27 +2,12 @@ import { notFound } from "next/navigation"; import { use } from "react"; -import type { CreateFlowStep } from "../types"; +import { VALID_STEPS } from "../utils/flowSteps"; interface PageProps { params: Promise<{ step: string }>; } -/** - * Valid step IDs for the create rule flow - */ -const VALID_STEPS: CreateFlowStep[] = [ - "informational", - "text", - "select", - "upload", - "review", - "cards", - "right-rail", - "final-review", - "completed", -]; - /** * Dynamic route handler for create flow steps * @@ -33,7 +18,7 @@ export default function CreateFlowStepPage({ params }: PageProps) { const { step } = use(params); // Validate step exists - if (!VALID_STEPS.includes(step as CreateFlowStep)) { + if (!(VALID_STEPS as readonly string[]).includes(step)) { notFound(); } diff --git a/app/create/context/CreateFlowContext.tsx b/app/create/context/CreateFlowContext.tsx index bf9431d..94ce8d1 100644 --- a/app/create/context/CreateFlowContext.tsx +++ b/app/create/context/CreateFlowContext.tsx @@ -1,6 +1,13 @@ "use client"; -import { createContext, useContext, useState, type ReactNode } from "react"; +import { + createContext, + useCallback, + useContext, + useEffect, + useState, + type ReactNode, +} from "react"; import type { CreateFlowState, CreateFlowContextValue, @@ -9,6 +16,39 @@ import type { const CreateFlowContext = createContext(null); +const STORAGE_KEY = "create-flow-state"; +const DRAFT_STORAGE_KEY = "create-flow-draft"; + +function readStateFromStorage(key: string): CreateFlowState { + if (typeof window === "undefined") return {}; + try { + const raw = window.localStorage.getItem(key); + if (!raw) return {}; + const parsed = JSON.parse(raw) as CreateFlowState; + return typeof parsed === "object" && parsed !== null ? parsed : {}; + } catch { + return {}; + } +} + +function writeStateToStorage(key: string, value: CreateFlowState): void { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem(key, JSON.stringify(value)); + } catch { + // Ignore storage errors (e.g. quota, private mode) + } +} + +function removeFromStorage(key: string): void { + if (typeof window === "undefined") return; + try { + window.localStorage.removeItem(key); + } catch { + // Ignore + } +} + interface CreateFlowProviderProps { children: ReactNode; initialStep?: CreateFlowStep | null; @@ -17,27 +57,39 @@ interface CreateFlowProviderProps { /** * 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. + * Manages flow state with optional localStorage persistence and draft support. */ export function CreateFlowProvider({ children, initialStep = null, }: CreateFlowProviderProps) { - const [state, setState] = useState({}); + const [state, setState] = useState(() => + readStateFromStorage(STORAGE_KEY), + ); const [currentStep] = useState(initialStep); - const updateState = (updates: Partial) => { + useEffect(() => { + writeStateToStorage(STORAGE_KEY, state); + }, [state]); + + const updateState = useCallback((updates: Partial) => { setState((prevState) => ({ ...prevState, ...updates, })); - }; + }, []); + + const clearState = useCallback(() => { + setState({}); + removeFromStorage(STORAGE_KEY); + removeFromStorage(DRAFT_STORAGE_KEY); + }, []); const contextValue: CreateFlowContextValue = { state, currentStep, updateState, + clearState, }; return ( @@ -47,6 +99,16 @@ export function CreateFlowProvider({ ); } +/** Save current state as draft (e.g. on "Save & Exit"). Stub for CR-57. */ +export function saveCreateFlowDraft(state: CreateFlowState): void { + writeStateToStorage(DRAFT_STORAGE_KEY, state); +} + +/** Load draft state if present. Caller can merge into initial state when entering flow. */ +export function loadCreateFlowDraft(): CreateFlowState { + return readStateFromStorage(DRAFT_STORAGE_KEY); +} + /** * Hook to access Create Flow context * diff --git a/app/create/hooks/useCreateFlowNavigation.ts b/app/create/hooks/useCreateFlowNavigation.ts new file mode 100644 index 0000000..1fbed44 --- /dev/null +++ b/app/create/hooks/useCreateFlowNavigation.ts @@ -0,0 +1,81 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import { useCallback } from "react"; +import type { CreateFlowStep } from "../types"; +import { getNextStep, getPreviousStep, isValidStep } from "../utils/flowSteps"; + +/** + * Options passed to navigation handlers (e.g. for blur before navigate) + */ +const blurActiveElement = (): void => { + if ( + typeof document !== "undefined" && + document.activeElement instanceof HTMLElement + ) { + document.activeElement.blur(); + } +}; + +/** + * Hook for Create Rule Flow navigation. + * + * Must be used within the create flow (pathname like /create/[step]). + * Uses the current step from the URL and provides type-safe navigation. + */ +export function useCreateFlowNavigation(): { + currentStep: CreateFlowStep | null; + goToNextStep: () => void; + goToPreviousStep: () => void; + goToStep: (_step: CreateFlowStep) => void; + canGoNext: () => boolean; + canGoBack: () => boolean; + nextStep: CreateFlowStep | null; + previousStep: CreateFlowStep | null; +} { + const pathname = usePathname(); + const router = useRouter(); + + const currentStep = (pathname?.split("/").pop() ?? + null) as CreateFlowStep | null; + const validStep = isValidStep(currentStep) ? currentStep : null; + + const nextStep = getNextStep(validStep); + const previousStep = getPreviousStep(validStep); + + const goToNextStep = useCallback(() => { + blurActiveElement(); + if (nextStep) { + router.push(`/create/${nextStep}`); + } + }, [router, nextStep]); + + const goToPreviousStep = useCallback(() => { + blurActiveElement(); + if (previousStep) { + router.push(`/create/${previousStep}`); + } + }, [router, previousStep]); + + const goToStep = useCallback( + (step: CreateFlowStep) => { + blurActiveElement(); + router.push(`/create/${step}`); + }, + [router], + ); + + const canGoNext = useCallback(() => nextStep !== null, [nextStep]); + const canGoBack = useCallback(() => previousStep !== null, [previousStep]); + + return { + currentStep: validStep, + goToNextStep, + goToPreviousStep, + goToStep, + canGoNext, + canGoBack, + nextStep, + previousStep, + }; +} diff --git a/app/create/informational/page.tsx b/app/create/informational/page.tsx index a0c15b8..280a41a 100644 --- a/app/create/informational/page.tsx +++ b/app/create/informational/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState, useEffect } from "react"; import { useMediaQuery } from "../../hooks/useMediaQuery"; import HeaderLockup from "../../components/type/HeaderLockup"; import NumberedList from "../../components/type/NumberedList"; @@ -11,8 +12,17 @@ import NumberedList from "../../components/type/NumberedList"; * Responsive sizing: uses L/M for HeaderLockup and M/S for NumberedList based on 640px breakpoint. */ export default function InformationalPage() { + const [isMounted, setIsMounted] = useState(false); const isMdOrLarger = useMediaQuery("(min-width: 640px)"); + // Avoid flash: only use breakpoint after mount so SSR and first paint use same layout (desktop). + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer layout breakpoint until after mount to prevent flash + setIsMounted(true); + }, []); + + const effectiveMdOrLarger = !isMounted || isMdOrLarger; + const items = [ { title: "Tell us about your organization", @@ -39,11 +49,11 @@ export default function InformationalPage() { title="How CommunityRule helps groups like yours" description="This flow will give you recommendations to improve your community and help you put together a proposal for your group to consider. Alternatively, there is a workshop that your group can use to go through the process it together." justification="left" - size={isMdOrLarger ? "L" : "M"} + size={effectiveMdOrLarger ? "L" : "M"} /> {/* NumberedList: M size at 640px+, S size below 640px */} - + ); diff --git a/app/create/layout.tsx b/app/create/layout.tsx index 745e7af..25a65f8 100644 --- a/app/create/layout.tsx +++ b/app/create/layout.tsx @@ -1,12 +1,16 @@ "use client"; import type { ReactNode } from "react"; -import { usePathname, useRouter } from "next/navigation"; -import { CreateFlowProvider } from "./context/CreateFlowContext"; +import { useRouter } from "next/navigation"; +import { + CreateFlowProvider, + useCreateFlow, + saveCreateFlowDraft, +} from "./context/CreateFlowContext"; +import { useCreateFlowNavigation } from "./hooks/useCreateFlowNavigation"; import CreateFlowTopNav from "../components/utility/CreateFlowTopNav"; import CreateFlowFooter from "../components/utility/CreateFlowFooter"; import Button from "../components/buttons/Button"; -import type { CreateFlowStep } from "./types"; /** * Layout for the Create Rule Flow @@ -16,77 +20,38 @@ import type { CreateFlowStep } from "./types"; * Includes the create flow-specific TopNav and Footer components. */ function CreateFlowLayoutContent({ children }: { children: ReactNode }) { - const pathname = usePathname(); const router = useRouter(); + const { + currentStep, + nextStep, + previousStep, + goToNextStep, + goToPreviousStep, + } = useCreateFlowNavigation(); + const { state, clearState } = useCreateFlow(); - // Extract current step from pathname - const currentStep = pathname?.split("/").pop() as CreateFlowStep | undefined; - - // Define step order - const stepOrder: CreateFlowStep[] = [ - "informational", - "text", - "select", - "upload", - "review", - "cards", - "right-rail", - "final-review", - "completed", - ]; - - // Get next step - const getNextStep = (): CreateFlowStep | null => { - if (!currentStep) return null; - const currentIndex = stepOrder.indexOf(currentStep); - if (currentIndex === -1 || currentIndex === stepOrder.length - 1) { - return null; + const handleExit = (options?: { saveDraft?: boolean }) => { + const saveDraft = options?.saveDraft ?? false; + if (!saveDraft && typeof window !== "undefined") { + const confirmed = window.confirm( + "Leave create flow? Your progress will be lost.", + ); + if (!confirmed) return; } - return stepOrder[currentIndex + 1]; - }; - - // Get previous step - const getPreviousStep = (): CreateFlowStep | null => { - if (!currentStep) return null; - const currentIndex = stepOrder.indexOf(currentStep); - if (currentIndex === -1 || currentIndex === 0) { - return null; - } - return stepOrder[currentIndex - 1]; - }; - - const nextStep = getNextStep(); - const previousStep = getPreviousStep(); - - const handleNext = () => { - if ( - typeof document !== "undefined" && - document.activeElement instanceof HTMLElement - ) { - document.activeElement.blur(); - } - if (nextStep) { - router.push(`/create/${nextStep}`); - } - }; - - const handleBack = () => { - if ( - typeof document !== "undefined" && - document.activeElement instanceof HTMLElement - ) { - document.activeElement.blur(); - } - if (previousStep) { - router.push(`/create/${previousStep}`); + if (saveDraft) { + saveCreateFlowDraft(state); } + clearState(); + router.push("/"); }; const isCompletedStep = currentStep === "completed"; + const isRightRailStep = currentStep === "right-rail"; + const useFullHeightMain = isCompletedStep || isRightRailStep; return (
router.push("/create/final-review") : undefined } + onExit={handleExit} buttonPalette={isCompletedStep ? "inverse" : undefined} className={ isCompletedStep ? "!bg-[var(--color-teal-teal50,#c9fef9)]" : undefined } />
{children}
@@ -117,7 +83,7 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) { 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={handleNext} + onClick={goToNextStep} > {currentStep === "final-review" ? "Finalize CommunityRule" @@ -125,7 +91,7 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) { ) : null } - onBackClick={previousStep ? handleBack : undefined} + onBackClick={previousStep ? goToPreviousStep : undefined} /> )}
diff --git a/app/create/right-rail/page.tsx b/app/create/right-rail/page.tsx index d99b8e3..548b372 100644 --- a/app/create/right-rail/page.tsx +++ b/app/create/right-rail/page.tsx @@ -115,35 +115,41 @@ export default function RightRailPage() { if (showDesktopLayout) { return ( -
-
-
- -
-
- +
+
+
+ {/* Left column: sidebar stays put, does not scroll */} +
+ +
+ {/* Right column: card stack — this column scrolls independently */} +
+
+ +
+
@@ -151,8 +157,8 @@ export default function RightRailPage() { } return ( -
-
+
+
{ + // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer layout breakpoint until after mount to prevent flash + setIsMounted(true); + }, []); + + const effectiveMdOrLarger = !isMounted || isMdOrLarger; + // Sample options for MultiSelect components const [communitySizeOptions, setCommunitySizeOptions] = useState([ { id: "1", label: "1 member", state: "Unselected" as const }, @@ -81,7 +90,7 @@ export default function SelectPage() { return (
- {isMdOrLarger ? ( + {effectiveMdOrLarger ? ( // Two-column layout for 640px+
{/* Left column: HeaderLockup */} diff --git a/app/create/text/page.tsx b/app/create/text/page.tsx index 103af4b..462566b 100644 --- a/app/create/text/page.tsx +++ b/app/create/text/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useMediaQuery } from "../../hooks/useMediaQuery"; import HeaderLockup from "../../components/type/HeaderLockup"; import TextInput from "../../components/controls/TextInput"; @@ -12,9 +12,18 @@ import TextInput from "../../components/controls/TextInput"; * Responsive sizing: uses L/M for HeaderLockup and medium/small for TextInput based on 640px breakpoint. */ export default function TextPage() { + const [isMounted, setIsMounted] = useState(false); const isMdOrLarger = useMediaQuery("(min-width: 640px)"); const [value, setValue] = useState(""); + // Avoid flash: only use breakpoint after mount so SSR and first paint use same layout (desktop). + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer layout breakpoint until after mount to prevent flash + setIsMounted(true); + }, []); + + const effectiveMdOrLarger = !isMounted || isMdOrLarger; + const maxLength = 48; const characterCount = value.length; @@ -26,7 +35,7 @@ export default function TextPage() { title="What is your community called?" description="This will be the name of your community" justification="left" - size={isMdOrLarger ? "L" : "M"} + size={effectiveMdOrLarger ? "L" : "M"} /> {/* TextInput: medium size at 640px+, small size below 640px */} @@ -35,7 +44,7 @@ export default function TextPage() { placeholder="Enter your community name" value={value} onChange={(e) => setValue(e.target.value)} - inputSize={isMdOrLarger ? "medium" : "small"} + inputSize={effectiveMdOrLarger ? "medium" : "small"} formHeader={false} textHint={`${characterCount}/${maxLength}`} maxLength={maxLength} diff --git a/app/create/types.ts b/app/create/types.ts index b53f1c6..32b8204 100644 --- a/app/create/types.ts +++ b/app/create/types.ts @@ -36,7 +36,8 @@ export interface CreateFlowContextValue { state: CreateFlowState; currentStep: CreateFlowStep | null; updateState: (_updates: Partial) => void; - // Navigation handlers will be added in CR-56 + /** Clear all flow state (e.g. on exit). Also clears persisted draft. */ + clearState: () => void; } /** diff --git a/app/create/upload/page.tsx b/app/create/upload/page.tsx index 24afa68..b425569 100644 --- a/app/create/upload/page.tsx +++ b/app/create/upload/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState, useEffect } from "react"; import { useMediaQuery } from "../../hooks/useMediaQuery"; import HeaderLockup from "../../components/type/HeaderLockup"; import Upload from "../../components/controls/Upload"; @@ -12,8 +13,17 @@ import Upload from "../../components/controls/Upload"; * Responsive sizing: uses L/M for HeaderLockup based on 640px breakpoint. */ export default function UploadPage() { + const [isMounted, setIsMounted] = useState(false); const isMdOrLarger = useMediaQuery("(min-width: 640px)"); + // Avoid flash: only use breakpoint after mount so SSR and first paint use same layout (desktop). + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer layout breakpoint until after mount to prevent flash + setIsMounted(true); + }, []); + + const effectiveMdOrLarger = !isMounted || isMdOrLarger; + const handleUploadClick = () => { // TODO: Handle upload button click (e.g. open file picker) }; @@ -25,8 +35,8 @@ export default function UploadPage() { {/* Upload component: no label in create flow, max width 474px */} diff --git a/app/create/utils/flowSteps.ts b/app/create/utils/flowSteps.ts new file mode 100644 index 0000000..3b5138b --- /dev/null +++ b/app/create/utils/flowSteps.ts @@ -0,0 +1,76 @@ +/** + * Step definitions and helpers for the Create Rule Flow + * + * Single source of truth for step order and navigation helpers. + */ + +import type { CreateFlowStep } from "../types"; + +/** + * Ordered list of steps in the create rule flow + */ +export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [ + "informational", + "text", + "select", + "upload", + "review", + "cards", + "right-rail", + "final-review", + "completed", +] as const; + +/** + * Valid step IDs for the create flow (for validation) + */ +export const VALID_STEPS: readonly CreateFlowStep[] = FLOW_STEP_ORDER; + +/** + * First step in the flow (entry point) + */ +export const FIRST_STEP: CreateFlowStep = FLOW_STEP_ORDER[0]; + +/** + * Returns the next step in the flow, or null if current is last/invalid + */ +export function getNextStep( + currentStep: CreateFlowStep | null | undefined, +): CreateFlowStep | null { + if (!currentStep) return null; + const index = FLOW_STEP_ORDER.indexOf(currentStep); + if (index === -1 || index === FLOW_STEP_ORDER.length - 1) return null; + return FLOW_STEP_ORDER[index + 1] as CreateFlowStep; +} + +/** + * Returns the previous step in the flow, or null if current is first/invalid + */ +export function getPreviousStep( + currentStep: CreateFlowStep | null | undefined, +): CreateFlowStep | null { + if (!currentStep) return null; + const index = FLOW_STEP_ORDER.indexOf(currentStep); + if (index <= 0) return null; + return FLOW_STEP_ORDER[index - 1] as CreateFlowStep; +} + +/** + * Returns the index of the step (0-based), or -1 if invalid + */ +export function getStepIndex(step: CreateFlowStep | null | undefined): number { + if (!step) return -1; + return FLOW_STEP_ORDER.indexOf(step); +} + +/** + * Whether the given string is a valid create flow step + */ +export function isValidStep( + step: string | null | undefined, +): step is CreateFlowStep { + return ( + typeof step === "string" && + (VALID_STEPS as readonly string[]).includes(step) + ); +} diff --git a/tests/components/TopNav.test.tsx b/tests/components/TopNav.test.tsx index beaab5c..abef64d 100644 --- a/tests/components/TopNav.test.tsx +++ b/tests/components/TopNav.test.tsx @@ -1,7 +1,21 @@ import React from "react"; +import { vi } from "vitest"; import TopNav from "../../app/components/navigation/TopNav"; import { componentTestSuite } from "../utils/componentTestSuite"; +// Mock next/navigation (TopNav uses useRouter for Create Rule button and usePathname for nav state) +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + push: vi.fn(), + replace: vi.fn(), + prefetch: vi.fn(), + back: vi.fn(), + forward: vi.fn(), + refresh: vi.fn(), + }), + usePathname: () => "/", +})); + type TopNavProps = React.ComponentProps; // Test folderTop=false variant (standard header)