1import React from "react";
2
3interface StepItem {
4 label: string;
5 description?: string;
6}
7
8interface StepsProps {
9 steps: StepItem[];
10 current: number;
11 className?: string;
12}
13
14function CheckIcon() {
15 return (
16 <svg
17 width="13"
18 height="13"
19 viewBox="0 0 24 24"
20 fill="none"
21 stroke="currentColor"
22 strokeWidth="2.75"
23 strokeLinecap="round"
24 strokeLinejoin="round"
25 >
26 <polyline points="20 6 9 17 4 12" />
27 </svg>
28 );
29}
30
31/* Upcoming steps are sunken (not yet filled in), done and current steps
32 are dark raised faces; the current one also wears a soft halo. Each step owns an equal column, so labels get
33 real width instead of being squeezed under a 32px circle. */
34export function Steps({ steps, current, className = "" }: StepsProps) {
35 return (
36 <ol className={["flex items-start", className].filter(Boolean).join(" ")}>
37 {steps.map((step, i) => {
38 const isCompleted = i < current;
39 const isCurrent = i === current;
40 const reached = isCompleted || isCurrent;
41 const isLast = i === steps.length - 1;
42
43 return (
44 <li
45 key={i}
46 aria-current={isCurrent ? "step" : undefined}
47 className="relative flex-1 min-w-0 flex flex-col items-center text-center"
48 >
49 {/* One groove per gap, running between the two circles' edges
50 (columns are equal, so the next centre is exactly 100% away) */}
51 {!isLast && (
52 <span
53 aria-hidden
54 className="absolute top-[15px] h-1 -translate-y-1/2 rounded-full overflow-hidden"
55 style={{
56 left: "calc(50% + 25px)",
57 width: "calc(100% - 50px)",
58 background: "var(--well)",
59 boxShadow: "var(--well-shadow)",
60 }}
61 >
62 <span
63 className="block h-full rounded-full origin-left transition-transform duration-500 ease-[var(--ease-out-soft)]"
64 style={{
65 background: "var(--face-dark)",
66 transform: isCompleted ? "scaleX(1)" : "scaleX(0)",
67 }}
68 />
69 </span>
70 )}
71
72 <span
73 className={[
74 "relative w-[30px] h-[30px] rounded-full flex items-center justify-center text-[12.5px] font-semibold tabular-nums shrink-0",
75 "transition-[box-shadow,color] duration-300",
76 reached ? "text-white" : "text-neutral-400",
77 ].join(" ")}
78 style={{
79 background: reached ? "var(--face-dark)" : "var(--field)",
80 boxShadow: isCurrent
81 ? "var(--face-dark-shadow), 0 0 0 4px rgba(17,17,17,0.06), 0 0 0 5px rgba(17,17,17,0.03)"
82 : reached
83 ? "var(--face-dark-shadow)"
84 : "var(--field-shadow)",
85 }}
86 >
87 {isCompleted ? <CheckIcon /> : i + 1}
88 <span className="sr-only">{isCompleted ? " (completed)" : ""}</span>
89 </span>
90
91 <span className="mt-3 px-1.5 flex flex-col gap-0.5">
92 <span
93 className={[
94 "text-[13px] font-medium leading-tight transition-colors duration-200",
95 reached ? "text-neutral-900" : "text-neutral-500",
96 ].join(" ")}
97 >
98 {step.label}
99 </span>
100 {step.description && (
101 <span className="text-[12px] text-neutral-400 leading-snug">{step.description}</span>
102 )}
103 </span>
104 </li>
105 );
106 })}
107 </ol>
108 );
109}