import * as React from "react"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; // ─── Types ──────────────────────────────────────────────────────────────────── export type LoanWizardSection = { value: string; label: string; disabled?: boolean; }; export type LoanWizardApplicantOption = { id: string; label: string; }; export type LoanWizardShellProps = { // Header /** Broker logo / wordmark rendered in the top-left. */ logo?: React.ReactNode; onSaveAndLogOut?: () => void; /** Label for the primary header action. Defaults to "Login App". */ loginAppLabel?: string; onLoginApp?: () => void; // Hero /** Application headline. Defaults to WealthX copy. */ heroTitle?: string; heroSubtitle?: string; /** Broker info block rendered on the right of the hero. */ brokerSlot?: React.ReactNode; // Tab navigation sections: LoanWizardSection[]; activeSection: string; onSectionChange: (section: string) => void; // Applicant switcher (optional — shows a Main / Co-Applicant toggle) applicantOptions?: LoanWizardApplicantOption[]; activeApplicantId?: string; onApplicantChange?: (id: string) => void; // Content children?: React.ReactNode; // Bottom navigation onPrev?: () => void; onNext?: () => void; nextLabel?: string; /** Disable the bottom "Next" — e.g. the active section's step is incomplete. */ nextDisabled?: boolean; className?: string; }; export type { LoanWizardSection as LoanWizardSectionItem }; export type { LoanWizardApplicantOption as LoanWizardApplicant }; // ─── Component ──────────────────────────────────────────────────────────────── export function LoanWizardShell({ logo, onSaveAndLogOut, loginAppLabel = "Login App", onLoginApp, heroTitle = "Quick Online Application,\nThat Gets You Answers Fast!", heroSubtitle = "(This does not affect your credit score)", brokerSlot, sections, activeSection, onSectionChange, applicantOptions, activeApplicantId, onApplicantChange, children, onPrev, onNext, nextLabel = "Next", nextDisabled = false, className, }: LoanWizardShellProps) { const tabListRef = React.useRef(null); // Keep the active tab fully in view whenever it changes — e.g. stepping // through sections via Prev/Next can land on a tab that's scrolled out of // the overflow-x-auto tab bar on narrow viewports. Computed manually // (rather than scrollIntoView) so the active tab is always fully visible, // even the last tab where native "center"/"nearest" alignment can fall // short of the container's max scroll position. Queries the active // TabsTrigger via its data-active attribute rather than a ref, since the // shared TabsTrigger is a plain function component (no forwardRef) under // React 18. React.useEffect(() => { const container = tabListRef.current; const button = container?.querySelector( '[data-slot="tabs-trigger"][data-active]', ); if (!container || !button) return; const margin = 16; const buttonLeft = button.offsetLeft; const buttonRight = buttonLeft + button.offsetWidth; const visibleLeft = container.scrollLeft; const visibleRight = visibleLeft + container.clientWidth; let target = container.scrollLeft; if (buttonRight + margin > visibleRight) { target = buttonRight + margin - container.clientWidth; } else if (buttonLeft - margin < visibleLeft) { target = buttonLeft - margin; } target = Math.max( 0, Math.min(target, container.scrollWidth - container.clientWidth), ); container.scrollTo({ left: target, behavior: "auto" }); }, [activeSection]); return (
{/* ── Header ── */}
{logo}
{onSaveAndLogOut && ( )} {onLoginApp && ( )}
{/* ── Hero ── */}

{heroTitle.split("\n").map((line, i) => ( {line} {i < heroTitle.split("\n").length - 1 &&
}
))}

{heroSubtitle && (

{heroSubtitle}

)}
{brokerSlot &&
{brokerSlot}
}
{/* ── Tab navigation ── */} {/* pb-2 keeps the active-tab underline (rendered at bottom-[-5px]) from being clipped by this overflow-x-auto scroll container */}
{sections.map((s) => ( {s.label} ))}
{/* ── Content area ── */}
{/* Applicant switcher */} {applicantOptions && applicantOptions.length > 1 && ( {applicantOptions.map((opt) => ( {opt.label} ))} )} {/* sr-only h2 establishes heading hierarchy: h1 (hero) → h2 (section) → h3 (organisms) */}

{sections.find((s) => s.value === activeSection)?.label}

{/* Section content — constrained to ~628px matching design */}
{children}
{/* ── Bottom navigation ── */} {/* Sticky to the bottom of the viewport on mobile, where a section's content commonly runs taller than the screen — reverts to normal in-flow positioning at sm+ (unchanged from the original design). */} {(onPrev || onNext) && (
{onPrev ? ( ) : ( )} {onNext && ( )}
)}
); }