Organizes account setup, file import, and completion checklist into a successful first-workspace path.
@motion-lexicon
src/registry/blocks/onboarding-flow.tsx
"use client";
import { useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { MultiStepForm } from "@/components/motion-lexicon/multi-step-form";
import { OnboardingChecklist } from "@/components/motion-lexicon/onboarding-checklist";
import { FileDropzone } from "@/components/motion-lexicon/file-dropzone";
import { Drawer } from "@/components/motion-lexicon/drawer";
import { ProgressBar } from "@/components/motion-lexicon/progress-bar";
import { Tabs } from "@/components/motion-lexicon/tabs";
type Locale = "zh" | "en";
export type OnboardingFlowBlockProps = { locale?: Locale; className?: string };
export function OnboardingFlowBlock({ locale = "en", className = "" }: OnboardingFlowBlockProps) {
const copy = locale === "zh" ? zh : en;
const reduced = useReducedMotion() === true;
const [workspace, setWorkspace] = useState("Northstar");
const [files, setFiles] = useState<string[]>([]);
const [complete, setComplete] = useState(false);
const [helpOpen, setHelpOpen] = useState(false);
const steps = [{ id: "workspace", title: copy.stepWorkspace, description: copy.stepWorkspaceCopy, content: <label className="block"><span className="sr-only">{copy.workspace}</span><input value={workspace} onChange={(event) => setWorkspace(event.target.value)} className="min-h-11 w-full rounded-xl border border-neutral-200 bg-[#fafafa] px-3 text-[12px] outline-none focus-visible:ring-2 focus-visible:ring-[#4568ff]" /></label>, validate: () => workspace.trim() ? null : copy.workspaceError }, { id: "import", title: copy.stepImport, description: copy.stepImportCopy, content: <FileDropzone label={copy.importLabel} accept=".csv,.pdf,.md" maxFiles={3} onFiles={(next) => setFiles(next.map((file) => file.name))} /> }, { id: "invite", title: copy.stepInvite, description: copy.stepInviteCopy, content: <div className="rounded-xl border border-neutral-200 bg-[#fafafa] p-3 text-[11px] text-neutral-600"><span className="font-mono text-[9px] text-neutral-400">TEAM</span><div className="mt-3 flex items-center justify-between"><span>Mina · Editor</span><span className="rounded-full bg-emerald-100 px-2 py-1 text-[9px] text-emerald-800">{copy.ready}</span></div><div className="mt-2 flex items-center justify-between"><span>Noah · Producer</span><span className="rounded-full bg-emerald-100 px-2 py-1 text-[9px] text-emerald-800">{copy.ready}</span></div></div> }];
const progress = complete ? 100 : files.length ? 66 : workspace.trim() ? 33 : 0;
return (
<section data-page-block="onboarding-flow" className={`w-full overflow-hidden rounded-[18px] border border-neutral-200 bg-[#f7f7f5] text-[#242424] dark:border-white/10 dark:bg-[#151515] dark:text-neutral-100 ${className}`}>
<header className="flex min-h-16 items-center justify-between gap-4 border-b border-neutral-200 bg-white px-5 sm:px-7 dark:border-white/10 dark:bg-[#1b1b1b]">
<a href="#onboarding-home" className="flex min-h-11 items-center gap-2 text-[14px] font-semibold tracking-[-.03em] outline-none focus-visible:ring-2 focus-visible:ring-[#4568ff]"><span className="grid size-8 place-items-center rounded-[9px] bg-[#242424] text-white">A</span>Arc</a>
<span className="font-mono text-[9px] tracking-[.14em] text-neutral-400">{complete ? copy.complete : copy.setup}</span>
<button type="button" onClick={() => setComplete(false)} className="min-h-11 rounded-lg px-3 text-[11px] font-medium text-neutral-600 outline-none focus-visible:ring-2 focus-visible:ring-[#4568ff] dark:text-neutral-300">{copy.restart}</button>
</header>
<div id="onboarding-home" className="grid min-h-[650px] gap-6 p-4 sm:p-6 lg:grid-cols-[minmax(0,1.1fr)_minmax(300px,.9fr)] lg:p-9">
<div className="min-w-0"><span className="font-mono text-[9px] tracking-[.16em] text-neutral-400">{copy.eyebrow}</span><h1 className="mt-3 max-w-[12ch] text-[clamp(34px,5vw,54px)] font-semibold leading-[.9] tracking-[-.06em]">{copy.title}</h1><p className="mt-5 max-w-[48ch] text-[13px] leading-6 text-neutral-600 dark:text-neutral-300">{copy.intro}</p><div className="mt-7 max-w-xl"><MultiStepForm steps={steps} label={copy.setup} submitLabel={copy.finish} onSubmit={() => new Promise<void>((resolve) => { window.setTimeout(() => { setComplete(true); resolve(); }, reduced ? 0 : 350); })} /></div>{files.length ? <p role="status" className="mt-3 max-w-xl rounded-xl bg-blue-50 px-3 py-2 text-[11px] text-blue-700">{copy.imported}: {files.join(", ")}</p> : null}</div>
<aside className="grid content-start gap-4">
<ProgressBar value={progress} label={copy.setup} completeLabel={copy.complete} pendingLabel={copy.setup} />
<Tabs items={[{ value: "steps", label: copy.setup }, { value: "help", label: copy.help }]} label={copy.setup} renderPanel={(value) => <p className="text-[11px] leading-5 text-neutral-600 dark:text-neutral-300">{value === "steps" ? copy.intro : copy.helpCopy}</p>} />
<AnimatePresence mode="wait" initial={false}>{complete ? <motion.section key="complete" initial={reduced ? false : { opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} className="rounded-[16px] bg-[#242424] p-5 text-white"><span className="grid size-10 place-items-center rounded-full bg-[#dff0bf] text-[#1e2d1d]">✓</span><h2 className="mt-6 text-[24px] tracking-[-.05em]">{copy.doneTitle}</h2><p className="mt-3 text-[11px] leading-5 text-white/65">{copy.doneCopy}</p><button type="button" onClick={() => setComplete(false)} className="mt-6 min-h-11 w-full rounded-xl bg-white px-3 text-[11px] font-semibold text-[#242424] outline-none focus-visible:ring-2 focus-visible:ring-[#8eb9ff]">{copy.restart}</button></motion.section> : <motion.div key="checklist" initial={reduced ? false : { opacity: 0, y: -8 }} animate={{ opacity: 1, y: 0 }}><OnboardingChecklist title={copy.checklist} items={[{ id: "workspace", title: copy.checkWorkspace, detail: workspace || copy.unnamed, complete: Boolean(workspace) }, { id: "import", title: copy.checkImport, detail: files.length ? files[0] : copy.checkImportCopy }, { id: "invite", title: copy.checkInvite, detail: copy.checkInviteCopy }]} /></motion.div>}</AnimatePresence>
<section className="rounded-[16px] border border-neutral-200 bg-white p-4 text-[11px] dark:border-white/10 dark:bg-white/5"><span className="font-mono text-[9px] tracking-[.14em] text-neutral-400">{copy.help}</span><p className="mt-2 leading-5 text-neutral-600 dark:text-neutral-300">{copy.helpCopy}</p><button type="button" onClick={() => setHelpOpen(true)} className="mt-3 min-h-11 rounded-lg border border-neutral-200 px-3 font-medium outline-none focus-visible:ring-2 focus-visible:ring-[#4568ff] dark:border-white/15">{copy.contact}</button></section>
</aside>
</div>
<Drawer open={helpOpen} onOpenChange={setHelpOpen} title={copy.help} description={copy.helpCopy} side="right"><p className="text-[12px] leading-6 text-neutral-600 dark:text-neutral-300">{copy.contact}</p></Drawer>
</section>
);
}
const en = { setup: "SETUP", complete: "SETUP COMPLETE", restart: "Restart", eyebrow: "Welcome to Arc", title: "Make your first workspace feel like yours.", intro: "Name the space, bring in the working material, and invite the people who will carry the first release with you.", stepWorkspace: "Name your workspace", stepWorkspaceCopy: "Use the team or project name people already recognize.", workspace: "Workspace name", workspaceError: "Add a workspace name to continue.", stepImport: "Bring in a working file", stepImportCopy: "A brief, a note, or the first data export is enough to begin.", importLabel: "Drop a brief or browse files", stepInvite: "Confirm the first collaborators", stepInviteCopy: "You can add more people after the first shared space is ready.", ready: "Ready", finish: "Create workspace", imported: "Ready to import", checklist: "Your first session", checkWorkspace: "Name the workspace", unnamed: "Choose a name", checkImport: "Bring in a working file", checkImportCopy: "Optional for now", checkInvite: "Confirm collaborators", checkInviteCopy: "2 people ready", doneTitle: "Your workspace is ready.", doneCopy: "Northstar has a home, a starting file and its first collaborators. Open the workspace when you are ready to make the next decision.", help: "NEED A HAND?", helpCopy: "Keep this setup lightweight. You can revisit files, roles and notifications later.", contact: "Contact support" };
const zh = { setup: "设置", complete: "设置完成", restart: "重新开始", eyebrow: "欢迎来到 Arc", title: "让第一个工作区从一开始就属于你。", intro: "为它命名,带入正在使用的材料,再邀请会和你共同推进首个版本的人。", stepWorkspace: "命名工作区", stepWorkspaceCopy: "使用团队已经熟悉的项目或组织名称。", workspace: "工作区名称", workspaceError: "请先填写工作区名称。", stepImport: "导入工作文件", stepImportCopy: "一份简报、一条笔记或第一份数据导出都足够开始。", importLabel: "拖入简报或浏览文件", stepInvite: "确认首批协作者", stepInviteCopy: "首个共享空间准备好后,还可以继续添加成员。", ready: "已就绪", finish: "创建工作区", imported: "准备导入", checklist: "你的第一次会话", checkWorkspace: "命名工作区", unnamed: "选择一个名称", checkImport: "导入工作文件", checkImportCopy: "暂时可选", checkInvite: "确认协作者", checkInviteCopy: "2 人已就绪", doneTitle: "工作区已准备好。", doneCopy: "Northstar 已拥有一个主页、一个起始文件和最初的协作者。准备好做下一个决定时,打开工作区即可。", help: "需要帮助?", helpCopy: "保持这次设置足够轻量。文件、角色和通知都可以稍后再调整。", contact: "联系支持" };