feat(onboarding): add stepper, contextual preview and shell

This commit is contained in:
Théo LAGACHE
2026-06-16 16:39:54 +02:00
parent 35654feedd
commit 6098c2fe42
5 changed files with 181 additions and 7 deletions
+1
View File
@@ -22,6 +22,7 @@
"@better-auth/sso": "1.6.11",
"@hookform/resolvers": "^5.4.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@onboardjs/react": "1.0.0-rc.5",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
+45 -7
View File
@@ -29,6 +29,9 @@ importers:
'@modelcontextprotocol/sdk':
specifier: ^1.29.0
version: 1.29.0(zod@4.3.6)
'@onboardjs/react':
specifier: 1.0.0-rc.5
version: 1.0.0-rc.5(@onboardjs/core@1.0.0-rc.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@radix-ui/react-accordion':
specifier: ^1.2.12
version: 1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -2182,6 +2185,16 @@ packages:
'@octokit/types@16.0.0':
resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==}
'@onboardjs/core@1.0.0-rc.4':
resolution: {integrity: sha512-G22h1I2JUiEyDGf06YEUT2Afg26rDZM4Ku0PAZFqmr7HVukg9651UdPxZ1/9TVvAFFmW6Qq+u8oQ75KVJu80pw==}
'@onboardjs/react@1.0.0-rc.5':
resolution: {integrity: sha512-iqC4MjrQe4VLi3HKCkczFigkjLxdjWTYrzoI1jEi3QpKOVmDAn4bmXngazW2nfPSOItkxdQq38VO7q8J2yVMhg==}
peerDependencies:
'@onboardjs/core': '>=1.0.0-rc.1'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
'@opentelemetry/semantic-conventions@1.41.1':
resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==}
engines: {node: '>=14'}
@@ -6938,6 +6951,14 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
p-queue@8.1.1:
resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==}
engines: {node: '>=18'}
p-timeout@6.1.4:
resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==}
engines: {node: '>=14.16'}
pac-proxy-agent@7.2.0:
resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==}
engines: {node: '>= 14'}
@@ -9890,6 +9911,16 @@ snapshots:
dependencies:
'@octokit/openapi-types': 27.0.0
'@onboardjs/core@1.0.0-rc.4':
dependencies:
p-queue: 8.1.1
'@onboardjs/react@1.0.0-rc.5(@onboardjs/core@1.0.0-rc.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
'@onboardjs/core': 1.0.0-rc.4
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
'@opentelemetry/semantic-conventions@1.41.1': {}
'@paralleldrive/cuid2@2.3.1':
@@ -13498,7 +13529,7 @@ snapshots:
'@next/eslint-plugin-next': 16.2.7
eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0))
@@ -13521,7 +13552,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -13536,14 +13567,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)):
eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
transitivePeerDependencies:
- supports-color
@@ -13558,7 +13589,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10
eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
hasown: 2.0.4
is-core-module: 2.16.2
is-glob: 4.0.3
@@ -15178,6 +15209,13 @@ snapshots:
dependencies:
p-limit: 3.1.0
p-queue@8.1.1:
dependencies:
eventemitter3: 5.0.4
p-timeout: 6.1.4
p-timeout@6.1.4: {}
pac-proxy-agent@7.2.0:
dependencies:
'@tootallnate/quickjs-emscripten': 0.23.0
@@ -16474,7 +16512,7 @@ snapshots:
ansi-escapes: 4.3.2
supports-hyperlinks: 2.3.0
terser-webpack-plugin@5.6.1(esbuild@0.25.10)(postcss@8.5.10)(webpack@5.107.2(esbuild@0.27.7)(postcss@8.5.10)):
terser-webpack-plugin@5.6.1(esbuild@0.25.10)(postcss@8.5.10)(webpack@5.107.2(esbuild@0.25.10)(postcss@8.5.10)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1
@@ -16899,7 +16937,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 4.3.3
tapable: 2.3.3
terser-webpack-plugin: 5.6.1(esbuild@0.25.10)(postcss@8.5.10)(webpack@5.107.2(esbuild@0.27.7)(postcss@8.5.10))
terser-webpack-plugin: 5.6.1(esbuild@0.25.10)(postcss@8.5.10)(webpack@5.107.2(esbuild@0.25.10)(postcss@8.5.10))
watchpack: 2.5.1
webpack-sources: 3.5.0
transitivePeerDependencies:
@@ -0,0 +1,65 @@
"use client";
import { useOnboarding } from "@onboardjs/react";
import { Loader2 } from "lucide-react";
const DASHBOARD_PREVIEW_STEP_IDS = [
"account-info",
"security",
"preferences",
"org-create",
];
export const OnboardingPreview = () => {
const { state } = useOnboarding();
const stepId = state?.currentStep?.id;
if (stepId === "agent-waiting") {
return (
<div className="flex h-full items-center justify-center">
<Loader2 className="size-10 animate-spin text-primary" />
</div>
);
}
if (stepId === "finish") {
return null;
}
const org = state?.context.flowData.org as { name?: string; logoDataUrl?: string } | undefined;
const preferences = state?.context.flowData.preferences as { theme?: string; avatarDataUrl?: string } | undefined;
const isDashboardPreview = typeof stepId === "string" && DASHBOARD_PREVIEW_STEP_IDS.includes(stepId);
return (
<div className="flex h-full w-full items-center justify-center p-6">
<div
className={`w-full max-w-sm rounded-lg border border-white/10 overflow-hidden ${preferences?.theme === "light" ? "bg-white text-black" : "bg-zinc-900 text-white"}`}
>
<div className="flex items-center gap-2 border-b border-white/10 px-4 py-3">
{org?.logoDataUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={org.logoDataUrl} alt="" className="size-5 rounded" />
) : (
<div className="size-5 rounded bg-primary" />
)}
<span className="text-sm font-medium">{org?.name || "Your organisation"}</span>
</div>
{isDashboardPreview && (
<div className="flex">
<div className="w-16 border-r border-white/10 p-2 flex flex-col gap-2">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-2 rounded bg-white/10" />
))}
</div>
<div className="flex-1 p-3 flex flex-col gap-2">
<div className="h-3 w-1/2 rounded bg-white/10" />
<div className="h-16 rounded bg-white/5" />
<div className="h-16 rounded bg-white/5" />
</div>
</div>
)}
</div>
</div>
);
};
@@ -0,0 +1,50 @@
"use client";
import { useOnboarding } from "@onboardjs/react";
import { Button } from "@/components/ui/button";
import { OnboardingStepper } from "@/features/onboarding/onboarding-stepper";
import { OnboardingPreview } from "@/features/onboarding/onboarding-preview";
export const OnboardingShell = () => {
const { state, previous, skip, renderStep } = useOnboarding();
if (!state) return null;
const showSplit = state.currentStep?.id !== "agent-waiting" && state.currentStep?.id !== "finish";
return (
<div className="min-h-screen bg-zinc-950 text-white flex items-center justify-center p-4">
<div className="w-full max-w-4xl rounded-2xl bg-zinc-900 border border-white/10 shadow-2xl overflow-hidden flex flex-col md:flex-row min-h-[560px]">
<div className="flex-1 flex flex-col gap-6 p-8">
<OnboardingStepper />
<div className="flex-1">{renderStep()}</div>
<div className="flex justify-between">
<Button
type="button"
variant="ghost"
onClick={() => previous()}
disabled={state.isFirstStep || state.isLoading}
>
Back
</Button>
{state.isSkippable && (
<Button
type="button"
variant="ghost"
onClick={() => skip()}
disabled={state.isLoading}
>
Skip
</Button>
)}
</div>
</div>
{showSplit && (
<div className="hidden md:block w-[360px] border-l border-white/10 bg-zinc-950">
<OnboardingPreview />
</div>
)}
</div>
</div>
);
};
@@ -0,0 +1,20 @@
"use client";
import { useOnboarding } from "@onboardjs/react";
import { Progress } from "@/components/ui/progress";
export const OnboardingStepper = () => {
const { state } = useOnboarding();
if (!state) return null;
return (
<div className="flex flex-col gap-2 w-full max-w-sm">
<div className="flex justify-between text-xs text-muted-foreground">
<span>Step {state.currentStepNumber} of {state.totalSteps}</span>
<span>{Math.round(state.progressPercentage)}%</span>
</div>
<Progress value={state.progressPercentage} />
</div>
);
};