Skip to content

Commit 3b74bea

Browse files
committed
feat(frontend): improve UX for Real-time Balance Sync and Onboarding Progress Tracker
- Update typography hierarchy with consistent font weights and tracking - Fix incorrect state rendering when error and empty states coexist - Revamp UI components with improved spacing and visual feedback - Fix responsive layout issue in Onboarding Progress Tracker - Add horizontal connector lines for desktop view - Improve accessibility with better contrast and spacing - Enhance mobile responsiveness with adjusted breakpoints
1 parent e92392a commit 3b74bea

2 files changed

Lines changed: 62 additions & 54 deletions

File tree

frontend/src/components/OnboardingProgressTracker.tsx

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -296,21 +296,21 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
296296
{/* ── Header ──────────────────────────────────────────────────────── */}
297297
<div className="mb-5">
298298
<div className="flex items-baseline justify-between gap-2">
299-
<h2 className={`font-semibold text-pluto-900 dark:text-pluto-50 ${compact ? "text-base" : "text-lg"}`}>
299+
<h2 className={`font-bold tracking-tight text-pluto-900 dark:text-pluto-50 ${compact ? "text-base" : "text-lg"}`}>
300300
{i18n.title}
301301
</h2>
302-
<span className="shrink-0 tabular-nums text-sm font-semibold text-pluto-600 dark:text-pluto-300" aria-hidden="true">
302+
<span className="shrink-0 tabular-nums text-sm font-bold text-pluto-600 dark:text-pluto-300" aria-hidden="true">
303303
{i18n.percentCompleteLabel(progressPercent)}
304304
</span>
305305
</div>
306306

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

311311
{/* CSS-animated progress bar — no framer-motion */}
312312
<div
313-
className="mt-3 h-2 overflow-hidden rounded-full bg-pluto-100 dark:bg-pluto-800"
313+
className="mt-4 h-2.5 overflow-hidden rounded-full bg-pluto-100 dark:bg-pluto-800"
314314
role="progressbar"
315315
aria-valuenow={progressPercent}
316316
aria-valuemin={0}
@@ -319,15 +319,13 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
319319
aria-describedby={progressSummaryId}
320320
>
321321
<div
322-
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 ${
323-
prefersReducedMotion ? "" : ""
324-
}`}
322+
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`}
325323
style={{ width: `${progressPercent}%` }}
326324
data-testid="progress-bar-fill"
327325
/>
328326
</div>
329327

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

343341
{/* ── Steps list ───────────────────────────────────────────────────── */}
344342
<MotionOl
345-
className={orientation === "horizontal" ? "flex flex-col gap-3 sm:flex-row sm:gap-2" : "flex flex-col gap-1"}
343+
className={orientation === "horizontal" ? "flex flex-col gap-3 md:flex-row md:gap-2" : "flex flex-col gap-1"}
346344
role="list"
347345
aria-label={i18n.stepsList}
348346
aria-orientation={orientation}
@@ -370,27 +368,28 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
370368
variants={stepVariants}
371369
className={`
372370
group relative rounded-2xl border border-transparent
373-
px-3 py-2.5 transition-colors duration-200
371+
px-3 py-3 transition-colors duration-200
374372
hover:border-pluto-100 hover:bg-white/80
375373
dark:hover:border-pluto-800/60 dark:hover:bg-pluto-900/50
376374
focus-within:border-pluto-200 dark:focus-within:border-pluto-700
377-
${orientation === "horizontal" ? "flex flex-1 flex-col gap-2" : "flex flex-row gap-3"}
375+
${orientation === "horizontal" ? "flex flex-1 flex-col gap-2 md:items-center md:text-center" : "flex flex-row gap-3"}
378376
`}
379377
>
380378
{/* Step indicator button */}
381-
<button
382-
type="button"
383-
onClick={() => handleStepClick(step.id)}
384-
className={`
385-
relative flex-shrink-0
386-
${compact ? "h-8 w-8" : "h-10 w-10"}
387-
rounded-full border-2 font-semibold
388-
transition-all duration-200
389-
focus:outline-none focus-visible:ring-2
390-
focus-visible:ring-pluto-400 focus-visible:ring-offset-2
391-
focus-visible:ring-offset-white dark:focus-visible:ring-offset-pluto-950
392-
${indicatorColorClass}
393-
`}
379+
<button
380+
type="button"
381+
onClick={() => handleStepClick(step.id)}
382+
className={`
383+
relative flex-shrink-0
384+
${compact ? "h-8 w-8" : "h-10 w-10"}
385+
rounded-full border-2 font-semibold
386+
transition-all duration-200
387+
focus:outline-none focus-visible:ring-2
388+
focus-visible:ring-pluto-400 focus-visible:ring-offset-2
389+
focus-visible:ring-offset-white dark:focus-visible:ring-offset-pluto-950
390+
${orientation === "horizontal" ? "mx-auto md:mx-0" : ""}
391+
${indicatorColorClass}
392+
`}
394393
aria-label={i18n.stepAriaLabel(index + 1, step.title, step.completed, step.required)}
395394
aria-pressed={isCurrent}
396395
aria-current={isCurrent ? "step" : undefined}
@@ -414,7 +413,7 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
414413
</button>
415414

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

440-
<StatusBadge
441-
completed={step.completed}
442-
isCurrent={isCurrent}
443-
compact={compact}
444-
completedLabel={i18n.completed}
445-
inProgressLabel={i18n.inProgress}
446-
pendingLabel={i18n.pending}
447-
/>
439+
<div className={orientation === "horizontal" ? "flex justify-center md:justify-center" : ""}>
440+
<StatusBadge
441+
completed={step.completed}
442+
isCurrent={isCurrent}
443+
compact={compact}
444+
completedLabel={i18n.completed}
445+
inProgressLabel={i18n.inProgress}
446+
pendingLabel={i18n.pending}
447+
/>
448+
</div>
448449
</div>
449450

450451
{/* Vertical connector */}
@@ -454,6 +455,13 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
454455
aria-hidden="true"
455456
/>
456457
)}
458+
{/* Horizontal connector */}
459+
{orientation === "horizontal" && index < sortedSteps.length - 1 && (
460+
<div
461+
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`}
462+
aria-hidden="true"
463+
/>
464+
)}
457465
</MotionLi>
458466
);
459467
})}
@@ -487,8 +495,8 @@ export const OnboardingProgressTracker = memo(function OnboardingProgressTracker
487495
<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" />
488496
</MotionSvg>
489497
<div>
490-
<h4 className="font-semibold text-pluto-900 dark:text-pluto-50">{i18n.successTitle}</h4>
491-
<p className="mt-0.5 text-sm text-pluto-700 dark:text-pluto-300">{i18n.successMessage}</p>
498+
<h4 className="font-bold text-pluto-900 dark:text-pluto-50">{i18n.successTitle}</h4>
499+
<p className="mt-1 text-sm text-pluto-700 dark:text-pluto-300">{i18n.successMessage}</p>
492500
</div>
493501
</div>
494502
</MotionDiv>

frontend/src/components/RealTimeBalanceSync.tsx

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export function RealTimeBalanceSync({
9393

9494
return (
9595
<motion.section
96-
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}`}
96+
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}`}
9797
aria-label={t("sectionAriaLabel")}
9898
aria-busy={isLoading}
9999
{...animProps}
@@ -108,9 +108,9 @@ export function RealTimeBalanceSync({
108108
{liveRegionText}
109109
</div>
110110

111-
<div className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
112-
<div className="flex items-center gap-2">
113-
<h2 className="text-sm font-semibold text-slate-900 dark:text-slate-100">
111+
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
112+
<div className="flex items-center gap-2.5">
113+
<h2 className="text-base font-bold tracking-tight text-slate-900 dark:text-white">
114114
{t("title")}
115115
</h2>
116116
<AnimatePresence mode="wait">
@@ -159,7 +159,7 @@ export function RealTimeBalanceSync({
159159
}
160160
aria-label={t("refreshButton")}
161161
aria-describedby={liveId}
162-
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"
162+
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"
163163
>
164164
<motion.span
165165
className="relative z-10 flex items-center gap-1.5"
@@ -220,19 +220,19 @@ export function RealTimeBalanceSync({
220220
animate={shouldReduceMotion ? undefined : { opacity: 1, y: 0 }}
221221
exit={shouldReduceMotion ? undefined : { opacity: 0 }}
222222
transition={{ duration: 0.2 }}
223-
className="mb-2 text-xs text-red-600"
223+
className="mb-3 text-sm font-medium text-red-600 dark:text-red-400"
224224
>
225225
{error}
226226
</motion.p>
227227
) : null}
228228
</AnimatePresence>
229229

230-
{balances.length === 0 && !isLoading ? (
230+
{!error && balances.length === 0 && !isLoading ? (
231231
<motion.div
232232
key="empty"
233233
initial={shouldReduceMotion ? undefined : { opacity: 0, y: 10 }}
234234
animate={shouldReduceMotion ? undefined : { opacity: 1, y: 0 }}
235-
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"
235+
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"
236236
aria-live="polite"
237237
>
238238
<motion.div
@@ -257,7 +257,7 @@ export function RealTimeBalanceSync({
257257
/>
258258
<div className="relative">
259259
<svg
260-
className="mx-auto mb-2 h-8 w-8 text-slate-300 dark:text-slate-600"
260+
className="mx-auto mb-3 h-10 w-10 text-slate-300 dark:text-slate-600"
261261
fill="none"
262262
stroke="currentColor"
263263
viewBox="0 0 24 24"
@@ -270,7 +270,7 @@ export function RealTimeBalanceSync({
270270
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
271271
/>
272272
</svg>
273-
<p className="text-center text-xs font-medium text-slate-500 dark:text-slate-400">
273+
<p className="text-center text-sm font-medium text-slate-500 dark:text-slate-400">
274274
{t("emptyState")}
275275
</p>
276276
</div>
@@ -280,7 +280,7 @@ export function RealTimeBalanceSync({
280280
key="skeleton"
281281
initial={shouldReduceMotion ? undefined : { opacity: 0 }}
282282
animate={shouldReduceMotion ? undefined : { opacity: 1 }}
283-
className="space-y-2 rounded-2xl border border-slate-100 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-800/50"
283+
className="space-y-3 rounded-2xl border border-slate-100 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-800/50"
284284
>
285285
{[1, 2, 3].map((i) => (
286286
<div key={i} className="flex items-center justify-between">
@@ -331,7 +331,7 @@ export function RealTimeBalanceSync({
331331
<motion.ul
332332
role="list"
333333
aria-label={t("balancesListAriaLabel")}
334-
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"
334+
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"
335335
variants={shouldReduceMotion ? undefined : listVariants}
336336
initial="hidden"
337337
animate="visible"
@@ -355,22 +355,22 @@ export function RealTimeBalanceSync({
355355
animate="visible"
356356
exit="exit"
357357
transition={{ duration: 0.25, ease: "easeOut" }}
358-
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"
358+
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"
359359
aria-label={t("balanceItemAriaLabel", {
360360
asset: b.code,
361361
balance: formattedBalance,
362362
})}
363363
>
364364
<div className="flex items-center gap-2">
365-
<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">
365+
<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">
366366
{b.code.slice(0, 2)}
367367
</div>
368-
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
368+
<span className="text-sm font-semibold text-slate-700 dark:text-slate-300">
369369
{b.code}
370370
</span>
371371
</div>
372372
<motion.span
373-
className="text-sm tabular-nums font-semibold text-slate-900 dark:text-slate-100"
373+
className="text-base tabular-nums font-bold text-slate-900 dark:text-white"
374374
key={`${b.code}-${b.balance}`}
375375
initial={
376376
shouldReduceMotion
@@ -401,7 +401,7 @@ export function RealTimeBalanceSync({
401401

402402
{lastUpdated && (
403403
<motion.div
404-
className="mt-3 flex items-center gap-2"
404+
className="mt-4 flex items-center gap-2"
405405
initial={shouldReduceMotion ? undefined : { opacity: 0, y: 5 }}
406406
animate={shouldReduceMotion ? undefined : { opacity: 1, y: 0 }}
407407
transition={{ delay: 0.2, duration: 0.3 }}
@@ -429,10 +429,10 @@ export function RealTimeBalanceSync({
429429
/>
430430
</div>
431431
<p className="text-xs text-slate-400 dark:text-slate-500">
432-
<span className="font-medium text-slate-500 dark:text-slate-400">
432+
<span className="font-semibold text-slate-500 dark:text-slate-400">
433433
{t("updatedLabel")}
434434
</span>{" "}
435-
<time dateTime={lastUpdated.toISOString()}>
435+
<time dateTime={lastUpdated.toISOString()} className="tabular-nums">
436436
{lastUpdated.toLocaleTimeString(locale, {
437437
hour: "numeric",
438438
minute: "2-digit",

0 commit comments

Comments
 (0)