onboarding: extract OnboardingStep shell (P1-11); eliminate 6 copies of motion+header chrome

This commit is contained in:
ZacharyZhang-NY committed 2026-04-16 23:00:01 -04:00
1 parent 3eb5ed1069
commit 1b11b53f84
6 files changed
+68 -95

No files matched your search

@@ -1,8 +1,7 @@
import { motion } from "framer-motion";
import { Buildings, FolderOpen } from "@phosphor-icons/react";
import type { ConnectorId } from "@shared/types";
import { ActionButton, Input, TextArea, Select } from "../ui";
import { getStepNumber, TOTAL_STEPS } from "./OnboardingHero";
import { OnboardingStep } from "./OnboardingStep";
import { useOnboarding } from "./OnboardingContext";
interface CompanyStepProps {
@@ -32,22 +31,11 @@ export function CompanyStep({ onContinue, onBack }: CompanyStepProps) {
} = useOnboarding();
return (
<motion.div
key="company"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="bg-card border border-border shadow-sm w-full rounded-lg p-7"
<OnboardingStep
step="company"
eyebrow={t("onboarding.stepLabelCompany")}
title={t("onboarding.step1Title")}
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{t("onboarding.stepLabelCompany")} {getStepNumber("company")} / {TOTAL_STEPS}
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-foreground">
{t("onboarding.step1Title")}
</div>
<div className="mb-6 h-px bg-[color:var(--border)]" />
<div className="space-y-4">
<Input
label={t("onboarding.companyName")}
@@ -139,6 +127,6 @@ export function CompanyStep({ onContinue, onBack }: CompanyStepProps) {
disabled={!companyName.trim() || !selectedConnectorId}
/>
</div>
</motion.div>
</OnboardingStep>
);
}
@@ -1,6 +1,5 @@
import { motion } from "framer-motion";
import { ArrowRight, Brain, UserCircle, type Icon } from "@phosphor-icons/react";
import { getStepNumber, TOTAL_STEPS } from "./OnboardingHero";
import { OnboardingStep } from "./OnboardingStep";
import { useOnboarding } from "./OnboardingContext";
interface ExecutionModeStepProps {
@@ -12,22 +11,11 @@ export function ExecutionModeStep({ onContinue, onBack }: ExecutionModeStepProps
const { executionMode, setExecutionMode, t } = useOnboarding();
return (
<motion.div
key="executionmode"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="bg-card border border-border shadow-sm w-full rounded-lg p-7"
<OnboardingStep
step="executionmode"
eyebrow={t("onboarding.stepLabelMode")}
title={t("onboarding.modeStepTitle")}
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{t("onboarding.stepLabelMode")} {getStepNumber("executionmode")} / {TOTAL_STEPS}
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-foreground">
{t("onboarding.modeStepTitle")}
</div>
<div className="mb-6 h-px bg-[color:var(--border)]" />
<div className="space-y-3">
<ModeCard
selected={executionMode === "ai_ceo"}
@@ -68,7 +56,7 @@ export function ExecutionModeStep({ onContinue, onBack }: ExecutionModeStepProps
<ArrowRight size={14} />
</button>
</div>
</motion.div>
</OnboardingStep>
);
}
@@ -1,7 +1,6 @@
import { motion } from "framer-motion";
import { ActionButton } from "../ui";
import { ModelToggleList, StatusBadge } from "../execution/ModelManagementShared";
import { getStepNumber, TOTAL_STEPS } from "./OnboardingHero";
import { OnboardingStep } from "./OnboardingStep";
import { useOnboarding } from "./OnboardingContext";
interface ModelPoolStepProps {
@@ -31,25 +30,12 @@ export function ModelPoolStep({ onBack, onContinue }: ModelPoolStepProps) {
: t("onboarding.modelPoolContinueHint");
return (
<motion.div
key="modelpool"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="w-full rounded-lg border border-border bg-card p-7 shadow-sm"
<OnboardingStep
step="modelpool"
eyebrow={t("onboarding.stepLabelModelPool")}
title={t("onboarding.modelPoolStepTitle")}
subtitle={t("onboarding.modelPoolStepSubtitle")}
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{t("onboarding.stepLabelModelPool")} {getStepNumber("modelpool")} / {TOTAL_STEPS}
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-foreground">
{t("onboarding.modelPoolStepTitle")}
</div>
<div className="mb-3 text-[13px] leading-relaxed text-muted-foreground">
{t("onboarding.modelPoolStepSubtitle")}
</div>
<div className="mb-6 h-px bg-[color:var(--border)]" />
<div className="mb-4 rounded-lg border border-border bg-background/70 px-4 py-3 text-[12px] leading-relaxed text-muted-foreground">
{t("onboarding.modelPoolStepDescription")}
</div>
@@ -113,6 +99,6 @@ export function ModelPoolStep({ onBack, onContinue }: ModelPoolStepProps) {
disabled={!canContinue}
/>
</div>
</motion.div>
</OnboardingStep>
);
}
@@ -1,4 +1,3 @@
import { motion } from "framer-motion";
import {
CheckCircle,
XCircle,
@@ -8,7 +7,7 @@ import {
} from "@phosphor-icons/react";
import { ActionButton } from "../ui";
import type { TerminalCommandKey } from "@shared/contracts";
import { getStepNumber, TOTAL_STEPS } from "./OnboardingHero";
import { OnboardingStep } from "./OnboardingStep";
import { useOnboarding } from "./OnboardingContext";
interface NodeCheckStepProps {
@@ -50,22 +49,11 @@ export function NodeCheckStep({ onContinue, onBack }: NodeCheckStepProps) {
const platformInfo = INSTALL_COMMANDS[platform] ?? INSTALL_COMMANDS.linux;
return (
<motion.div
key="nodecheck"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="bg-card border border-border shadow-sm w-full rounded-lg p-7"
<OnboardingStep
step="nodecheck"
eyebrow={`${t("onboarding.stepLabel1")} — ${t("onboarding.nodeCheckStep")}`}
title={t("onboarding.nodeCheckTitle")}
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{t("onboarding.stepLabel1")} {getStepNumber("nodecheck")} / {TOTAL_STEPS} — {t("onboarding.nodeCheckStep")}
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-foreground">
{t("onboarding.nodeCheckTitle")}
</div>
<div className="mb-6 h-px bg-[color:var(--border)]" />
{/* Status */}
<div className="mb-5 flex items-center gap-2.5 text-[13px]">
{nodeChecking ? (
@@ -132,6 +120,6 @@ export function NodeCheckStep({ onContinue, onBack }: NodeCheckStepProps) {
disabled={!nodeStatus?.installed}
/>
</div>
</motion.div>
</OnboardingStep>
);
}
@@ -0,0 +1,36 @@
import { motion } from "framer-motion";
import type { ReactNode } from "react";
import { getStepNumber, TOTAL_STEPS, type OnboardingStepName } from "./OnboardingHero";
interface OnboardingStepProps {
step: OnboardingStepName;
eyebrow: string;
title: string;
subtitle?: ReactNode;
children: ReactNode;
}
export function OnboardingStep({ step, eyebrow, title, subtitle, children }: OnboardingStepProps) {
return (
<motion.div
key={step}
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="bg-card border border-border shadow-sm w-full rounded-lg p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{eyebrow} {getStepNumber(step)} / {TOTAL_STEPS}
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-foreground">
{title}
</div>
{subtitle ? (
<div className="mb-4 text-[13px] leading-relaxed text-muted-foreground">{subtitle}</div>
) : null}
<div className="mb-6 h-px bg-[color:var(--border)]" />
{children}
</motion.div>
);
}
@@ -1,5 +1,4 @@
import { useState } from "react";
import { motion } from "framer-motion";
import { ArrowRight, FloppyDisk, SignIn } from "@phosphor-icons/react";
import { ActionButton } from "../ui";
import {
@@ -9,7 +8,7 @@ import {
StatusBadge,
INPUT_CLS,
} from "./ProvidersStepShared";
import { getStepNumber, TOTAL_STEPS } from "./OnboardingHero";
import { OnboardingStep } from "./OnboardingStep";
import { useOnboarding } from "./OnboardingContext";
/* ------------------------------------------------------------------ */
@@ -87,24 +86,12 @@ export function ProvidersStep({ onContinue, onBack }: ProvidersStepProps) {
}
return (
<motion.div
key="providers"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="w-full rounded-lg border border-border bg-card p-7 shadow-sm"
<OnboardingStep
step="providers"
eyebrow={t("onboarding.stepLabelProviders")}
title={t("onboarding.providersTitle")}
subtitle={t("onboarding.providersSubtitle")}
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{t("onboarding.stepLabelProviders")} {getStepNumber("providers")} / {TOTAL_STEPS}
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-foreground">
{t("onboarding.providersTitle")}
</div>
<div className="mb-4 text-[13px] leading-relaxed text-muted-foreground">
{t("onboarding.providersSubtitle")}
</div>
<div className="mb-6 h-px bg-[color:var(--border)]" />
<div className="space-y-3">
{/* ---- Card 1: Claude Code (Official) ---- */}
@@ -281,6 +268,6 @@ export function ProvidersStep({ onContinue, onBack }: ProvidersStepProps) {
/>
</div>
</div>
</motion.div>
</OnboardingStep>
);
}