Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions ONBOARDING_FLOW_HOVER_STATES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Onboarding Flow Hover States

This document captures the hover-state refinements applied to the onboarding flow so the registration experience better matches the global Drips Wave theme.

## Updated surfaces

- Registration form inputs now lift slightly on hover, brighten to white, and use Pluto border and shadow accents.
- The primary onboarding CTA now uses Pluto theme hover, focus, and active feedback instead of a flat black-only treatment.
- Secondary onboarding links now transition to Pluto accent tones for a more consistent navigation affordance.
- The onboarding progress tracker now uses Pluto-tinted container, step, and badge hover states while preserving completion and error semantics.

## Theme alignment

- Reused the existing `pluto` Tailwind color scale already defined in [frontend/tailwind.config.js](/Users/marvellous/Desktop/Stellar_Payment_API/frontend/tailwind.config.js).
- Kept the existing monochrome foundation, then layered Pluto hover accents so the change feels consistent with the current Drips Wave visual language.
- Preserved keyboard focus visibility and active-state feedback for accessible non-mouse interaction.

## Verification

- Added Playwright coverage in [frontend/tests/e2e/onboarding-hover.spec.ts](/Users/marvellous/Desktop/Stellar_Payment_API/frontend/tests/e2e/onboarding-hover.spec.ts).
- Verified the new hover behavior against the register onboarding flow on both desktop and mobile Playwright projects.
48 changes: 28 additions & 20 deletions frontend/src/app/(public)/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,36 @@ import GuestGuard from "@/components/GuestGuard";
export default function RegisterPage() {
return (
<GuestGuard>
<main className="mx-auto flex min-h-screen max-w-lg flex-col justify-center gap-10 px-6 py-14 md:py-20 bg-white">
<header className="flex flex-col gap-6 text-center">
<p className="font-bold text-xs uppercase tracking-[0.4em] text-[#6B6B6B] motion-safe:animate-pulse">Onboarding</p>
<h1 className="text-4xl md:text-5xl font-bold text-[#0A0A0A] tracking-tight font-serif uppercase">Join PLUTO</h1>
<p className="text-sm font-medium text-[#6B6B6B] leading-relaxed">
Create your merchant profile to start accepting modern payments and managing assets on the PLUTO infrastructure.
</p>
</header>
<main className="mx-auto flex min-h-screen max-w-lg flex-col justify-center gap-10 bg-white px-6 py-14 md:py-20">
<header className="flex flex-col gap-6 text-center">
<p className="font-bold text-xs uppercase tracking-[0.4em] text-[#6B6B6B] motion-safe:animate-pulse">
Onboarding
</p>
<h1 className="font-serif text-4xl font-bold uppercase tracking-tight text-[#0A0A0A] md:text-5xl">
Join PLUTO
</h1>
<p className="text-sm font-medium leading-relaxed text-[#6B6B6B]">
Create your merchant profile to start accepting modern payments and
managing assets on the PLUTO infrastructure.
</p>
</header>

<div className="relative overflow-hidden">
<RegistrationForm />
</div>
<div className="relative overflow-hidden">
<RegistrationForm />
</div>

<footer className="text-center">
<p className="text-xs font-bold uppercase tracking-widest text-[#6B6B6B]">
Already have an account?{" "}
<Link href="/login" className="text-[#0A0A0A] underline underline-offset-8 font-bold hover:text-black">
Log in here
</Link>
</p>
</footer>
</main>
<footer className="text-center">
<p className="text-xs font-bold uppercase tracking-widest text-[#6B6B6B]">
Already have an account?{" "}
<Link
href="/login"
className="font-bold text-[#0A0A0A] underline underline-offset-8 decoration-[#B8D4E8] transition-colors duration-200 hover:text-pluto-600 focus-visible:text-pluto-700"
>
Log in here
</Link>
</p>
</footer>
</main>
</GuestGuard>
);
}
59 changes: 29 additions & 30 deletions frontend/src/components/OnboardingProgressTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,29 +193,29 @@ export const OnboardingProgressTracker: React.FC<

{/* Container */}
<div
className={`rounded-lg border border-gray-200 bg-white p-6 shadow-sm ${
className={`rounded-[2rem] border border-pluto-100 bg-[linear-gradient(180deg,rgba(255,255,255,0.98),rgba(240,246,251,0.92))] p-6 shadow-[0_20px_50px_rgba(13,27,46,0.08)] ${
compact ? "p-4" : ""
}`}
>
{/* Header */}
<div className="mb-6">
<div className="flex items-baseline justify-between">
<h2 className="text-lg font-semibold text-gray-900">
<h2 className="text-lg font-semibold text-pluto-900">
{t("onboarding.title") || "Onboarding Progress"}
</h2>
<span className="text-sm font-medium text-gray-600">
<span className="text-sm font-medium text-pluto-700">
{progressPercentage}%
</span>
</div>
<p className="mt-1 text-sm text-gray-600">
<p className="mt-1 text-sm text-[#6B6B6B]">
{t("onboarding.subtitle") ||
"Complete all required steps to finish setup"}
</p>

{/* Overall progress bar */}
<div className="mt-4 h-2 overflow-hidden rounded-full bg-gray-200">
<div className="mt-4 h-2 overflow-hidden rounded-full bg-pluto-100">
<motion.div
className="h-full bg-gradient-to-r from-blue-500 to-blue-600"
className="h-full bg-gradient-to-r from-pluto-500 via-pluto-600 to-pluto-700"
variants={progressBarVariants}
initial="hidden"
animate="visible"
Expand All @@ -230,11 +230,11 @@ export const OnboardingProgressTracker: React.FC<
</div>

{/* Status text */}
<p className="mt-2 text-xs text-gray-500">
<p className="mt-2 text-xs text-[#6B6B6B]">
{sortedSteps.filter((s) => s.completed).length} of{" "}
{sortedSteps.length} steps completed
{isOnboardingComplete && (
<span className="ml-2 inline-flex items-center gap-1 text-green-600 font-medium">
<span className="ml-2 inline-flex items-center gap-1 font-medium text-pluto-700">
<svg
className="h-3 w-3"
fill="currentColor"
Expand Down Expand Up @@ -266,30 +266,29 @@ export const OnboardingProgressTracker: React.FC<
<AnimatePresence mode="popLayout">
{sortedSteps.map((step, index) => {
const isCurrentStep = currentStep === step.id;
const isPastStep = sortedSteps.findIndex((s) => s.id === currentStep) > index;

return (
<motion.div
key={step.id}
role="listitem"
variants={stepVariants}
className={`flex gap-4 ${
className={`group relative rounded-3xl border border-transparent px-3 py-3 transition-colors duration-200 hover:border-pluto-100 hover:bg-white/90 ${
orientation === "horizontal"
? "flex-1 flex-col"
: "flex-row"
? "flex flex-1 flex-col"
: "flex flex-row gap-4"
}`}
>
{/* Step indicator */}
<button
onClick={() => handleStepClick(step.id)}
className={`relative flex-shrink-0 ${
compact ? "h-8 w-8" : "h-10 w-10"
} rounded-full border-2 font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
} rounded-full border-2 font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-pluto-400 focus:ring-offset-2 ${
step.completed
? "border-green-500 bg-green-50"
? "border-pluto-500 bg-pluto-100 text-pluto-800 shadow-[0_10px_24px_rgba(74,111,165,0.14)]"
: isCurrentStep
? "border-blue-500 bg-blue-50"
: "border-gray-300 bg-gray-50 hover:border-gray-400"
? "border-pluto-600 bg-pluto-50 text-pluto-700 shadow-[0_12px_28px_rgba(74,111,165,0.12)]"
: "border-pluto-200 bg-white text-pluto-700 group-hover:border-pluto-400 group-hover:bg-pluto-50 group-hover:text-pluto-800 group-hover:shadow-[0_10px_24px_rgba(13,27,46,0.08)]"
}`}
aria-label={`Step ${showStepNumbers ? index + 1 : ""}: ${step.title}${
step.completed ? ". Completed" : ""
Expand All @@ -308,7 +307,7 @@ export const OnboardingProgressTracker: React.FC<
animate="visible"
>
<svg
className="h-5 w-5 text-green-600"
className="h-5 w-5 text-pluto-700"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
Expand All @@ -325,8 +324,8 @@ export const OnboardingProgressTracker: React.FC<
key="number"
className={`text-${compact ? "sm" : "base"} ${
isCurrentStep
? "text-blue-600"
: "text-gray-600"
? "text-pluto-700"
: "text-pluto-600"
}`}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
Expand All @@ -345,8 +344,8 @@ export const OnboardingProgressTracker: React.FC<
transition={{ delay: 0.1 }}
>
<h3
className={`font-medium text-gray-900 ${
step.completed ? "text-green-700 line-through" : ""
className={`font-medium text-pluto-900 transition-colors duration-200 group-hover:text-pluto-800 ${
step.completed ? "text-pluto-700 line-through" : ""
} ${compact ? "text-sm" : "text-base"}`}
>
{step.title}
Expand All @@ -360,7 +359,7 @@ export const OnboardingProgressTracker: React.FC<
</span>
)}
</h3>
<p className={`text-gray-600 ${compact ? "text-xs" : "text-sm"}`}>
<p className={`text-[#6B6B6B] transition-colors duration-200 group-hover:text-pluto-700 ${compact ? "text-xs" : "text-sm"}`}>
{step.description}
</p>

Expand All @@ -369,10 +368,10 @@ export const OnboardingProgressTracker: React.FC<
<motion.span
className={`inline-flex text-xs font-semibold rounded-full px-2 py-1 ${
step.completed
? "bg-green-100 text-green-800"
? "bg-pluto-100 text-pluto-800"
: isCurrentStep
? "bg-blue-100 text-blue-800"
: "bg-gray-100 text-gray-800"
? "bg-pluto-200 text-pluto-800"
: "bg-pluto-50 text-pluto-700 group-hover:bg-pluto-100"
}`}
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
Expand All @@ -390,7 +389,7 @@ export const OnboardingProgressTracker: React.FC<
{/* Connector line (vertical orientation only) */}
{orientation === "vertical" &&
index < sortedSteps.length - 1 && (
<div className="absolute left-5 top-10 h-3 w-0.5 bg-gray-200" />
<div className="absolute left-8 top-[calc(100%_-_0.5rem)] h-3 w-0.5 bg-pluto-200" />
)}
</motion.div>
);
Expand All @@ -402,7 +401,7 @@ export const OnboardingProgressTracker: React.FC<
<AnimatePresence>
{isOnboardingComplete && sortedSteps.length > 0 && (
<motion.div
className="mt-6 rounded-lg bg-green-50 p-4 border border-green-200"
className="mt-6 rounded-2xl border border-pluto-200 bg-pluto-50 p-4"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
Expand All @@ -411,7 +410,7 @@ export const OnboardingProgressTracker: React.FC<
>
<div className="flex items-start gap-3">
<motion.svg
className="h-5 w-5 text-green-600 flex-shrink-0 mt-0.5"
className="mt-0.5 h-5 w-5 flex-shrink-0 text-pluto-600"
fill="currentColor"
viewBox="0 0 20 20"
animate={{ scale: [1, 1.2, 1] }}
Expand All @@ -424,10 +423,10 @@ export const OnboardingProgressTracker: React.FC<
/>
</motion.svg>
<div>
<h4 className="font-semibold text-green-900">
<h4 className="font-semibold text-pluto-900">
{t("onboarding.successTitle") || "Onboarding Complete!"}
</h4>
<p className="text-sm text-green-700 mt-1">
<p className="mt-1 text-sm text-pluto-700">
{t("onboarding.successMessage") ||
"You have successfully completed all required onboarding steps."}
</p>
Expand Down
42 changes: 26 additions & 16 deletions frontend/src/components/RegistrationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import { Spinner } from "./ui/Spinner";

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const BUSINESS_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9\s&'.,-]{1,79}$/;
const FIELD_BASE_CLASSES =
"rounded-2xl border bg-[#F9F9F9] p-4 text-sm font-bold text-[#0A0A0A] placeholder-[#A0A0A0] transition-all duration-200 focus:bg-white focus:outline-none motion-safe:hover:-translate-y-0.5";
const FIELD_THEME_CLASSES =
"border-[#E8E8E8] hover:border-pluto-300 hover:bg-white hover:shadow-[0_14px_30px_rgba(13,27,46,0.06)] focus:border-pluto-500 focus:shadow-[0_0_0_1px_rgba(74,111,165,0.18),0_18px_36px_rgba(13,27,46,0.08)]";
const FIELD_ERROR_CLASSES =
"border-red-500/50 hover:border-red-400 focus:border-red-500 focus:shadow-[0_0_0_1px_rgba(239,68,68,0.16),0_16px_32px_rgba(239,68,68,0.08)]";

// Memoized password strength indicator to prevent unnecessary re-renders
const PasswordStrengthIndicator = React.memo(
Expand Down Expand Up @@ -225,7 +231,7 @@ const RegistrationForm = React.memo(function RegistrationForm() {

<a
href="/dashboard"
className="text-center text-xs font-bold uppercase tracking-widest text-[#6B6B6B] transition-colors underline underline-offset-8 hover:text-[#0A0A0A]"
className="text-center text-xs font-bold uppercase tracking-widest text-[#6B6B6B] underline underline-offset-8 decoration-[#B8D4E8] transition-colors duration-200 hover:text-pluto-600 focus-visible:text-pluto-700"
>
Enter Dashboard
</a>
Expand Down Expand Up @@ -271,10 +277,10 @@ const RegistrationForm = React.memo(function RegistrationForm() {
aria-describedby={
businessNameError ? "business-name-error" : undefined
}
className={`rounded-2xl border bg-[#F9F9F9] p-4 text-sm font-bold text-[#0A0A0A] placeholder-[#A0A0A0] transition-all focus:bg-white focus:outline-none ${
className={`${FIELD_BASE_CLASSES} ${
businessNameError
? "border-red-500/50 focus:border-red-500"
: "border-[#E8E8E8] focus:border-[#0A0A0A]"
? FIELD_ERROR_CLASSES
: FIELD_THEME_CLASSES
}`}
placeholder="PLUTO Merchant"
/>
Expand Down Expand Up @@ -308,10 +314,10 @@ const RegistrationForm = React.memo(function RegistrationForm() {
}}
aria-invalid={Boolean(emailError)}
aria-describedby={emailError ? "primary-email-error" : undefined}
className={`rounded-2xl border bg-[#F9F9F9] p-4 text-sm font-bold text-[#0A0A0A] placeholder-[#A0A0A0] transition-all focus:bg-white focus:outline-none ${
className={`${FIELD_BASE_CLASSES} ${
emailError
? "border-red-500/50 focus:border-red-500"
: "border-[#E8E8E8] focus:border-[#0A0A0A]"
? FIELD_ERROR_CLASSES
: FIELD_THEME_CLASSES
}`}
placeholder="owner@business.com"
/>
Expand Down Expand Up @@ -344,10 +350,10 @@ const RegistrationForm = React.memo(function RegistrationForm() {
}}
aria-invalid={Boolean(passwordError)}
aria-describedby={passwordError ? "password-error" : undefined}
className={`rounded-2xl border bg-[#F9F9F9] p-4 text-sm font-bold text-[#0A0A0A] placeholder-[#A0A0A0] transition-all focus:bg-white focus:outline-none ${
className={`${FIELD_BASE_CLASSES} ${
passwordError
? "border-red-500/50 focus:border-red-500"
: "border-[#E8E8E8] focus:border-[#0A0A0A]"
? FIELD_ERROR_CLASSES
: FIELD_THEME_CLASSES
}`}
placeholder="••••••••"
/>
Expand Down Expand Up @@ -387,10 +393,10 @@ const RegistrationForm = React.memo(function RegistrationForm() {
aria-describedby={
notificationEmailError ? "notification-email-error" : undefined
}
className={`rounded-2xl border bg-[#F9F9F9] p-4 text-sm font-bold text-[#0A0A0A] placeholder-[#A0A0A0] transition-all focus:bg-white focus:outline-none ${
className={`${FIELD_BASE_CLASSES} ${
notificationEmailError
? "border-red-500/50 focus:border-red-500"
: "border-[#E8E8E8] focus:border-[#0A0A0A]"
? FIELD_ERROR_CLASSES
: FIELD_THEME_CLASSES
}`}
placeholder="alerts@business.com"
/>
Expand All @@ -409,15 +415,19 @@ const RegistrationForm = React.memo(function RegistrationForm() {
<button
type="submit"
disabled={loading || !isFormValid}
className="group relative flex h-16 items-center justify-center rounded-2xl bg-[#0A0A0A] px-8 text-xs font-bold uppercase tracking-[0.3em] text-white transition-all hover:bg-black shadow-xl shadow-black/10 disabled:cursor-not-allowed disabled:opacity-50"
className="group relative flex h-16 items-center justify-center overflow-hidden rounded-2xl bg-pluto-900 px-8 text-xs font-bold uppercase tracking-[0.3em] text-white shadow-[0_18px_40px_rgba(13,27,46,0.18)] transition-all duration-200 hover:-translate-y-0.5 hover:bg-pluto-700 hover:shadow-[0_26px_50px_rgba(13,27,46,0.24)] focus-visible:bg-pluto-800 active:translate-y-0 active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-50"
>
<span
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(184,212,232,0.32),transparent_58%)] opacity-0 transition-opacity duration-200 group-hover:opacity-100 group-focus-visible:opacity-100"
/>
{loading ? (
<span className="flex items-center gap-3">
<span className="relative z-10 flex items-center gap-3">
<Spinner size="sm" className="text-white" />
Initializing Account...
</span>
) : (
"Create Professional Profile"
<span className="relative z-10">Create Professional Profile</span>
)}
</button>
</form>
Expand Down
Loading
Loading