返回 JoyAI-Echo
StepHeader.tsx
1 import { Check, ChevronLeft, ChevronRight } from "lucide-react";
2
3 import { Button } from "@/components/ui/button";
4 import { cn } from "@/lib/utils";
5
6 export type StepId = 1 | 2 | 3 | 4;
7
8 const STEPS: Array<{ id: StepId; label: string }> = [
9 { id: 1, label: "Story" },
10 { id: 2, label: "Shot Plan" },
11 { id: 3, label: "Generate" },
12 { id: 4, label: "Final Cut" },
13 ];
14
15 interface StepHeaderProps {
16 currentStep: StepId;
17 onPrev: () => void;
18 onNext: () => void;
19 progress?: { done: number; total: number } | null;
20 }
21
22 export function StepHeader({
23 currentStep,
24 onPrev,
25 onNext,
26 progress,
27 }: StepHeaderProps) {
28 const canGoBack = false; // currentStep <= 2 && currentStep > 1;
29 const progressPct =
30 progress && progress.total > 0
31 ? Math.round((progress.done / progress.total) * 100)
32 : null;
33
34 return (
35 <div className="relative shrink-0">
36 <div className="flex items-center gap-1 px-3 py-2.5">
37 <Button
38 variant="ghost"
39 size="icon"
40 disabled={!canGoBack}
41 onClick={onPrev}
42 className={cn(
43 "h-7 w-7 rounded-lg text-muted-foreground transition-all duration-200 hover:bg-accent/60 hover:text-foreground",
44 !canGoBack && "invisible",
45 )}
46 >
47 <ChevronLeft className="h-4 w-4" />
48 </Button>
49
50 <div className="flex flex-1 items-center justify-center gap-0">
51 {STEPS.map((step, idx) => {
52 const completed = step.id < currentStep;
53 const active = step.id === currentStep;
54 const reached = step.id <= currentStep;
55 return (
56 <div key={step.id} className="flex items-center">
57 {idx > 0 && (
58 <div className="relative mx-1.5 w-6">
59 <div className="absolute inset-y-1/2 h-px w-full bg-border/50" />
60 <div
61 className={cn(
62 "absolute inset-y-1/2 h-px bg-foreground/20 transition-all duration-500 ease-out",
63 reached ? "w-full" : "w-0",
64 )}
65 />
66 </div>
67 )}
68 <div
69 className={cn(
70 "group relative flex flex-col items-center gap-0.5 rounded-xl px-4 py-2 transition-all duration-300",
71 active &&
72 "bg-foreground/[0.05] ring-1 ring-inset ring-foreground/[0.07]",
73 )}
74 >
75 <span
76 className={cn(
77 "text-[13px] tabular-nums transition-all duration-300",
78 active && "font-medium text-foreground/70",
79 completed && "font-medium text-foreground/40",
80 !active &&
81 !completed &&
82 reached &&
83 "font-medium text-foreground/35",
84 !reached &&
85 !active &&
86 !completed &&
87 "font-normal text-muted-foreground/30",
88 )}
89 >
90 {completed ? (
91 <Check className="h-3 w-3" strokeWidth={2.5} />
92 ) : (
93 String(step.id).padStart(2, "0")
94 )}
95 </span>
96 <span
97 className={cn(
98 "text-[13px] transition-all duration-300",
99 active && "font-medium text-foreground/85",
100 completed && "font-normal text-foreground/45",
101 !active &&
102 !completed &&
103 reached &&
104 "font-normal text-foreground/40",
105 !reached &&
106 !active &&
107 !completed &&
108 "font-normal text-muted-foreground/35",
109 )}
110 >
111 {step.label}
112 </span>
113
114 {active && progressPct !== null && (
115 <span className="mt-0.5 text-[13px] tabular-nums text-foreground/40">
116 {progressPct}%
117 </span>
118 )}
119 </div>
120 </div>
121 );
122 })}
123 </div>
124
125 {progress && (
126 <span className="mr-1 text-[15px] tabular-nums text-muted-foreground/70">
127 {progress.done}/{progress.total}
128 </span>
129 )}
130
131 <Button
132 variant="ghost"
133 size="icon"
134 onClick={onNext}
135 disabled={currentStep >= 4}
136 className={cn(
137 "h-7 w-7 rounded-lg text-muted-foreground transition-all duration-200 hover:bg-accent/60 hover:text-foreground",
138 currentStep >= 4 && "invisible",
139 )}
140 >
141 <ChevronRight className="h-4 w-4" />
142 </Button>
143 </div>
144
145 {progress && progress.total > 0 && (
146 <div className="absolute bottom-0 left-0 right-0 h-[2px] bg-border/40">
147 <div
148 className="h-full bg-foreground/30 transition-all duration-700 ease-out"
149 style={{ width: `${(progress.done / progress.total) * 100}%` }}
150 />
151 </div>
152 )}
153
154 {!progress && <div className="h-px w-full bg-border/60" />}
155 </div>
156 );
157 }
158
158 lines Plain Text