File size: 4,903 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
 * WizardShell β€” chrome for the multi-step new-project wizard.
 *
 * Renders the step indicator (numbered chips with completion
 * state), the page title for the current step, and the Back/Next
 * footer. The actual step body is passed in as `children`.
 *
 * Keyboard: Enter on the Next button submits; ESC is owned by the
 * surrounding host (back to landing).
 */

import React from "react";
import { Check, ChevronLeft, ChevronRight } from "lucide-react";
import { PrimaryButton } from "./ui";

export interface StepDef {
  key: string;
  label: string;
}

export interface WizardShellProps {
  steps: StepDef[];
  activeIndex: number;
  title: string;
  subtitle?: string;
  canGoBack: boolean;
  canGoNext: boolean;
  nextLabel?: string;
  submitting?: boolean;
  onBack: () => void;
  onNext: () => void;
  children: React.ReactNode;
}

export function WizardShell({
  steps, activeIndex, title, subtitle,
  canGoBack, canGoNext, nextLabel, submitting,
  onBack, onNext, children,
}: WizardShellProps) {
  // Three-row enterprise layout: stepper + title stay pinned at
  // the top, footer stays pinned at the bottom, and only the
  // middle content area scrolls. Follows the Linear / Stripe /
  // Notion wizard pattern so users never lose the Next button
  // regardless of viewport height or field length.
  return (
    <div className="flex flex-col h-full w-full">
      {/* ───── Row 1 β€” Fixed header (stepper + step title) ───── */}
      <header className="shrink-0 border-b border-[#2a2a2a]">
        <div className="max-w-3xl mx-auto px-6 pt-6 pb-4">
          <Stepper steps={steps} activeIndex={activeIndex} />
          <div className="mt-5">
            <h1 className="text-2xl font-medium">{title}</h1>
            {subtitle && <p className="text-sm text-[#aaa] mt-1">{subtitle}</p>}
          </div>
        </div>
      </header>

      {/* ───── Row 2 β€” Scrollable content (flex-1 + overflow) ─── */}
      <div className="flex-1 min-h-0 overflow-y-auto">
        <div className="max-w-3xl mx-auto px-6 py-6">{children}</div>
      </div>

      {/* ───── Row 3 β€” Fixed action bar (Back / Next) ───────── */}
      <footer className="shrink-0 border-t border-[#2a2a2a] bg-[#0f0f0f]">
        <div className="max-w-3xl mx-auto px-6 py-3 flex items-center justify-between gap-3">
          {/* Back β€” ghost/text button so visual weight sits on Next */}
          <button
            type="button"
            onClick={onBack}
            disabled={!canGoBack || submitting}
            className={[
              "inline-flex items-center gap-1.5",
              "px-2.5 py-2 text-sm font-medium rounded",
              "text-[#aaa] hover:text-[#f1f1f1]",
              "disabled:text-[#555] disabled:cursor-not-allowed",
              "transition-colors focus:outline-none",
              "focus-visible:ring-2 focus-visible:ring-[#3ea6ff]",
            ].join(" ")}
          >
            <ChevronLeft className="w-4 h-4" aria-hidden />
            Back
          </button>
          {/* Next β€” prominent primary with forward chevron after the label */}
          <PrimaryButton
            onClick={onNext}
            disabled={!canGoNext}
            loading={submitting}
            size="lg"
          >
            <span>{nextLabel || "Next"}</span>
            {!submitting && <ChevronRight className="w-4 h-4" aria-hidden />}
          </PrimaryButton>
        </div>
      </footer>
    </div>
  );
}

function Stepper({ steps, activeIndex }: { steps: StepDef[]; activeIndex: number }) {
  return (
    <ol
      aria-label="Wizard progress"
      className="flex items-center gap-2 overflow-x-auto"
    >
      {steps.map((s, i) => {
        const done = i < activeIndex;
        const active = i === activeIndex;
        return (
          <li key={s.key} className="flex items-center gap-2 shrink-0">
            <span
              aria-current={active ? "step" : undefined}
              className={[
                "w-7 h-7 rounded-full flex items-center justify-center text-xs font-semibold border",
                done && "bg-[#3ea6ff] border-[#3ea6ff] text-black",
                active && "bg-transparent border-[#3ea6ff] text-[#3ea6ff]",
                !done && !active && "bg-transparent border-[#3f3f3f] text-[#777]",
              ].filter(Boolean).join(" ")}
            >
              {done ? <Check className="w-3.5 h-3.5" aria-hidden /> : i + 1}
            </span>
            <span
              className={[
                "text-xs whitespace-nowrap",
                active ? "text-[#f1f1f1] font-medium" : "text-[#777]",
              ].join(" ")}
            >
              {s.label}
            </span>
            {i < steps.length - 1 && <span className="w-6 h-px bg-[#3f3f3f] mx-1" aria-hidden />}
          </li>
        );
      })}
    </ol>
  );
}