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
78 changes: 43 additions & 35 deletions frontend/src/components/OnboardingProgressTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -296,21 +296,21 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
{/* ── Header ──────────────────────────────────────────────────────── */}
<div className="mb-5">
<div className="flex items-baseline justify-between gap-2">
<h2 className={`font-semibold text-pluto-900 dark:text-pluto-50 ${compact ? "text-base" : "text-lg"}`}>
<h2 className={`font-bold tracking-tight text-pluto-900 dark:text-pluto-50 ${compact ? "text-base" : "text-lg"}`}>
{i18n.title}
</h2>
<span className="shrink-0 tabular-nums text-sm font-semibold text-pluto-600 dark:text-pluto-300" aria-hidden="true">
<span className="shrink-0 tabular-nums text-sm font-bold text-pluto-600 dark:text-pluto-300" aria-hidden="true">
{i18n.percentCompleteLabel(progressPercent)}
</span>
</div>

<p className={`mt-1 text-[#6B6B6B] dark:text-pluto-400 ${compact ? "text-xs" : "text-sm"}`}>
<p className={`mt-1.5 text-[#6B6B6B] dark:text-pluto-400 ${compact ? "text-xs" : "text-sm"}`}>
{i18n.subtitle}
</p>

{/* CSS-animated progress bar — no framer-motion */}
<div
className="mt-3 h-2 overflow-hidden rounded-full bg-pluto-100 dark:bg-pluto-800"
className="mt-4 h-2.5 overflow-hidden rounded-full bg-pluto-100 dark:bg-pluto-800"
role="progressbar"
aria-valuenow={progressPercent}
aria-valuemin={0}
Expand All @@ -319,15 +319,13 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
aria-describedby={progressSummaryId}
>
<div
className={`h-full rounded-full bg-gradient-to-r from-pluto-400 via-pluto-500 to-pluto-600 dark:from-pluto-500 dark:via-pluto-400 dark:to-pluto-300 origin-left transition-[width] duration-500 ease-out ${
prefersReducedMotion ? "" : ""
}`}
className={`h-full rounded-full bg-gradient-to-r from-pluto-400 via-pluto-500 to-pluto-600 dark:from-pluto-500 dark:via-pluto-400 dark:to-pluto-300 origin-left transition-[width] duration-500 ease-out`}
style={{ width: `${progressPercent}%` }}
data-testid="progress-bar-fill"
/>
</div>

<p className="mt-1.5 flex items-center gap-1.5 text-xs text-[#6B6B6B] dark:text-pluto-400" aria-hidden="true">
<p className="mt-2 flex items-center gap-1.5 text-xs text-[#6B6B6B] dark:text-pluto-400" aria-hidden="true">
{i18n.stepsCompletedLabel(completedCount, sortedSteps.length)}
{isComplete && (
<span className="inline-flex items-center gap-1 font-semibold text-pluto-600 dark:text-pluto-300">
Expand All @@ -342,7 +340,7 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker

{/* ── Steps list ───────────────────────────────────────────────────── */}
<MotionOl
className={orientation === "horizontal" ? "flex flex-col gap-3 sm:flex-row sm:gap-2" : "flex flex-col gap-1"}
className={orientation === "horizontal" ? "flex flex-col gap-3 md:flex-row md:gap-2" : "flex flex-col gap-1"}
role="list"
aria-label={i18n.stepsList}
aria-orientation={orientation}
Expand Down Expand Up @@ -370,27 +368,28 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
variants={stepVariants}
className={`
group relative rounded-2xl border border-transparent
px-3 py-2.5 transition-colors duration-200
px-3 py-3 transition-colors duration-200
hover:border-pluto-100 hover:bg-white/80
dark:hover:border-pluto-800/60 dark:hover:bg-pluto-900/50
focus-within:border-pluto-200 dark:focus-within:border-pluto-700
${orientation === "horizontal" ? "flex flex-1 flex-col gap-2" : "flex flex-row gap-3"}
${orientation === "horizontal" ? "flex flex-1 flex-col gap-2 md:items-center md:text-center" : "flex flex-row gap-3"}
`}
>
{/* Step indicator button */}
<button
type="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-all duration-200
focus:outline-none focus-visible:ring-2
focus-visible:ring-pluto-400 focus-visible:ring-offset-2
focus-visible:ring-offset-white dark:focus-visible:ring-offset-pluto-950
${indicatorColorClass}
`}
<button
type="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-all duration-200
focus:outline-none focus-visible:ring-2
focus-visible:ring-pluto-400 focus-visible:ring-offset-2
focus-visible:ring-offset-white dark:focus-visible:ring-offset-pluto-950
${orientation === "horizontal" ? "mx-auto md:mx-0" : ""}
${indicatorColorClass}
`}
aria-label={i18n.stepAriaLabel(index + 1, step.title, step.completed, step.required)}
aria-pressed={isCurrent}
aria-current={isCurrent ? "step" : undefined}
Expand All @@ -414,7 +413,7 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
</button>

{/* Step text */}
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className={`flex min-w-0 flex-1 flex-col gap-1 ${orientation === "horizontal" ? "md:text-center" : ""}`}>
<h3
id={stepDescId}
className={`
Expand All @@ -437,14 +436,16 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
{step.description}
</p>

<StatusBadge
completed={step.completed}
isCurrent={isCurrent}
compact={compact}
completedLabel={i18n.completed}
inProgressLabel={i18n.inProgress}
pendingLabel={i18n.pending}
/>
<div className={orientation === "horizontal" ? "flex justify-center md:justify-center" : ""}>
<StatusBadge
completed={step.completed}
isCurrent={isCurrent}
compact={compact}
completedLabel={i18n.completed}
inProgressLabel={i18n.inProgress}
pendingLabel={i18n.pending}
/>
</div>
</div>

{/* Vertical connector */}
Expand All @@ -454,6 +455,13 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
aria-hidden="true"
/>
)}
{/* Horizontal connector */}
{orientation === "horizontal" && index < sortedSteps.length - 1 && (
<div
className={`hidden md:block absolute left-[calc(100%+4px)] top-1/2 -translate-y-1/2 ${compact ? "w-2 h-px" : "w-3 h-px"} bg-pluto-200 dark:bg-pluto-700`}
aria-hidden="true"
/>
)}
</MotionLi>
);
})}
Expand Down Expand Up @@ -487,8 +495,8 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</MotionSvg>
<div>
<h4 className="font-semibold text-pluto-900 dark:text-pluto-50">{i18n.successTitle}</h4>
<p className="mt-0.5 text-sm text-pluto-700 dark:text-pluto-300">{i18n.successMessage}</p>
<h4 className="font-bold text-pluto-900 dark:text-pluto-50">{i18n.successTitle}</h4>
<p className="mt-1 text-sm text-pluto-700 dark:text-pluto-300">{i18n.successMessage}</p>
</div>
</div>
</MotionDiv>
Expand Down
38 changes: 19 additions & 19 deletions frontend/src/components/RealTimeBalanceSync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function RealTimeBalanceSync({

return (
<motion.section
className={`w-full rounded-3xl border border-slate-200 bg-white p-4 shadow-sm transition-all duration-200 ease-out dark:border-slate-700 dark:bg-slate-900 ${isLoading ? "ring-2 ring-sky-200 dark:ring-sky-800" : ""} ${className}`}
className={`w-full rounded-3xl border border-slate-200 bg-white p-5 shadow-sm transition-all duration-200 ease-out dark:border-slate-700 dark:bg-slate-900 ${isLoading ? "ring-2 ring-sky-200 dark:ring-sky-800" : ""} ${className}`}
aria-label={t("sectionAriaLabel")}
aria-busy={isLoading}
{...animProps}
Expand All @@ -108,9 +108,9 @@ export function RealTimeBalanceSync({
{liveRegionText}
</div>

<div className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold text-slate-900 dark:text-slate-100">
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2.5">
<h2 className="text-base font-bold tracking-tight text-slate-900 dark:text-white">
{t("title")}
</h2>
<AnimatePresence mode="wait">
Expand Down Expand Up @@ -159,7 +159,7 @@ export function RealTimeBalanceSync({
}
aria-label={t("refreshButton")}
aria-describedby={liveId}
className="group relative inline-flex items-center justify-center overflow-hidden rounded-xl border border-slate-200 bg-gradient-to-br from-sky-50 to-white px-3 py-1.5 text-xs font-semibold text-sky-600 shadow-sm transition-all duration-200 hover:border-sky-300 hover:from-sky-100 hover:to-sky-50 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-sky-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 dark:border-slate-700 dark:from-slate-800 dark:to-slate-900 dark:text-sky-400 dark:hover:border-sky-600 dark:hover:from-slate-700 dark:hover:to-slate-800"
className="group relative inline-flex items-center justify-center overflow-hidden rounded-xl border border-slate-200 bg-gradient-to-br from-sky-50 to-white px-3.5 py-2 text-sm font-semibold text-sky-600 shadow-sm transition-all duration-200 hover:border-sky-300 hover:from-sky-100 hover:to-sky-50 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-sky-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 dark:border-slate-700 dark:from-slate-800 dark:to-slate-900 dark:text-sky-400 dark:hover:border-sky-600 dark:hover:from-slate-700 dark:hover:to-slate-800"
>
<motion.span
className="relative z-10 flex items-center gap-1.5"
Expand Down Expand Up @@ -220,19 +220,19 @@ export function RealTimeBalanceSync({
animate={shouldReduceMotion ? undefined : { opacity: 1, y: 0 }}
exit={shouldReduceMotion ? undefined : { opacity: 0 }}
transition={{ duration: 0.2 }}
className="mb-2 text-xs text-red-600"
className="mb-3 text-sm font-medium text-red-600 dark:text-red-400"
>
{error}
</motion.p>
) : null}
</AnimatePresence>

{balances.length === 0 && !isLoading ? (
{!error && balances.length === 0 && !isLoading ? (
<motion.div
key="empty"
initial={shouldReduceMotion ? undefined : { opacity: 0, y: 10 }}
animate={shouldReduceMotion ? undefined : { opacity: 1, y: 0 }}
className="group relative overflow-hidden rounded-2xl border border-slate-100 bg-gradient-to-br from-slate-50 to-slate-100/50 px-4 py-6 transition-all duration-300 hover:border-slate-200 hover:shadow-sm dark:border-slate-800 dark:from-slate-800 dark:to-slate-900/50"
className="group relative overflow-hidden rounded-2xl border border-slate-100 bg-gradient-to-br from-slate-50 to-slate-100/50 px-4 py-8 transition-all duration-300 hover:border-slate-200 hover:shadow-sm dark:border-slate-800 dark:from-slate-800 dark:to-slate-900/50"
aria-live="polite"
>
<motion.div
Expand All @@ -257,7 +257,7 @@ export function RealTimeBalanceSync({
/>
<div className="relative">
<svg
className="mx-auto mb-2 h-8 w-8 text-slate-300 dark:text-slate-600"
className="mx-auto mb-3 h-10 w-10 text-slate-300 dark:text-slate-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
Expand All @@ -270,7 +270,7 @@ export function RealTimeBalanceSync({
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
/>
</svg>
<p className="text-center text-xs font-medium text-slate-500 dark:text-slate-400">
<p className="text-center text-sm font-medium text-slate-500 dark:text-slate-400">
{t("emptyState")}
</p>
</div>
Expand All @@ -280,7 +280,7 @@ export function RealTimeBalanceSync({
key="skeleton"
initial={shouldReduceMotion ? undefined : { opacity: 0 }}
animate={shouldReduceMotion ? undefined : { opacity: 1 }}
className="space-y-2 rounded-2xl border border-slate-100 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-800/50"
className="space-y-3 rounded-2xl border border-slate-100 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-800/50"
>
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center justify-between">
Expand Down Expand Up @@ -331,7 +331,7 @@ export function RealTimeBalanceSync({
<motion.ul
role="list"
aria-label={t("balancesListAriaLabel")}
className="overflow-hidden rounded-2xl border border-slate-100 bg-slate-50 divide-y divide-slate-100 dark:border-slate-800 dark:bg-slate-800/50 dark:divide-slate-800"
className="overflow-hidden rounded-2xl border border-slate-100 bg-slate-50 divide-y divide-slate-100 dark:border-slate-800 dark:bg-slate-800/50 dark:divide-slate-700"
variants={shouldReduceMotion ? undefined : listVariants}
initial="hidden"
animate="visible"
Expand All @@ -355,22 +355,22 @@ export function RealTimeBalanceSync({
animate="visible"
exit="exit"
transition={{ duration: 0.25, ease: "easeOut" }}
className="group relative flex items-center justify-between gap-4 py-3 px-3 transition-colors hover:bg-white/50 dark:hover:bg-slate-700/30"
className="group relative flex items-center justify-between gap-4 py-3.5 px-4 transition-colors hover:bg-white/50 dark:hover:bg-slate-700/30"
aria-label={t("balanceItemAriaLabel", {
asset: b.code,
balance: formattedBalance,
})}
>
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-sky-100 to-blue-100 text-xs font-bold text-sky-700 dark:from-sky-900/30 dark:to-blue-900/30 dark:text-sky-400">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-sky-100 to-blue-100 text-xs font-bold text-sky-700 dark:from-sky-900/30 dark:to-blue-900/30 dark:text-sky-400">
{b.code.slice(0, 2)}
</div>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
<span className="text-sm font-semibold text-slate-700 dark:text-slate-300">
{b.code}
</span>
</div>
<motion.span
className="text-sm tabular-nums font-semibold text-slate-900 dark:text-slate-100"
className="text-base tabular-nums font-bold text-slate-900 dark:text-white"
key={`${b.code}-${b.balance}`}
initial={
shouldReduceMotion
Expand Down Expand Up @@ -401,7 +401,7 @@ export function RealTimeBalanceSync({

{lastUpdated && (
<motion.div
className="mt-3 flex items-center gap-2"
className="mt-4 flex items-center gap-2"
initial={shouldReduceMotion ? undefined : { opacity: 0, y: 5 }}
animate={shouldReduceMotion ? undefined : { opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.3 }}
Expand Down Expand Up @@ -429,10 +429,10 @@ export function RealTimeBalanceSync({
/>
</div>
<p className="text-xs text-slate-400 dark:text-slate-500">
<span className="font-medium text-slate-500 dark:text-slate-400">
<span className="font-semibold text-slate-500 dark:text-slate-400">
{t("updatedLabel")}
</span>{" "}
<time dateTime={lastUpdated.toISOString()}>
<time dateTime={lastUpdated.toISOString()} className="tabular-nums">
{lastUpdated.toLocaleTimeString(locale, {
hour: "numeric",
minute: "2-digit",
Expand Down